diff --git a/.github/workflows/api-verification.yml b/.github/workflows/api-verification.yml index 56726817dcb..95c9b959a1e 100644 --- a/.github/workflows/api-verification.yml +++ b/.github/workflows/api-verification.yml @@ -71,7 +71,7 @@ jobs: - name: "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }})" if: steps.environment_step.outputs.SKIP_API_CHECK != 'true' run: | - echo "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }}):" >> $GITHUB_STEP_SUMMARY + echo "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }}):" | tee -a ${GITHUB_STEP_SUMMARY} BIN_PACKAGE=$(ls "${GITHUB_WORKSPACE}/artifacts/" | grep "${PKG_NAME}[0-9]*_${{ steps.environment_step.outputs.KEYMAN_VERSION }}-1${{ steps.environment_step.outputs.PRERELEASE_TAG }}+$(lsb_release -c -s)1_amd64.deb") cd ${{ github.workspace }}/keyman/linux @@ -80,7 +80,7 @@ jobs: --bin-pkg "${GITHUB_WORKSPACE}/artifacts/${BIN_PACKAGE}" \ --git-sha "${{ steps.environment_step.outputs.GIT_SHA }}" \ --git-base "${{ steps.environment_step.outputs.GIT_BASE }}" \ - verify 2>> $GITHUB_STEP_SUMMARY + verify 2> >(tee -a ${GITHUB_STEP_SUMMARY} >&2) - name: Archive .symbols file if: steps.environment_step.outputs.SKIP_API_CHECK != 'true' && always() diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index 9a990634b20..6ce42cf6d56 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -137,7 +137,7 @@ jobs: strategy: fail-fast: true matrix: - dist: [jammy, noble, questing] + dist: [jammy, noble, resolute] steps: - name: Checkout diff --git a/.gitignore b/.gitignore index 7b872195880..eaf8f5f3ebf 100644 --- a/.gitignore +++ b/.gitignore @@ -186,3 +186,7 @@ lcov.info # flag file for build script .configured + +# see common/tools/api-extractor/README.md +tsdoc.json +api-extractor.json diff --git a/HISTORY.md b/HISTORY.md index f2d3cab56b0..399e3077f2b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,77 @@ # Keyman Version History +## 19.0.258 alpha 2026-07-15 + +* chore(android): remove doc for non-existing deprecated functions (#16244) + +## 19.0.257 alpha 2026-07-14 + +* chore(linux): remove questing, add stonking (#16236) +* chore(android): cleanup Android build scripts and artifact filenames (#16234) +* chore(web): declare scope for stubAndKeyboardCache members (#16221) +* fix(android): improve clarity of keyboard script error popup (#16228) +* chore(android): log legacy cloud keyboards (#16237) +* chore(android): cleanup font variable names, remove obsolete code (#16211) + +## 19.0.256 alpha 2026-07-13 + +* fix(android): add permissions for Sentry and set default keyboard (#16216) +* maint(resources): add extra debug reporting for builder (#16230) + +## 19.0.255 alpha 2026-07-11 + +* maint(android): use KEYMAN_TIER instead of TIER.md (#16219) +* feat(ios): add keyman-version to package-version check (#16138) +* docs(web): cleanup of index pages and page titles (#16171) +* feat(android): add keyman-version to package-version check (#16139) + +## 19.0.254 alpha 2026-07-10 + +* fix(android): handle blank keyboard tap properly (#16217) +* fix(web): define IME interfaces in `KeyboardInterfaceBase` (#16170) + +## 19.0.253 alpha 2026-07-09 + +* fix(web): resync model in `set osk()` (as used by Server) (#16127) +* chore(developer): consolidate api-extractor usage in Developer (#16135) + +## 19.0.252 alpha 2026-07-08 + +* chore(linux): reorder buttons on kmp install window (#16210) +* refactor(web): rename `keymanweb` to `keyman` in UI modules (#16174) +* chore: remove deprecated "X-UA-Compatible" and "apple-mobile-web-app-capable" metas (#16176) +* fix(android): refactor `KMLog` - simplification and added resilience (#16152) +* fix(android): differentiate between fontPath and fontUrl in Keyman Engine for Android (#16188) +* fix(web): fixes cookie unit-tests and path for OSK resources during testing (#16209) + +## 19.0.251 alpha 2026-07-02 + +* fix(android): skip test on Windows that triggers androidx WebKit bug (#16184) + +## 19.0.250 alpha 2026-07-01 + +* fix(android): use `WebViewAssetLoader` for loading assets on Android (address regression in 19.0.242) (#16146) +* fix(ios): bundle in KMW's globe-hint.css (#16142) +* fix(ios): load host page, keyboards through a consistent WKURLSchemeHandler (#16136) + +## 19.0.249 alpha 2026-06-30 + +* fix(windows): add update property to remote check (#16126) +* chore(android): allow to build FV app in docker container (#16163) +* fix(developer): warn only on race when destroying TAppSourceHttpResponder (#16140) +* fix(developer): map shift key nextlayer property when importing OSK (#16110) + +## 19.0.248 alpha 2026-06-29 + +* chore: add missing line to history (#16154) +* maint(resources): DRY out `PRInformation` interface (#16158) +* fix(developer): prevent clone of legacy keyboards with no source (#16111) +* fix(developer): consolidate user options in TypeScript code (#16134) +* fix(developer): fixup references to layer after deleting in Touch Layout Editor (#16129) +* fix(web): revert regression in setting `activeKeyboard` in `set osk` (#16156) +* test(web): add e2e tests for examples from guide (#16108) +* maint(linux): show output of API check also in log file (#16153) + ## 19.0.247 alpha 2026-06-16 * chore: update multi-labeler to 5.0.0 (#16100) @@ -39,6 +111,7 @@ ## 19.0.241 alpha 2026-06-02 +* chore: web-core preflight (#16015) * chore(web): web-core preflight - strip core references (#16040) * docs: add note on how to use composer on dockerized websites (#16029) * fix(web): fix race displaying active keyboard in menu (#16042) diff --git a/VERSION.md b/VERSION.md index a0b5f4d5f24..48c4023fd88 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.248 \ No newline at end of file +19.0.259 \ No newline at end of file diff --git a/android/KMAPro/build.sh b/android/KMAPro/build.sh index 06370c06d23..ba3ea5aa597 100755 --- a/android/KMAPro/build.sh +++ b/android/KMAPro/build.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# Build Keyman for Android app (KMAPro) +# Keyman is copyright (C) SIL Global. MIT License. +# ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" @@ -9,111 +10,92 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "$KEYMAN_ROOT/resources/build/utils.inc.sh" . "$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" # ################################ Main script ################################ -# Definition of global compile constants - -CONFIG="release" -BUILD_FLAGS="build -x lint -x test" # Gradle build w/o test -TEST_FLAGS="-x assembleRelease lintRelease testRelease" # Gradle test w/o build -DAEMON_FLAG= - builder_describe "Builds Keyman for Android app." \ "@/android/KMEA" \ "clean" \ "configure" \ "build" \ - "test Runs lint and unit tests." \ - "publish-symbols Publishes symbols to Sentry." \ + "test Runs lint and unit tests." \ + "publish-symbols Publishes symbols to Sentry." \ "publish-play-store Publishes the APK to the Play Store." -# parse before describe_outputs to check debug flags builder_parse "$@" if builder_is_debug_build; then - builder_heading "### Debug config ####" - CONFIG="debug" BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug" TEST_FLAGS="-x assembleDebug lintDebug testDebug" +else + BUILD_FLAGS="build -x lint -x test" # Gradle build w/o test + TEST_FLAGS="-x assembleRelease lintRelease testRelease" # Gradle test w/o build fi +if builder_is_ci_build; then + GRADLE_DAEMON=--no-daemon +else + GRADLE_DAEMON= +fi + +# Locations for bundled keyboard and lexical model + KEYBOARD_PACKAGE_ID="sil_euro_latin" KEYBOARDS_TARGET="android/KMAPro/kMAPro/src/main/assets/${KEYBOARD_PACKAGE_ID}.kmp" MODEL_PACKAGE_ID="nrc.en.mtnt" MODELS_TARGET="android/KMAPro/kMAPro/src/main/assets/${MODEL_PACKAGE_ID}.model.kmp" + builder_describe_outputs \ configure "/${MODELS_TARGET}" \ - build /android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk - -#### Build - - -# Parse args - -if builder_is_ci_build; then - DAEMON_FLAG=--no-daemon -fi + build /android/KMAPro/kMAPro/build/outputs/apk/$BUILDER_CONFIGURATION/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk #### Build action definitions #### -# Check about cleaning artifact paths -if builder_start_action clean; then - rm -rf "$KEYMAN_ROOT/android/KMAPro/kMAPro/build/outputs" - builder_finish_action success clean -fi - -if builder_start_action configure; then - +do_configure() { downloadKeyboardPackage "$KEYBOARD_PACKAGE_ID" "${KEYMAN_ROOT}/$KEYBOARDS_TARGET" downloadModelPackage "$MODEL_PACKAGE_ID" "${KEYMAN_ROOT}/$MODELS_TARGET" +} - builder_finish_action success configure -fi - -if builder_start_action build; then - +do_build() { # Copy Keyman Engine for Android - cp "${KEYMAN_ROOT}/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs/keyman-engine.aar" + cp "${KEYMAN_ROOT}/android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs/keyman-engine.aar" # Convert markdown to html for offline help build_help_html android KMAPro/kMAPro/src/main/assets/info - echo "BUILD_FLAGS $BUILD_FLAGS" - ./gradlew $DAEMON_FLAG clean $BUILD_FLAGS + builder_echo "BUILD_FLAGS $BUILD_FLAGS" + ./gradlew $GRADLE_DAEMON $BUILD_FLAGS - mv "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION}.apk" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk" - - builder_finish_action success build -fi - -if builder_start_action test; then - - echo "TEST_FLAGS $TEST_FLAGS" - ./gradlew $DAEMON_FLAG $TEST_FLAGS + mv "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$BUILDER_CONFIGURATION/keyman-${KEYMAN_VERSION}.apk" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$BUILDER_CONFIGURATION/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk" +} - builder_finish_action success test -fi +do_test() { + builder_echo "TEST_FLAGS $TEST_FLAGS" + ./gradlew $GRADLE_DAEMON $TEST_FLAGS +} -publish_symbols() { +do_publish_symbols() { if builder_is_ci_build && builder_is_ci_build_level_release; then # TODO: what does publishSentry even do? - ./gradlew $DAEMON_FLAG publishSentry + ./gradlew $GRADLE_DAEMON publishSentry builder_echo "Making a Sentry release for tag $KEYMAN_VERSION_GIT_TAG" sentry-cli upload-dif -p keyman-android --include-sources sentry-cli releases -p keyman-android files $KEYMAN_VERSION_GIT_TAG upload-sourcemaps ./ fi } -publish_play_store() { +do_publish_play_store() { if builder_is_ci_build && builder_is_ci_build_level_release; then generateReleaseNotes # Publish Keyman for Android to Play Store - ./gradlew $DAEMON_FLAG publishReleaseApk + ./gradlew $GRADLE_DAEMON publishReleaseApk fi } -builder_run_action publish-symbols publish_symbols -builder_run_action publish-play-store publish_play_store +builder_run_action clean rm -rf "$KEYMAN_ROOT/android/KMAPro/kMAPro/build" +builder_run_action configure do_configure +builder_run_action build do_build +builder_run_action test do_test +builder_run_action publish-symbols do_publish_symbols +builder_run_action publish-play-store do_publish_play_store diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index 77ab2fa7368..cb5e52731a5 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -8,8 +8,7 @@ plugins { ext.rootPath = '../../' apply from: "$rootPath/version.gradle" - -String tier = new File('../../TIER.md').getText('UTF-8').trim(); +String tier = System.getenv("KEYMAN_TIER"); java { toolchain { @@ -173,10 +172,11 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'com.google.android.material:material:1.12.0' implementation 'com.stepstone.stepper:material-stepper:4.3.1' + implementation 'androidx.webkit:webkit:1.14.0' implementation files('libs/keyman-engine.aar') implementation 'io.sentry:sentry-android:8.19.1' implementation 'androidx.preference:preference:1.2.1' - implementation "com.android.installreferrer:installreferrer:2.2" + implementation 'com.android.installreferrer:installreferrer:2.2' // Add dependency for generating QR Codes // (Even though it's embedded in KMEA, because we're manually copying keyman-engine.aar, diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java index 3308caff9ac..4c24da2559d 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java @@ -1,9 +1,10 @@ -/** - * Copyright (C) 2017 SIL International. All rights reserved. +/* + * Keyman is copyright (C) SIL Global. MIT License. */ package com.tavultesoft.kmapro; +import java.io.File; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; @@ -386,7 +387,7 @@ protected void onResume() { super.onResume(); if (webView != null) { if (didFinishLoading) { - String fontFilename = KMManager.getKeyboardTextFontFilename(); + String fontFilename = getKeyboardTextFontFilenameOnly(); if (!loadedFont.equals(fontFilename)) { webView.reload(); } @@ -432,8 +433,19 @@ public void onBackPressed() { } } + /** + * Returns the filename without path of the display font of the current keyboard. + */ + private String getKeyboardTextFontFilenameOnly() { + String fontPath = KMManager.getKeyboardTextFontFilename(); + if (fontPath == null || fontPath.isEmpty()) { + return ""; + } + return new File(fontPath).getName(); + } + private void loadFont() { - String font = KMManager.getKeyboardTextFontFilename(); + String font = getKeyboardTextFontFilenameOnly(); if (!font.isEmpty()) { loadedFont = font; String fontUrl = String.format("%s%s", fontBaseUri, font); diff --git a/android/KMEA/app/build.gradle b/android/KMEA/app/build.gradle index c9ae92c1c2d..8f77d155f01 100644 --- a/android/KMEA/app/build.gradle +++ b/android/KMEA/app/build.gradle @@ -8,7 +8,7 @@ ext.rootPath = '../../' apply from: "$rootPath/version.gradle" base { - // Specify library filename keyman-engine-$CONFIG.aar + // Specify library filename keyman-engine-$BUILDER_CONFIGURATION.aar archivesName = "keyman-engine" } @@ -72,6 +72,7 @@ dependencies { implementation 'commons-io:commons-io:2.16.1' implementation 'io.sentry:sentry-android:8.19.1' implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.webkit:webkit:1.14.0' // Robolectric testImplementation 'androidx.test.ext:junit:1.2.1' diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 48672c05728..883b9b6bb55 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -22,10 +22,6 @@ sentryManager.init(); window.addEventListener('load', init, false); -function loadDefaultKeyboard() { - notifyHost('reloadAfterError'); -} - function init() { //document.body.style.backgroundColor="transparent"; //window.console.log('Device type = '+device); @@ -82,7 +78,19 @@ function init() { keyman.addEventListener('keyboardchange', setIsChiral); keyman.core.languageProcessor.on('statechange', onStateChange); - document.body.addEventListener('touchend', loadDefaultKeyboard); + // If the keyboard fails to display, this usually indicates that a Javascript + // error has occurred, perhaps in the active keyboard. So we have a last-gasp + // fallback in that situation; if the user touches the blank document, we will + // attempt to reload the default keyboard. + document.addEventListener('touchend', (event) => { + if(event.target == document.documentElement) { + // The error will be reported through to Sentry if the user has error + // reporting enabled; breadcrumbs can give us helpful details on possible + // root causes for the error + window.console.error('user touched the document body, which can only happen if the keyboard was not presented. This is usually caused by another error.'); + notifyHost('reloadAfterError'); + } + }); notifyHost('pageLoaded'); } 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 9152f843604..bffb0c13072 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 @@ -1,5 +1,5 @@ -/** - * Copyright (C) 2017-2018 SIL International. All rights reserved. +/* + * Keyman is copyright (C) SIL Global. MIT License. */ package com.keyman.engine; @@ -23,6 +23,7 @@ import com.keyman.engine.util.FileUtils; import com.keyman.engine.util.KMLog; import com.keyman.engine.util.KMString; +import com.keyman.engine.util.WebViewUtils; import android.annotation.SuppressLint; import android.content.Context; @@ -83,9 +84,8 @@ final class KMKeyboard extends WebView { */ protected static KMManager.BannerType currentBanner = KMManager.BannerType.HTML; - private static String txtFont = ""; - private static String oskFont = null; - private static String keyboardRoot = ""; + private static String textFontPath = ""; + private static String oskFontPath = ""; private final String fontUndefined = "undefined"; private GestureDetector gestureDetector; private static ArrayList kbEventListeners = null; @@ -310,7 +310,7 @@ public boolean onConsoleMessage(ConsoleMessage cm) { // This duplicates the sendKMWError message, which itself duplicates the reporting now // managed by sentry-manager on the js side in patch in #6890. It does not give us // additional useful information. So we don't re-send to Sentry. - sendError(packageID, keyboardID, "", false); + sendError(packageID, keyboardID, "", null); } return true; @@ -366,8 +366,7 @@ public void loadKeyboard() { } else { KMManager.SystemKeyboardWebViewClient.setKeyboardLoaded(false); } - - String htmlPath = "file://" + getContext().getDir("data", Context.MODE_PRIVATE) + "/" + KMManager.KMFilename_KeyboardHtml; + String htmlPath = WebViewUtils.buildAssetUrl(KMManager.KMFilename_KeyboardHtml); loadUrl(htmlPath); setBackgroundColor(0); } @@ -550,7 +549,7 @@ protected void toggleSuggestionBanner(HashMap associatedLexicalM * @return String */ public static String textFontFilename() { - return txtFont; + return textFontPath; } /** @@ -558,9 +557,10 @@ public static String textFontFilename() { * @return String */ public static String oskFontFilename() { - return oskFont; + return oskFontPath; } + // REVIEW: this method seems to be unused and undocumented. Can we remove it? /** * Return the full path to the special OSK font, * which is with all the keyboard assets at the root app_data folder @@ -601,7 +601,11 @@ public boolean setKeyboard(String packageID, String keyboardID, String languageI } if (!KMManager.shouldAllowSetKeyboard() || kbInfo == null) { - sendError(packageID, keyboardID, languageID, true); + if(!KMManager.shouldAllowSetKeyboard()) { + sendError(packageID, keyboardID, languageID, "setKeyboard.short failed with shouldAllowSetKeyboard == false"); + } else { + sendError(packageID, keyboardID, languageID, "setKeyboard.short failed with kbInfo == null"); + } kbInfo = KeyboardController.getInstance().getKeyboardInfo(0); retVal = false; } else { @@ -632,7 +636,7 @@ public boolean prepareKeyboardSwitch(String packageID, String keyboardID, String if (!KMManager.shouldAllowSetKeyboard() || (packageID.equals(KMManager.KMDefault_UndefinedPackageID) && keyboardVersion == null)) { - sendError(packageID, keyboardID, languageID, true); + sendError(packageID, keyboardID, languageID, "prepareKeyboardSwitch"); Keyboard kbInfo = KeyboardController.getInstance().getKeyboardInfo(0); packageID = kbInfo.getPackageID(); keyboardID = kbInfo.getKeyboardID(); @@ -646,8 +650,6 @@ public boolean prepareKeyboardSwitch(String packageID, String keyboardID, String } String kbKey = KMString.format("%s_%s", languageID, keyboardID); - setKeyboardRoot(packageID); - // Escape single-quoted names for javascript call keyboardName = keyboardName.replaceAll("\'", "\\\\'"); // Double-escaped-backslash b/c regex. @@ -662,15 +664,15 @@ public boolean prepareKeyboardSwitch(String packageID, String keyboardID, String } public boolean setKeyboard(String packageID, String keyboardID, String languageID, - String keyboardName, String languageName, String kFont, - String kOskFont) { + String keyboardName, String languageName, String textFontFilename, + String oskFontFilename) { return setKeyboard(packageID, keyboardID, languageID, keyboardName, languageName, - kFont, kOskFont, null); + textFontFilename, oskFontFilename, null); } public boolean setKeyboard(String packageID, String keyboardID, String languageID, - String keyboardName, String languageName, String kFont, - String kOskFont, String displayName) { + String keyboardName, String languageName, String textFontFilename, + String oskFontFilename, String displayName) { if (packageID == null || keyboardID == null || languageID == null || keyboardName == null || languageName == null) { return false; } @@ -686,15 +688,19 @@ public boolean setKeyboard(String packageID, String keyboardID, String languageI if (!KMManager.shouldAllowSetKeyboard() || (packageID.equals(KMManager.KMDefault_UndefinedPackageID) && keyboardVersion == null)) { - sendError(packageID, keyboardID, languageID, true); + if(!KMManager.shouldAllowSetKeyboard()) { + sendError(packageID, keyboardID, languageID, "setKeyboard.full failed with shouldAllowSetKeyboard == false"); + } else { + sendError(packageID, keyboardID, languageID, "setKeyboard.full failed with packageID == KMDefault_UndefinedPackageID and keyboardVersion == null"); + } Keyboard kbInfo = KeyboardController.getInstance().getKeyboardInfo(0); packageID = kbInfo.getPackageID(); keyboardID = kbInfo.getKeyboardID(); languageID = kbInfo.getLanguageID(); keyboardName = kbInfo.getKeyboardName(); languageName = kbInfo.getLanguageName(); - kFont = kbInfo.getFont(); - kOskFont = kbInfo.getOSKFont(); + textFontFilename = kbInfo.getFont(); + oskFontFilename = kbInfo.getOSKFont(); retVal = false; // Keyboard changed, so determine version again @@ -702,20 +708,18 @@ public boolean setKeyboard(String packageID, String keyboardID, String languageI KMManager.getLatestKeyboardFileVersion(getContext(), packageID, keyboardID) : null; } - setKeyboardRoot(packageID); - - if(kOskFont == null || kOskFont.isEmpty()) - kOskFont = kFont; + if(oskFontFilename == null || oskFontFilename.isEmpty()) + oskFontFilename = textFontFilename; - JSONObject jDisplayFont = makeFontPaths(kFont); - JSONObject jOskFont = makeFontPaths(kOskFont); + JSONObject textFont = makeFontObject(textFontFilename, packageID); + JSONObject oskFont = makeFontObject(oskFontFilename, packageID); - txtFont = getFontFilename(jDisplayFont); - oskFont = getFontFilename(jOskFont); + textFontPath = getFontFilename(textFontFilename, packageID); + oskFontPath = getFontFilename(oskFontFilename, packageID); String kbKey = KMString.format("%s_%s", languageID, keyboardID); - String keyboardPath = makeKeyboardPath(packageID, keyboardID, keyboardVersion); + String keyboardUrl = makeKeyboardUrl(packageID, keyboardID, keyboardVersion); JSONObject reg = new JSONObject(); try { @@ -723,11 +727,11 @@ public boolean setKeyboard(String packageID, String keyboardID, String languageI reg.put("KI", "Keyboard_" + keyboardID); reg.put("KLC", languageID); reg.put("KL", languageName); - reg.put("KF", keyboardPath); + reg.put("KF", keyboardUrl); reg.put("KP", packageID); - if (jDisplayFont != null) reg.put("KFont", jDisplayFont); - if (jOskFont != null) reg.put("KOskFont", jOskFont); + if (textFont != null) reg.put("KFont", textFont); + if (oskFont != null) reg.put("KOskFont", oskFont); if (displayName != null) reg.put("displayName", displayName); } catch(JSONException e) { KMLog.LogException(TAG, "", e); @@ -784,53 +788,68 @@ public void setChirality(boolean flag) { } public boolean getChirality() { - return this.isChiral; - } - // Display localized Toast notification that keyboard selection failed, so loading default keyboard. - // Also sends a message to Sentry (not localized) - private void sendError(String packageID, String keyboardID, String languageID, boolean reportToSentry) { + /** + * Display localized Toast notification that keyboard selection failed, so + * loading default keyboard. Also sends a message to Sentry (not localized) + */ + private void sendError(String packageID, String keyboardID, String languageID, String sentryMessage) { this.currentKeyboardErrorReports++; if(this.currentKeyboardErrorReports == 1) { - BaseActivity.makeToast(context, R.string.fatal_keyboard_error_short, Toast.LENGTH_LONG, packageID, keyboardID, languageID); + BaseActivity.makeToast(context, R.string.fatal_keyboard_error, Toast.LENGTH_LONG, packageID, keyboardID, languageID); } - if(this.currentKeyboardErrorReports < 5 && DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled() && reportToSentry) { - // We'll only report up to 5 errors in a given keyboard to avoid spamming - // errors and using unnecessary bandwidth doing so - // Don't use localized string R.string.fatal_keyboard_error msg for Sentry - String msg = KMString.format("Error in keyboard %1$s:%2$s for %3$s language.", - packageID, keyboardID, languageID); + if(this.currentKeyboardErrorReports < 5 && sentryMessage != null && DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { + // We'll only report up to 5 errors in a given keyboard to avoid spamming errors and using unnecessary bandwidth doing so + String msg = KMString.format("Script error in keyboard webview\nContext: %4$s\nActive package: %1$s\nActive keyboard: %2$s\nActive language: %3$s", + packageID, keyboardID, languageID, sentryMessage); Sentry.captureMessage(msg); } } - // Set the base path of the keyboard depending on the package ID - private void setKeyboardRoot(String packageID) { + /** + * Return the root URL for the data folder. Even though this is a local + * location this returns a URL with a magic domain so that it can be + * loaded with fetch() in the webview. + */ + private String getDataRootUrl() { + return WebViewUtils.buildAssetUrl(""); + } + + /** + * Return the root path for the data folder as a file path. This should be + * used where the file is not loaded through the webview, but is instead + * used by the app directly. + */ + private String getDataRootPath() { + return context.getDir("data", Context.MODE_PRIVATE).toString() + File.separator; + } + + private String getPackageRootUrl(String packageID) { if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - this.keyboardRoot = (context.getDir("data", Context.MODE_PRIVATE).toString() + - File.separator + KMManager.KMDefault_UndefinedPackageID + File.separator); - } else { - this.keyboardRoot = (context.getDir("data", Context.MODE_PRIVATE).toString() + - File.separator + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator); + return getDataRootUrl() + KMManager.KMDefault_UndefinedPackageID + "/"; } + return getDataRootUrl() + KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; } - public String getKeyboardRoot() { - return this.keyboardRoot; + private String getPackageRootPath(String packageID) { + if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { + return getDataRootPath() + KMManager.KMDefault_UndefinedPackageID + File.separator; + } + return getDataRootPath() + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator; } - private String makeKeyboardPath(String packageID, String keyboardID, String keyboardVersion) { - String keyboardPath; + private String makeKeyboardUrl(String packageID, String keyboardID, String keyboardVersion) { + String keyboardUrl = getPackageRootUrl(packageID); if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - keyboardPath = getKeyboardRoot() + keyboardID + "-" + keyboardVersion + ".js"; + keyboardUrl += keyboardID + "-" + keyboardVersion + ".js"; } else { - keyboardPath = getKeyboardRoot() + keyboardID + ".js"; + keyboardUrl += keyboardID + ".js"; } - return keyboardPath; + return keyboardUrl; } private void sendKMWError(int lineNumber, String sourceId, String message) { @@ -912,42 +931,23 @@ private void saveCurrentKeyboardIndex() { } /** - * getFontFilename - * Parse a Font JSON object and return the font filename (ending in .ttf or .otf) - * @param fontObj JSONObject - Font JSON object - * @return String - Filename for the font. If font is invalid, return "" + * Return the full path to the font file. If the font is invalid, return empty string. + * @param fontFilename String - Font filename + * @param packageID String - Package ID + * @return String - Full path to the font file. If fontFilename is invalid, return "". */ - private String getFontFilename(JSONObject fontObj) { - String font = ""; - if (fontObj == null) { - return font; + private String getFontFilename(String fontFilename, String packageID) { + if(fontFilename == null || fontFilename.equals("")) { + return ""; } - try { - JSONArray sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles); - if (sourceArray != null) { - String fontFile; - int length = sourceArray.length(); - for (int i = 0; i < length; i++) { - fontFile = sourceArray.getString(i); - if (FileUtils.hasFontExtension(fontFile)) { - font = fontFile; - break; - } - } - } else { - String fontFile = fontObj.optString(KMManager.KMKey_FontFiles); - if (fontFile != null) { - if (FileUtils.hasFontExtension(fontFile)) { - font = fontFile; - } - } - } - } catch (JSONException e) { - KMLog.LogException(TAG, "", e); - font = ""; + + if (!FileUtils.hasFontExtension(fontFilename)) { + // QUESTION: do we log this? + return ""; } - return font; + String fontRoot = KMManager.isDefaultFont(fontFilename) ? getDataRootPath() : getPackageRootPath(packageID); + return fontRoot + fontFilename; } @SuppressLint("InflateParams") @@ -1042,13 +1042,28 @@ public void onDismiss() { } /** - * Take a font JSON object and adjust to pass to JS - * 1. Replace "source" keys for "files" keys - * 2. Create full font paths for .ttf or .svg - * @param font String font JSON object as a string - * @return JSONObject of modified font information with full paths. If font is invalid, return `null` + * Create a JSON object consisting of the font family and the URLs of the + * font files on the local device. + * + * The `font` parameter can either be the filename of the font (with an + * extension recognized as font), or a Font object or JSON string. + * In the former case a new JSON object is created with the font family + * derived from the filename, and the font filename prefixed with path + * to the fonts. + * In the latter case the legacy `sources` key is renamed to `files`. + * If `files` is a single string it will be prefixed with the path to the + * fonts. If `files` is an array, the array is iterated until finding + * the first file with a font extension which is then prefixed with the + * path to the fonts. + * + * @param font A string containing either the font filename or a font + * JSON object as a string + * @param packageID The package ID of the keyboard + * + * @return JSONObject of modified font information with full paths. If font + * is invalid, return `null`. */ - private JSONObject makeFontPaths(String font) { + private JSONObject makeFontObject(String font, String packageID) { if(font == null || font.equals("")) { return null; @@ -1059,14 +1074,17 @@ private JSONObject makeFontPaths(String font) { JSONObject jfont = new JSONObject(); jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length()-4)); JSONArray jfiles = new JSONArray(); - jfiles.put(keyboardRoot + font); + String fontRoot = KMManager.isDefaultFont(font) ? getDataRootUrl() : getPackageRootUrl(packageID); + jfiles.put(fontRoot + font); jfont.put(KMManager.KMKey_FontFiles, jfiles); return jfont; } + // REVIEW: Why do we need the complicated code below? Can this still + // happen, or can we remove it? (see also getFontFilename) + KMLog.LogInfo(TAG, "Got font without font extension: " + font); + JSONObject fontObj = new JSONObject(font); - JSONArray sourceArray; - String fontFile; // Replace "sources" key with "files" if (fontObj.has(KMManager.KMKey_FontSource)) { @@ -1076,16 +1094,18 @@ private JSONObject makeFontPaths(String font) { Object obj = fontObj.get(KMManager.KMKey_FontFiles); if (obj instanceof String) { - fontFile = fontObj.getString(KMManager.KMKey_FontFiles); - fontObj.put(KMManager.KMKey_FontFiles, keyboardRoot + obj); + String fontFile = fontObj.getString(KMManager.KMKey_FontFiles); + String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRootUrl() : getPackageRootUrl(packageID); + fontObj.put(KMManager.KMKey_FontFiles, fontRoot + obj); return fontObj; } else if (obj instanceof JSONArray) { - sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles); + JSONArray sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles); if (sourceArray != null) { for (int i = 0; i < sourceArray.length(); i++) { - fontFile = sourceArray.getString(i); + String fontFile = sourceArray.getString(i); if (FileUtils.hasFontExtension(fontFile)) { - fontObj.put(KMManager.KMKey_FontFiles, keyboardRoot + fontFile); + String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRootUrl() : getPackageRootUrl(packageID); + fontObj.put(KMManager.KMKey_FontFiles, fontRoot + fontFile); fontObj.remove(KMManager.KMKey_FontSource); return fontObj; } @@ -1094,7 +1114,6 @@ private JSONObject makeFontPaths(String font) { } } catch (JSONException e) { KMLog.LogException(TAG, "Failed to make font for '"+font+"'", e); - return null; } return null; 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 853d997d7e2..f043a56681e 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 @@ -1,5 +1,5 @@ -/** - * Copyright (C) 2023 SIL International. All rights reserved. +/* + * Keyman is copyright (C) SIL Global. MIT License. */ package com.keyman.engine; @@ -9,9 +9,13 @@ import android.graphics.RectF; import android.net.Uri; import android.util.Log; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.RelativeLayout; +import androidx.webkit.WebViewAssetLoader; +import androidx.webkit.WebViewAssetLoader.InternalStoragePathHandler; import com.keyman.engine.KeyboardEventHandler.EventType; import com.keyman.engine.KMManager; @@ -19,6 +23,7 @@ import com.keyman.engine.KMManager.SuggestionType; import com.keyman.engine.util.KMLog; import com.keyman.engine.data.Keyboard; +import com.keyman.engine.util.WebViewUtils; import org.json.JSONObject; @@ -31,11 +36,16 @@ public final class KMKeyboardWebViewClient extends WebViewClient { public Context context; private KeyboardType keyboardType; private boolean keyboardLoaded; + private WebViewAssetLoader assetLoader; KMKeyboardWebViewClient(Context context, KeyboardType keyboardType) { this.context = context; this.keyboardType = keyboardType; this.keyboardLoaded = false; + this.assetLoader = new WebViewAssetLoader.Builder() + .addPathHandler(WebViewUtils.ASSET_DATA_PATH, + new InternalStoragePathHandler(context, context.getDir("data", Context.MODE_PRIVATE))) + .build(); if (keyboardType != KeyboardType.KEYBOARD_TYPE_INAPP && keyboardType != KeyboardType.KEYBOARD_TYPE_SYSTEM) { KMLog.LogError(TAG, String.format("Cannot initialize: Invalid keyboard type: %s", keyboardType.toString())); @@ -58,6 +68,11 @@ public void setKeyboardLoaded(boolean keyboardLoaded) { public void onPageStarted(WebView view, String url, Bitmap favicon) { } + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + return this.assetLoader.shouldInterceptRequest(request.getUrl()); + } + @Override public void onPageFinished(WebView view, String url) { Log.d("KMEA", String.format("onPageFinished: [%s] %s", keyboardType.toString(), url)); @@ -74,46 +89,44 @@ private void pageLoaded(WebView view, String url) { kmKeyboard.keyboardSet = false; KMManager.currentLexicalModel = null; - if (url.startsWith("file")) { // TODO: is this test necessary? - this.keyboardLoaded = true; + this.keyboardLoaded = true; - SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0); - if (index < 0) { - index = 0; + SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0); + Keyboard keyboardInfo = null; + if (index >= 0) { + keyboardInfo = KMManager.getKeyboardInfo(context, index); + } + String langId = null; + if (keyboardInfo != null) { + langId = keyboardInfo.getLanguageID(); + kmKeyboard.setKeyboard(keyboardInfo); + } else { + // Revert to default (index 0) or fallback keyboard + keyboardInfo = KMManager.getKeyboardInfo(context, 0); + if (keyboardInfo == null) { + // Don't log to Sentry because some keyboard apps like FV don't install keyboards until the user chooses + keyboardInfo = KMManager.getDefaultKeyboard(context); } - Keyboard keyboardInfo = KMManager.getKeyboardInfo(context, index); - String langId = null; if (keyboardInfo != null) { langId = keyboardInfo.getLanguageID(); kmKeyboard.setKeyboard(keyboardInfo); - } else { - // Revert to default (index 0) or fallback keyboard - keyboardInfo = KMManager.getKeyboardInfo(context, 0); - if (keyboardInfo == null) { - // Don't log to Sentry because some keyboard apps like FV don't install keyboards until the user chooses - keyboardInfo = KMManager.getDefaultKeyboard(context); - } - if (keyboardInfo != null) { - langId = keyboardInfo.getLanguageID(); - kmKeyboard.setKeyboard(keyboardInfo); - } } + } - KMManager.registerAssociatedLexicalModel(langId); + KMManager.registerAssociatedLexicalModel(langId); - kmKeyboard.showHelpBubbleAfterDelay(2000, true); // check if it should be shown at that time! + kmKeyboard.showHelpBubbleAfterDelay(2000, true); // check if it should be shown at that time! - kmKeyboard.callJavascriptAfterLoad(); - kmKeyboard.setSpacebarText(KMManager.getSpacebarText()); + kmKeyboard.callJavascriptAfterLoad(); + kmKeyboard.setSpacebarText(KMManager.getSpacebarText()); - KeyboardEventHandler.notifyListeners(KMTextView.kbEventListeners, keyboardType, EventType.KEYBOARD_LOADED, null); + KeyboardEventHandler.notifyListeners(KMTextView.kbEventListeners, keyboardType, EventType.KEYBOARD_LOADED, null); - // Special handling for in-app TextView context keymanapp/keyman#3809 - if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP && - KMTextView.activeView != null && KMTextView.activeView.getClass() == KMTextView.class) { - KMTextView.updateTextContext(); - } + // Special handling for in-app TextView context keymanapp/keyman#3809 + if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP && + KMTextView.activeView != null && KMTextView.activeView.getClass() == KMTextView.class) { + KMTextView.updateTextContext(); } } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index f87219f5128..e46041fddb1 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -1,5 +1,5 @@ -/** - * Copyright (C) 2017 SIL International. All rights reserved. +/* + * Keyman is copyright (C) SIL Global. MIT License. */ package com.keyman.engine; @@ -406,6 +406,10 @@ public static String getLexicalModelsDir() { return getResourceRoot() + KMDefault_LexicalModelPackages + File.separator; } + public static String getLexicalModelsUrl() { + return WebViewUtils.buildAssetUrl(KMDefault_LexicalModelPackages + "/"); + } + public static String getCloudDir() { return getResourceRoot() + KMDefault_UndefinedPackageID + File.separator; } @@ -496,6 +500,19 @@ public static boolean isReservedNamespace(String packageID) { return false; } + /** + * Check if the given font is the default font or the default OSK font. + * @param fontName String - The font name and extension + * @return boolean true if `fontName` is DejaVuSans.ttf or keymanweb-osk.ttf + * or null or empty string, false otherwise + */ + public static boolean isDefaultFont(String fontName) { + if (fontName == null || fontName.isEmpty()) { + return true; + } + return fontName.equals(KMDefault_KeyboardFont) || fontName.equals(KMFilename_Osk_Ttf_Font); + } + public static void initialize(final Context context, KeyboardType keyboardType) { appContext = context.getApplicationContext(); @@ -1612,6 +1629,16 @@ public static void setMaySendCrashReport(boolean override) { */ public static Typeface getFontTypeface(Context context, String fontFilename) { try { + if (fontFilename == null || fontFilename.isEmpty()) { + return null; + } + if (fontFilename.startsWith("http://") || fontFilename.startsWith("https://") + || fontFilename.startsWith("file://")) { + // Font file is not local, so cannot load Typeface + KMLog.LogError(TAG, "Font file is not local: " + fontFilename); + return null; + } + if ((fontFilename != null) && FileUtils.hasFontExtension(fontFilename)) { // Ignore .woff files if Android 7.0 / 7.1 (Issue #4896) if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) && @@ -1650,8 +1677,9 @@ public static boolean registerLexicalModel(HashMap lexicalModelI String modelID = lexicalModelInfo.get(KMKey_LexicalModelID); String languageID = lexicalModelInfo.get(KMKey_LanguageID); boolean modelFileExists = true; - File modelFile = new File(getLexicalModelsDir(), pkgID + File.separator + modelID + ".model.js"); - String path = "file://" + modelFile.getAbsolutePath(); + String modelFilename = pkgID + File.separator + modelID + ".model.js"; + File modelFile = new File(getLexicalModelsDir(), modelFilename); + String url = getLexicalModelsUrl() + modelFilename; // Disable sugestions if lexical-model file doesn't exist if (!modelFile.exists()) { @@ -1666,7 +1694,7 @@ public static boolean registerLexicalModel(HashMap lexicalModelI modelObj.put("id", modelID); languageJSONArray.put(languageID); modelObj.put("languages", languageJSONArray); - modelObj.put("path", path); + modelObj.put("path", url); modelObj.put("CustomHelpLink", lexicalModelInfo.get(KMKey_CustomHelpLink)); } catch (JSONException e) { KMLog.LogException(TAG, "Invalid lexical model to register", e); @@ -2225,7 +2253,12 @@ public boolean accept(File pathname) { File kmpJSONFile = new File(path); if (!kmpJSONFile.exists()) { if (!KMManager.isTestMode()) { - KMLog.LogError(TAG, path + " not found. Returning version 1.0"); + if (packageID.equals(KMDefault_PackageID)) { + // Suppress error log for the default fallback keyboard if it's not installed + Log.d(TAG, path + " not found. Returning version 1.0"); + } else { + KMLog.LogError(TAG, path + " not found. Returning version 1.0"); + } } return "1.0"; } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java b/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java index 4676d59c775..5bd4d8d6849 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java @@ -45,7 +45,7 @@ public class CloudRepository { public static final String API_STAGING_HOST = "api.keyman.com"; // #7227 disabling: "api.keyman-staging.com"; public static final String API_MODEL_LANGUAGE_FORMATSTR = "https://%s/model?q=bcp47:%s"; - public static final String API_PACKAGE_VERSION_FORMATSTR = "https://%s/package-version?platform=android%s%s"; + public static final String API_PACKAGE_VERSION_FORMATSTR = "https://%s/package-version?platform=android&keyman-version=%s%s%s"; private Dataset memCachedDataset; private Calendar lastLoad; // To be used for Dataset caching. @@ -205,7 +205,7 @@ private CloudApiTypes.CloudApiParam prepareResourcesUpdateQuery(Context aContext } } - String queryURL = String.format(API_PACKAGE_VERSION_FORMATSTR, getHost(), keyboardQuery, lexicalModelQuery); + String queryURL = String.format(API_PACKAGE_VERSION_FORMATSTR, getHost(), KMManager.getVersion(), keyboardQuery, lexicalModelQuery); return new CloudApiTypes.CloudApiParam( CloudApiTypes.ApiTarget.PackageVersion, queryURL).setType(CloudApiTypes.JSONType.Object); } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java index 0dbfe5d0239..896fb23e965 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java @@ -1,5 +1,5 @@ -/** - * Copyright (C) 2020 SIL International. All rights reserved. +/* + * Keyman is copyright (C) SIL Global. MIT License. */ package com.keyman.engine.data; @@ -15,6 +15,7 @@ import com.keyman.engine.util.FileUtils; import com.keyman.engine.util.KMLog; import com.keyman.engine.util.KMString; +import com.keyman.engine.util.WebViewUtils; import org.json.JSONArray; import org.json.JSONException; @@ -48,6 +49,7 @@ public class Keyboard extends LanguageResource implements Serializable { */ public Keyboard(JSONObject installedObj) { this.fromJSON(installedObj); + logIfLegacyKeyboard(); } /** @@ -78,6 +80,8 @@ public Keyboard(JSONObject languageJSON, JSONObject keyboardJSON) { this.helpLink = keyboardJSON.optString(KMManager.KMKey_CustomHelpLink, KMString.format(HELP_URL_FORMATSTR, HELP_URL_HOST, this.resourceID, this.version)); + + logIfLegacyKeyboard(); } catch (JSONException e) { KMLog.LogException(TAG, "Keyboard exception parsing JSON: ", e); } @@ -95,6 +99,8 @@ public Keyboard(String packageID, String keyboardID, String keyboardName, this.isNewKeyboard = isNewKeyboard; this.font = (font != null) ? font : ""; this.oskFont = (oskFont != null) ? oskFont : ""; + + logIfLegacyKeyboard(); } public Keyboard(Keyboard k) { @@ -107,6 +113,12 @@ public Keyboard(Keyboard k) { this.displayName = k.getDisplayName(); } + private void logIfLegacyKeyboard() { + if (this.packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { + KMLog.LogInfo(TAG, "Constructing legacy keyboard: " + this.resourceID); + } + } + public String getKeyboardID() { return getResourceID(); } public String getKeyboardName() { return getResourceName(); } @@ -173,24 +185,24 @@ public JSONObject toJSON() { return o; } - private String getKeyboardRoot(Context context) { - String keyboardRoot = context.getDir("data", Context.MODE_PRIVATE).toString() + - File.separator; + private String getDataRoot() { + return WebViewUtils.buildAssetUrl(""); + } + private String getPackageRoot() { if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - return keyboardRoot + KMManager.KMDefault_UndefinedPackageID + File.separator; - } else { - return keyboardRoot + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator; + return getDataRoot() + KMManager.KMDefault_UndefinedPackageID + "/"; } + return getDataRoot() + KMManager.KMDefault_AssetPackages + "/" + packageID + "/"; } - public String getKeyboardPath(Context context) { + private String getKeyboardUrl() { String keyboardID = this.getKeyboardID(); String keyboardVersion = this.getVersion(); if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { - return getKeyboardRoot(context) + keyboardID + "-" + keyboardVersion + ".js"; + return getPackageRoot() + keyboardID + "-" + keyboardVersion + ".js"; } else { - return getKeyboardRoot(context) + keyboardID + ".js"; + return getPackageRoot() + keyboardID + ".js"; } } @@ -202,17 +214,17 @@ public String toStub(Context context) { stubObj.put("KI", "Keyboard_" + this.getKeyboardID()); stubObj.put("KLC", this.getLanguageID()); stubObj.put("KL", this.getLanguageName()); - stubObj.put("KF", this.getKeyboardPath(context)); + stubObj.put("KF", this.getKeyboardUrl()); stubObj.put("KP", this.getPackageID()); String displayFont = this.getFont(); if(displayFont != null) { - stubObj.put("KFont", this.buildDisplayFontObject(displayFont, context)); + stubObj.put("KFont", this.buildDisplayFontObject(displayFont)); } String oskFont = this.getOSKFont(); if(oskFont != null) { - stubObj.put("KOskFont", this.buildDisplayFontObject(oskFont, context)); + stubObj.put("KOskFont", this.buildDisplayFontObject(oskFont)); } String displayName = this.getDisplayName(); @@ -228,34 +240,33 @@ public String toStub(Context context) { } /** - * Take a font JSON object and adjust to pass to JS - * 1. Replace "source" keys for "files" keys - * 2. Create full font paths for .ttf or .svg - * @param font String font JSON object as a string - * @return JSONObject of modified font information with full paths. If font is invalid, return `null` + * Create a JSON object consisting of the font family and the font + * file URL. + * + * @param font A string containing the font filename + * @return JSONObject of modified font information with full URL. If font + * is invalid, return `null`. */ - private JSONObject buildDisplayFontObject(String font, Context context) { + private JSONObject buildDisplayFontObject(String font) { if(font == null || font.equals("")) { return null; } - String keyboardRoot = this.getKeyboardRoot(context); + String fontRoot = KMManager.isDefaultFont(font) ? this.getDataRoot() : this.getPackageRoot(); try { if (FileUtils.hasFontExtension(font)) { JSONObject jfont = new JSONObject(); jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length() - 4)); JSONArray jfiles = new JSONArray(); - jfiles.put(keyboardRoot + font); + jfiles.put(fontRoot + font); jfont.put(KMManager.KMKey_FontFiles, jfiles); return jfont; - } else { - return null; } } catch (JSONException e) { KMLog.LogException(TAG, "Failed to make font for '"+font+"'", e); - return null; } + return null; } /** diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java index 1ce55bf0ce4..633f5c853b8 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java @@ -24,16 +24,11 @@ public final class KMLog { private static final String TAG = "KMLog"; - private static final String KEYBOARD_TAG = "keyboardId"; - private static final String KEYBOARD_COUNT_TAG = "installedKeyboardCount"; - private static final String MODEL_TAG = "modelId"; - private static final String LANGCODE_TAG = "languageCode"; - - // Some of the methods used to generate debug logging information can, themselves, - // trigger errors that can also trigger the same logging. We must not get - // caught in an infinite loop / stack-overflow; this field helps us avoid states - // that would otherwise cause error-looping, etc. - private static boolean isLogging = false; + private static final String KEYBOARD_TAG = "keyman.keyboardId"; + private static final String KEYBOARD_COUNT_TAG = "keyman.installedKeyboardCount"; + private static final String MODEL_TAG = "keyman.modelId"; + private static final String LANGCODE_TAG = "keyman.languageCode"; + private static final String DEBUG_LOGGING_ERROR_TAG = "keyman.debugLoggingError"; private static void tagDebugInfo() { String kbdId = ""; @@ -57,13 +52,16 @@ private static void tagDebugInfo() { } } } catch (Exception ex) { - String msg = ex.getMessage() == null ? "" : ex.getMessage(); - Sentry.setExtra("debugLoggingError", msg); + Sentry.setTag(DEBUG_LOGGING_ERROR_TAG, ex.getMessage() == null ? "" : ex.getMessage()); } - Sentry.setExtra(KEYBOARD_TAG, kbdId); - Sentry.setExtra(KEYBOARD_COUNT_TAG, "" + kbdCount); - Sentry.setExtra(LANGCODE_TAG, lngCode); - Sentry.setExtra(MODEL_TAG, modelId); + Sentry.setTag(KEYBOARD_TAG, kbdId); + Sentry.setTag(KEYBOARD_COUNT_TAG, "" + kbdCount); + Sentry.setTag(LANGCODE_TAG, lngCode); + Sentry.setTag(MODEL_TAG, modelId); + } + + private static boolean canLogToSentry() { + return DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled(); } /** @@ -72,19 +70,20 @@ private static void tagDebugInfo() { * @param msg String of the info message */ public static void LogInfo(String tag, String msg) { - if(isLogging) { + if (msg == null || msg.isEmpty()) { return; } - isLogging = true; - if (msg != null && !msg.isEmpty()) { - Log.i(tag, msg); - if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { - tagDebugInfo(); - Sentry.captureMessage(msg, SentryLevel.INFO); - } + Log.i(tag, msg); + + if (!canLogToSentry()) { + return; } - isLogging = false; + + Sentry.withScope(scope -> { + tagDebugInfo(); + Sentry.captureMessage(msg, SentryLevel.INFO); + }); } /** @@ -98,15 +97,9 @@ public static void LogBreadcrumb(String tag, String msg, boolean addStackTrace) return; } - if(isLogging) { - return; - } - isLogging = true; - Log.i(tag, msg); - if (!DependencyUtil.libraryExists(LibraryType.SENTRY) || !Sentry.isEnabled()) { - isLogging = false; + if (!canLogToSentry()) { return; } @@ -114,27 +107,30 @@ public static void LogBreadcrumb(String tag, String msg, boolean addStackTrace) crumb.setMessage(msg); crumb.setLevel(SentryLevel.INFO); - if(addStackTrace) { - StackTraceElement[] rawTrace = Thread.currentThread().getStackTrace(); - - // The call that gets us the stack-trace above... shows up in the - // stack trace, so we'll skip the first few (redundant) entries. - int skipCount = 3; - - // Sentry does limit the size of messages... so let's just - // keep 10 entries and call it a day. - int limit = Math.min(rawTrace.length, 10 + skipCount); - if(rawTrace.length > skipCount) { - String[] trace = new String[limit - skipCount]; - for (int i = skipCount; i < limit; i++) { - trace[i-skipCount] = rawTrace[i].toString(); + try { + if(addStackTrace) { + StackTraceElement[] rawTrace = Thread.currentThread().getStackTrace(); + + // The call that gets us the stack-trace above... shows up in the + // stack trace, so we'll skip the first few (redundant) entries. + int skipCount = 3; + + // Sentry does limit the size of messages... so let's just + // keep 10 entries and call it a day. + if(rawTrace.length > skipCount) { + int limit = Math.min(rawTrace.length, 10 + skipCount); + String[] trace = new String[limit - skipCount]; + for (int i = skipCount; i < limit; i++) { + trace[i-skipCount] = rawTrace[i].toString(); + } + crumb.setData("stacktrace", trace); } - crumb.setData("stacktrace", trace); } + } catch (Exception e) { + Sentry.captureException(e); } - tagDebugInfo(); + Sentry.addBreadcrumb(crumb); - isLogging = false; } /** @@ -143,23 +139,32 @@ public static void LogBreadcrumb(String tag, String msg, boolean addStackTrace) * @param msg String of the error message */ public static void LogError(String tag, String msg) { - if(isLogging) { + if (msg == null || msg.isEmpty()) { return; } - isLogging = true; - if (msg != null && !msg.isEmpty()) { - Log.e(tag, msg); + Log.e(tag, msg); + + try { + // On alpha and beta tiers, we pop an error toast so testers can be aware + // of the error if (KMManager.getTier(BuildConfig.KEYMAN_ENGINE_VERSION_NAME) != KMManager.Tier.STABLE) { BaseActivity.makeToast(null, msg, Toast.LENGTH_LONG); } - - if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { - tagDebugInfo(); - Sentry.captureMessage(msg, SentryLevel.ERROR); + } catch(Exception e) { + if(canLogToSentry()) { + Sentry.captureException(e); } } - isLogging = false; + + if (!canLogToSentry()) { + return; + } + + Sentry.withScope(scope -> { + tagDebugInfo(); + Sentry.captureMessage(msg, SentryLevel.ERROR); + }); } /** @@ -169,28 +174,7 @@ public static void LogError(String tag, String msg) { * @param e Throwable exception */ public static void LogException(String tag, String msg, Throwable e) { - if(isLogging) { - return; - } - isLogging = true; - String errorMsg = ""; - if (msg != null && !msg.isEmpty()) { - errorMsg = msg + "\n" + e; - } else if (e != null) { - errorMsg = e.getMessage(); - } - Log.e(tag, errorMsg, e); - - if (KMManager.getTier(BuildConfig.KEYMAN_ENGINE_VERSION_NAME) != KMManager.Tier.STABLE) { - BaseActivity.makeToast(null, errorMsg, Toast.LENGTH_LONG); - } - - if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { - tagDebugInfo(); - Sentry.addBreadcrumb(errorMsg); - Sentry.captureException(e); - } - isLogging = false; + KMLog.LogExceptionWithData(tag, msg, null, null, e); } /** @@ -203,25 +187,48 @@ public static void LogException(String tag, String msg, Throwable e) { */ public static void LogExceptionWithData(String tag, String msg, String objName, Object obj, Throwable e) { - if(isLogging) { + String errorMsg = ""; + try { + if (msg != null && !msg.isEmpty()) { + errorMsg = msg + "\n" + e; + } else if (e != null) { + errorMsg = e.toString(); + } + + // On alpha and beta tiers, we pop an error toast so testers can be aware + // of the exception + if (KMManager.getTier(BuildConfig.KEYMAN_ENGINE_VERSION_NAME) != KMManager.Tier.STABLE) { + BaseActivity.makeToast(null, errorMsg, Toast.LENGTH_LONG); + } + } catch (Exception innerE) { + if (canLogToSentry()) { + Sentry.captureException(innerE); + } + } + + Log.e(tag, errorMsg); + + if (!canLogToSentry()) { return; } - isLogging = true; - if (obj != null && DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { + + Sentry.addBreadcrumb(errorMsg); + Sentry.withScope(scope -> { tagDebugInfo(); - String objStr = null; try { - objStr = obj.toString(); - Sentry.setExtra(objName, objStr); - } catch (Exception innerE) { - LogException(TAG, "Sentry.setExtra failed for " + objName, innerE); + if(obj != null && objName != null) { + Sentry.setExtra(objName, obj.toString()); + } + } catch(Exception innerE) { + Sentry.captureException(innerE); } - // Report the original exception - LogException(tag, msg, e); - // And remove the exception-specific tagged data, lest it also be - // tracked on subsequent errors not associated with the current call. - Sentry.removeExtra(objName); - } - isLogging = false; + Sentry.captureException(e); + + if(obj != null && objName != null) { + // And remove the exception-specific tagged data, lest it also be + // tracked on subsequent errors not associated with the current call. + Sentry.removeExtra(objName); + } + }); } } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java index 0586ecbcc8c..11c64434da3 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/WebViewUtils.java @@ -42,6 +42,28 @@ public enum SystemWebViewStatus { private static final String CHROME_INSTALL_PATTERN_FORMATSTR = "^.*Chrome/([\\d.]+).*$"; private static final Pattern installPattern = Pattern.compile(CHROME_INSTALL_PATTERN_FORMATSTR); + /** + * Reserved magic domain for loading files from the local device. At runtime + * the WebViewAssetLoader will replace the protocol and domain with the + * internal storage path. + * See https://developer.android.com/reference/androidx/webkit/WebViewAssetLoader + */ + private static final String MAGIC_DEFAULT_DOMAIN = "https://appassets.androidplatform.net"; + + /** + * Path under the asset domain where all assets live + */ + public static final String ASSET_DATA_PATH = "/data/"; + + /** + * Build a full URL to the provided asset + */ + public static String buildAssetUrl(String assetPath) { + String appendAsset = assetPath == null ? "" : + (assetPath.startsWith("/") ? assetPath.substring(1) : assetPath); + return WebViewUtils.MAGIC_DEFAULT_DOMAIN + WebViewUtils.ASSET_DATA_PATH + appendAsset; + } + /** * Get the Keyman Engine mode based on the Chrome version. * @param context - The context diff --git a/android/KMEA/app/src/main/res/values-am-rET/strings.xml b/android/KMEA/app/src/main/res/values-am-rET/strings.xml index 1bb95d51576..0cf43b8b9d2 100644 --- a/android/KMEA/app/src/main/res/values-am-rET/strings.xml +++ b/android/KMEA/app/src/main/res/values-am-rET/strings.xml @@ -80,8 +80,6 @@ የእገዛ ፋይልን ለማየት ፋይል ፕሮቫይደሩ ቤተ መጽሐፍት ያስፈልገዋል %1$s የገበታ ሰሌዳ ፋታል ችግር ዝርዝር %1$s:%2$s ለ %3$s ቋንቋ፤ መደበኛ ቋንቋ ጭኗል። - - የፊደል ገበታ ሰሌዳው ችግር %1$s:%2$s ለ %3$s ቋንቋ። ተያያዥነት ያለውን መዝገበ ቃላት ለመጫን እየፈተሸ ነው። የሚወርደውን መዝገበ ቃላት ከኪይማን ሰርቨር ጋር መገናኘት አልተቻለም diff --git a/android/KMEA/app/src/main/res/values-ar-rSA/strings.xml b/android/KMEA/app/src/main/res/values-ar-rSA/strings.xml index cb2e9f0d512..3e1b901f86d 100644 --- a/android/KMEA/app/src/main/res/values-ar-rSA/strings.xml +++ b/android/KMEA/app/src/main/res/values-ar-rSA/strings.xml @@ -88,8 +88,6 @@ مكتبة FileProvider مطلوبة لعرض ملفات المساعدة: %1$s خطأ كبير في لوحة مفاتيح مع %1$s:%2$s لأجل لغة %3$s. سيتم تحميل لوحة المفاتيح الأساسية. - - خطأ في لوحة مفاتيح %1$s:%2$s لأجل لغة %3$s. يجري التحقق من قاموس ذي صلة للتحميل لا يمكن الاتصال بخادم Keyman للتحقق من تنزيل القاموس المرتبط diff --git a/android/KMEA/app/src/main/res/values-az-rAZ/strings.xml b/android/KMEA/app/src/main/res/values-az-rAZ/strings.xml index 9b3d86e8d08..06c4b6b92ee 100644 --- a/android/KMEA/app/src/main/res/values-az-rAZ/strings.xml +++ b/android/KMEA/app/src/main/res/values-az-rAZ/strings.xml @@ -80,8 +80,6 @@ FileProvider (Fayl Təminatçısı) kitabxanasına kömək sənədinə baxmaq lazımdır:%1$s %3$s dili üçün %1$s ilə ölümcül klaviatura xətası :%3$s dili üçün %2$s. Varsayılan klaviatura yüklənir. - - %3$s dil üçün %1$s:%2$s klaviaturasında xəta. Yükləmək üçün əlaqəli lüğət yoxlanılır Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-b+el/strings.xml b/android/KMEA/app/src/main/res/values-b+el/strings.xml index 967386eb36a..268b3091ce5 100644 --- a/android/KMEA/app/src/main/res/values-b+el/strings.xml +++ b/android/KMEA/app/src/main/res/values-b+el/strings.xml @@ -80,8 +80,6 @@ Ἀπαιτεῖται βιβλιοθήκη FileProvider γιὰ νὰ δεῖτε ἀρχεῖο βοηθείας: %1$s Μοιραῖο σφάλμα πληκτρολογίου στὸ %1$s:%2$s γιὰ τὴν %3$s γλῶσσα. Φορτώνεται προεπιλεγμένο πληκτρολόγιο. - - Σφάλμα στὸ πληκτρολόγιο %1$s:%2$s γιὰ τὴν γλῶσσα \"%3$s\". Ἀναζήτηση σχετικοῦ λεξικοῦ πρὸς μεταφόρτωσιν Ἀδυναμία συνδέσεως μὲ τὸν διακομιστὴ Keyman γιὰ τὴν εὕρεση σχετικοῦ λεξικοῦ πρὸς μεταφόρτωσιν diff --git a/android/KMEA/app/src/main/res/values-b+es+419/strings.xml b/android/KMEA/app/src/main/res/values-b+es+419/strings.xml index f1ec1d2d973..06413a82e22 100644 --- a/android/KMEA/app/src/main/res/values-b+es+419/strings.xml +++ b/android/KMEA/app/src/main/res/values-b+es+419/strings.xml @@ -80,8 +80,6 @@ Se necesita la librería FileProvider para ver el archivo de ayuda: %1$s Error fatal del teclado con %1$s:%2$s para el idioma %3$s. Cargando teclado predeterminado. - - Error en el teclado %1$s:%2$s para el idioma %3$s. Keyman requiere que WebView esté instalado.\" diff --git a/android/KMEA/app/src/main/res/values-b+shu+latn/strings.xml b/android/KMEA/app/src/main/res/values-b+shu+latn/strings.xml index 58be85253b4..5c86164db9f 100644 --- a/android/KMEA/app/src/main/res/values-b+shu+latn/strings.xml +++ b/android/KMEA/app/src/main/res/values-b+shu+latn/strings.xml @@ -84,8 +84,6 @@ FilProvider library bindawar ley fakkakanal shu\'ul da: %1$s Mushkila kabirey bey %1$s:%2$s layl %3$s lugga. Dassasan shabal keyboard hanal gabul. - - Mushkila fyl keyboard %1$s:%2$s layl %3$s lugga. Shafiyan kakkad abu kalimat shaba da ley dassasana Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-bwr-rNG/strings.xml b/android/KMEA/app/src/main/res/values-bwr-rNG/strings.xml index b2082bf2a1c..abd597c8d2c 100644 --- a/android/KMEA/app/src/main/res/values-bwr-rNG/strings.xml +++ b/android/KMEA/app/src/main/res/values-bwr-rNG/strings.xml @@ -80,8 +80,6 @@ Mda akwa bara library na ar FileProvider kamnyar ka mdǝ wutǝ fayil ar dlara: %1$s Dametǝdzi na ar Fatal akwa keyboard ka %1$s:%2$s ala %3$s hulfur mnya. Akwa hara lodi ar default keyboard. - - Dametǝdzi akwa keyboard %1$s:%2$s na ar %3$s hulfur mnya. Akwa pakǝ dictionary na kǝɓa kari kamnyar katsa hara dawunlodi ni Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-ckl-rNG/strings.xml b/android/KMEA/app/src/main/res/values-ckl-rNG/strings.xml index 61768013b74..aca85a16398 100644 --- a/android/KMEA/app/src/main/res/values-ckl-rNG/strings.xml +++ b/android/KMEA/app/src/main/res/values-ckl-rNG/strings.xml @@ -80,8 +80,6 @@ Gada bara library nǝr FileProvider akuyaka gǝ wul su akwa fail nǝr dlarɛ: %1$s Fatal keyboard error with %1$s:%2$s for %3$s language. Loading default keyboard. - - Su kura kal kalakwa kibod %1$s:%2$s nǝr %3$s nya. Ata kǝra chakǝnta akuyaka ɓui suyere tara tsukwar bwar dictionary kadǝbar ka ndǝ download ko sǝgǝl akǝr Gadiyata mǝzi bui sava nǝr Keyman wɛ akuya ka gǝ bui ka sǝgǝl aka jiliyir dikshinari namtǝ gada bara. diff --git a/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml b/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml index 1270bc7f21d..7fbf846156d 100644 --- a/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml +++ b/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml @@ -84,8 +84,6 @@ Knihovna FileProvider potřebná k zobrazení nápovědy: %1$s Fatální chyba klávesnice s %1$s:%2$s pro jazyk %3$s . Načítání výchozí klávesnice. - - Chyba v klávesnici %1$s:%2$s pro jazyk %3$s. Keyman vyžaduje, aby byl nainstalován WebView.“ diff --git a/android/KMEA/app/src/main/res/values-de-rDE/strings.xml b/android/KMEA/app/src/main/res/values-de-rDE/strings.xml index 6c79a08291d..fc36e8d3b1e 100644 --- a/android/KMEA/app/src/main/res/values-de-rDE/strings.xml +++ b/android/KMEA/app/src/main/res/values-de-rDE/strings.xml @@ -80,8 +80,6 @@ FileProvider-Bibliothek zum Anzeigen der Hilfe-Datei: %1$s Fataler Tastaturfehler mit %1$s:%2$s für %3$s Sprache. Standard-Tastatur wird geladen. - - Fehler in Tastatur %1$s:%2$s für %3$s Sprache. Keyman benötigt die Installation von WebView.\" diff --git a/android/KMEA/app/src/main/res/values-es-rES/strings.xml b/android/KMEA/app/src/main/res/values-es-rES/strings.xml index 4846da0dac1..8bb803c5459 100644 --- a/android/KMEA/app/src/main/res/values-es-rES/strings.xml +++ b/android/KMEA/app/src/main/res/values-es-rES/strings.xml @@ -80,8 +80,6 @@ Biblioteca de FileProvider necesaria para ver el archivo de ayuda: %1$s Error de teclado fatal con %1$s:%2$s para %3$s idioma. Cargando teclado predeterminado. - - Error en el teclado %1$s:%2$s para %3$s idioma. Keyman requiere que WebView esté instalado.\" diff --git a/android/KMEA/app/src/main/res/values-ff-rNG/strings.xml b/android/KMEA/app/src/main/res/values-ff-rNG/strings.xml index 2ada2b94418..873583bfd1d 100644 --- a/android/KMEA/app/src/main/res/values-ff-rNG/strings.xml +++ b/android/KMEA/app/src/main/res/values-ff-rNG/strings.xml @@ -80,8 +80,6 @@ Babal fayel ɗo mari haaje laarugo fayel mballa: %1$s Kibod man wari bee masala bee %1$s:%2$s jee %3$s ɗemngal. Ɗo nastina kibod ko naftirta. - - Masala nder kibod %1$s:%2$s jee %3$s ɗemngal. Ɗo tefa deftere ma`ana kalimaji ngam jippina Waawaa seŋaade e sarworde Keyman ngam ƴeewtaade so saggitorde yahdoonde heen ena woodi diff --git a/android/KMEA/app/src/main/res/values-ff-rZA/strings.xml b/android/KMEA/app/src/main/res/values-ff-rZA/strings.xml index 7d52d61f6bf..17d5c1d50ed 100644 --- a/android/KMEA/app/src/main/res/values-ff-rZA/strings.xml +++ b/android/KMEA/app/src/main/res/values-ff-rZA/strings.xml @@ -80,8 +80,6 @@ Deftordu FileProvider ena soklaa ngam yiyde fiilde ballal: %1$s Juumre tappirde halkoore e %1$s: %2$s mo ɗemngal %3$s. Nana loowa tappirde woowaande. - - Juumre e tappirde %1$s:%2$s ɗemngal %3$s language. Nana ƴeewtoo saggitorde yahdiinde ngam aawtaade Waawaa seŋaade e sarworde Keyman ngam ƴeewtaade so saggitorde aawtoonde ena woodi diff --git a/android/KMEA/app/src/main/res/values-fr-rFR/strings.xml b/android/KMEA/app/src/main/res/values-fr-rFR/strings.xml index fc23855ed19..9e5fdd27deb 100644 --- a/android/KMEA/app/src/main/res/values-fr-rFR/strings.xml +++ b/android/KMEA/app/src/main/res/values-fr-rFR/strings.xml @@ -80,8 +80,6 @@ Librairie FileProvider nécessaire pour afficher le fichier d\'aide : %1$s Erreur fatale de clavier avec %1$s:%2$s pour %3$s langue. Chargement du clavier par défaut. - - Erreur dans le clavier %1$s:%2$s pour la langue %3$s. Vérification du dictionnaire associé à télécharger Impossible de se connecter au serveur Keyman pour chercher un dictionnaire à télécharger diff --git a/android/KMEA/app/src/main/res/values-ha-rHG/strings.xml b/android/KMEA/app/src/main/res/values-ha-rHG/strings.xml index 78960c0f0cb..804afa5b494 100644 --- a/android/KMEA/app/src/main/res/values-ha-rHG/strings.xml +++ b/android/KMEA/app/src/main/res/values-ha-rHG/strings.xml @@ -80,8 +80,6 @@ Ana buƙatar wajen karatu na FileProvider domin duba fayil ɗin taimako: %1$s Matsalar madanni da %1$s:%2$s domin %3$s yaruka. Saukar da madanni mai kyau. - - Kuskure a yaren %1$s:%2$s for %3$s madanni. Duba ƙamus mai alaƙa domin a sauke Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-hia-rNG/strings.xml b/android/KMEA/app/src/main/res/values-hia-rNG/strings.xml index 5af3bf14fae..7cce5b3c27e 100644 --- a/android/KMEA/app/src/main/res/values-hia-rNG/strings.xml +++ b/android/KMEA/app/src/main/res/values-hia-rNG/strings.xml @@ -82,8 +82,6 @@ FileProvider library needed to view help file: %1$s Fatal keyboard nda kuskure %1$s:%2$s anga %3$s gwaɗaha luwa. Kalt girgira keyboard. - - Kuskure an keyboard %1$s:%2$s for %3$s gwaɗa luwa. Naghata anga gwaft dictionary da sangata Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-in-rID/strings.xml b/android/KMEA/app/src/main/res/values-in-rID/strings.xml index 2bbdf848278..c3c4667bef3 100644 --- a/android/KMEA/app/src/main/res/values-in-rID/strings.xml +++ b/android/KMEA/app/src/main/res/values-in-rID/strings.xml @@ -78,8 +78,6 @@ Butuh FileProvider library untuk menampilkan berkas bantuan: %1$s Terjadi masalah dengan papan tombol %1$s:%2$s untuk bahasa %3$s. Papan tombol diubah ke setelah baku. - - Error in keyboard %1$s:%2$s for %3$s language. Mengecek kamus yang terkait untuk diunduh Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-it-rIT/strings.xml b/android/KMEA/app/src/main/res/values-it-rIT/strings.xml index fb8651a9158..4df84872032 100644 --- a/android/KMEA/app/src/main/res/values-it-rIT/strings.xml +++ b/android/KMEA/app/src/main/res/values-it-rIT/strings.xml @@ -80,8 +80,6 @@ Libreria FileProvider necessaria per visualizzare il file d\'aiuto: %1$s Errore fatale della tastiera con %1$s:%2$s per %3$s lingua. Caricamento tastiera predefinita. - - Errore nella tastiera %1$s:%2$s per %3$s lingua. Keyman richiede che WebView sia installato.\" diff --git a/android/KMEA/app/src/main/res/values-km-rKH/strings.xml b/android/KMEA/app/src/main/res/values-km-rKH/strings.xml index 55340f80cba..e53a560cee2 100644 --- a/android/KMEA/app/src/main/res/values-km-rKH/strings.xml +++ b/android/KMEA/app/src/main/res/values-km-rKH/strings.xml @@ -78,8 +78,6 @@ ត្រូវការបណ្ណាល័យ FileProvider ដើម្បី​មើលឯកសារជំនួយ៖ %1$s មាន​បញ្ហាធ្ងន់ធ្ងរចំពោះក្ដារចុច %1$s:%2$s សម្រាប់​ភាសា %3$s។ កំពុង​ផ្ទុក​ក្ដារចុច​លំនាំដើម។ - - បញ្ហាក្នុងក្ដារចុច %1$s:%2$s សម្រាប់ភាសា %3$s។ Keyman តម្រូវឱ្យដំឡើង WebView ។\" diff --git a/android/KMEA/app/src/main/res/values-kn-rIN/strings.xml b/android/KMEA/app/src/main/res/values-kn-rIN/strings.xml index e281e0bfe01..a24e4926e52 100644 --- a/android/KMEA/app/src/main/res/values-kn-rIN/strings.xml +++ b/android/KMEA/app/src/main/res/values-kn-rIN/strings.xml @@ -80,8 +80,6 @@ ಸಹಾಯ ಫೈಲ್ ವೀಕ್ಷಿಸಲು FileProvider ಲೈಬ್ರರಿ ಅಗತ್ಯವಿದೆ: %1$s %1$s:%2$s ಕೀಲಿಮಣೆಯಲ್ಲಿ %3$s ಭಾಷೆಯೊಂದಿಗೆ ದೋಷವಿದೆ. ಅದರ ಬದಲಿಗೆ ಪೂರ್ವನಿರ್ಧರಿತ ಕೀಲಿಮಣೆ ಲೋಡ್ ಆಗುತ್ತಿದೆ. - - %1$s:%2$s ಕೀಲಿಮಣೆಯಲ್ಲಿ %3$s ಭಾಷೆಯೊಂದಿಗೆ ದೋಷವಿದೆ. ಸಂಬಂಧಿಸಿದ ಶಬ್ದಕೋಶವನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ಪರಿಶೀಲಿಸಲಾಗುತ್ತಿದೆ ಸಂಬಂಧಿಸಿದ ಶಬ್ದಕೋಶವನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ಕೀಮ್ಯಾನ್ ಸರ್ವರ್ ಅನ್ನು ಸಂಪರ್ಕಿಸಲು ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲಾ diff --git a/android/KMEA/app/src/main/res/values-kr-rNG/strings.xml b/android/KMEA/app/src/main/res/values-kr-rNG/strings.xml index fba50a2eaf9..6f6ae410e23 100644 --- a/android/KMEA/app/src/main/res/values-kr-rNG/strings.xml +++ b/android/KMEA/app/src/main/res/values-kr-rNG/strings.xml @@ -80,8 +80,6 @@ FileProvider library needed to view help file: %1$s Fatal keyboard error with %1$s:%2$s for %3$s language. Loading default keyboard. - - Taltǝ keyboard dǝn mbeji %1$s:%2$s for %3$s tǝlama. Dictionary shiro kǝllata mane download nzǝ diye Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-mfi-rNG/strings.xml b/android/KMEA/app/src/main/res/values-mfi-rNG/strings.xml index b35d9417157..3c1e862c2be 100644 --- a/android/KMEA/app/src/main/res/values-mfi-rNG/strings.xml +++ b/android/KMEA/app/src/main/res/values-mfi-rNG/strings.xml @@ -80,8 +80,6 @@ FileProvider library a kátá zhara file mla: %1$s Kuskure Fatal keyboard antara%1$s:%2$s gé %3$s nare. femhe nde default keyboard. - - Kuskure am keyboard %1$s:%2$s for %3$s nare. Zharanahe dictionary harzhe gé tsukwanahe Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-mnw-rMM/strings.xml b/android/KMEA/app/src/main/res/values-mnw-rMM/strings.xml index 9cde6021807..980fe6ce45d 100644 --- a/android/KMEA/app/src/main/res/values-mnw-rMM/strings.xml +++ b/android/KMEA/app/src/main/res/values-mnw-rMM/strings.xml @@ -78,8 +78,6 @@ တၚ်နၚ်လိက် FileProvider မိက်ဂွံဗဵု ဝှါၚ်အရီုဗၚ်: %1$s ကဳဗုဒ်ယောၚ်ယာဇၞော်ဇၞော်နကဵု %1$s:%2$s သွက် ဘာသာ %3$s ။ ဂြဲပ္တိုန်မံၚ် ကဳဗုဒ်ပကတိ။ - - တၚ်ယောၚ်ယာ ပ္ဍဲကဳဗုဒ် %1$s:%2$s သွက် ဘာသာ %3$s သ္ဂောံဂြဲဖျေံ Keyman ဂှ် ဒးသုၚ်စောဲ WebView ရောၚ်။ diff --git a/android/KMEA/app/src/main/res/values-mrt-rNG/strings.xml b/android/KMEA/app/src/main/res/values-mrt-rNG/strings.xml index b30da84ab0f..766705f358f 100644 --- a/android/KMEA/app/src/main/res/values-mrt-rNG/strings.xml +++ b/android/KMEA/app/src/main/res/values-mrt-rNG/strings.xml @@ -80,8 +80,6 @@ FileProvider library needed to view help file: %1$s Fatal keyboard error with %1$s:%2$s for %3$s language. Loading default keyboard. - - Ŋya nur keyboard %1$s:%2$s anu %3$s kra kalkal. Luba dictionary kuku nagu acittu downloadi Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-nl-rNL/strings.xml b/android/KMEA/app/src/main/res/values-nl-rNL/strings.xml index 79f79f253a2..b08264a69a2 100644 --- a/android/KMEA/app/src/main/res/values-nl-rNL/strings.xml +++ b/android/KMEA/app/src/main/res/values-nl-rNL/strings.xml @@ -80,8 +80,6 @@ FileProvider bibliotheek nodig om het hulpbestand te bekijken: %1$s Fatale toetsenbord fout met %1$s:%2$s voor %3$s taal. Standaard toetsenbord wordt geladen. - - Fout in toetsenbord %1$s:%2$s voor %3$s taal. Downloaden van bijbehorende woordenboek Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-pl-rPL/strings.xml b/android/KMEA/app/src/main/res/values-pl-rPL/strings.xml index 2eabe369531..ec0ae4fe1bf 100644 --- a/android/KMEA/app/src/main/res/values-pl-rPL/strings.xml +++ b/android/KMEA/app/src/main/res/values-pl-rPL/strings.xml @@ -84,8 +84,6 @@ Biblioteka FileProvider potrzebna, aby wyświetlić plik pomocy: %1$s Błąd krytycznej klawiatury z %1$s:%2$s dla %3$s języka. Ładowanie domyślnej klawiatury. - - Błąd w klawiaturze %1$s:%2$s dla %3$s języka. Sprawdzanie dla skojarzonego słownika do pobrania Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml b/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml index c4c79680390..a4254109268 100644 --- a/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml +++ b/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml @@ -80,8 +80,6 @@ Biblioteca de FileProvider necessária para ver o arquivo de ajuda: %1$s Erro fatal de teclado com %1$s:%2$s para %3$s idioma. Carregando o teclado padrão. - - Erro no teclado %1$s:%2$s para %3$s idioma. Keyman requer que o WebView seja instalado.\" diff --git a/android/KMEA/app/src/main/res/values-ru-rRU/strings.xml b/android/KMEA/app/src/main/res/values-ru-rRU/strings.xml index 678a22dc1b1..fd93b4a6804 100644 --- a/android/KMEA/app/src/main/res/values-ru-rRU/strings.xml +++ b/android/KMEA/app/src/main/res/values-ru-rRU/strings.xml @@ -84,8 +84,6 @@ Библиотека файлового провайдера необходима для просмотра файла справки: %1$s Неустранимая ошибка клавиатуры %1$s:%2$s для %3$s языка. Загрузка по умолчанию клавиатуры. - - Ошибка в клавиатуре %1$s:%2$s для языка %3$s. Проверка соответствующего словаря для загрузки Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-sv-rSE/strings.xml b/android/KMEA/app/src/main/res/values-sv-rSE/strings.xml index ca9e0ba02ab..88dfcdb4bc8 100644 --- a/android/KMEA/app/src/main/res/values-sv-rSE/strings.xml +++ b/android/KMEA/app/src/main/res/values-sv-rSE/strings.xml @@ -80,8 +80,6 @@ Filleverantörsbibliotek behövs för att visa hjälpfilen: %1$s Kritiskt tangentbordsfel med %1$s:%2$s för %3$s språk. Laddar standardtangentbordet. - - Fel i tangentbordet %1$s:%2$s för %3$s språk. Söker efter tillhörande ordbok att ladda ner Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-uk-rUA/strings.xml b/android/KMEA/app/src/main/res/values-uk-rUA/strings.xml index bc53218a876..5b3b18636e7 100644 --- a/android/KMEA/app/src/main/res/values-uk-rUA/strings.xml +++ b/android/KMEA/app/src/main/res/values-uk-rUA/strings.xml @@ -84,8 +84,6 @@ Бібліотека FileProvider необхідна для перегляду файлу довідки: %1$s Критична помилка в клавіатурі з %1$s:%2$s для мови %3$s . Завантаження клавіатури за замовчуванням. - - Помилка в клавіатурі %1$s:%2$s для мови %3$s. Перевірка пов\'язаного словника для завантаження Cannot connect to Keyman server to check for associated dictionary to download diff --git a/android/KMEA/app/src/main/res/values-vi-rVN/strings.xml b/android/KMEA/app/src/main/res/values-vi-rVN/strings.xml index bf130166e36..b6965b8dfd6 100644 --- a/android/KMEA/app/src/main/res/values-vi-rVN/strings.xml +++ b/android/KMEA/app/src/main/res/values-vi-rVN/strings.xml @@ -78,8 +78,6 @@ Cần có Thư viện FileProvider để xem phần trợ giúp file: %1$s Lỗi bàn phím nghiêm trọng với %1$s: %2$s cho ngôn ngữ %3$s. Đang tải bàn phím mặc định. - - Lỗi trong bàn phím %1$s:%2$s cho ngôn ngữ %3$s. Keyman yêu cầu phải cài đặt WebView\". diff --git a/android/KMEA/app/src/main/res/values-zh-rCN/strings.xml b/android/KMEA/app/src/main/res/values-zh-rCN/strings.xml index 62011953f90..fdc6e19f9ce 100644 --- a/android/KMEA/app/src/main/res/values-zh-rCN/strings.xml +++ b/android/KMEA/app/src/main/res/values-zh-rCN/strings.xml @@ -78,8 +78,6 @@ 需要文件提供者文库来查看帮助文件: %1$s %1$s键盘错误:%2$s为%3$s 语言。正在加载默认键盘。 - - 对于 %3$s 语言,键盘出现错误 %1$s:%2$s 。 正在检查相关字典以下载 无法连接到 Keyman 服务器以检查要下载的相关字典 diff --git a/android/KMEA/app/src/main/res/values/strings.xml b/android/KMEA/app/src/main/res/values/strings.xml index 86f2076586e..ad56c6f370f 100644 --- a/android/KMEA/app/src/main/res/values/strings.xml +++ b/android/KMEA/app/src/main/res/values/strings.xml @@ -123,13 +123,9 @@ FileProvider library needed to view help file: %1$s - - - Fatal keyboard error with %1$s:%2$s for %3$s language. Loading default keyboard. - - - Error in keyboard %1$s:%2$s for %3$s language. + + Keyman encountered an error and will restart diff --git a/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java b/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java index 91fc7dd3709..afc206c9da6 100644 --- a/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java +++ b/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java @@ -1,5 +1,9 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ package com.keyman.engine; +import android.content.Context; import android.util.Log; import androidx.test.core.app.ApplicationProvider; @@ -32,7 +36,11 @@ public class KMManagerTest { // For some keyboard list tests, load an existing keyboard list. // Can't use @Before because context is null before running tests. public void loadOldKeyboardsList() { - KMManager.initialize(ApplicationProvider.getApplicationContext(), KMManager.KeyboardType.KEYBOARD_TYPE_INAPP); + Context context = ApplicationProvider.getApplicationContext(); + // Create appData directory accessed in KMKeyboardWebViewClient + File dataDir = context.getDir("data", Context.MODE_PRIVATE); + dataDir.mkdirs(); + KMManager.initialize(context, KMManager.KeyboardType.KEYBOARD_TYPE_INAPP); File keyboards_dat = new File(TEST_RESOURCE_ROOT, OLD_KEYBOARDS_LIST); if (keyboards_dat == null || !keyboards_dat.exists()) { @@ -111,6 +119,11 @@ public void test_getTier() { */ @Test public void create_newKeyboardsList() { + if(File.separatorChar != '/') { + Log.w(TAG, "create_newKeyboardsList skipped due to #16183"); + return; + } + loadOldKeyboardsList(); dat_list = new ArrayList>(); @@ -182,6 +195,12 @@ public void create_newKeyboardsList() { @Test public void test_updateOldKeyboardsList() { + // #16183 + if(File.separatorChar != '/') { + Log.w(TAG, "test_updateOldKeyboardsList skipped due to #16183"); + return; + } + loadOldKeyboardsList(); Assert.assertNotNull(dat_list); diff --git a/android/KMEA/app/src/test/java/com/keyman/engine/util/FileUtilsTest.java b/android/KMEA/app/src/test/java/com/keyman/engine/util/FileUtilsTest.java index 5118159c9b4..9541a59afe2 100644 --- a/android/KMEA/app/src/test/java/com/keyman/engine/util/FileUtilsTest.java +++ b/android/KMEA/app/src/test/java/com/keyman/engine/util/FileUtilsTest.java @@ -23,7 +23,7 @@ public void test_download() { List logs = ShadowLog.getLogs(); // The logs contain type 4, but we only care about type 6 for connection messages - Assert.assertEquals(2, logs.size()); + Assert.assertEquals("Size did not match 2: " + logs.toString(), 2, logs.size()); Assert.assertEquals("Connection", logs.get(0).tag); Assert.assertEquals("Initialization failed:\njava.net.MalformedURLException: no protocol: invalidURL", logs.get(0).msg); diff --git a/android/KMEA/app/src/test/java/com/keyman/engine/util/WebViewUtilsTest.java b/android/KMEA/app/src/test/java/com/keyman/engine/util/WebViewUtilsTest.java index 342934200af..bfb867ecc97 100644 --- a/android/KMEA/app/src/test/java/com/keyman/engine/util/WebViewUtilsTest.java +++ b/android/KMEA/app/src/test/java/com/keyman/engine/util/WebViewUtilsTest.java @@ -26,37 +26,48 @@ public void getContext() { @Test public void test_ChromeEmpty_EngineWebViewVersionStatusDisabled() { String chromeVersion = ""; - Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion), - WebViewUtils.EngineWebViewVersionStatus.DISABLED); + Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DISABLED, + WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion)); } @Test public void test_Chrome36_EngineWebViewVersionStatusDisabled() { double chromeVersionFloat = Float.parseFloat(WebViewUtils.KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME) - 1.0; String chromeVersion = String.valueOf(chromeVersionFloat); - Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion), - WebViewUtils.EngineWebViewVersionStatus.DISABLED); + Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DISABLED, + WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion)); } @Test public void test_Chrome37_EngineWebViewVersionStatusDegraded() { String chromeVersion = WebViewUtils.KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME; - Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion), - WebViewUtils.EngineWebViewVersionStatus.DEGRADED); + Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DEGRADED, + WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion)); } @Test public void test_Chrome94_EngineWebViewVersionStatusDegraded() { double chromeVersionFloat = Float.parseFloat(WebViewUtils.KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME) - 1.0; String chromeVersion = String.valueOf(chromeVersionFloat); - Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion), - WebViewUtils.EngineWebViewVersionStatus.DEGRADED); + Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DEGRADED, + WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion)); } @Test public void test_Chrome95_EngineWebViewVersionStatusFull() { String chromeVersion = WebViewUtils.KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME; - Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion), - WebViewUtils.EngineWebViewVersionStatus.FULL); + Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.FULL, + WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion)); } + + @Test + public void test_buildAssetUrl() { + Assert.assertEquals("https://appassets.androidplatform.net/data/", WebViewUtils.buildAssetUrl("")); + Assert.assertEquals("https://appassets.androidplatform.net/data/", WebViewUtils.buildAssetUrl(null)); + Assert.assertEquals("https://appassets.androidplatform.net/data/foo", WebViewUtils.buildAssetUrl("foo")); + Assert.assertEquals("https://appassets.androidplatform.net/data/foo/", WebViewUtils.buildAssetUrl("foo/")); + Assert.assertEquals("https://appassets.androidplatform.net/data/foo/", WebViewUtils.buildAssetUrl("/foo/")); + Assert.assertEquals("https://appassets.androidplatform.net/data/foo/bar.html", WebViewUtils.buildAssetUrl("foo/bar.html")); + } + } diff --git a/android/KMEA/build.sh b/android/KMEA/build.sh index 28d9877285c..85a1894672b 100755 --- a/android/KMEA/build.sh +++ b/android/KMEA/build.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. +# # Build Keyman Engine for Android using Keyman Web artifacts # ## START STANDARD BUILD SCRIPT INCLUDE @@ -7,20 +9,9 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../resources/build/builder-full.inc.sh" ## END STANDARD BUILD SCRIPT INCLUDE -. "$KEYMAN_ROOT/resources/build/utils.inc.sh" # ################################ Main script ################################ -# Definition of global compile constants - -KEYMAN_ANDROID_ROOT="$KEYMAN_ROOT/android" -KEYMAN_WEB_ROOT="$KEYMAN_ROOT/web" -ENGINE_ASSETS="$KEYMAN_ANDROID_ROOT/KMEA/app/src/main/assets" -CONFIG="release" -BUILD_FLAGS="aR -x lint -x test" # Gradle build w/o test -TEST_FLAGS="-x aR lintRelease testRelease" # Gradle test w/o build -JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\build\test-results\testReleaseUnitTest\']" - builder_describe "Builds Keyman Engine for Android." \ "@/web/src/app/webview" \ "@/common/web/sentry-manager" \ @@ -30,78 +21,64 @@ builder_describe "Builds Keyman Engine for Android." \ "test Runs lint and unit tests." \ ":engine Builds Engine" -# parse before describe_outputs to check debug flags builder_parse "$@" +# Definition of global compile constants + if builder_is_debug_build; then - builder_heading "### Debug config ####" - CONFIG="debug" BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug" TEST_FLAGS="-x assembleDebug lintDebug testDebug" JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\build\test-results\testDebugUnitTest\']" +else + BUILD_FLAGS="assembleRelease -x lint -x test" # Gradle build w/o test + TEST_FLAGS="-x assembleRelease lintRelease testRelease" # Gradle test w/o build + JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\build\test-results\testReleaseUnitTest\']" fi -builder_describe_outputs \ - build:engine /android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar - -#### Build - - -# Parse args - -DAEMON_FLAG= if builder_is_ci_build; then - DAEMON_FLAG=--no-daemon + GRADLE_DAEMON=--no-daemon +else + GRADLE_DAEMON= fi -#### Build action definitions #### - -# Check about cleaning artifact paths -if builder_start_action clean:engine; then - rm -rf "$KEYMAN_ROOT/android/KMEA/app/build/outputs" - builder_finish_action success clean:engine -fi - -if builder_start_action configure:engine; then - - builder_finish_action success configure:engine -fi - -# Destinations that will need the keymanweb artifacts +builder_describe_outputs \ + build:engine /android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar +#### Build action definitions #### -if builder_start_action build:engine; then +do_build() { + local ENGINE_ASSETS="$KEYMAN_ROOT/android/KMEA/app/src/main/assets" # Copy KeymanWeb artifacts - echo "Copying Keyman Web artifacts" - cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js" "$ENGINE_ASSETS/keymanweb-webview.js" - cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js.map" "$ENGINE_ASSETS/keymanweb-webview.js.map" - cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/map-polyfill.js" "$ENGINE_ASSETS/map-polyfill.js" - cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/ajax-loader.gif" - cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/kmwosk.css" - cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/globe-hint.css" "$ENGINE_ASSETS/globe-hint.css" - cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/keymanweb-osk.ttf" "$ENGINE_ASSETS/keymanweb-osk.ttf" + builder_echo "Copying Keyman Web artifacts" + cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/keymanweb-webview.js" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/keymanweb-webview.js.map" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/webview/$BUILDER_CONFIGURATION/map-polyfill.js" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/globe-hint.css" "$ENGINE_ASSETS/" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/keymanweb-osk.ttf" "$ENGINE_ASSETS/" cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "$ENGINE_ASSETS/keyman-sentry.js" - echo "Copying es6-shim polyfill" - cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "$ENGINE_ASSETS/es6-shim.min.js" - - echo "BUILD_FLAGS $BUILD_FLAGS" - # Build without test - ./gradlew $DAEMON_FLAG clean $BUILD_FLAGS + builder_echo "Copying es6-shim polyfill" + cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "$ENGINE_ASSETS/" - builder_finish_action success build:engine -fi + builder_echo "BUILD_FLAGS $BUILD_FLAGS" -if builder_start_action test:engine; then + # Build without test + ./gradlew $GRADLE_DAEMON $BUILD_FLAGS +} +do_test() { if builder_is_ci_build; then # Report JUnit test results to CI echo "$JUNIT_RESULTS" fi - echo "TEST_FLAGS: $TEST_FLAGS" - ./gradlew $DAEMON_FLAG $TEST_FLAGS + builder_echo "TEST_FLAGS: $TEST_FLAGS" + ./gradlew $GRADLE_DAEMON $TEST_FLAGS +} - builder_finish_action success test:engine -fi +builder_run_action clean:engine rm -rf build app/build +builder_run_action build:engine do_build +builder_run_action test:engine do_test diff --git a/android/Samples/KMSample1/app/build.gradle b/android/Samples/KMSample1/app/build.gradle index ae4ff55d7e4..4643d0e796e 100644 --- a/android/Samples/KMSample1/app/build.gradle +++ b/android/Samples/KMSample1/app/build.gradle @@ -2,6 +2,9 @@ plugins { id 'com.android.application' } +ext.rootPath = '../../../' +apply from: "$rootPath/version.gradle" + java { toolchain { languageVersion = JavaLanguageVersion.of(21) @@ -17,13 +20,22 @@ android { noCompress "kmp" } + buildFeatures { + buildConfig = true + } + defaultConfig { applicationId "com.keyman.kmsample1" minSdkVersion 21 targetSdkVersion 35 - versionCode 1 - versionName "1.0" + + // KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle + versionCode KEYMAN_VERSION_CODE as Integer + versionName KEYMAN_VERSION_NAME + buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\"" + archivesBaseName = "KMSample1-$KEYMAN_VERSION_FOR_FILENAME" } + buildTypes { release { minifyEnabled false @@ -40,7 +52,6 @@ android { repositories { google() - } dependencies { @@ -50,4 +61,5 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' implementation files('libs/keyman-engine.aar') implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.webkit:webkit:1.14.0' } diff --git a/android/Samples/KMSample1/app/src/main/AndroidManifest.xml b/android/Samples/KMSample1/app/src/main/AndroidManifest.xml index 3385b035cc7..c903366c8d2 100644 --- a/android/Samples/KMSample1/app/src/main/AndroidManifest.xml +++ b/android/Samples/KMSample1/app/src/main/AndroidManifest.xml @@ -1,5 +1,8 @@ + + + lexicalModelInfo = new HashMap(); @@ -132,7 +133,7 @@ protected void onPause() { public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); KMManager.onConfigurationChanged(newConfig); - + resizeTextView(textView.isKeyboardVisible()); lastOrientation = newConfig.orientation; } diff --git a/android/Samples/KMSample1/build.gradle b/android/Samples/KMSample1/build.gradle index 18d4c46cd56..43ca9c592c9 100644 --- a/android/Samples/KMSample1/build.gradle +++ b/android/Samples/KMSample1/build.gradle @@ -3,7 +3,6 @@ buildscript { repositories { google() - jcenter() mavenCentral() } dependencies { @@ -17,7 +16,6 @@ buildscript { allprojects { repositories { google() - jcenter() mavenCentral() } } diff --git a/android/Samples/KMSample1/build.sh b/android/Samples/KMSample1/build.sh index d1d41bc2003..d6fca146531 100755 --- a/android/Samples/KMSample1/build.sh +++ b/android/Samples/KMSample1/build.sh @@ -1,21 +1,16 @@ #!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. # -# Samples: KMsample1 ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../../resources/build/builder-full.inc.sh" ## END STANDARD BUILD SCRIPT INCLUDE -. "$KEYMAN_ROOT/resources/build/utils.inc.sh" +. "${KEYMAN_ROOT}/android/android-utils.inc.sh" ################################ Main script ################################ -# Definition of global compile constants - -CONFIG="release" -SAMPLE_FLAGS="build" - builder_describe "Build KMSample1 app for Android." \ "@/android/KMEA" \ "clean" \ @@ -24,55 +19,19 @@ builder_describe "Build KMSample1 app for Android." \ "test" \ ":app KMSample1" -# parse before describe_outputs to check debug flags builder_parse "$@" -ARTIFACT="app-release-unsigned.apk" - -if builder_is_debug_build; then - builder_heading "### Debug config ####" - CONFIG="debug" - SAMPLE_FLAGS="assembleDebug" - ARTIFACT="app-$CONFIG.apk" -fi - +android_set_gradle_environment builder_describe_outputs \ - build:app /android/Samples/KMSample1/app/build/outputs/apk/$CONFIG/$ARTIFACT - - - -# Parse args - -if builder_is_ci_build; then - SAMPLE_FLAGS="$SAMPLE_FLAGS -no-daemon" -fi + build:app "/android/Samples/KMSample1/app/build/outputs/apk/${BUILDER_CONFIGURATION}/KMSample1-${KEYMAN_VERSION_FOR_FILENAME}-${ARCHIVE_TARGET}.apk" #### Build action definitions #### -# Check about cleaning artifact paths -if builder_start_action clean:app; then - rm -rf "$KEYMAN_ROOT/android/Samples/KMSample1/app/build/outputs" - builder_finish_action success clean:app -fi - -if builder_start_action configure:app; then - - builder_finish_action success configure:app -fi - -# Building KMSample1 -if builder_start_action build:app; then - - # Copy Keyman Engine for Android - cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "$KEYMAN_ROOT/android/Samples/KMSample1/app/libs/keyman-engine.aar" - - ./gradlew clean $SAMPLE_FLAGS - - builder_finish_action success build:app -fi +do_build() { + cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar" "$KEYMAN_ROOT/android/Samples/KMSample1/app/libs/keyman-engine.aar" + ./gradlew $GRADLE_DAEMON $GRADLE_TARGET +} -if builder_start_action test:app; then - # TODO: define tests - builder_finish_action success test:app -fi +builder_run_action clean:app rm -rf build app/build +builder_run_action build:app do_build diff --git a/android/Samples/KMSample2/app/build.gradle b/android/Samples/KMSample2/app/build.gradle index a148a7ab193..a3c9658056b 100644 --- a/android/Samples/KMSample2/app/build.gradle +++ b/android/Samples/KMSample2/app/build.gradle @@ -2,6 +2,9 @@ plugins { id 'com.android.application' } +ext.rootPath = '../../../' +apply from: "$rootPath/version.gradle" + java { toolchain { languageVersion = JavaLanguageVersion.of(21) @@ -17,13 +20,22 @@ android { noCompress "kmp" } + buildFeatures { + buildConfig = true + } + defaultConfig { applicationId "com.keyman.kmsample2" minSdkVersion 21 targetSdkVersion 35 - versionCode 1 - versionName "1.0" + + // KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle + versionCode KEYMAN_VERSION_CODE as Integer + versionName KEYMAN_VERSION_NAME + buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\"" + archivesBaseName = "KMSample2-$KEYMAN_VERSION_FOR_FILENAME" } + buildTypes { release { minifyEnabled false @@ -49,4 +61,5 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' implementation files('libs/keyman-engine.aar') implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.webkit:webkit:1.14.0' } diff --git a/android/Samples/KMSample2/app/src/main/AndroidManifest.xml b/android/Samples/KMSample2/app/src/main/AndroidManifest.xml index 4c2ada08ada..eb2ffd215b7 100644 --- a/android/Samples/KMSample2/app/src/main/AndroidManifest.xml +++ b/android/Samples/KMSample2/app/src/main/AndroidManifest.xml @@ -1,5 +1,8 @@ + + + lexicalModelInfo = new HashMap(); @@ -262,4 +263,4 @@ public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { public boolean onKeyLongPress(int keyCode, KeyEvent event) { return interpreter.onKeyLongPress(keyCode, event); } -} \ No newline at end of file +} diff --git a/android/Samples/KMSample2/build.gradle b/android/Samples/KMSample2/build.gradle index 18d4c46cd56..43ca9c592c9 100644 --- a/android/Samples/KMSample2/build.gradle +++ b/android/Samples/KMSample2/build.gradle @@ -3,7 +3,6 @@ buildscript { repositories { google() - jcenter() mavenCentral() } dependencies { @@ -17,7 +16,6 @@ buildscript { allprojects { repositories { google() - jcenter() mavenCentral() } } diff --git a/android/Samples/KMSample2/build.sh b/android/Samples/KMSample2/build.sh index 3c60795e8d5..14a9a49507a 100755 --- a/android/Samples/KMSample2/build.sh +++ b/android/Samples/KMSample2/build.sh @@ -1,21 +1,16 @@ #!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. # -# Samples: KMSample2 ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../../resources/build/builder-full.inc.sh" ## END STANDARD BUILD SCRIPT INCLUDE -. "$KEYMAN_ROOT/resources/build/utils.inc.sh" +. "${KEYMAN_ROOT}/android/android-utils.inc.sh" ################################ Main script ################################ -# Definition of global compile constants - -CONFIG="release" -SAMPLE_FLAGS="build" - builder_describe "Build KMSample2 app for Android." \ "@/android/KMEA" \ "clean" \ @@ -24,55 +19,19 @@ builder_describe "Build KMSample2 app for Android." \ "test" \ ":app KMSample2" -# parse before describe_outputs to check debug flags builder_parse "$@" -ARTIFACT="app-release-unsigned.apk" - -if builder_is_debug_build; then - builder_heading "### Debug config ####" - CONFIG="debug" - SAMPLE_FLAGS="assembleDebug" - ARTIFACT="app-$CONFIG.apk" -fi - - +android_set_gradle_environment builder_describe_outputs \ - build:app /android/Samples/KMSample2/app/build/outputs/apk/$CONFIG/$ARTIFACT - - - -# Parse args - -if builder_is_ci_build; then - SAMPLE_FLAGS="$SAMPLE_FLAGS -no-daemon" -fi + build:app "/android/Samples/KMSample2/app/build/outputs/apk/${BUILDER_CONFIGURATION}/KMSample2-${KEYMAN_VERSION_FOR_FILENAME}-${ARCHIVE_TARGET}.apk" #### Build action definitions #### -# Check about cleaning artifact paths -if builder_start_action clean:app; then - rm -rf "$KEYMAN_ROOT/android/Samples/KMSample2/app/build/outputs" - builder_finish_action success clean:app -fi - -if builder_start_action configure:app; then - - builder_finish_action success configure:app -fi - -# Building KMSample2 -if builder_start_action build:app; then - # Copy Keyman Engine for Android - cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "$KEYMAN_ROOT/android/Samples/KMSample2/app/libs/keyman-engine.aar" - - ./gradlew clean $SAMPLE_FLAGS - - builder_finish_action success build:app -fi +do_build() { + cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${BUILDER_CONFIGURATION}.aar" "$KEYMAN_ROOT/android/Samples/KMSample2/app/libs/keyman-engine.aar" + ./gradlew $GRADLE_DAEMON $GRADLE_TARGET +} -if builder_start_action test:app; then - # TODO: define tests - builder_finish_action success test:app -fi +builder_run_action clean:app rm -rf build app/build +builder_run_action build:app do_build diff --git a/android/Tests/KeyboardHarness/app/build.gradle b/android/Tests/KeyboardHarness/app/build.gradle index 25d5c8250ab..070ee5c63b1 100644 --- a/android/Tests/KeyboardHarness/app/build.gradle +++ b/android/Tests/KeyboardHarness/app/build.gradle @@ -29,10 +29,11 @@ android { minSdkVersion 21 targetSdkVersion 35 - // KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle + // KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle versionCode KEYMAN_VERSION_CODE as Integer versionName KEYMAN_VERSION_NAME buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\"" + archivesBaseName = "KeyboardHarness-$KEYMAN_VERSION_FOR_FILENAME" } buildTypes { release { @@ -60,4 +61,5 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' implementation files('libs/keyman-engine.aar') implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.webkit:webkit:1.14.0' } diff --git a/android/Tests/KeyboardHarness/app/src/main/AndroidManifest.xml b/android/Tests/KeyboardHarness/app/src/main/AndroidManifest.xml index 3385b035cc7..c903366c8d2 100644 --- a/android/Tests/KeyboardHarness/app/src/main/AndroidManifest.xml +++ b/android/Tests/KeyboardHarness/app/src/main/AndroidManifest.xml @@ -1,5 +1,8 @@ + + + keyboardInfo) -``` - -### Parameters - -`context` -: The context. - -`keyboardInfo` -: A dictionary of keyboard information with keys and values defined as - `HashMap`. - -### Returns - -Returns `true` if the keyboard was added successfully, `false` -otherwise. - -## Description - -Use this method to include a keyboard in the keyboards list so that it -can be selected from the keyboards menu. If the keyboard with same -keyboard ID and language ID exists, it updates the existing keyboard -info. - - - ------------------------------------------------------------------------ ## Examples diff --git a/android/docs/engine/KMManager/getCurrentKeyboardInfo.md b/android/docs/engine/KMManager/getCurrentKeyboardInfo.md index 4eb789e736d..9aceb3653b4 100644 --- a/android/docs/engine/KMManager/getCurrentKeyboardInfo.md +++ b/android/docs/engine/KMManager/getCurrentKeyboardInfo.md @@ -28,34 +28,6 @@ Use this method to get details of the currently selected keyboard. Details include package ID, keyboard ID, language ID, keyboard name, language name and fonts. - - ------------------------------------------------------------------------- - -## Syntax (Deprecated) - -``` javascript -KMManager.getCurrentKeyboardInfo(Context context) -``` - -### Parameters - -`context` -: The context. - -### Returns - -(Deprecated) Returns an information dictionary of the current keyboard -with keys and values defined as `HashMap`. - -## Description - -Use this method to get details of the currently selected keyboard. -Details include keyboard ID, language ID, keyboard name, language name -and fonts. - - - ------------------------------------------------------------------------ ## Examples diff --git a/android/docs/engine/KMManager/getFontTypeface.md b/android/docs/engine/KMManager/getFontTypeface.md index 4149d92b360..9dd86928e71 100644 --- a/android/docs/engine/KMManager/getFontTypeface.md +++ b/android/docs/engine/KMManager/getFontTypeface.md @@ -19,7 +19,7 @@ KMManager.getFontTypeface(Context context, String fontFilename) : The context. `fontFilename` -: The filename of the font. +: The filename and full path of the font. ### Returns @@ -29,7 +29,7 @@ if it exists, `null` otherwise. ## Description Use this method to create a new typeface from the font file with -specified filename if it exists in `assets/fonts/` folder. +specified filename if it exists. ## Examples @@ -37,9 +37,10 @@ specified filename if it exists in `assets/fonts/` folder. The following script illustrate the use of `getFontTypeface()`: -``` javascript +``` java KMTextView textView = (KMTextView) findViewById(R.id.kmTextView); - Typeface fontTypeface = KMManager.getFontTypeface(this, "aava1.ttf"); + String textFontFilename = KMManager.getKeyboardTextFontFilename(); + Typeface fontTypeface = KMManager.getFontTypeface(this, textFontFilename); textView.setTypeface(fontTypeface); ``` diff --git a/android/docs/engine/KMManager/getKeyboardFontFilename.md b/android/docs/engine/KMManager/getKeyboardFontFilename.md index 086922224d4..005e6264a35 100644 --- a/android/docs/engine/KMManager/getKeyboardFontFilename.md +++ b/android/docs/engine/KMManager/getKeyboardFontFilename.md @@ -22,6 +22,8 @@ empty string otherwise. Use this method to get the font filename of the selected keyboard. +Deprecated. Use `getKeyboardTextFontFilename()` instead. + ## Examples ### Example: Using `getKeyboardFontFilename()` @@ -34,5 +36,6 @@ The following script illustrate the use of `getKeyboardFontFilename()`: ## See also +- [`getKeyboardTextFontFilename()`](getKeyboardTextFontFilename) - [`getKeyboardFontTypeface()` (Deprecated)](getKeyboardFontTypeface) - [`getFontTypeface()`](getFontTypeface) diff --git a/android/docs/engine/KMManager/getKeyboardFontTypeface.md b/android/docs/engine/KMManager/getKeyboardFontTypeface.md index 1f414ce14fd..a2858141e06 100644 --- a/android/docs/engine/KMManager/getKeyboardFontTypeface.md +++ b/android/docs/engine/KMManager/getKeyboardFontTypeface.md @@ -28,6 +28,8 @@ exists, `null` otherwise. Use this method to create a new typeface from the selected keyboard's font if it has any. +Deprecated. Use `getKeyboardTextFontFilename()` instead. + ## Examples ### Example: Using `getKeyboardFontTypeface()` @@ -44,3 +46,4 @@ The following script illustrate the use of `getKeyboardFontTypeface()`: - [`getFontTypeface()`](getFontTypeface) - [`getKeyboardFontFilename()` (Deprecated)](getKeyboardFontFilename) +- [`getKeyboardTextFontFilename()`](getKeyboardTextFontFilename) diff --git a/android/docs/engine/KMManager/getKeyboardInfo.md b/android/docs/engine/KMManager/getKeyboardInfo.md index 5838e0ca233..859c4b3bb7c 100644 --- a/android/docs/engine/KMManager/getKeyboardInfo.md +++ b/android/docs/engine/KMManager/getKeyboardInfo.md @@ -31,35 +31,6 @@ Use this method to get details of the keyboard at given position in keyboards list. Details include keyboard ID, language ID, keyboard name, language name and fonts. - - ------------------------------------------------------------------------- - -## Syntax (Deprecated) - -``` javascript -KMManager.getKeyboardInfo(Context context, int index) -``` - -### Parameters - -`context` -: The context. - -`index` -: 0-based position of the keyboard in keyboards list. - -### Returns - -(Deprecated) Returns an information dictionary of the specified keyboard -with keys and values defined as `HashMap`. - -## Description - -Use this method to get details of the keyboard at given position in -keyboards list. Details include keyboard ID, language ID, keyboard name, -language name and fonts. - ## Examples ### Example: Using `getKeyboardInfo()` diff --git a/android/docs/engine/KMManager/getKeyboardOskFontFilename.md b/android/docs/engine/KMManager/getKeyboardOskFontFilename.md index 8465ea4e8c4..7406effb9d5 100644 --- a/android/docs/engine/KMManager/getKeyboardOskFontFilename.md +++ b/android/docs/engine/KMManager/getKeyboardOskFontFilename.md @@ -5,18 +5,25 @@ title: KMManager.getKeyboardOskFontFilename() ## Summary The **`getKeyboardOskFontFilename()`** method returns the selected -keyboard's OSK font filename. +keyboard's OSK font filename and full path. ## Syntax -``` javascript +``` java KMManager.getKeyboardOskFontFilename() ``` ### Returns -Returns the selected keyboard's OSK font filename as `String` if it has -any, empty string otherwise. +Returns the selected keyboard's OSK font filename and full path as +`String` if it has any, empty string otherwise. Note that the +on-screen keyboard will fallback to the keyboard text font if +no OSK font is specified. + +The OSK font should not be used for a text view, because some +OSK fonts are appropriate for use only in the on screen keyboard; +see [`&displayMap`](/developer/language/reference/displaymap) for +reference. ## Description @@ -29,8 +36,8 @@ Use this method to get the OSK font filename of the selected keyboard. The following script illustrate the use of `getKeyboardOskFontFilename()`: -``` javascript - String oskFontFilename = KMManager.getKeyboardOskFontFilename(); +``` java +String oskFontFilename = KMManager.getKeyboardOskFontFilename(); ``` ## See also diff --git a/android/docs/engine/KMManager/getKeyboardTextFontFilename.md b/android/docs/engine/KMManager/getKeyboardTextFontFilename.md index 5c381cc23a0..f1e26e51a37 100644 --- a/android/docs/engine/KMManager/getKeyboardTextFontFilename.md +++ b/android/docs/engine/KMManager/getKeyboardTextFontFilename.md @@ -5,18 +5,18 @@ title: KMManager.getKeyboardTextFontFilename() ## Summary The **`getKeyboardTextFontFilename()`** method returns the selected -keyboard's text font filename. +keyboard's text font filename and full path. ## Syntax -``` javascript +``` java KMManager.getKeyboardTextFontFilename() ``` ### Returns -Returns the selected keyboard's text font filename as `String` if it has -any, empty string otherwise. +Returns the selected keyboard's text font filename and full path as +`String` if it has any, empty string otherwise. ## Description @@ -29,8 +29,8 @@ Use this method to get the text font filename of the selected keyboard. The following script illustrate the use of `getKeyboardTextFontFilename()`: -``` javascript - String textFontFilename = KMManager.getKeyboardTextFontFilename(); +``` java +String textFontFilename = KMManager.getKeyboardTextFontFilename(); ``` ## See also diff --git a/android/docs/engine/KMManager/getKeyboardsList.md b/android/docs/engine/KMManager/getKeyboardsList.md index 26c520953b7..4f38761665c 100644 --- a/android/docs/engine/KMManager/getKeyboardsList.md +++ b/android/docs/engine/KMManager/getKeyboardsList.md @@ -26,33 +26,6 @@ otherwise. Use this method to get details of all keyboard's in keyboards menu. - - ------------------------------------------------------------------------- - -## Syntax (Deprecated) - -``` javascript -KMManager.getKeyboardsList(Context context) -``` - -### Parameters - -`context` -: The context. - -### Returns - -(Deprecated) Returns keyboards list as -`ArrayList>` if it exists, `null` -otherwise. - -## Description - -Use this method to get details of all keyboard's in keyboards menu. - - - ------------------------------------------------------------------------ ## Examples diff --git a/android/docs/engine/KMManager/setKeyboard.md b/android/docs/engine/KMManager/setKeyboard.md index 4a152f2480d..8faafae31de 100644 --- a/android/docs/engine/KMManager/setKeyboard.md +++ b/android/docs/engine/KMManager/setKeyboard.md @@ -35,8 +35,8 @@ normally returned by [`getKeyboardInfo()`](getKeyboardInfo). ```java KMManager.setKeyboard(String packageID, String keyboardID, String languageID) -KMManager.setKeyboard(String packageID, String keyboardID, String languageID, String keyboardName, String languageName, String kFont, String kOskFont) -KMManager.setKeyboard(String packageID, String keyboardID, String languageID, String keyboardName, String languageName, String kFont, String kOskFont, String displayName) +KMManager.setKeyboard(String packageID, String keyboardID, String languageID, String keyboardName, String languageName, String textFontFilename, String oskFontFilename) +KMManager.setKeyboard(String packageID, String keyboardID, String languageID, String keyboardName, String languageName, String textFontFilename, String oskFontFilename, String displayName) ``` ### Parameters @@ -56,13 +56,13 @@ KMManager.setKeyboard(String packageID, String keyboardID, String languageID, St `languageName` : Name of the associated language. -`kFont` -: Filename or description of the font to type with the keyboard. Can be `null` +`textFontFilename` +: Filename of the font to type with the keyboard. Can be `null` or empty string. -`kOskFont` -: Filename or description of the font displayed on the keyboard. Can be `null` - or empty string. +`oskFontFilename` +: Filename of the font displayed on the keyboard. Can be `null` + or empty string, and if `null` or empty string, uses `textFontFilename`. `displayName` : A text string to display on the spacebar identifying this keyboard; if `null`, diff --git a/android/version.gradle b/android/version.gradle index ea7342166c9..677f4926eff 100644 --- a/android/version.gradle +++ b/android/version.gradle @@ -88,6 +88,16 @@ def getVersionGitTag = { -> return "release@$KEYMAN_VERSION_NAME" } } + +def getVersionForFilename = { -> + String env_version_for_filename = System.getenv("KEYMAN_VERSION_FOR_FILENAME") + if(env_version_for_filename != null) { + return env_version_for_filename + } + // Building probably from IDE + return "$KEYMAN_VERSION_NAME" +} + ext { KEYMAN_VERSION_CODE=getVersionCode() KEYMAN_VERSION_MD=getVersionMD() @@ -95,6 +105,7 @@ ext { KEYMAN_VERSION_ENVIRONMENT=getVersionEnvironment() KEYMAN_VERSION_TAG=System.getenv("KEYMAN_VERSION_TAG") KEYMAN_VERSION_GIT_TAG=getVersionGitTag() + KEYMAN_VERSION_FOR_FILENAME=getVersionForFilename() } //println "version.gradle: KEYMAN_VERSION_TAG: " + KEYMAN_VERSION_TAG \ No newline at end of file diff --git a/common/test/keyboards/invalid/README.md b/common/test/keyboards/invalid/compiler/README.md similarity index 100% rename from common/test/keyboards/invalid/README.md rename to common/test/keyboards/invalid/compiler/README.md diff --git a/common/test/keyboards/invalid/source/CERR_400A_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_400A_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_400A_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_400A_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_400D_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_400D_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_400D_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_400D_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_400E_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_400E_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_400E_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_400E_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4013_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4013_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4013_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4013_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4015_k_014___groups_and_virtual_keys.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4015_k_014___groups_and_virtual_keys.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4015_k_014___groups_and_virtual_keys.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4015_k_014___groups_and_virtual_keys.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4016_k_014___groups_and_virtual_keys.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4016_k_014___groups_and_virtual_keys.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4016_k_014___groups_and_virtual_keys.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4016_k_014___groups_and_virtual_keys.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4017_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4017_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4017_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4017_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4019_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4019_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4019_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4019_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_401D_k_034___options_double_set_reset.kmn b/common/test/keyboards/invalid/compiler/source/CERR_401D_k_034___options_double_set_reset.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_401D_k_034___options_double_set_reset.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_401D_k_034___options_double_set_reset.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4026_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4026_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4026_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4026_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4029_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4029_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4029_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4029_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_402B_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_402B_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_402B_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_402B_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_402D_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_402D_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_402D_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_402D_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4031_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4031_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4031_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4031_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4034_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4034_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4034_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4034_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4038_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4038_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4038_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4038_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4039_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4039_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4039_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4039_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_403A_014_groups_and_virtual_keys.kmn b/common/test/keyboards/invalid/compiler/source/CERR_403A_014_groups_and_virtual_keys.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_403A_014_groups_and_virtual_keys.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_403A_014_groups_and_virtual_keys.kmn diff --git a/common/test/keyboards/invalid/source/CERR_403E_imsample.kmn b/common/test/keyboards/invalid/compiler/source/CERR_403E_imsample.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_403E_imsample.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_403E_imsample.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4040_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4040_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4040_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4040_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4044_014_groups_and_virtual_keys.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4044_014_groups_and_virtual_keys.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4044_014_groups_and_virtual_keys.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4044_014_groups_and_virtual_keys.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4045_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4045_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4045_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4045_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4046_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4046_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4046_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4046_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4047_k_022___options_with_preset.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4047_k_022___options_with_preset.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4047_k_022___options_with_preset.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4047_k_022___options_with_preset.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4049_k_022___options_with_preset.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4049_k_022___options_with_preset.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4049_k_022___options_with_preset.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4049_k_022___options_with_preset.kmn diff --git a/common/test/keyboards/invalid/source/CERR_404A_k_025___options_with_reset.kmn b/common/test/keyboards/invalid/compiler/source/CERR_404A_k_025___options_with_reset.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_404A_k_025___options_with_reset.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_404A_k_025___options_with_reset.kmn diff --git a/common/test/keyboards/invalid/source/CERR_404B_k_025___options_with_reset.kmn b/common/test/keyboards/invalid/compiler/source/CERR_404B_k_025___options_with_reset.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_404B_k_025___options_with_reset.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_404B_k_025___options_with_reset.kmn diff --git a/common/test/keyboards/invalid/source/CERR_404C_k_023___options_with_save.kmn b/common/test/keyboards/invalid/compiler/source/CERR_404C_k_023___options_with_save.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_404C_k_023___options_with_save.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_404C_k_023___options_with_save.kmn diff --git a/common/test/keyboards/invalid/source/CERR_404D_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_404D_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_404D_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_404D_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_404F_k_026___system_stores.kmn b/common/test/keyboards/invalid/compiler/source/CERR_404F_k_026___system_stores.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_404F_k_026___system_stores.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_404F_k_026___system_stores.kmn diff --git a/common/test/keyboards/invalid/source/CERR_405C_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_405C_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_405C_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_405C_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_405D_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_405D_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_405D_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_405D_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_405E_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_405E_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_405E_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_405E_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4060_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4060_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4060_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4060_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4061_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4061_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4061_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4061_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4064_caps_lock_layer_3620.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4064_caps_lock_layer_3620.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4064_caps_lock_layer_3620.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4064_caps_lock_layer_3620.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4065_test_expansion_invalid.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4065_test_expansion_invalid.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4065_test_expansion_invalid.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4065_test_expansion_invalid.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4066_test_expansion_invalid.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4066_test_expansion_invalid.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4066_test_expansion_invalid.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4066_test_expansion_invalid.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4067_test_expansion_invalid.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4067_test_expansion_invalid.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4067_test_expansion_invalid.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4067_test_expansion_invalid.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4068_test_expansion_invalid.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4068_test_expansion_invalid.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4068_test_expansion_invalid.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4068_test_expansion_invalid.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4069_test_casedkeys_invalid_1.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4069_test_casedkeys_invalid_1.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4069_test_casedkeys_invalid_1.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4069_test_casedkeys_invalid_1.kmn diff --git a/common/test/keyboards/invalid/source/CERR_406A_test_casedkeys_invalid_2.kmn b/common/test/keyboards/invalid/compiler/source/CERR_406A_test_casedkeys_invalid_2.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_406A_test_casedkeys_invalid_2.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_406A_test_casedkeys_invalid_2.kmn diff --git a/common/test/keyboards/invalid/source/CERR_406B_test_casedkeys_mnemonic_1.kmn b/common/test/keyboards/invalid/compiler/source/CERR_406B_test_casedkeys_mnemonic_1.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_406B_test_casedkeys_mnemonic_1.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_406B_test_casedkeys_mnemonic_1.kmn diff --git a/common/test/keyboards/invalid/source/CERR_406C_test_5963_readonlygroup_usenonreadonly.kmn b/common/test/keyboards/invalid/compiler/source/CERR_406C_test_5963_readonlygroup_usenonreadonly.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_406C_test_5963_readonlygroup_usenonreadonly.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_406C_test_5963_readonlygroup_usenonreadonly.kmn diff --git a/common/test/keyboards/invalid/source/CERR_406E_test_5963_readonlygroup_misplacedcontext.kmn b/common/test/keyboards/invalid/compiler/source/CERR_406E_test_5963_readonlygroup_misplacedcontext.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_406E_test_5963_readonlygroup_misplacedcontext.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_406E_test_5963_readonlygroup_misplacedcontext.kmn diff --git a/common/test/keyboards/invalid/source/CERR_406F_test_5963_newcontext_2.kmn b/common/test/keyboards/invalid/compiler/source/CERR_406F_test_5963_newcontext_2.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_406F_test_5963_newcontext_2.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_406F_test_5963_newcontext_2.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4070_test_5963_postkeystroke_2.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4070_test_5963_postkeystroke_2.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4070_test_5963_postkeystroke_2.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4070_test_5963_postkeystroke_2.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4071_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4071_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4071_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4071_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4072_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4072_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4072_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4072_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_4073_balochi_phonetic.kmn b/common/test/keyboards/invalid/compiler/source/CERR_4073_balochi_phonetic.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_4073_balochi_phonetic.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_4073_balochi_phonetic.kmn diff --git a/common/test/keyboards/invalid/source/CERR_80C0_test_expansion_absurd.kmn b/common/test/keyboards/invalid/compiler/source/CERR_80C0_test_expansion_absurd.kmn similarity index 100% rename from common/test/keyboards/invalid/source/CERR_80C0_test_expansion_absurd.kmn rename to common/test/keyboards/invalid/compiler/source/CERR_80C0_test_expansion_absurd.kmn diff --git a/common/test/keyboards/invalid/source/CERR__balochi_phonetic.ico b/common/test/keyboards/invalid/compiler/source/CERR__balochi_phonetic.ico similarity index 100% rename from common/test/keyboards/invalid/source/CERR__balochi_phonetic.ico rename to common/test/keyboards/invalid/compiler/source/CERR__balochi_phonetic.ico diff --git a/common/test/keyboards/invalid/engine/README.md b/common/test/keyboards/invalid/engine/README.md new file mode 100644 index 00000000000..305a6d20912 --- /dev/null +++ b/common/test/keyboards/invalid/engine/README.md @@ -0,0 +1,12 @@ +# Invalid keyboards for testing Keyman Engine + +## javascript_error.kmp + +A keyboard package that has a Javascript keyboard with an error in `gs()` to test runtime keyboard handling + +## TODO: other invalid keyboards to add here + +* invalid_packages - missing files, metadata, etc +* script_load_error - unparseable JS +* invalid kmx +* invalid fonts diff --git a/common/test/keyboards/invalid/engine/javascript_error/HISTORY.md b/common/test/keyboards/invalid/engine/javascript_error/HISTORY.md new file mode 100644 index 00000000000..fc4cd373a59 --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/HISTORY.md @@ -0,0 +1,6 @@ +Banne Change History +==================== + +1.0 (2024-07-12) +---------------- +* Created by Sophia Ku diff --git a/common/test/keyboards/invalid/engine/javascript_error/LICENSE.md b/common/test/keyboards/invalid/engine/javascript_error/LICENSE.md new file mode 100644 index 00000000000..9d9ff006e34 --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2024 SIL + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/common/test/keyboards/invalid/engine/javascript_error/README.md b/common/test/keyboards/invalid/engine/javascript_error/README.md new file mode 100644 index 00000000000..64ade5335d3 --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/README.md @@ -0,0 +1,14 @@ +# javascript_error keyboard + +## Description + +Keyboard with a script error in `gs()`, taken originally from release/b/banne + +The .js and .kmp files in modified_build/ have been manually tweaked to throw the error: + +```js + this.gs=function(t,e) { + throw new Error("javascript_error.js - runtime error"); + // return this.g_main_0(t,e); + }; +``` \ No newline at end of file diff --git a/common/test/keyboards/invalid/engine/javascript_error/javascript_error.kpj b/common/test/keyboards/invalid/engine/javascript_error/javascript_error.kpj new file mode 100644 index 00000000000..d3a678044b5 --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/javascript_error.kpj @@ -0,0 +1,8 @@ + + + + 2.0 + True + True + + diff --git a/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.js b/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.js new file mode 100644 index 00000000000..47c3661e20d --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.js @@ -0,0 +1,816 @@ +if(typeof keyman === 'undefined') { + console.log('Keyboard requires KeymanWeb 10.0 or later'); + if(typeof tavultesoft !== 'undefined') tavultesoft.keymanweb.util.alert("This keyboard requires KeymanWeb 10.0 or later"); +} else { +KeymanWeb.KR(new Keyboard_javascript_error()); +} +function Keyboard_javascript_error() +{ + var modCodes = keyman.osk.modifierCodes; + var keyCodes = keyman.osk.keyCodes; + + this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9; + this.KI="Keyboard_javascript_error"; + this.KN="javascript_error"; + this.KMINVER="10.0"; + this.KV={F:' 1em "Arial"',K102:0}; + this.KV.KLS={ + "default": ["`","1","2","3","4","5","6","7","8","9","0","-","=","","","","q","w","e","r","t","y","u","i","o","p","[","]","\\","","","","a","s","d","f","g","h","j","k","l",";","'","","","","","","\\","z","x","c","v","b","n","m",",",".","/","","","","","",""], + "shift": ["~","!","@","#","$","%","^","&","*","(",")","_","+","","","","Q","W","E","R","T","Y","U","I","O","P","{","}","|","","","","A","S","D","F","G","H","J","K","L",":","«","","","","","","|","Z","X","C","V","B","N","M","<",">","?","","","","","",""] + }; + this.KV.BK=(function(x){ + var + empty=Array.apply(null, Array(65)).map(String.prototype.valueOf,""), + result=[], v, i, + modifiers=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt']; + for(i=modifiers.length-1;i>=0;i--) { + v = x[modifiers[i]]; + if(v || result.length > 0) { + result=(v ? v : empty).slice().concat(result); + } + } + return result; + })(this.KV.KLS); + this.KDU=0; + this.KH=''; + this.KM=0; + this.KBVER="1.0"; + this.KMBM=modCodes.SHIFT /* 0x0010 */; + this.KVKL={ + "phone": { + "font": "Tahoma", + "layer": [ + { + "id": "default", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_Q", + "text": "q" + }, + { + "id": "K_W", + "text": "w" + }, + { + "id": "K_E", + "text": "e" + }, + { + "id": "K_R", + "text": "r" + }, + { + "id": "K_T", + "text": "t" + }, + { + "id": "K_Y", + "text": "y" + }, + { + "id": "K_U", + "text": "u" + }, + { + "id": "K_I", + "text": "i" + }, + { + "id": "K_O", + "text": "o" + }, + { + "id": "K_P", + "text": "p" + } + ] + }, + { + "id": "2", + "key": [ + { + "id": "K_A", + "text": "a", + "pad": "50" + }, + { + "id": "K_S", + "text": "s", + "sk": [ + { + "text": "š", + "id": "U_0161" + } + ] + }, + { + "id": "K_D", + "text": "d" + }, + { + "id": "K_F", + "text": "f" + }, + { + "id": "K_G", + "text": "g" + }, + { + "id": "K_H", + "text": "h" + }, + { + "id": "K_J", + "text": "j" + }, + { + "id": "K_K", + "text": "k" + }, + { + "id": "K_L", + "text": "l" + }, + { + "id": "T_new_88", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "sp": "1", + "nextlayer": "shift" + }, + { + "id": "K_Z", + "text": "z" + }, + { + "id": "K_X", + "text": "x" + }, + { + "id": "K_C", + "text": "c" + }, + { + "id": "K_V", + "text": "v" + }, + { + "id": "K_B", + "text": "b" + }, + { + "id": "K_N", + "text": "n" + }, + { + "id": "K_M", + "text": "m" + }, + { + "id": "K_PERIOD", + "text": ".", + "sk": [ + { + "text": ",", + "id": "K_COMMA" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "‹", + "id": "U_2039" + }, + { + "text": "›", + "id": "U_203A" + }, + { + "text": "«", + "id": "U_00AB", + "layer": "shift" + }, + { + "text": "»", + "id": "U_00BB" + }, + { + "text": "\\", + "id": "K_BKSLASH" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": "4", + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": "150", + "sp": "1", + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "width": "610" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "150", + "sp": "1" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_Q", + "text": "Q" + }, + { + "id": "K_W", + "text": "W" + }, + { + "id": "K_E", + "text": "E" + }, + { + "id": "K_R", + "text": "R" + }, + { + "id": "K_T", + "text": "T" + }, + { + "id": "K_Y", + "text": "Y" + }, + { + "id": "K_U", + "text": "U" + }, + { + "id": "K_I", + "text": "I" + }, + { + "id": "K_O", + "text": "O" + }, + { + "id": "K_P", + "text": "P" + } + ] + }, + { + "id": "2", + "key": [ + { + "id": "K_A", + "text": "A", + "pad": "50" + }, + { + "id": "K_S", + "text": "S", + "sk": [ + { + "text": "Š", + "id": "U_0160" + } + ] + }, + { + "id": "K_D", + "text": "D" + }, + { + "id": "K_F", + "text": "F" + }, + { + "id": "K_G", + "text": "G" + }, + { + "id": "K_H", + "text": "H" + }, + { + "id": "K_J", + "text": "J" + }, + { + "id": "K_K", + "text": "K" + }, + { + "id": "K_L", + "text": "L" + }, + { + "id": "T_new_122", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "sp": "2", + "nextlayer": "default" + }, + { + "id": "K_Z", + "text": "Z" + }, + { + "id": "K_X", + "text": "X" + }, + { + "id": "K_C", + "text": "C" + }, + { + "id": "K_V", + "text": "V" + }, + { + "id": "K_B", + "text": "B" + }, + { + "id": "K_N", + "text": "N" + }, + { + "id": "K_M", + "text": "M" + }, + { + "id": "K_PERIOD", + "text": ".", + "layer": "default", + "sk": [ + { + "text": ",", + "id": "K_COMMA", + "layer": "default" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "‹", + "id": "U_2039", + "layer": "default" + }, + { + "text": "›", + "id": "U_203A" + }, + { + "text": "«", + "id": "U_00AB", + "layer": "shift" + }, + { + "text": "»", + "id": "U_00BB" + }, + { + "text": "\\", + "id": "K_BKSLASH", + "layer": "default" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON", + "layer": "default" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "sp": "1" + } + ] + }, + { + "id": "4", + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": "150", + "sp": "1", + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "width": "610" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "150", + "sp": "1" + } + ] + } + ] + }, + { + "id": "numeric", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0", + "width": "100" + } + ] + }, + { + "id": "2", + "key": [ + { + "id": "K_4", + "text": "$", + "pad": "50", + "layer": "shift" + }, + { + "id": "K_2", + "text": "@", + "layer": "shift" + }, + { + "id": "K_3", + "text": "#", + "layer": "shift" + }, + { + "id": "K_5", + "text": "%", + "layer": "shift" + }, + { + "id": "K_7", + "text": "&", + "layer": "shift" + }, + { + "id": "K_HYPHEN", + "text": "-" + }, + { + "id": "K_HYPHEN", + "text": "_", + "layer": "shift" + }, + { + "id": "K_EQUAL", + "text": "=", + "layer": "default" + }, + { + "id": "K_BKSLASH", + "text": "|", + "layer": "shift" + }, + { + "id": "K_BKSLASH", + "text": "\\", + "layer": "default" + } + ] + }, + { + "id": "3", + "key": [ + { + "id": "K_LBRKT", + "text": "[", + "pad": "110", + "sk": [ + { + "text": "<", + "id": "K_COMMA", + "layer": "shift" + }, + { + "text": "{", + "id": "K_LBRKT", + "layer": "shift" + } + ] + }, + { + "id": "K_9", + "text": "(", + "layer": "shift", + "sk": [ + { + "text": "‹", + "id": "U_2039" + }, + { + "text": "«", + "id": "U_00AB" + } + ] + }, + { + "id": "K_0", + "text": ")", + "layer": "shift", + "sk": [ + { + "text": "›", + "id": "U_203A" + }, + { + "text": "»", + "id": "U_00BB" + } + ] + }, + { + "id": "K_RBRKT", + "text": "]", + "sk": [ + { + "text": ">", + "id": "K_PERIOD", + "layer": "shift" + }, + { + "text": "}", + "id": "K_RBRKT", + "layer": "shift" + } + ] + }, + { + "id": "U_0027", + "text": "'" + }, + { + "id": "U_0022", + "text": "\"" + }, + { + "id": "K_EQUAL", + "text": "+", + "layer": "shift" + }, + { + "id": "K_8", + "text": "*", + "layer": "shift" + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": "4", + "key": [ + { + "id": "K_LOWER", + "text": "*abc*", + "width": "150", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "width": "610" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "150", + "sp": "1" + } + ] + } + ] + } + ], + "displayUnderlying": false + } +}; + this.KVER="19.0.254.0"; + this.KVS=[]; + this.gs=function(t,e) { + throw new Error("javascript_error.js - runtime error"); + // return this.g_main_0(t,e); + }; + this.g_main_0=function(t,e) { + var k=KeymanWeb,r=0,m=0; + if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_QUOTE /* 0xDE */)) { + if(k.KFCM(1,t,['«'])){ + r=m=1; // Line 26 + k.KDC(1,t); + k.KO(-1,t,"»"); + } + else if(k.KFCM(1,t,['»'])){ + r=m=1; // Line 27 + k.KDC(1,t); + k.KO(-1,t,"«"); + } + else if(k.KFCM(1,t,[';'])){ + r=m=1; // Line 30 + k.KDC(1,t); + k.KO(-1,t,"\""); + } + else if(1){ + r=m=1; // Line 25 + k.KDC(0,t); + k.KO(-1,t,"«"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_QUOTE /* 0xDE */)) { + if(k.KFCM(1,t,['\''])){ + r=m=1; // Line 20 + k.KDC(1,t); + k.KO(-1,t,"‹"); + } + else if(k.KFCM(1,t,['‹'])){ + r=m=1; // Line 21 + k.KDC(1,t); + k.KO(-1,t,"›"); + } + else if(k.KFCM(1,t,['›'])){ + r=m=1; // Line 22 + k.KDC(1,t); + k.KO(-1,t,"‹"); + } + else if(k.KFCM(1,t,['<'])){ + r=m=1; // Line 33 + k.KDC(1,t); + k.KO(-1,t,"‹"); + } + else if(k.KFCM(1,t,['>'])){ + r=m=1; // Line 34 + k.KDC(1,t); + k.KO(-1,t,"›"); + } + else if(k.KFCM(1,t,['s'])){ + r=m=1; // Line 42 + k.KDC(1,t); + k.KO(-1,t,"š"); + } + else if(k.KFCM(1,t,['S'])){ + r=m=1; // Line 43 + k.KDC(1,t); + k.KO(-1,t,"Š"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_COMMA /* 0xBC */)) { + if(k.KFCM(1,t,['<'])){ + r=m=1; // Line 35 + k.KDC(1,t); + k.KO(-1,t,"«"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_PERIOD /* 0xBE */)) { + if(k.KFCM(1,t,['>'])){ + r=m=1; // Line 36 + k.KDC(1,t); + k.KO(-1,t,"»"); + } + } + return r; + }; +} diff --git a/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.keyboard_info b/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.keyboard_info new file mode 100644 index 00000000000..234993db7d2 --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.keyboard_info @@ -0,0 +1,39 @@ +{ + "id": "javascript_error", + "name": "javascript_error", + "license": "mit", + "authorName": "Marc Durdin", + "description": "

javascript_error test keyboard

", + "languages": { + "amf": { + "examples": [], + "languageName": "Hamer-Banne", + "displayName": "Hamer-Banne" + } + }, + "lastModifiedDate": "2026-07-13T07:51:37.473Z", + "packageFilename": "javascript_error.kmp", + "packageFileSize": 6898, + "jsFilename": "javascript_error.js", + "jsFileSize": 20557, + "packageIncludes": [ + "visualKeyboard", + "welcome", + "documentation" + ], + "version": "1.0", + "encodings": [ + "unicode" + ], + "platformSupport": { + "windows": "full", + "macos": "full", + "linux": "full", + "desktopWeb": "full", + "ios": "full", + "android": "full", + "mobileWeb": "full" + }, + "minKeymanVersion": "10.0", + "helpLink": "https://help.keyman.com/keyboard/javascript_error" +} \ No newline at end of file diff --git a/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.kmp b/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.kmp new file mode 100644 index 00000000000..a7e846f2456 Binary files /dev/null and b/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.kmp differ diff --git a/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.kmx b/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.kmx new file mode 100644 index 00000000000..d4fcf5dc0f4 Binary files /dev/null and b/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.kmx differ diff --git a/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.kvk b/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.kvk new file mode 100644 index 00000000000..be522b991fd Binary files /dev/null and b/common/test/keyboards/invalid/engine/javascript_error/modified_build/javascript_error.kvk differ diff --git a/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.keyman-touch-layout b/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.keyman-touch-layout new file mode 100644 index 00000000000..6bd2c4cb685 --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.keyman-touch-layout @@ -0,0 +1,701 @@ +{ + "phone": { + "font": "Tahoma", + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "text": "q" + }, + { + "id": "K_W", + "text": "w" + }, + { + "id": "K_E", + "text": "e" + }, + { + "id": "K_R", + "text": "r" + }, + { + "id": "K_T", + "text": "t" + }, + { + "id": "K_Y", + "text": "y" + }, + { + "id": "K_U", + "text": "u" + }, + { + "id": "K_I", + "text": "i" + }, + { + "id": "K_O", + "text": "o" + }, + { + "id": "K_P", + "text": "p" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_A", + "text": "a", + "pad": 50 + }, + { + "id": "K_S", + "text": "s", + "sk": [ + { + "text": "š", + "id": "U_0161" + } + ] + }, + { + "id": "K_D", + "text": "d" + }, + { + "id": "K_F", + "text": "f" + }, + { + "id": "K_G", + "text": "g" + }, + { + "id": "K_H", + "text": "h" + }, + { + "id": "K_J", + "text": "j" + }, + { + "id": "K_K", + "text": "k" + }, + { + "id": "K_L", + "text": "l" + }, + { + "id": "T_new_88", + "text": "", + "width": 10, + "sp": 10 + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "sp": 1, + "nextlayer": "shift" + }, + { + "id": "K_Z", + "text": "z" + }, + { + "id": "K_X", + "text": "x" + }, + { + "id": "K_C", + "text": "c" + }, + { + "id": "K_V", + "text": "v" + }, + { + "id": "K_B", + "text": "b" + }, + { + "id": "K_N", + "text": "n" + }, + { + "id": "K_M", + "text": "m" + }, + { + "id": "K_PERIOD", + "text": ".", + "sk": [ + { + "text": ",", + "id": "K_COMMA" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "‹", + "id": "U_2039" + }, + { + "text": "›", + "id": "U_203A" + }, + { + "text": "«", + "id": "U_00AB", + "layer": "shift" + }, + { + "text": "»", + "id": "U_00BB" + }, + { + "text": "\\", + "id": "K_BKSLASH" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": 100, + "sp": 1 + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": 150, + "sp": 1, + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 120, + "sp": 1 + }, + { + "id": "K_SPACE", + "text": "", + "width": 610, + "sp": 0 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 150, + "sp": 1 + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "text": "Q" + }, + { + "id": "K_W", + "text": "W" + }, + { + "id": "K_E", + "text": "E" + }, + { + "id": "K_R", + "text": "R" + }, + { + "id": "K_T", + "text": "T" + }, + { + "id": "K_Y", + "text": "Y" + }, + { + "id": "K_U", + "text": "U" + }, + { + "id": "K_I", + "text": "I" + }, + { + "id": "K_O", + "text": "O" + }, + { + "id": "K_P", + "text": "P" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_A", + "text": "A", + "pad": 50 + }, + { + "id": "K_S", + "text": "S", + "sk": [ + { + "text": "Š", + "id": "U_0160" + } + ] + }, + { + "id": "K_D", + "text": "D" + }, + { + "id": "K_F", + "text": "F" + }, + { + "id": "K_G", + "text": "G" + }, + { + "id": "K_H", + "text": "H" + }, + { + "id": "K_J", + "text": "J" + }, + { + "id": "K_K", + "text": "K" + }, + { + "id": "K_L", + "text": "L" + }, + { + "id": "T_new_122", + "text": "", + "width": 10, + "sp": 10 + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "sp": 2, + "nextlayer": "default" + }, + { + "id": "K_Z", + "text": "Z" + }, + { + "id": "K_X", + "text": "X" + }, + { + "id": "K_C", + "text": "C" + }, + { + "id": "K_V", + "text": "V" + }, + { + "id": "K_B", + "text": "B" + }, + { + "id": "K_N", + "text": "N" + }, + { + "id": "K_M", + "text": "M" + }, + { + "id": "K_PERIOD", + "text": ".", + "layer": "default", + "sk": [ + { + "text": ",", + "id": "K_COMMA", + "layer": "default" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "‹", + "id": "U_2039", + "layer": "default" + }, + { + "text": "›", + "id": "U_203A" + }, + { + "text": "«", + "id": "U_00AB", + "layer": "shift" + }, + { + "text": "»", + "id": "U_00BB" + }, + { + "text": "\\", + "id": "K_BKSLASH", + "layer": "default" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON", + "layer": "default" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "sp": 1 + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": 150, + "sp": 1, + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 120, + "sp": 1 + }, + { + "id": "K_SPACE", + "text": "", + "width": 610, + "sp": 0 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 150, + "sp": 1 + } + ] + } + ] + }, + { + "id": "numeric", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0", + "width": 100 + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_4", + "text": "$", + "pad": 50, + "layer": "shift" + }, + { + "id": "K_2", + "text": "@", + "layer": "shift" + }, + { + "id": "K_3", + "text": "#", + "layer": "shift" + }, + { + "id": "K_5", + "text": "%", + "layer": "shift" + }, + { + "id": "K_7", + "text": "&", + "layer": "shift" + }, + { + "id": "K_HYPHEN", + "text": "-" + }, + { + "id": "K_HYPHEN", + "text": "_", + "layer": "shift" + }, + { + "id": "K_EQUAL", + "text": "=", + "layer": "default" + }, + { + "id": "K_BKSLASH", + "text": "|", + "layer": "shift" + }, + { + "id": "K_BKSLASH", + "text": "\\", + "layer": "default" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_LBRKT", + "text": "[", + "pad": 110, + "sk": [ + { + "text": "<", + "id": "K_COMMA", + "layer": "shift" + }, + { + "text": "{", + "id": "K_LBRKT", + "layer": "shift" + } + ] + }, + { + "id": "K_9", + "text": "(", + "layer": "shift", + "sk": [ + { + "text": "‹", + "id": "U_2039" + }, + { + "text": "«", + "id": "U_00AB" + } + ] + }, + { + "id": "K_0", + "text": ")", + "layer": "shift", + "sk": [ + { + "text": "›", + "id": "U_203A" + }, + { + "text": "»", + "id": "U_00BB" + } + ] + }, + { + "id": "K_RBRKT", + "text": "]", + "sk": [ + { + "text": ">", + "id": "K_PERIOD", + "layer": "shift" + }, + { + "text": "}", + "id": "K_RBRKT", + "layer": "shift" + } + ] + }, + { + "id": "U_0027", + "text": "'" + }, + { + "id": "U_0022", + "text": "\"" + }, + { + "id": "K_EQUAL", + "text": "+", + "layer": "shift" + }, + { + "id": "K_8", + "text": "*", + "layer": "shift" + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": 100, + "sp": 1 + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_LOWER", + "text": "*abc*", + "width": 150, + "sp": 1, + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 120, + "sp": 1 + }, + { + "id": "K_SPACE", + "text": "", + "width": 610, + "sp": 0 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 150, + "sp": 1 + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.kmn b/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.kmn new file mode 100644 index 00000000000..38925963e73 --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.kmn @@ -0,0 +1,43 @@ +c banne generated from template at 2024-07-12 10:16:50 +c with name "Banne" +store(&VERSION) '10.0' +store(&NAME) 'javascript_error' +store(©RIGHT) 'Copyright © SIL' +store(&KEYBOARDVERSION) '1.0' +store(&TARGETS) 'any' +store(&VISUALKEYBOARD) 'javascript_error.kvks' +store(&LAYOUTFILE) 'javascript_error.keyman-touch-layout' + +begin Unicode > use(main) + +group(main) using keys + +c ------------------------------------------------------------------------------ +c punctuation characters +c ------------------------------------------------------------------------------ + +c Toggle between ‹ and › by continuing to input ' +"'" + "'" > U+2039 c ‹ +U+2039 + "'" > U+203A c › +U+203A + "'" > U+2039 c ‹ + +c Toggle between « and » by continuing to input " ++ '"' > U+00AB c « +U+00AB + '"' > U+00BB c » +U+00BB + '"' > U+00AB c « + +c Revert to default " character +";" + '"' > '"' + +c Use angle brackets to type characters +"<" + "'" > U+2039 c ‹ +">" + "'" > U+203A c › +"<" + "<" > U+00AB c « +">" + ">" > U+00BB c » + +c ------------------------------------------------------------------------------ +c s with caron +c ------------------------------------------------------------------------------ + +"s" + "'" > U+0161 c š +"S" + "'" > U+0160 c Š diff --git a/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.kps b/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.kps new file mode 100644 index 00000000000..56d8e9edb8b --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.kps @@ -0,0 +1,75 @@ + + + + 17.0.326.0 + 7.0 + + + + readme.htm + ..\LICENSE.md + + + + + + + + + + javascript_error + Copyright © SIL + Marc Durdin + javascript_error test keyboard + + + + + ..\build\javascript_error.kmx + + 0 + .kmx + + + ..\build\javascript_error.js + + 0 + .js + + + ..\build\javascript_error.kvk + + 0 + .kvk + + + welcome.htm + + 0 + .htm + + + readme.htm + + 0 + .htm + + + ..\LICENSE.md + + 0 + .md + + + + + javascript_error + javascript_error + 1.0 + + Hamer-Banna + + + + + diff --git a/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.kvks b/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.kvks new file mode 100644 index 00000000000..d4171d4e012 --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/source/javascript_error.kvks @@ -0,0 +1,110 @@ + + +
+ 10.0 + banne + +
+ + + ` + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 + - + = + q + w + e + r + t + y + u + i + o + p + [ + ] + \ + a + s + d + f + g + h + j + k + l + ; + ' + \ + z + x + c + v + b + n + m + , + . + / + + + ~ + ! + @ + # + $ + % + ^ + & + * + ( + ) + _ + + + Q + W + E + R + T + Y + U + I + O + P + { + } + | + A + S + D + F + G + H + J + K + L + : + « + | + Z + X + C + V + B + N + M + < + > + ? + + +
diff --git a/common/test/keyboards/invalid/engine/javascript_error/source/readme.htm b/common/test/keyboards/invalid/engine/javascript_error/source/readme.htm new file mode 100644 index 00000000000..80f2c3f038e --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/source/readme.htm @@ -0,0 +1,22 @@ + + + + + + Banne + + + + +

javascript_error

+ +

+Keyboard with a script error in `gs()`, taken originally from release/b/banne. The .js and .kmp files in modified_build/ have been manually tweaked to throw an error. +

+ + + diff --git a/common/test/keyboards/invalid/engine/javascript_error/source/welcome.htm b/common/test/keyboards/invalid/engine/javascript_error/source/welcome.htm new file mode 100644 index 00000000000..acc98782a02 --- /dev/null +++ b/common/test/keyboards/invalid/engine/javascript_error/source/welcome.htm @@ -0,0 +1,178 @@ + + + + + + Start Using Banne + + + + +

Start Using Banne

+ +

+Keyboard with a script error in `gs()`, taken originally from release/b/banne. The .js and .kmp files in modified_build/ have been manually tweaked to throw an error. +

+ +

Keyboard Layout

+

Keystrokes

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Base+Modifier=Result
s+'=š
S+'=Š
+

Punctuation

+

+ The ' key has been modified to cycle between (U+2039) and (U+203A) from the second keypress onwards. +
+ The first keypress will produce the ' (U+0027) character as expected, and pressing it again will turn the ' into a . Pressing the ' key one more time will produce a . +

+

+ The " key has been modified to automatically type the character « (U+00AB). To cancel this, pressing the ; key before typing a " will produce a normal ". +
+ Pressing the " key repeatedly will cycle between the » (U+00BB) and « characters. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Base+Modifier=Result
'+'=
<+'=
>+'=
" =«
<+<=«
>+>=»
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Base+Modifier=Result
+'=
+'=
«+"=»
»+"=«
;+"="
+
+ + + \ No newline at end of file diff --git a/common/test/resources/keyboards/test_8568_deadkeys.html b/common/test/resources/keyboards/test_8568_deadkeys.html index 054f19b4f81..4ce169551f5 100644 --- a/common/test/resources/keyboards/test_8568_deadkeys.html +++ b/common/test/resources/keyboards/test_8568_deadkeys.html @@ -6,12 +6,6 @@ - - - - - - KeymanWeb #8568 diff --git a/common/tools/api-extractor/README.md b/common/tools/api-extractor/README.md new file mode 100644 index 00000000000..0462e78bcb4 --- /dev/null +++ b/common/tools/api-extractor/README.md @@ -0,0 +1,41 @@ +# Introduction + +These files are used by `typescript_run_api_extractor()` in typescript.inc.sh. + +This is setup only for Developer projects at this time as outputs go into +developer/docs and developer/build; future generalization requires changing only +`$report_temp` and `$report_folder` parameters in +`typescript_run_api_extractor()`. + +## api-extractor.template.json + +* Reference: https://api-extractor.com + +api-extractor.template.json contains a template for api-extractor; these +parameters cannot be passed in to the tool, so we modify this template as needed +with the following parameters: + +* `$keyman_root`: the `$KEYMAN_ROOT` variable, with backslash \ translated to + forward slash / +* `$index_d_ts`: the filename `index.d.ts` or the corresponding filename for the + entry point of the project +* `$project_path`: the path for the module, relative to the base of the repo +* `$report_temp`: a temporary path for output files for api-extractor +* `$report_folder`: target folder for completed api-extractor API documentation + +## tsdoc.template.json + +* Reference: https://tsdoc.org/pages/packages/tsdoc-config/ + +tsdoc.template.json is copied (unmodified) from this folder into tsdoc.json in +project folders (alongside tsconfig.json) before running api-extractor and +removed again afterwards; there is no way to specify an alternate location for +the file. + +tsdoc.template.json includes a definition for "@since" which has been proposed +in https://github.com/microsoft/tsdoc/issues/136. + +## Notes + +See also .github/workflows/api-verification.yml which does API validation of +Core for desktop platforms. \ No newline at end of file diff --git a/developer/config/api-extractor.base.json b/common/tools/api-extractor/api-extractor.base.json similarity index 99% rename from developer/config/api-extractor.base.json rename to common/tools/api-extractor/api-extractor.base.json index 2c11f6402db..4ba43a64513 100644 --- a/developer/config/api-extractor.base.json +++ b/common/tools/api-extractor/api-extractor.base.json @@ -161,7 +161,7 @@ * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/etc/" */ - "reportFolder": "../docs/api/etc/", + // "reportFolder": "../docs/api/etc/", /** * Specifies the folder where the temporary report file is written. The file name portion is determined by @@ -176,7 +176,7 @@ * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/temp/" */ - "reportTempFolder": "../build/api/", + // "reportTempFolder": "../build/api/", /** * Whether "forgotten exports" should be included in the API report file. Forgotten exports are declarations @@ -206,7 +206,7 @@ * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/build/temp/.api.json" */ - "apiJsonFilePath": "../build/api/.api.json", + // "apiJsonFilePath": "../build/api/.api.json", /** * Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations diff --git a/common/tools/api-extractor/api-extractor.template.json b/common/tools/api-extractor/api-extractor.template.json new file mode 100644 index 00000000000..ccc76623cd2 --- /dev/null +++ b/common/tools/api-extractor/api-extractor.template.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "$keyman_root/common/tools/api-extractor/api-extractor.base.json", + "mainEntryPointFilePath": "/build/src/$index_d_ts", + "docModel": { + "enabled": true, + "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/$project_path", + "apiJsonFilePath": "$report_temp/.api.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "$report_folder/", + "reportTempFolder": "$report_temp/" + } +} diff --git a/common/tools/api-extractor/tsdoc.template.json b/common/tools/api-extractor/tsdoc.template.json new file mode 100644 index 00000000000..2a5a8d130b8 --- /dev/null +++ b/common/tools/api-extractor/tsdoc.template.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"], + "tagDefinitions": [ + { + "tagName": "@since", + "syntaxKind": "block", + "allowMultiple": false + } + ], + + "supportForTags": { + "@since": true + } +} \ No newline at end of file diff --git a/common/windows/delphi/general/Upload_Settings.pas b/common/windows/delphi/general/Upload_Settings.pas index 8811ec6f10d..2763a042be4 100644 --- a/common/windows/delphi/general/Upload_Settings.pas +++ b/common/windows/delphi/general/Upload_Settings.pas @@ -86,6 +86,8 @@ function API_UserAgent: string; // = 'Keyman for Windows/...' function API_UserAgent_Developer: string; // = 'Keyman Developer/...' function API_UserAgent_Diagnostics: string; +function API_Path_Keyboard(const id: string): string; + function KeymanCom_Protocol_Server: string; // = 'https://keyman.com'; function MakeAPIURL(path: string): string; @@ -121,6 +123,8 @@ implementation S_KeymanCom_Staging = 'https://keyman.com'; // #7227 disabling: 'https://keyman-staging.com'; S_APIServer_Staging = 'api.keyman.com'; // #7227 disabling: 'api.keyman-staging.com'; + S_API_Path_Keyboard = '/keyboard/%0:s'; + const URLPath_PackageDownload_Format = '/go/package/download/%0:s?platform=windows&tier=%1:s&bcp47=%2:s&update=%3:d'; URL_KeymanDeveloper_HelpKmcMessage_Format = S_Host_KmnSh+'/%0:s'; @@ -187,4 +191,9 @@ function URL_KmcMessage(const id: string): string; Result := Format(URL_KeymanDeveloper_HelpKmcMessage_Format, [id.ToLower]); end; +function API_Path_Keyboard(const id: string): string; +begin + Result := Format(S_API_Path_Keyboard, [id]); +end; + end. diff --git a/developer/docs/api/etc/kmc-copy.api.md b/developer/docs/api/etc/kmc-copy.api.md index f61dd59e108..3ec2789a6c3 100644 --- a/developer/docs/api/etc/kmc-copy.api.md +++ b/developer/docs/api/etc/kmc-copy.api.md @@ -214,7 +214,7 @@ export interface CopierOptions extends CompilerBaseOptions { relocateExternalFiles?: boolean; } -// @public (undocumented) +// @public export class KeymanProjectCopier implements KeymanCompiler { // Warning: (ae-forgotten-export) The symbol "CopierAsyncCallbacks" needs to be exported by the entry point main.d.ts // diff --git a/developer/docs/api/etc/kmc-kmn.api.md b/developer/docs/api/etc/kmc-kmn.api.md index 0805075e56a..b4372a99c25 100644 --- a/developer/docs/api/etc/kmc-kmn.api.md +++ b/developer/docs/api/etc/kmc-kmn.api.md @@ -933,21 +933,21 @@ declare namespace Osk { } export { Osk } -// @public (undocumented) +// @public function parseMapping(mapping: any): PuaMap; -// @public (undocumented) +// @public type PuaMap = { [index: string]: string; }; -// @public (undocumented) +// @public function remapTouchLayout(source: TouchLayout.TouchLayoutFile, map: PuaMap): boolean; -// @public (undocumented) +// @public function remapVisualKeyboard(vk: VisualKeyboard.VisualKeyboard, map: PuaMap): boolean; -// @public (undocumented) +// @public interface StringRef { // (undocumented) str: string; @@ -955,7 +955,7 @@ interface StringRef { usages: StringRefUsage[]; } -// @public (undocumented) +// @public interface StringRefUsage { // (undocumented) count: number; diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index f2c2b9e7d0c..a60bba6f25a 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -79,3 +79,5 @@ export { getFontFamily, getFontFamilySync } from './font-family.js'; export * as ValidIds from './valid-ids.js'; export * as ProjectLoader from './project-loader.js'; + +export { optionsManager, KeymanDeveloperOption, KeymanDeveloperOptions, KeymanDeveloperOptionsPath } from './keyman-developer-options.js'; diff --git a/developer/src/common/web/utils/src/keyman-developer-options.ts b/developer/src/common/web/utils/src/keyman-developer-options.ts new file mode 100644 index 00000000000..3117ab8633b --- /dev/null +++ b/developer/src/common/web/utils/src/keyman-developer-options.ts @@ -0,0 +1,128 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * User options for Keyman Developer. These are stored in options.json in the + * user profile; the location varies by operating system or may be stored in + * browser storage on web sites. + * + * The node-based loader is implemented in both kmc and Keyman Developer Server, + * in order to keep node dependencies out of the developer-utils module. + */ + +/** + * The standard path under the user profile where options.json is stored; use + * `path.join(os.homedir(), ...KeymanDeveloperOptionsPath)` or similar + */ +export const KeymanDeveloperOptionsPath = [/* '~', */ '.keymandeveloper', 'options.json']; + +/** + * The set of standard user options for Keyman Developer. Corresponds to + * TKeymanDeveloperOptions in `developer/src/tike/main/KeymanDeveloperOptions.pas` + */ +export interface KeymanDeveloperOptions { + "use tab char": boolean; + "link font sizes": boolean; + "indent size": number; + "use old debugger": boolean; + "editor theme": string; + "debugger break when exiting line": boolean; + "debugger single step after break": boolean; + "debugger show store offset": boolean; + "debugger recompile with debug info": boolean; + "debugger auto reset before compilng": boolean; + "auto save before compiling": boolean; + "osk auto save before importing": boolean; + "web host port": number; + "server keep alive": boolean; + "server use local addresses": boolean; + "server ngrok token": string; + "server ngrok region": string; + "server use ngrok": boolean; + "server show console window": boolean; + "char map disable database lookups": boolean; + "char map auto lookup": boolean; + "open keyboard files in source view": boolean; + "display theme": string; + "external editor path": string; + "smtp server": string; + "test email addresses": string; + "web ladder length": number; + "default project path": string; + "automatically report errors": boolean; + "automatically report usage": boolean; + "toolbar visible": boolean; + "active project": string; + "prompt to upgrade projects": boolean; +}; + +/** + * A single Keyman Developer user option. + */ +export type KeymanDeveloperOption = keyof KeymanDeveloperOptions; + +const DEFAULT_OPTIONS: KeymanDeveloperOptions = { + // Corresponds to TKeymanDeveloperOptions.Read in KeymanDeveloperOptions.pas + "use tab char": false, + "link font sizes": true, + "indent size": 4, + "use old debugger": false, + "editor theme": '', + "debugger break when exiting line": true, + "debugger single step after break": false, + "debugger show store offset": false, + "debugger recompile with debug info": false, + "debugger auto reset before compilng": false, + "auto save before compiling": false, + "osk auto save before importing": false, + "web host port": 8008, + "server keep alive": false, + "server use local addresses": true, + "server ngrok token": '', + "server ngrok region": '', + "server use ngrok": false, + "server show console window": false, + "char map disable database lookups": false, + "char map auto lookup": true, + "open keyboard files in source view": false, + "display theme": 'Windows10', + "external editor path": '', + "smtp server": '', + "test email addresses": '', + "web ladder length": 100, + "default project path": '', // Note: this diverges from Delphi code, which uses CSIDL_PERSONAL on Windows, but it is not used in Server + "automatically report errors": true, + "automatically report usage": true, + "toolbar visible": true, + "active project": '', + "prompt to upgrade projects": true, +} + + +class KeymanDeveloperOptionsManager { + private options: KeymanDeveloperOptions = {...DEFAULT_OPTIONS}; + constructor() {} + + public load(blob: Uint8Array | null) { + this.options = {...DEFAULT_OPTIONS}; + if(blob !== null && blob !== undefined) { + const data = JSON.parse(new TextDecoder('utf-8').decode(blob)); + if(typeof data == 'object') { + // TODO: verify fields in options + this.options = {...DEFAULT_OPTIONS, ...data}; + return true; + } + } + return false; + } + + public get(valueName: T): KeymanDeveloperOptions[T] { + return this.options[valueName]; + } + + public clear() { + this.options = {...DEFAULT_OPTIONS}; + } +} + +export const optionsManager = new KeymanDeveloperOptionsManager(); + diff --git a/developer/src/kmc-analyze/build.sh b/developer/src/kmc-analyze/build.sh index f28c822b5a3..1bfde715855 100755 --- a/developer/src/kmc-analyze/build.sh +++ b/developer/src/kmc-analyze/build.sh @@ -27,5 +27,5 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-analyze index.d.ts builder_run_action test typescript_run_eslint_mocha_tests 75 diff --git a/developer/src/kmc-analyze/config/api-extractor.json b/developer/src/kmc-analyze/config/api-extractor.json deleted file mode 100644 index 855dc1f4471..00000000000 --- a/developer/src/kmc-analyze/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/index.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-analyze" - } -} diff --git a/developer/src/kmc-analyze/src/osk-character-use/index.ts b/developer/src/kmc-analyze/src/osk-character-use/index.ts index 7058ad67760..4343a3b8c87 100644 --- a/developer/src/kmc-analyze/src/osk-character-use/index.ts +++ b/developer/src/kmc-analyze/src/osk-character-use/index.ts @@ -304,7 +304,7 @@ export class AnalyzeOskCharacterUse { * * - .json: returns the final aggregated data as an array of strings, which * can be joined to form a JSON blob of an object with a single member, - * `map`, which is an array of {@link Osk.StringResult} objects. + * `map`, which is an array of {@link @keymanapp/kmc-kmn#Osk.StringResult} objects. * * @param format - file format to return - can be '.txt', '.md', or '.json' * @returns an array of strings, formatted according to the `format` @@ -324,7 +324,7 @@ export class AnalyzeOskCharacterUse { /** * Load a JSON-format result file to merge from - * @param filename + * @param filename - the full path to the JSON result file to load * @returns */ private loadPreviousMap(filename: string): Osk.StringResult[] { diff --git a/developer/src/kmc-copy/build.sh b/developer/src/kmc-copy/build.sh index fe9ffc10868..f3bc50c85a0 100755 --- a/developer/src/kmc-copy/build.sh +++ b/developer/src/kmc-copy/build.sh @@ -33,7 +33,7 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-copy main.d.ts # note: `export TEST_SAVE_ARTIFACTS=1` to save a copy of artifacts to temp path # note: `export TEST_SAVE_FIXTURES=1` to get a copy of cloud-based fixtures saved to online/ diff --git a/developer/src/kmc-copy/config/api-extractor.json b/developer/src/kmc-copy/config/api-extractor.json deleted file mode 100644 index 12a33719245..00000000000 --- a/developer/src/kmc-copy/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-copy" - } -} diff --git a/developer/src/kmc-copy/src/KeymanProjectCopier.ts b/developer/src/kmc-copy/src/KeymanProjectCopier.ts index 4218cc6e941..d9bd33ae91b 100644 --- a/developer/src/kmc-copy/src/KeymanProjectCopier.ts +++ b/developer/src/kmc-copy/src/KeymanProjectCopier.ts @@ -81,6 +81,10 @@ export interface CopierResult extends KeymanCompilerResult { artifacts: CopierArtifacts; }; +/** + * @public + * Copy a project and rename internal references + */ export class KeymanProjectCopier implements KeymanCompiler { options: CopierOptions; callbacks: CompilerCallbacks; @@ -111,8 +115,8 @@ export class KeymanProjectCopier implements KeymanCompiler { * artifacts on success. The files are passed in by name, and the compiler * will use callbacks as passed to the {@link KeymanProjectCopier.init} * function to read any input files by disk. - * @param source Source file or folder to copy. Can be a local file or folder, https://github.com/.../repo[/path], or cloud:id - * @returns Binary artifacts on success, null on failure. + * @param source - Source file or folder to copy. Can be a local file or folder, https://github.com/.../repo[/path], or cloud:id + * @returns Binary artifacts on success, null on failure. */ public async run(source: string): Promise { @@ -174,7 +178,7 @@ export class KeymanProjectCopier implements KeymanCompiler { /** * Resolve the source project file to either a local filesystem file, * or a reference on GitHub - * @param source + * @param source - URI to a project file * @returns path to .kpj (either local or remote) */ private async getSourceProject(source: string): Promise { @@ -196,7 +200,7 @@ export class KeymanProjectCopier implements KeymanCompiler { /** * Resolve source path to the contained project file; the project * file must have the same basename as the folder in this case - * @param source + * @param source - local file path to a .kpj project file * @returns */ private getLocalFolderProject(source: string): string { @@ -212,7 +216,7 @@ export class KeymanProjectCopier implements KeymanCompiler { /** * Resolve source path to the input .kpj filename, folder name * is not relevant when .kpj filename is passed in - * @param source + * @param source - local file path to a .kpj project file * @returns */ private getLocalFileProject(source: string): string { @@ -224,7 +228,7 @@ export class KeymanProjectCopier implements KeymanCompiler { * `[https://]github.com/owner/repo/branch/path/to/kpj` * The path must be fully qualified, referencing the .kpj file; it * cannot just be the folder where the .kpj is found - * @param source + * @param source - URL to a .kpj project file on GitHub * @returns a promise: GitHub reference to the source for the keyboard, or null on failure */ private async getGitHubSourceProject(source: string): Promise { @@ -273,7 +277,7 @@ export class KeymanProjectCopier implements KeymanCompiler { * The `keyboard_id` parameter should be a valid id (a-z0-9_), as found at * https://keyman.com/keyboards; alternatively if it is a model_id, it should * have the format author.bcp47.uniq - * @param source + * @param source - a reference to a keyboard or model project on Keyman Cloud * @returns a promise: GitHub reference to the source for the keyboard, or null on failure */ private async getCloudSourceProject(source: string): Promise { @@ -613,8 +617,8 @@ export class KeymanProjectCopier implements KeymanCompiler { /** * renames matching filename to the output filename pattern, and prepends the * outputPath - * @param filename - * @param outputPath + * @param filename - input filename to rename + * @param outputPath - target filename path * @returns */ private generateNewFilename(filename: string, outputPath: string): string { diff --git a/developer/src/kmc-generate/build.sh b/developer/src/kmc-generate/build.sh index d67cbaf21a1..323c78937b6 100755 --- a/developer/src/kmc-generate/build.sh +++ b/developer/src/kmc-generate/build.sh @@ -39,5 +39,5 @@ do_build() { builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build do_build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-generate main.d.ts builder_run_action test typescript_run_eslint_mocha_tests diff --git a/developer/src/kmc-generate/config/api-extractor.json b/developer/src/kmc-generate/config/api-extractor.json deleted file mode 100644 index 5ab7b50e162..00000000000 --- a/developer/src/kmc-generate/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-generate" - } -} diff --git a/developer/src/kmc-keyboard-info/build.sh b/developer/src/kmc-keyboard-info/build.sh index fe8ecfac583..abd4604d96a 100755 --- a/developer/src/kmc-keyboard-info/build.sh +++ b/developer/src/kmc-keyboard-info/build.sh @@ -32,7 +32,7 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-keyboard-info index.d.ts builder_run_action test typescript_run_eslint_mocha_tests #------------------------------------------------------------------------------------------------------------------- diff --git a/developer/src/kmc-keyboard-info/config/api-extractor.json b/developer/src/kmc-keyboard-info/config/api-extractor.json deleted file mode 100644 index b98feb9963c..00000000000 --- a/developer/src/kmc-keyboard-info/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/index.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-keyboard-info" - } -} diff --git a/developer/src/kmc-kmn/build.sh b/developer/src/kmc-kmn/build.sh index deb57d283e8..4444b3fa6e2 100755 --- a/developer/src/kmc-kmn/build.sh +++ b/developer/src/kmc-kmn/build.sh @@ -76,5 +76,5 @@ function do_test() { } builder_run_action build do_build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-kmn main.d.ts builder_run_action test do_test diff --git a/developer/src/kmc-kmn/config/api-extractor.json b/developer/src/kmc-kmn/config/api-extractor.json deleted file mode 100644 index 5df197da3e9..00000000000 --- a/developer/src/kmc-kmn/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-kmn" - } -} diff --git a/developer/src/kmc-kmn/src/compiler/osk.ts b/developer/src/kmc-kmn/src/compiler/osk.ts index 81f97280859..f58c95bed26 100644 --- a/developer/src/kmc-kmn/src/compiler/osk.ts +++ b/developer/src/kmc-kmn/src/compiler/osk.ts @@ -4,17 +4,27 @@ import { SchemaValidators } from "@keymanapp/common-types"; import { KmnCompilerMessages } from "./kmn-compiler-messages.js"; import { CompilerCallbacks, KvksFileReader } from "@keymanapp/developer-utils"; +/** + * @public + * Records the number of references to an OSK key cap string for a specific + * file + */ export interface StringRefUsage { filename: string; count: number; }; +/** + * @public + * Tracks usage of a single OSK key cap string across multiple files + */ export interface StringRef { str: string; usages: StringRefUsage[]; }; /** + * @public * Represents a single key cap found by `AnalyzeOskCharacterUse` */ export interface StringResult { @@ -25,13 +35,23 @@ export interface StringResult { /** hexadecimal single character in PUA range, without 'U+' prefix, e.g. 'F100' */ pua: string; /** files in which the string is referenced; will be an array of - * {@link StringRefUsage} if includeCounts is true, otherwise will be an array + * {@link @keymanapp/kmc-kmn#Osk.StringRefUsage} if includeCounts is true, otherwise will be an array * of strings listing files in which the key cap may be found */ usages: StringRefUsage[] | string[]; }; +/** + * @public + * Maps a source OSK key cap string to a PUA character + */ export type PuaMap = {[index:string]: string}; +/** + * @public + * Parse a map object loaded from a displaymap file into a PuaMap + * @param mapping - source object to parse, must be in displayMap JSON format + * @returns + */ export function parseMapping(mapping: any) { if(!SchemaValidators.default.displayMap(mapping)) /* c8 ignore next 3 */ @@ -62,6 +82,13 @@ function remap(text: string, map: PuaMap) { return text; } +/** + * @public + * Remap key caps in the `vk` visual keyboard object to use PUA characters from `map` + * @param vk - source visual keyboard object to remap, updated in place + * @param map - PUA string mapping to apply + * @returns + */ export function remapVisualKeyboard(vk: VisualKeyboard.VisualKeyboard, map: PuaMap): boolean { let dirty = false; for(const key of vk.keys) { @@ -75,6 +102,13 @@ export function remapVisualKeyboard(vk: VisualKeyboard.VisualKeyboard, map: PuaM return dirty; } +/** + * @public + * Remap key caps in the `source` touch layout object to use PUA characters from `map` + * @param source - source touch layout object to remap, updated in place + * @param map - PUA string mapping to apply + * @returns + */ export function remapTouchLayout(source: TouchLayout.TouchLayoutFile, map: PuaMap) { let dirty = false; const scanKey = (key: TouchLayout.TouchLayoutKey | TouchLayout.TouchLayoutSubKey) => { diff --git a/developer/src/kmc-ldml/build.sh b/developer/src/kmc-ldml/build.sh index 99cfb68cd12..4c653544b6a 100755 --- a/developer/src/kmc-ldml/build.sh +++ b/developer/src/kmc-ldml/build.sh @@ -85,5 +85,5 @@ builder_run_action clean do_clean builder_run_action configure do_configure builder_run_action build do_build builder_run_action build-fixtures do_build_fixtures -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-ldml main.d.ts builder_run_action test typescript_run_eslint_mocha_tests 85 diff --git a/developer/src/kmc-ldml/config/api-extractor.json b/developer/src/kmc-ldml/config/api-extractor.json deleted file mode 100644 index 9022460bf8a..00000000000 --- a/developer/src/kmc-ldml/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-ldml" - } -} diff --git a/developer/src/kmc-model-info/build.sh b/developer/src/kmc-model-info/build.sh index fb4743700f4..75b1d5d17e2 100755 --- a/developer/src/kmc-model-info/build.sh +++ b/developer/src/kmc-model-info/build.sh @@ -29,5 +29,5 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-model-info index.d.ts builder_run_action test typescript_run_eslint_mocha_tests 55 diff --git a/developer/src/kmc-model-info/config/api-extractor.json b/developer/src/kmc-model-info/config/api-extractor.json deleted file mode 100644 index 73bd2798926..00000000000 --- a/developer/src/kmc-model-info/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/index.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-model-info" - } -} diff --git a/developer/src/kmc-model/build.sh b/developer/src/kmc-model/build.sh index 4b5f15cc307..c01a4cdb765 100755 --- a/developer/src/kmc-model/build.sh +++ b/developer/src/kmc-model/build.sh @@ -36,5 +36,5 @@ function do_build() { builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build do_build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-model main.d.ts builder_run_action test typescript_run_eslint_mocha_tests diff --git a/developer/src/kmc-model/config/api-extractor.json b/developer/src/kmc-model/config/api-extractor.json deleted file mode 100644 index de898e137df..00000000000 --- a/developer/src/kmc-model/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-model" - } -} diff --git a/developer/src/kmc-package/build.sh b/developer/src/kmc-package/build.sh index f1d6ba8dcc0..fe2102ff404 100755 --- a/developer/src/kmc-package/build.sh +++ b/developer/src/kmc-package/build.sh @@ -34,5 +34,5 @@ builder_parse "$@" builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure node_select_version_and_npm_ci builder_run_action build tsc --build -builder_run_action api api-extractor run --local --verbose +builder_run_action api typescript_run_api_extractor developer/src/kmc-package main.d.ts builder_run_action test typescript_run_eslint_mocha_tests diff --git a/developer/src/kmc-package/config/api-extractor.json b/developer/src/kmc-package/config/api-extractor.json deleted file mode 100644 index 6ba3ee01fd1..00000000000 --- a/developer/src/kmc-package/config/api-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../../config/api-extractor.base.json", - "mainEntryPointFilePath": "/build/src/main.d.ts", - "docModel": { - "enabled": true, - "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-package" - } -} diff --git a/developer/src/kmc/src/commands/buildClasses/BuildProject.ts b/developer/src/kmc/src/commands/buildClasses/BuildProject.ts index 76fdfe1d04d..6d51a3914ac 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildProject.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildProject.ts @@ -46,7 +46,7 @@ class ProjectBuilder { // Give a hint if the project is v1.0 if(this.project.options.version != '2.0') { - if(getOption("prompt to upgrade projects", true)) { + if(getOption("prompt to upgrade projects")) { this.callbacks.reportMessage(InfrastructureMessages.Hint_ProjectIsVersion10()); } } diff --git a/developer/src/kmc/src/util/KeymanSentry.ts b/developer/src/kmc/src/util/KeymanSentry.ts index 9ee05ecb7fa..658f750d37c 100644 --- a/developer/src/kmc/src/util/KeymanSentry.ts +++ b/developer/src/kmc/src/util/KeymanSentry.ts @@ -23,7 +23,7 @@ export class KeymanSentry { return true; } - return getOption('automatically report errors', true); + return getOption('automatically report errors'); } static init(options?: SentryNodeOptions) { diff --git a/developer/src/kmc/src/util/options.ts b/developer/src/kmc/src/util/options.ts index 9d1a93895ef..51217628cf2 100644 --- a/developer/src/kmc/src/util/options.ts +++ b/developer/src/kmc/src/util/options.ts @@ -1,69 +1,32 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Load Keyman Developer's options from the standard Options location. This + * small loader is duplicated in Keyman Developer Server, because we do not have + * a shared node-aware module at this time. + */ + import * as os from 'node:os'; import * as fs from 'node:fs'; import * as path from 'node:path'; +import { KeymanDeveloperOption, KeymanDeveloperOptions, KeymanDeveloperOptionsPath, optionsManager } from '@keymanapp/developer-utils'; -export interface KeymanDeveloperOptions { - "use tab char"?: boolean; - "link font sizes"?: boolean; - "indent size"?: number; - "use old debugger"?: boolean; - "editor theme"?: string; - "debugger break when exiting line"?: boolean; - "debugger single step after break"?: boolean; - "debugger show store offset"?: boolean; - "debugger recompile with debug info"?: boolean; - "debugger auto reset before compilng"?: boolean; - "auto save before compiling"?: boolean; - "osk auto save before importing"?: boolean; - "web host port"?: number; - "server keep alive"?: boolean; - "server use local addresses"?: boolean; - "server ngrok token"?: string; - "server ngrok region"?: string; - "server use ngrok"?: boolean; - "server show console window"?: boolean; - "char map disable database lookups"?: boolean; - "char map auto lookup"?: boolean; - "open keyboard files in source view"?: boolean; - "display theme"?: string; - "external editor path"?: string; - "smtp server"?: string; - "test email addresses"?: string; - "web ladder length"?: number; - "default project path"?: string; - "automatically report errors"?: boolean; - "automatically report usage"?: boolean; - "toolbar visible"?: boolean; - "active project"?: string; - "prompt to upgrade projects"?: boolean; -}; - -type KeymanDeveloperOption = keyof KeymanDeveloperOptions; - -// Default has no options set, and unit tests will use the defaults (won't call -// `loadOptions()`) -let options: KeymanDeveloperOptions = {}; - -// We only load the options from disk once on first use -let optionsLoaded = false; +let optionsLoaded: boolean = false; -export async function loadOptions(): Promise { +export async function loadOptions(): Promise { if(optionsLoaded) { - return options; + return true; } + optionsLoaded = true; - options = {}; try { - const optionsFile = path.join(os.homedir(), '.keymandeveloper', 'options.json'); + const optionsFile = path.join(os.homedir(), ...KeymanDeveloperOptionsPath); if(fs.existsSync(optionsFile)) { for(let i = 0; i < 5; i++) { try { - const data = JSON.parse(fs.readFileSync(optionsFile, 'utf-8')); - if(typeof data == 'object') { - options = data; - } - break; - } catch(e) { + const data = fs.readFileSync(optionsFile) as Uint8Array; + return optionsManager.load(data); + } catch(e: any) { if(e?.code == 'EBUSY') { await new Promise(resolve => setTimeout(resolve, 500)); } else { @@ -76,20 +39,20 @@ export async function loadOptions(): Promise { } catch(e) { // Nothing to report here, sadly -- because we cannot rely on Sentry at this // low level. - options = {}; } - optionsLoaded = true; - return options; + + optionsManager.clear(); + return false; } -export function getOption(valueName: T, defaultValue: KeymanDeveloperOptions[T]): KeymanDeveloperOptions[T] { - return options[valueName] ?? defaultValue; +export function getOption(valueName: T): KeymanDeveloperOptions[T] { + return optionsManager.get(valueName); } /** * unit tests will clear options before running, for consistency */ export function clearOptions() { - options = {}; - optionsLoaded = true; + optionsLoaded = false; + return optionsManager.clear(); } \ No newline at end of file diff --git a/developer/src/kmconvert/Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas b/developer/src/kmconvert/Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas index 5f19d98a2ae..4f3b9d41358 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas @@ -319,6 +319,23 @@ function TVisualKeyboardToTouchLayoutConverter.SetupModifierKeysForImportedLayou for l in p.Layers do begin + // Find the Shift key and assign the next layer + k := l.FindKeyById('K_SHIFT'); + if Assigned(k) then + begin + if l.id = 'default' then + begin + k.NextLayer := 'shift'; + end + else + begin + // All layers other than default will return to default layer + // when shift is pressed, because we do not currently map + // shift+other mod layers with use of the Shift key in the import + k.NextLayer := 'default'; + end; + end; + // Find the Ctrl key for the layer k := l.FindKeyById('K_LCONTROL'); if not Assigned(k) then diff --git a/developer/src/server/src/KeymanSentry.ts b/developer/src/server/src/KeymanSentry.ts index 9dba30a0d41..b531e802357 100644 --- a/developer/src/server/src/KeymanSentry.ts +++ b/developer/src/server/src/KeymanSentry.ts @@ -23,7 +23,7 @@ export class KeymanSentry { return true; } - return getOption('automatically report errors', true); + return getOption('automatically report errors'); } static init(options?: SentryNodeOptions) { diff --git a/developer/src/server/src/config.ts b/developer/src/server/src/config.ts deleted file mode 100644 index e60ec6d0a04..00000000000 --- a/developer/src/server/src/config.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { mkdirSync } from 'fs'; -import { loadJsonFile } from './load-json-file.js'; - -export class Configuration { - public readonly appDataPath: string; - public readonly cachePath: string; - public readonly cacheStateFilename: string; - public readonly lockFilename: string; - public readonly pidFilename: string; - public readonly configFilename: string; - - /* Configuration values - set in config.json by TIKE */ - - public readonly port: number; - - /* ngrok Configuration */ - - public readonly useNgrok: boolean; - public readonly ngrokToken: string; - public readonly ngrokVisible: boolean; - - public ngrokEndpoint: string = ''; - - constructor() { - - this.appDataPath = (process.env.APPDATA || - (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share")) + - '/Keyman/Keyman Developer/Server/'; - this.cachePath = this.appDataPath + 'cache/'; - this.cacheStateFilename = this.appDataPath + 'cache.json'; - this.lockFilename = this.appDataPath + 'lock.json'; - this.pidFilename = this.appDataPath + 'pid.json'; - this.configFilename = this.appDataPath + 'config.json'; - - mkdirSync(this.cachePath, { recursive: true}); - - const cfg = loadJsonFile(this.configFilename); - - this.port = cfg?.port ?? 8008; - - // ngrok configuration - this.useNgrok = cfg?.useNgrok ?? false; - this.ngrokToken = cfg?.ngrokToken ?? ''; - this.ngrokVisible = cfg?.ngrokVisible ?? false; - } -}; - -export const configuration = new Configuration(); \ No newline at end of file diff --git a/developer/src/server/src/data.ts b/developer/src/server/src/data.ts index 6600d57b4d9..8d2753ea214 100644 --- a/developer/src/server/src/data.ts +++ b/developer/src/server/src/data.ts @@ -1,5 +1,5 @@ import { writeFileSync } from 'fs'; -import { configuration } from './config.js'; +import { standardPaths } from './standardPaths.js'; import { loadJsonFile } from './load-json-file.js'; export interface DebugObject { @@ -97,7 +97,7 @@ export class SiteData { } private loadState() { - const state = loadJsonFile(configuration.cacheStateFilename); + const state = loadJsonFile(standardPaths.cacheStateFilename); this.loadDebugObject(DebugKeyboard, state?.keyboards, this.keyboards); this.loadDebugObject(DebugModel, state?.models, this.models); this.loadDebugObject(DebugFont, state?.fonts, this.fonts); @@ -106,7 +106,7 @@ export class SiteData { } public saveState() { - writeFileSync(configuration.cacheStateFilename, JSON.stringify(this, null, 2), 'utf-8'); + writeFileSync(standardPaths.cacheStateFilename, JSON.stringify(this, null, 2), 'utf-8'); } }; diff --git a/developer/src/server/src/handlers/api/debugobject/register.ts b/developer/src/server/src/handlers/api/debugobject/register.ts index a1f7a72cd4c..bb28c87a586 100644 --- a/developer/src/server/src/handlers/api/debugobject/register.ts +++ b/developer/src/server/src/handlers/api/debugobject/register.ts @@ -2,7 +2,7 @@ import * as express from 'express'; import { DebugObject, isValidId, simplifyId } from "../../../data.js"; import * as fs from 'fs'; import * as crypto from 'crypto'; -import { configuration } from '../../../config.js'; +import { standardPaths } from '../../../standardPaths.js'; import chalk from 'chalk'; // We allow only 12 objects of each type in the cache @@ -41,7 +41,7 @@ export function apiRegisterFile (intf: new () => O, root: o.lastUse = new Date(); o.id = id; - o.filename = configuration.cachePath + o.filenameFromId(id); + o.filename = standardPaths.cachePath + o.filenameFromId(id); fs.writeFileSync(o.filename, file); o.sha256 = crypto.createHash('sha256').update(file).digest('hex'); diff --git a/developer/src/server/src/index.ts b/developer/src/server/src/index.ts index 36fd8431d8b..e248b19625a 100644 --- a/developer/src/server/src/index.ts +++ b/developer/src/server/src/index.ts @@ -5,12 +5,12 @@ import express from 'express'; import multer from 'multer'; import * as ws from 'ws'; import { KeymanSentry } from './KeymanSentry.js'; -import { configuration } from './config.js'; +import { standardPaths } from './standardPaths.js'; import { environment } from './environment.js'; import setupRoutes from './routes.js'; import { shutdown } from './shutdown.js'; import { initTray } from './tray.js'; -import { loadOptions } from './options.js'; +import { getOption, loadOptions } from './options.js'; const options = { ngrokLog: false, // Set this to true if you need to see ngrok logs in the console @@ -18,13 +18,13 @@ const options = { /* Lock file - report on PID and prevent multiple instances cleanly */ -console.log(`Starting Keyman Developer Server ${environment.versionWithTag}, listening on port ${configuration.port}.`); - // We need to load the Keyman Developer options before attempting to initialize // Sentry. `loadOptions` silently suppresses exceptions and returns a default // set of options if an error occurs. await loadOptions(); +console.log(`Starting Keyman Developer Server ${environment.versionWithTag}, listening on port ${getOption('web host port')}.`); + KeymanSentry.init(); try { await run(); @@ -80,10 +80,11 @@ export async function run() { let server = null; try { - server = app.listen(configuration.port); + server = app.listen(getOption("web host port")); } catch(err) { console.error(err); // TODO handle and cleanup EADDRINUSE, throw anything else + return; } /* Attach the web socket server */ @@ -96,14 +97,14 @@ export async function run() { /* Launch ngrok if enabled */ - configuration.ngrokEndpoint = ''; - if(configuration.useNgrok) { + standardPaths.ngrokEndpoint = ''; + if(getOption("server use ngrok")) { await startNGrok(); } /* Load the tray icon */ - tray.start(configuration.port, configuration.ngrokEndpoint); + tray.start(getOption("web host port"), standardPaths.ngrokEndpoint); } async function loadNGrok() { @@ -129,8 +130,8 @@ async function startNGrok() { let started = false; const listener = await ngrok.forward({ proto: 'http', - addr: configuration.port, - authtoken: configuration.ngrokToken, + addr: getOption("web host port"), + authtoken: getOption("server ngrok token"), onLogEvent: (msg: string) => { if(options.ngrokLog) { console.log(chalk.cyan(('\n'+msg).split('\n').join('\n[ngrok] ').trim())); @@ -139,19 +140,19 @@ async function startNGrok() { onStatusChange: (state: string) => { if(state == 'connected' && started) { // We only announce reconnection after initial start - configuration.ngrokEndpoint = listener.url() ?? ''; - console.log(chalk.blueBright('ngrok tunnel reconnected at %s'), configuration.ngrokEndpoint); + standardPaths.ngrokEndpoint = listener.url() ?? ''; + console.log(chalk.blueBright('ngrok tunnel reconnected at %s'), standardPaths.ngrokEndpoint); } else if(state == 'closed') { - configuration.ngrokEndpoint = ''; + standardPaths.ngrokEndpoint = ''; console.log(chalk.blueBright('ngrok tunnel closed')); } } }); started = true; - configuration.ngrokEndpoint = listener.url(); - console.log(chalk.blueBright('ngrok tunnel established at %s'), configuration.ngrokEndpoint); + standardPaths.ngrokEndpoint = listener.url() ?? ""; + console.log(chalk.blueBright('ngrok tunnel established at %s'), standardPaths.ngrokEndpoint); } catch(e) { - configuration.ngrokEndpoint = ''; + standardPaths.ngrokEndpoint = ''; console.error(chalk.red('ngrok tunnel failed to connect with an error: %s'), e); return false; } @@ -168,8 +169,8 @@ function getRunningInstancePid(pidFilename: string) { } function writeLockFile() { - const lockFilename = configuration.lockFilename.replaceAll(/[\\\/]/g, path.sep); - const pidFilename = configuration.pidFilename.replaceAll(/[\\\/]/g, path.sep); + const lockFilename = standardPaths.lockFilename.replaceAll(/[\\\/]/g, path.sep); + const pidFilename = standardPaths.pidFilename.replaceAll(/[\\\/]/g, path.sep); // console.debug(`Testing existence of ${lockFilename}`); if(fs.existsSync(lockFilename)) { diff --git a/developer/src/server/src/options.ts b/developer/src/server/src/options.ts index 45799ba5f29..a911283be39 100644 --- a/developer/src/server/src/options.ts +++ b/developer/src/server/src/options.ts @@ -1,70 +1,32 @@ -// TODO: this is duplicated in kmc +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Load Keyman Developer's options from the standard Options location. This + * small loader is duplicated in kmc, because we do not have a shared node-aware + * module at this time. + */ + import * as os from 'node:os'; import * as fs from 'node:fs'; import * as path from 'node:path'; +import { KeymanDeveloperOption, KeymanDeveloperOptions, KeymanDeveloperOptionsPath, optionsManager } from '@keymanapp/developer-utils'; -export interface KeymanDeveloperOptions { - "use tab char"?: boolean; - "link font sizes"?: boolean; - "indent size"?: number; - "use old debugger"?: boolean; - "editor theme"?: string; - "debugger break when exiting line"?: boolean; - "debugger single step after break"?: boolean; - "debugger show store offset"?: boolean; - "debugger recompile with debug info"?: boolean; - "debugger auto reset before compilng"?: boolean; - "auto save before compiling"?: boolean; - "osk auto save before importing"?: boolean; - "web host port"?: number; - "server keep alive"?: boolean; - "server use local addresses"?: boolean; - "server ngrok token"?: string; - "server ngrok region"?: string; - "server use ngrok"?: boolean; - "server show console window"?: boolean; - "char map disable database lookups"?: boolean; - "char map auto lookup"?: boolean; - "open keyboard files in source view"?: boolean; - "display theme"?: string; - "external editor path"?: string; - "smtp server"?: string; - "test email addresses"?: string; - "web ladder length"?: number; - "default project path"?: string; - "automatically report errors"?: boolean; - "automatically report usage"?: boolean; - "toolbar visible"?: boolean; - "active project"?: string; - "prompt to upgrade projects"?: boolean; -}; - -type KeymanDeveloperOption = keyof KeymanDeveloperOptions; - -// Default has no options set, and unit tests will use the defaults (won't call -// `loadOptions()`) -let options: KeymanDeveloperOptions = {}; - -// We only load the options from disk once on first use -let optionsLoaded = false; +let optionsLoaded: boolean = false; -export async function loadOptions(): Promise { +export async function loadOptions(): Promise { if(optionsLoaded) { - return options; + return true; } + optionsLoaded = true; - options = {}; try { - const optionsFile = path.join(os.homedir(), '.keymandeveloper', 'options.json'); + const optionsFile = path.join(os.homedir(), ...KeymanDeveloperOptionsPath); if(fs.existsSync(optionsFile)) { for(let i = 0; i < 5; i++) { try { - const data = JSON.parse(fs.readFileSync(optionsFile, 'utf-8')); - if(typeof data == 'object') { - options = data; - } - break; - } catch(e) { + const data = fs.readFileSync(optionsFile) as Uint8Array; + return optionsManager.load(data); + } catch(e: any) { if(e?.code == 'EBUSY') { await new Promise(resolve => setTimeout(resolve, 500)); } else { @@ -77,20 +39,20 @@ export async function loadOptions(): Promise { } catch(e) { // Nothing to report here, sadly -- because we cannot rely on Sentry at this // low level. - options = {}; } - optionsLoaded = true; - return options; + + optionsManager.clear(); + return false; } -export function getOption(valueName: T, defaultValue: KeymanDeveloperOptions[T]): KeymanDeveloperOptions[T] { - return options[valueName] ?? defaultValue; +export function getOption(valueName: T): KeymanDeveloperOptions[T] { + return optionsManager.get(valueName); } /** * unit tests will clear options before running, for consistency */ export function clearOptions() { - options = {}; - optionsLoaded = true; + optionsLoaded = false; + return optionsManager.clear(); } \ No newline at end of file diff --git a/developer/src/server/src/routes.ts b/developer/src/server/src/routes.ts index 3876bf0d7a1..acccf234020 100644 --- a/developer/src/server/src/routes.ts +++ b/developer/src/server/src/routes.ts @@ -12,9 +12,10 @@ import handleIncPackagesJson from './handlers/inc/packages-json.js'; import apiPackageRegister from './handlers/api/package/register.js'; import handleIncKeyboardsCss from './handlers/inc/keyboards-css.js'; import { Environment } from './version-data.js'; -import { configuration } from './config.js'; +import { standardPaths } from './standardPaths.js'; import chalk from 'chalk'; import { shutdown } from './shutdown.js'; +import { getOption } from './options.js'; export default function setupRoutes(app: express.Express, upload: multer.Multer, wsServer: ws.WebSocketServer, environment: Environment ) { @@ -163,7 +164,7 @@ export default function setupRoutes(app: express.Express, upload: multer.Multer, /* ngrok data */ app.get('/api/status', (_req,res,next) => { - const response = { ngrokEnabled: configuration.useNgrok, ngrokEndpoint: configuration.ngrokEndpoint }; + const response = { ngrokEnabled: getOption("server use ngrok"), ngrokEndpoint: standardPaths.ngrokEndpoint }; res.send(response); next(); }); diff --git a/developer/src/server/src/site/app.webmanifest b/developer/src/server/src/site/app.webmanifest new file mode 100644 index 00000000000..288b3059630 --- /dev/null +++ b/developer/src/server/src/site/app.webmanifest @@ -0,0 +1,9 @@ +{ + "name": "Keyman Developer Server", + "display": "standalone", + "id": "com.keyman.developer.server.self-hosted", + "icons": [{ + "src": "keyman-developer.svg", + "sizes": "any" + }] +} \ No newline at end of file diff --git a/developer/src/server/src/site/index.html b/developer/src/server/src/site/index.html index 8147a849a46..cd9cb5ae02f 100644 --- a/developer/src/server/src/site/index.html +++ b/developer/src/server/src/site/index.html @@ -5,7 +5,7 @@ - + Keyman Developer Keyboard Test Site diff --git a/developer/src/server/src/standardPaths.ts b/developer/src/server/src/standardPaths.ts new file mode 100644 index 00000000000..bc84104d9e7 --- /dev/null +++ b/developer/src/server/src/standardPaths.ts @@ -0,0 +1,33 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Path and URL constants (in some cases calculated) + */ +import { mkdirSync } from 'node:fs'; + +class StandardPaths { + public readonly appDataPath: string; + public readonly cachePath: string; + public readonly cacheStateFilename: string; + public readonly lockFilename: string; + public readonly pidFilename: string; + + /* ngrok Configuration */ + + public ngrokEndpoint: string = ''; + + constructor() { + + this.appDataPath = (process.env.APPDATA || + (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share")) + + '/Keyman/Keyman Developer/Server/'; + this.cachePath = this.appDataPath + 'cache/'; + this.cacheStateFilename = this.appDataPath + 'cache.json'; + this.lockFilename = this.appDataPath + 'lock.json'; + this.pidFilename = this.appDataPath + 'pid.json'; + + mkdirSync(this.cachePath, {recursive: true}); + } +}; + +export const standardPaths = new StandardPaths(); diff --git a/developer/src/tike/http/Keyman.Developer.System.HttpServer.AppSource.pas b/developer/src/tike/http/Keyman.Developer.System.HttpServer.AppSource.pas index 904d389a089..37c4b483b40 100644 --- a/developer/src/tike/http/Keyman.Developer.System.HttpServer.AppSource.pas +++ b/developer/src/tike/http/Keyman.Developer.System.HttpServer.AppSource.pas @@ -76,11 +76,22 @@ destructor TAppSourceHttpResponder.Destroy; begin T := FSources.LockList; try - // Note: Unlike regular functions, Assert has short-circuit evaluation - // intrinsics on the first param which makes it safe to dereference T[0] in - // the second parameter. - Assert(T.Count = 0, 'TAppSourceHttpResponder.Sources should be empty at destruction '+ - '(T.Count='+IntToStr(T.Count)+', T[0].Filename='+T[0].Filename+')'); + // There is a race where RegisterSource is called on the server side + // where a request is started in the form but the server does not respond + // before the form is destroyed: + // 1. http request starts on form + // 2. Form destroyed, calls UnregisterSource + // 3. http request received in TAppSourceHttpResponder, + // RespondTouchEditorState calls RegisterSource + // 4. Ooops + + // There is another race somewhere with unsaved text editors, or else a + // resource leak. For now, we'll report this as a message rather than crash. + if T.Count > 0 then + begin + TKeymanSentryClient.Instance.ReportMessage('TAppSourceHttpResponder.Sources should be empty at destruction '+ + '(T.Count='+IntToStr(T.Count)+', T[0].Filename='+T[0].Filename+')', True); + end; finally FSources.UnlockList; end; diff --git a/developer/src/tike/main/Keyman.Developer.System.KeymanDeveloperPaths.pas b/developer/src/tike/main/Keyman.Developer.System.KeymanDeveloperPaths.pas index 6beeafe62e5..1322010798d 100644 --- a/developer/src/tike/main/Keyman.Developer.System.KeymanDeveloperPaths.pas +++ b/developer/src/tike/main/Keyman.Developer.System.KeymanDeveloperPaths.pas @@ -20,7 +20,6 @@ TKeymanDeveloperPaths = class sealed const S_Kmc = 'kmc.cmd'; class function KmcPath: string; static; - const S_ServerConfigJson = 'config.json'; class function ServerDataPath: string; static; class function ServerPath: string; static; diff --git a/developer/src/tike/main/KeymanDeveloperOptions.pas b/developer/src/tike/main/KeymanDeveloperOptions.pas index 22109f58ff2..09c6afcd9c6 100644 --- a/developer/src/tike/main/KeymanDeveloperOptions.pas +++ b/developer/src/tike/main/KeymanDeveloperOptions.pas @@ -81,7 +81,6 @@ TKeymanDeveloperOptions = class procedure optWriteString(const nm, value: string); procedure optWriteBool(const nm: string; value: Boolean); procedure optWriteInt(const nm: string; value: Integer); - procedure WriteServerConfigurationJson; class function Get_Initial_DefaultProjectPath: string; static; function BackOffAndSaveJson(const Filename: string; const JSON: TJSONObject): Boolean; public @@ -476,8 +475,6 @@ procedure TKeymanDeveloperOptions.Write; finally FreeAndNil(json); end; - - WriteServerConfigurationJson; end; function TKeymanDeveloperOptions.BackOffAndSaveJson(const Filename: string; const JSON: TJSONObject): Boolean; @@ -513,23 +510,6 @@ function TKeymanDeveloperOptions.BackOffAndSaveJson(const Filename: string; cons Result := False; end; -procedure TKeymanDeveloperOptions.WriteServerConfigurationJson; -var - o: TJSONObject; -begin - o := TJSONObject.Create; - try - o.AddPair('port', TJSONNumber.Create(FServerDefaultPort)); - o.AddPair('ngrokToken', FServerNgrokToken); - o.AddPair('useNgrok', TJSONBool.Create(FServerUseNgrok)); - o.AddPair('ngrokVisible', TJSONBool.Create(FServerServerShowConsoleWindow)); - ForceDirectories(TKeymanDeveloperPaths.ServerDataPath); - BackOffAndSaveJSON(TKeymanDeveloperPaths.ServerDataPath + TKeymanDeveloperPaths.S_ServerConfigJson, o); - finally - o.Free; - end; -end; - procedure TKeymanDeveloperOptions.optWriteBool(const nm: string; value: Boolean); begin json.AddPair(nm, TJSONBool.Create(value)); diff --git a/developer/src/tike/main/UframeTextEditor.pas b/developer/src/tike/main/UframeTextEditor.pas index de6e2937749..b9f5f8ce227 100644 --- a/developer/src/tike/main/UframeTextEditor.pas +++ b/developer/src/tike/main/UframeTextEditor.pas @@ -486,7 +486,12 @@ procedure TframeTextEditor.LoadFileInBrowser(const AData: string); function GenerateNewFilename: string; begin Inc(FInitialFilenameIndex); - Result := '*texteditor*'+IntToStr(FInitialFilenameIndex); + Result := '*texteditor'; + if Owner <> nil then + Result := Result + '*' + Owner.ClassName; + if Parent <> nil then + Result := Result + '*' + Parent.Name; + Result := Result + '*'+IntToStr(FInitialFilenameIndex); end; function EncodeFont(const prefix: string; f: TFont): string; begin diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.dfm index fb286bccc3b..e06066a2e1a 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.dfm +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.dfm @@ -1,119 +1,127 @@ -inherited frmCloneKeymanCloudProjectParameters: TfrmCloneKeymanCloudProjectParameters - BorderIcons = [biSystemMenu, biMaximize] - Caption = 'Clone Project from Keyman Cloud' - ClientHeight = 506 - ClientWidth = 840 - Position = poScreenCenter - ExplicitWidth = 856 - ExplicitHeight = 545 - PixelsPerInch = 96 - TextHeight = 13 - object cmdOK: TButton - Left = 680 - Top = 473 - Width = 73 - Height = 25 - Anchors = [akRight, akBottom] - Caption = 'OK' - Default = True - TabOrder = 2 - OnClick = cmdOKClick - end - object cmdCancel: TButton - Left = 759 - Top = 473 - Width = 73 - Height = 25 - Anchors = [akRight, akBottom] - Cancel = True - Caption = 'Cancel' - ModalResult = 2 - TabOrder = 3 - end - object panWebHost: TPanel - Left = 0 - Top = 0 - Width = 842 - Height = 334 - Anchors = [akLeft, akTop, akRight, akBottom] - BevelOuter = bvNone - TabOrder = 0 - end - object gbNewProjectDetails: TGroupBox - Left = 8 - Top = 340 - Width = 823 - Height = 129 - Anchors = [akLeft, akRight, akBottom] - Caption = 'New Project Details' - TabOrder = 1 - object lblFileName: TLabel - Left = 16 - Top = 24 - Width = 76 - Height = 13 - Caption = '&New project ID:' - FocusControl = editKeyboardID - end - object lblProjectFilename: TLabel - Left = 16 - Top = 102 - Width = 101 - Height = 13 - Caption = 'New project &filename' - FocusControl = editProjectFilename - end - object lblPath: TLabel - Left = 16 - Top = 75 - Width = 83 - Height = 13 - Caption = 'Destination &path:' - FocusControl = editPath - end - object editKeyboardID: TEdit - Left = 148 - Top = 21 - Width = 205 - Height = 21 - TabOrder = 0 - OnChange = editKeyboardIDChange - end - object editProjectFilename: TEdit - Left = 148 - Top = 99 - Width = 669 - Height = 21 - TabStop = False - ParentColor = True - ReadOnly = True - TabOrder = 4 - OnChange = editKeyboardIDChange - end - object cmdBrowse: TButton - Left = 744 - Top = 72 - Width = 73 - Height = 21 - Caption = '&Browse...' - TabOrder = 3 - OnClick = cmdBrowseClick - end - object editPath: TEdit - Left = 148 - Top = 72 - Width = 590 - Height = 21 - TabOrder = 2 - OnChange = editPathChange - end - object chkRelocateExternal: TCheckBox - Left = 148 - Top = 48 - Width = 257 - Height = 17 - Caption = 'Relocate &external files into new project folder' - TabOrder = 1 - end - end -end +inherited frmCloneKeymanCloudProjectParameters: TfrmCloneKeymanCloudProjectParameters + BorderIcons = [biSystemMenu, biMaximize] + Caption = 'Clone Project from Keyman Cloud' + ClientHeight = 506 + ClientWidth = 840 + Position = poScreenCenter + ExplicitWidth = 856 + ExplicitHeight = 545 + PixelsPerInch = 96 + TextHeight = 13 + object lblMessage: TLabel + Left = 8 + Top = 478 + Width = 314 + Height = 13 + Caption = 'The keyboard %0:s has no source available. It cannot be cloned.' + FocusControl = editPath + end + object cmdOK: TButton + Left = 680 + Top = 473 + Width = 73 + Height = 25 + Anchors = [akRight, akBottom] + Caption = 'OK' + Default = True + TabOrder = 2 + OnClick = cmdOKClick + end + object cmdCancel: TButton + Left = 759 + Top = 473 + Width = 73 + Height = 25 + Anchors = [akRight, akBottom] + Cancel = True + Caption = 'Cancel' + ModalResult = 2 + TabOrder = 3 + end + object panWebHost: TPanel + Left = 0 + Top = 0 + Width = 842 + Height = 334 + Anchors = [akLeft, akTop, akRight, akBottom] + BevelOuter = bvNone + TabOrder = 0 + end + object gbNewProjectDetails: TGroupBox + Left = 8 + Top = 340 + Width = 823 + Height = 129 + Anchors = [akLeft, akRight, akBottom] + Caption = 'New Project Details' + TabOrder = 1 + object lblFileName: TLabel + Left = 16 + Top = 24 + Width = 76 + Height = 13 + Caption = '&New project ID:' + FocusControl = editKeyboardID + end + object lblProjectFilename: TLabel + Left = 16 + Top = 102 + Width = 101 + Height = 13 + Caption = 'New project &filename' + FocusControl = editProjectFilename + end + object lblPath: TLabel + Left = 16 + Top = 75 + Width = 83 + Height = 13 + Caption = 'Destination &path:' + FocusControl = editPath + end + object editKeyboardID: TEdit + Left = 148 + Top = 21 + Width = 205 + Height = 21 + TabOrder = 0 + OnChange = editKeyboardIDChange + end + object editProjectFilename: TEdit + Left = 148 + Top = 99 + Width = 669 + Height = 21 + TabStop = False + ParentColor = True + ReadOnly = True + TabOrder = 4 + OnChange = editKeyboardIDChange + end + object cmdBrowse: TButton + Left = 744 + Top = 72 + Width = 73 + Height = 21 + Caption = '&Browse...' + TabOrder = 3 + OnClick = cmdBrowseClick + end + object editPath: TEdit + Left = 148 + Top = 72 + Width = 590 + Height = 21 + TabOrder = 2 + OnChange = editPathChange + end + object chkRelocateExternal: TCheckBox + Left = 148 + Top = 48 + Width = 257 + Height = 17 + Caption = 'Relocate &external files into new project folder' + TabOrder = 1 + end + end +end diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas index c3f09a6406b..66f321befb5 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas @@ -46,6 +46,7 @@ TfrmCloneKeymanCloudProjectParameters = class(TTikeForm) cmdBrowse: TButton; editPath: TEdit; chkRelocateExternal: TCheckBox; + lblMessage: TLabel; procedure cmdOKClick(Sender: TObject); procedure editSourceProjectFilenameChange(Sender: TObject); procedure FormCreate(Sender: TObject); @@ -57,6 +58,7 @@ TfrmCloneKeymanCloudProjectParameters = class(TTikeForm) cef: TframeCEFHost; dlgBrowse: TBrowse4Folder; FKeymanID: string; + FSourceAvailable: Boolean; frmDownloadProgress: TfrmDownloadProgress; function GetBasePath: string; function GetKeyboardID: string; @@ -64,6 +66,7 @@ TfrmCloneKeymanCloudProjectParameters = class(TTikeForm) procedure EnableControls; procedure SetKeyboardID(const Value: string); procedure UpdateProjectFilename; + procedure UpdateMessage; function GetProjectFilename: string; function GetSourceProjectFilename: string; function GetRelocateExternal: Boolean; @@ -71,6 +74,7 @@ TfrmCloneKeymanCloudProjectParameters = class(TTikeForm) procedure cefLoadEnd(Sender: TObject); procedure DownloadCallback(Owner: TfrmDownloadProgress; var Result: Boolean); procedure DownloadWrapperCallback(var Cancelled: Boolean); + function IsKeyboardSourceAvailable(const id: string): Boolean; protected function GetHelpTopic: string; override; public @@ -86,9 +90,11 @@ function ShowCloneKeymanCloudProjectParameters(Owner: TComponent): Boolean; implementation uses + System.JSON, System.Net.UrlClient, Vcl.ComCtrls, + HttpUploader, KeymanDeveloperOptions, Keyman.Developer.System.KmcWrapper, Keyman.Developer.System.HelpTopics, @@ -182,6 +188,7 @@ procedure TfrmCloneKeymanCloudProjectParameters.FormCreate(Sender: TObject); dlgBrowse.Root := Desktop; dlgBrowse.Title := 'Select folder to save project to'; + UpdateMessage; EnableControls; end; @@ -201,9 +208,100 @@ procedure TfrmCloneKeymanCloudProjectParameters.cefLoadEnd(Sender: TObject); not u.Path.StartsWith(URLSubPath_KeymanDeveloper_Clone_Keyboards_Custom) then FKeymanID := u.Path.Substring(URLSubPath_KeymanDeveloper_Clone_Keyboards.Length) else FKeymanID := ''; + + FSourceAvailable := IsKeyboardSourceAvailable(FKeymanID); + UpdateMessage; EnableControls; end; +function TfrmCloneKeymanCloudProjectParameters.IsKeyboardSourceAvailable(const id: string): Boolean; + + function GetKeyboardDataFromApiServer(const id: string): string; + var + http: THTTPUploader; + begin + http := THTTPUploader.Create(nil); + try + http.Request.HostName := API_Server; + http.Request.Protocol := API_Protocol; + http.Request.UrlPath := API_Path_Keyboard(id); + try + http.Upload; + except + // Silently swallow network errors + on E:Exception do Exit(''); + end; + + if (http.Response.StatusCode < 200) or (http.Response.StatusCode > 299) then + begin + // Keyboard not found or invalid response + Exit(''); + end; + + Result := UTF8ToString(PAnsiChar(http.Response.MessageBodyAsString)); + finally + FreeAndNil(http); + end; + end; + + function GetSourcePathFromBody(const body: string): string; + var + val: TJSONValue; + obj: TJSONObject; + begin + try + val := TJSONObject.ParseJSONValue(body); + except + // Not a valid response + Exit(''); + end; + try + if not (val is TJSONObject) then + begin + // Not a valid response + Exit(''); + end; + + obj := val as TJSONObject; + val := obj.Values['sourcePath']; + if not Assigned(val) or not (val is TJSONString) then + begin + // no sourcePath property + Exit(''); + end; + + Result := (val as TJSONString).Value; + finally + val.Free; + end; + end; + +var + body, sourcePath: string; +begin + if id = '' then + begin + Exit(False); + end; + + body := GetKeyboardDataFromApiServer(id); + if body = '' then + begin + Exit(False); + end; + + sourcePath := GetSourcePathFromBody(body); + if sourcePath = '' then + begin + Exit(False); + end; + + // Keyboards in legacy/ do not have source available. Keyboards in + // release/ and experimental/ have source, and other new categories will + // also have source in future. + Result := not sourcePath.startsWith('legacy'); +end; + procedure TfrmCloneKeymanCloudProjectParameters.cmdBrowseClick(Sender: TObject); begin dlgBrowse.InitialDir := editPath.Text; @@ -221,22 +319,26 @@ procedure TfrmCloneKeymanCloudProjectParameters.cmdOKClick(Sender: TObject); procedure TfrmCloneKeymanCloudProjectParameters.editKeyboardIDChange(Sender: TObject); begin UpdateProjectFilename; + UpdateMessage; EnableControls; end; procedure TfrmCloneKeymanCloudProjectParameters.editSourceProjectFilenameChange(Sender: TObject); begin + UpdateMessage; EnableControls; end; procedure TfrmCloneKeymanCloudProjectParameters.editPathChange(Sender: TObject); begin UpdateProjectFilename; + UpdateMessage; EnableControls; end; procedure TfrmCloneKeymanCloudProjectParameters.editVersionChange(Sender: TObject); begin + UpdateMessage; EnableControls; end; @@ -248,7 +350,8 @@ procedure TfrmCloneKeymanCloudProjectParameters.EnableControls; (FKeymanID <> '') and (Trim(editPath.Text) <> '') and (Trim(editKeyboardID.Text) <> '') and - TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True); + TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True) and + FSourceAvailable; cmdOK.Enabled := e; end; @@ -292,9 +395,41 @@ function TfrmCloneKeymanCloudProjectParameters.Validate: Boolean; procedure TfrmCloneKeymanCloudProjectParameters.SetKeyboardID(const Value: string); begin editKeyboardID.Text := Value; + UpdateMessage; EnableControls; end; +procedure TfrmCloneKeymanCloudProjectParameters.UpdateMessage; +var + msg: string; +begin + if FKeymanID = '' then + begin + msg := 'Please choose a keyboard from the search form above.'; + end + else if not FSourceAvailable then + begin + msg := Format('The keyboard %0:s has no source available. It cannot be cloned.', [FKeymanID]); + end + else if Trim(editPath.Text) = '' then + begin + msg := 'A valid destination path must be selected.'; + end + else if Trim(editKeyboardID.Text) = '' then + begin + msg := 'Please enter a valid new project identifier.'; + end + else if not TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True) then + begin + msg := 'Please enter a valid new project identifier.'; + end + else + begin + msg := ''; + end; + lblMessage.Caption := msg; +end; + procedure TfrmCloneKeymanCloudProjectParameters.UpdateProjectFilename; begin editProjectFilename.Text := diff --git a/developer/src/tike/xml/app/editor/index.html b/developer/src/tike/xml/app/editor/index.html index b30fa799cf6..9054aea37fc 100644 --- a/developer/src/tike/xml/app/editor/index.html +++ b/developer/src/tike/xml/app/editor/index.html @@ -9,10 +9,6 @@ - - - - Text Editor diff --git a/developer/src/tike/xml/layoutbuilder/layer-controls.js b/developer/src/tike/xml/layoutbuilder/layer-controls.js index 8fa04d369ee..0f1797d413e 100644 --- a/developer/src/tike/xml/layoutbuilder/layer-controls.js +++ b/developer/src/tike/xml/layoutbuilder/layer-controls.js @@ -31,7 +31,8 @@ $(function() { $('#btnDelLayer').click(function () { if ($('#selLayer option').length == 1) return; builder.saveUndo(); - KVKL[builder.lastPlatform].layer.splice(builder.lastLayerIndex, 1); + const deletedLayer = KVKL[builder.lastPlatform].layer.splice(builder.lastLayerIndex, 1)[0]; + updateNextLayerReferences(KVKL[builder.lastPlatform], deletedLayer.id, ''); builder.selectPlatform(); builder.generate(false,true); }); @@ -45,29 +46,8 @@ $(function() { // Layer dialogs // - function submitLayerProperties() { - var newLayerName = $('#layerName').val(); - if (!newLayerName.match(/^[a-zA-Z0-9_-]+$/)) { - alert('Layer name must contain only alphanumerics, underscore and hyphen.'); - return false; - } - for(var i = 0; i < KVKL[builder.lastPlatform].layer.length; i++) { - if(i != builder.lastLayerIndex && KVKL[builder.lastPlatform].layer[i].id == newLayerName) { - alert('Layer name must not already be in use for the current platform.'); - return false; - } - } - - builder.saveUndo(); - builder.generate(); - - var platform = KVKL[builder.lastPlatform]; - var oldLayerName = platform.layer[builder.lastLayerIndex].id; - - let fixup = function(key) { - if (key.layer == oldLayerName) { - key.layer = newLayerName; - } + function updateNextLayerReferences(platform, oldLayerName, newLayerName) { + const fixup = function(key) { if (key.nextlayer == oldLayerName) { key.nextlayer = newLayerName; } @@ -94,6 +74,28 @@ $(function() { }); } }); + } + + function submitLayerProperties() { + const newLayerName = $('#layerName').val(); + if (!newLayerName.match(/^[a-zA-Z0-9_-]+$/)) { + alert('Layer name must contain only alphanumerics, underscore and hyphen.'); + return false; + } + for(let i = 0; i < KVKL[builder.lastPlatform].layer.length; i++) { + if(i != builder.lastLayerIndex && KVKL[builder.lastPlatform].layer[i].id == newLayerName) { + alert('Layer name must not already be in use for the current platform.'); + return false; + } + } + + builder.saveUndo(); + builder.generate(); + + const platform = KVKL[builder.lastPlatform]; + const oldLayerName = platform.layer[builder.lastLayerIndex].id; + + updateNextLayerReferences(platform, oldLayerName, newLayerName); platform.layer[builder.lastLayerIndex].id = newLayerName; builder.prepareLayers(); diff --git a/ios/.gitignore b/ios/.gitignore index 31d4700aecc..556562d4b6e 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -13,6 +13,7 @@ samples/KMSample2/KeymanEngine.xcframework samples/KMSample2/build engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanios.js engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanweb-osk.ttf +engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/globe-hint.css engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/kmwosk.css engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keyman.js.map engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanweb-webview.js diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 94dea1d5c79..2365c073a56 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ CE8B0BBD248734240045EB2E /* KeymanPackageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8B0BBC248734240045EB2E /* KeymanPackageTests.swift */; }; CE8B0BBF248764ED0045EB2E /* KMPResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8B0BBE248764ED0045EB2E /* KMPResource.swift */; }; CE8B5BB22491DA540075CCB0 /* 13.0 Cloud to Package Migration.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CE8B5BB12491DA530075CCB0 /* 13.0 Cloud to Package Migration.bundle */; }; + CE8E6B1E2FEC17B100F5E731 /* WebViewSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8E6B1D2FEC17A900F5E731 /* WebViewSchemeHandler.swift */; }; CE8EDEB123F53D1A009E1FF6 /* FileManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A079DD1223194B100581263 /* FileManagementTests.swift */; }; CE8EDEB323F53F96009E1FF6 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */; }; CE969BE8251AD8B500376D6A /* PackageWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE969BE7251AD8B500376D6A /* PackageWebViewController.swift */; }; @@ -459,6 +460,7 @@ CE8B0BBC248734240045EB2E /* KeymanPackageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanPackageTests.swift; sourceTree = ""; }; CE8B0BBE248764ED0045EB2E /* KMPResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMPResource.swift; sourceTree = ""; }; CE8B5BB12491DA530075CCB0 /* 13.0 Cloud to Package Migration.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "13.0 Cloud to Package Migration.bundle"; sourceTree = ""; }; + CE8E6B1D2FEC17A900F5E731 /* WebViewSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSchemeHandler.swift; sourceTree = ""; }; CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = ""; }; CE969BE7251AD8B500376D6A /* PackageWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageWebViewController.swift; sourceTree = ""; }; CE96E42C24D1229A005B8E5A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -804,6 +806,7 @@ CE79B24823C711FF007E72AE /* KeyboardScaleMap.swift */, C0EF3E7A1F95B65300CE9BD4 /* KeymanWebDelegate.swift */, C0C16A881FA8146300F090BA /* KeymanWebViewController.swift */, + CE8E6B1D2FEC17A900F5E731 /* WebViewSchemeHandler.swift */, C0A5FF361F6682EB00BE740C /* PopoverView.swift */, ); path = Keyboard; @@ -1522,6 +1525,7 @@ 9A079DCA222E050E00581263 /* LexicalModelKeymanPackage.swift in Sources */, 9A60764422893A4E003BCFBA /* SettingsViewController.swift in Sources */, C06085B41F9485E40057E5B9 /* UIButton+Helpers.swift in Sources */, + CE8E6B1E2FEC17B100F5E731 /* WebViewSchemeHandler.swift in Sources */, C0959CD41F99C44E00B616BC /* Constants.swift in Sources */, C0452BAD1F9F21270064431A /* Keyboard.swift in Sources */, 29B30C232B564F9900C342A4 /* KeymanEngineLogger.swift in Sources */, diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift index 4bd5375bc92..02560e69290 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift @@ -38,7 +38,8 @@ class KeymanWebViewController: UIViewController { let storage: Storage weak var delegate: KeymanWebDelegate? private var useSpecialFont = false - private var userContentController = WKUserContentController() + private let userContentController = WKUserContentController() + private let schemeHandler: WebViewSchemeHandler private let keymanWebViewName: String = "keyman" // Views @@ -69,8 +70,9 @@ class KeymanWebViewController: UIViewController { init(storage: Storage) { self.storage = storage + self.schemeHandler = WebViewSchemeHandler(storage: storage) + super.init(nibName: nil, bundle: nil) - _ = view } @@ -122,6 +124,7 @@ class KeymanWebViewController: UIViewController { config.preferences = prefs config.suppressesIncrementalRendering = false config.userContentController = self.userContentController + config.setURLSchemeHandler(schemeHandler, forURLScheme: schemeHandler.scheme) webView = KeymanWebView(frame: CGRect(origin: .zero, size: keyboardSize), configuration: config) webView!.isOpaque = false @@ -182,7 +185,7 @@ extension KeymanWebViewController { } view = nil } - + func languageMenuPosition(_ completion: @escaping (CGRect) -> Void) { webView!.evaluateJavaScript("langMenuPos();") { result, _ in guard let result = result as? String, !result.isEmpty else { @@ -279,20 +282,28 @@ extension KeymanWebViewController { // family does not have to match the name in the font file. It only has to be unique. return [ "family": "\(keyboard.id)__\(isOsk ? "osk" : "display")", - "files": font.source.map { storage.fontURL(forResource: keyboard, filename: $0)!.absoluteString } + "files": font.source.map { + schemeHandler.buildUrlForFile( + fileURL: storage.fontURL(forResource: keyboard, filename: $0)! + ).absoluteString + } ] } func setKeyboard(_ keyboard: InstallableKeyboard) throws { let fileURL = storage.keyboardURL(for: keyboard) + let loadingURL = schemeHandler.buildUrlForFile( + fileURL: storage.keyboardURL(for: keyboard) + ) + var stub: [String: Any] = [ "KI": "Keyboard_\(keyboard.id)", "KN": keyboard.name, "KLC": keyboard.languageID, "KL": keyboard.languageName, - "KF": fileURL.absoluteString + "KF": loadingURL.absoluteString ] - + if let packageID = keyboard.packageID { stub["KP"] = packageID } @@ -367,7 +378,7 @@ extension KeymanWebViewController { let stub: [String: Any] = [ "id": lexicalModel.id, "languages": [lexicalModel.languageID], // Change when InstallableLexicalModel is updated to store an array - "path": fileURL.absoluteString + "path": schemeHandler.buildUrlForFile(fileURL: fileURL).absoluteString ] guard FileManager.default.fileExists(atPath: fileURL.path) else { @@ -861,7 +872,9 @@ extension KeymanWebViewController { // MARK: - Show/hide views func reloadKeyboard() { - webView!.loadFileURL(Storage.active.kmwURL, allowingReadAccessTo: Storage.active.baseDir) + let hostPageFileUrl = URL(fileURLWithPath: Resources.kmwFilename, relativeTo: storage.baseDir) + let hostPageUrl = schemeHandler.buildUrlForFile(fileURL: hostPageFileUrl) + webView!.load(URLRequest(url: hostPageUrl)) isLoading = true updateSpacebarText() diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/WebViewSchemeHandler.swift b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/WebViewSchemeHandler.swift new file mode 100644 index 00000000000..0b09f797328 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/WebViewSchemeHandler.swift @@ -0,0 +1,97 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Joshua Horton on 2026-06-24. + * + * WebViewKeyboardLoader implements a URLSchemeHandler that allows + * the hosted Keyman Engine for Web to access all files, consistently, + * via a http-like protocol, preventing CORS access issues for files + * loaded dynamically. + */ + +import WebKit +import UniformTypeIdentifiers +import os.log + +func getMimeType(forExtension ext: String) -> String { + // Find the UTType associated with the file extension + if let utType = UTType(filenameExtension: ext) { + // Return the preferred MIME type if it exists + return utType.preferredMIMEType ?? "application/octet-stream" + } + return "application/octet-stream" +} + +class WebViewSchemeHandler: NSObject, WKURLSchemeHandler { + let storage: Storage + let scheme = "keyman-engine" + + init(storage: Storage) { + self.storage = storage + } + + func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) { + guard let url = urlSchemeTask.request.url else { + return + } + + var components = URLComponents(url: url, resolvingAgainstBaseURL: false)! + components.scheme = "file" + + let fileUrl = components.url + + let doError = { () -> Void in + let message = "Could not load url via WKURLSchemeHandler: \(url)" + let errorInfo = [ + NSLocalizedDescriptionKey: message + ] + let error = NSError(domain: "WebViewKeyboardLoader", code: 500, userInfo: errorInfo) + + os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message) + SentryManager.capture(error, message: message) + + urlSchemeTask.didFailWithError(error) + } + + guard fileUrl != nil else { + doError() + return + } + + do { + let fileContents = try Data(contentsOf: fileUrl!) + let fileExtension = fileUrl!.pathExtension + + let mimeType: String = getMimeType(forExtension: fileExtension) + let charset: String = (mimeType.hasPrefix("text/") || mimeType == "application/json") + ? "; charset=utf-8" + : "" + + let response = HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: [ + "Content-Type": "\(mimeType)\(charset)", + ] + )! + + urlSchemeTask.didReceive(response) + urlSchemeTask.didReceive(fileContents) + urlSchemeTask.didFinish() + } catch { + doError() + return + } + } + + func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) { + } + + func buildUrlForFile(fileURL: URL) -> URL { + var loadingURLBuilder = URLComponents() + loadingURLBuilder.scheme = scheme + loadingURLBuilder.path = fileURL.path + return loadingURLBuilder.url! + } +} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Queries/Queries+PackageVersion.swift b/ios/engine/KMEI/KeymanEngine/Classes/Queries/Queries+PackageVersion.swift index 69ca1e15a91..769089fdbc5 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Queries/Queries+PackageVersion.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Queries/Queries+PackageVersion.swift @@ -52,10 +52,10 @@ extension Queries { func dictionaryReducer(container: KeyedDecodingContainer, category: String) -> ([String: ResultComponent], Queries.IDCodingKey) throws -> [String: ResultComponent] { return { (dict, id) -> [String: ResultComponent] in var dict = dict - if let entry = try? container.decode(ResultEntry.self, forKey: id) { - dict[id.stringValue] = entry - } else if let error = try? container.decode(ResultError.self, forKey: id) { + if let error = try? container.decode(ResultError.self, forKey: id) { dict[id.stringValue] = error + } else if let entry = try? container.decode(ResultEntry.self, forKey: id) { + dict[id.stringValue] = entry } else { throw FetchError.decodingError(category, id.stringValue) } @@ -116,7 +116,10 @@ extension Queries { return URLQueryItem(name: resourceField, value: key.id) } - urlComponents.queryItems = queryItems + [URLQueryItem(name: "platform", value: "ios")] + urlComponents.queryItems = queryItems + [ + URLQueryItem(name: "platform", value: "ios"), + URLQueryItem(name: "keyman-version", value: Version.current.plainString) + ] let message = "Querying package versions through API endpoint: \(urlComponents.url!)" os_log("%{public}s", log:KeymanEngineLogger.resources, type: .info, message) SentryManager.breadcrumb(message) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift index 6775edb660c..fd8b98e8a88 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift @@ -245,6 +245,10 @@ extension Storage { resourceName: "kmwosk.css", dstDir: baseDir, excludeFromBackup: true) + try Storage.copy(from: bundle, + resourceName: "globe-hint.css", + dstDir: baseDir, + excludeFromBackup: true) try Storage.copy(from: bundle, resourceName: "keymanweb-osk.ttf", dstDir: baseDir, diff --git a/ios/engine/build.sh b/ios/engine/build.sh index 9b98249b9b0..f341e139836 100755 --- a/ios/engine/build.sh +++ b/ios/engine/build.sh @@ -102,7 +102,8 @@ function update_bundle ( ) { KMW_PRODUCT="$KEYMAN_ROOT/web/build/app/webview/$CONFIG" KMW_RESOURCES="$KEYMAN_ROOT/web/build/app/resources" - #Copy over the relevant resources! It's easiest to do if we navigate to the resulting folder. + # Copy relevant KeymanWeb resources; this list is also in Storage.swift + cp "$KMW_RESOURCES/osk/globe-hint.css" "$BUNDLE_PATH/globe-hint.css" cp "$KMW_RESOURCES/osk/kmwosk.css" "$BUNDLE_PATH/kmwosk.css" cp "$KMW_RESOURCES/osk/keymanweb-osk.ttf" "$BUNDLE_PATH/keymanweb-osk.ttf" cp "$KMW_PRODUCT/keymanweb-webview.js" "$BUNDLE_PATH/keymanweb-webview.js" diff --git a/linux/keyman-config/keyman_config/install_window.py b/linux/keyman-config/keyman_config/install_window.py index 2ca4289b4b3..367e60d39b4 100755 --- a/linux/keyman-config/keyman_config/install_window.py +++ b/linux/keyman-config/keyman_config/install_window.py @@ -167,13 +167,14 @@ def _show_version_message_dlg(self, viewkmp, msg): def _add_buttons(self): hbox = Gtk.Box(spacing=6) - button = Gtk.Button.new_with_mnemonic(_("_Install")) - button.connect("clicked", self.on_install_clicked) - hbox.pack_start(button, False, False, 0) - button = Gtk.Button.new_with_mnemonic(_("_Cancel")) button.connect("clicked", self.on_cancel_clicked) + hbox.pack_start(button, False, False, 0) + + button = Gtk.Button.new_with_mnemonic(_("_Install")) + button.connect("clicked", self.on_install_clicked) hbox.pack_end(button, False, False, 0) + bind_accelerator(self.accelerators, button, 'w') return hbox diff --git a/linux/scripts/launchpad.sh b/linux/scripts/launchpad.sh index b516d1eb53b..2789c9ec7ba 100755 --- a/linux/scripts/launchpad.sh +++ b/linux/scripts/launchpad.sh @@ -98,7 +98,7 @@ function upload_with_retry() { return 1 } -distributions="${DIST:-jammy noble questing resolute}" +distributions="${DIST:-jammy noble resolute stonking}" packageversion="${PACKAGEVERSION:-1~sil1}" retries="${RETRIES:-5}" diff --git a/oem/firstvoices/android/app/build.gradle b/oem/firstvoices/android/app/build.gradle index 9f42f940855..b09fe58fd1a 100644 --- a/oem/firstvoices/android/app/build.gradle +++ b/oem/firstvoices/android/app/build.gradle @@ -105,7 +105,7 @@ if (env_keys_json_file != null && file(env_keys_json_file).exists()) { // Deactivate lower conflicting version APKs resolutionStrategy.set(com.github.triplet.gradle.androidpublisher.ResolutionStrategy.IGNORE) - String tier = new File('../../../TIER.md').getText('UTF-8').trim(); + String tier = System.getenv("KEYMAN_TIER"); switch (tier) { case 'beta': track.set("beta") @@ -137,6 +137,7 @@ dependencies { implementation files('libs/keyman-engine.aar') implementation 'io.sentry:sentry-android:8.19.1' implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.webkit:webkit:1.14.0' } apply plugin: 'com.android.application' diff --git a/oem/firstvoices/android/build.gradle b/oem/firstvoices/android/build.gradle index 3f20182cf69..f24c9cdc53a 100644 --- a/oem/firstvoices/android/build.gradle +++ b/oem/firstvoices/android/build.gradle @@ -16,7 +16,6 @@ buildscript { allprojects { repositories { google() - jcenter() mavenCentral() } } diff --git a/oem/firstvoices/windows/src/xml/onlineupdate.xsl b/oem/firstvoices/windows/src/xml/onlineupdate.xsl index 931c0c106bc..95024605ac7 100644 --- a/oem/firstvoices/windows/src/xml/onlineupdate.xsl +++ b/oem/firstvoices/windows/src/xml/onlineupdate.xsl @@ -10,7 +10,6 @@ - <xsl:value-of select="$locale/string[@name='S_Update_Title']"/> diff --git a/resources/build/builder-basic.inc.sh b/resources/build/builder-basic.inc.sh index a2077b081aa..c3108ede0e7 100644 --- a/resources/build/builder-basic.inc.sh +++ b/resources/build/builder-basic.inc.sh @@ -177,19 +177,34 @@ function _builder_basic_print_build_number_for_teamcity() { } function _builder_basic_print_version_utils_debug() { - echo "KEYMAN_ROOT: $KEYMAN_ROOT" - echo "KEYMAN_VERSION: $KEYMAN_VERSION" - echo "KEYMAN_VERSION_WIN: $KEYMAN_VERSION_WIN" - echo "KEYMAN_VERSION_RELEASE: $KEYMAN_VERSION_RELEASE" - echo "KEYMAN_VERSION_MAJOR: $KEYMAN_VERSION_MAJOR" - echo "KEYMAN_VERSION_MINOR: $KEYMAN_VERSION_MINOR" - echo "KEYMAN_VERSION_PATCH: $KEYMAN_VERSION_PATCH" - echo "KEYMAN_TIER: $KEYMAN_TIER" - echo "KEYMAN_VERSION_TAG: $KEYMAN_VERSION_TAG" - echo "KEYMAN_VERSION_WITH_TAG: $KEYMAN_VERSION_WITH_TAG" - echo "KEYMAN_VERSION_GIT_TAG: $KEYMAN_VERSION_GIT_TAG" - echo "KEYMAN_VERSION_ENVIRONMENT: $KEYMAN_VERSION_ENVIRONMENT" - echo "KEYMAN_VERSION_FOR_FILENAME: $KEYMAN_VERSION_FOR_FILENAME" + if ! builder_is_debug_internal && ! builder_is_ci_build; then + return + fi + + if [[ ${_builder_basic_environment_printed:-false} == true ]]; then + # report this only once per build + return + fi + + builder_echo start builder_basic_debug "Builder internal environment variables" + + echo " KEYMAN_ROOT: $KEYMAN_ROOT" + echo " KEYMAN_VERSION: $KEYMAN_VERSION" + echo " KEYMAN_VERSION_WIN: $KEYMAN_VERSION_WIN" + echo " KEYMAN_VERSION_RELEASE: $KEYMAN_VERSION_RELEASE" + echo " KEYMAN_VERSION_MAJOR: $KEYMAN_VERSION_MAJOR" + echo " KEYMAN_VERSION_MINOR: $KEYMAN_VERSION_MINOR" + echo " KEYMAN_VERSION_PATCH: $KEYMAN_VERSION_PATCH" + echo " KEYMAN_TIER: $KEYMAN_TIER" + echo " KEYMAN_VERSION_TAG: $KEYMAN_VERSION_TAG" + echo " KEYMAN_VERSION_WITH_TAG: $KEYMAN_VERSION_WITH_TAG" + echo " KEYMAN_VERSION_GIT_TAG: $KEYMAN_VERSION_GIT_TAG" + echo " KEYMAN_VERSION_ENVIRONMENT: $KEYMAN_VERSION_ENVIRONMENT" + echo " KEYMAN_VERSION_FOR_FILENAME: $KEYMAN_VERSION_FOR_FILENAME" + + export _builder_basic_environment_printed=true + + builder_echo end builder_basic_debug success "Builder internal environment variables" } # TODO: consolidate with buildLevel, see #14285 @@ -284,8 +299,8 @@ _builder_basic_find_keyman_root _builder_basic_find_tier _builder_basic_find_version -# _builder_basic_print_version_utils_debug _builder_basic_print_build_number_for_teamcity +_builder_basic_print_version_utils_debug _builder_basic_find_should_sentry_release diff --git a/resources/build/test/build.sh b/resources/build/test/build.sh index d1dec4a5057..6c4c4dec2c0 100755 --- a/resources/build/test/build.sh +++ b/resources/build/test/build.sh @@ -1,5 +1,10 @@ #!/usr/bin/env bash +# Avoid timing reports and internal debugging in unit tests. These conflict with +# the printing of outputs and cause tests to fail. +export _builder_debug_internal=false +export _builder_timings=false + ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" diff --git a/resources/build/test/test.sh b/resources/build/test/test.sh index f13a68c7271..746ad480277 100755 --- a/resources/build/test/test.sh +++ b/resources/build/test/test.sh @@ -2,7 +2,10 @@ set -eu -# Avoid timing reports in unit tests + +# Avoid timing reports and internal debugging in unit tests. These conflict with +# the printing of outputs and cause tests to fail. +export _builder_debug_internal=false export _builder_timings=false ## START STANDARD BUILD SCRIPT INCLUDE diff --git a/resources/build/typescript.inc.sh b/resources/build/typescript.inc.sh index ef6f58d68a6..fe39d870d1e 100644 --- a/resources/build/typescript.inc.sh +++ b/resources/build/typescript.inc.sh @@ -58,3 +58,52 @@ typescript_run_eslint_mocha_tests() { echo "##teamcity[flowFinished flowId='unit_tests']" fi } + +# +# Run api-extractor, preparing config files for each folder. As the config files +# are largely identical for each project, we copy them in rather than +# duplicating them across the repo (as that is hard to maintain over time) +# +# NOTE: this is setup only for Developer projects at this time as outputs go +# into developer/docs and developer/build; future generalization requires +# changing only report_temp and report_folder parameters. +# +# See also: /common/tools/api-extractor/README.md +# +typescript_run_api_extractor() { + project_path="$1" + index_d_ts="$2" + + # tsdoc config file must be in same folder as tsconfig.json + cp "${KEYMAN_ROOT}/common/tools/api-extractor/tsdoc.template.json" "${THIS_SCRIPT_PATH}/tsdoc.json" + + # api-extractor configuration must be stored in a file, so patch the + # file with the relevant parameters + + if builder_is_windows; then + # replace \ with / on Windows in KEYMAN_ROOT path, so we don't end up with + # silly unescaped strings in JSON + keyman_root="$(echo "$KEYMAN_ROOT" | sed "s=\\\=/=g")" + else + keyman_root="${KEYMAN_ROOT}" + fi + + # For now, these two variables are Developer-specific + report_temp="$keyman_root/developer/build/api" + report_folder="$keyman_root/developer/docs/api/etc" + export project_path index_d_ts keyman_root report_temp report_folder + + envsubst "\$keyman_root,\$project_path,\$index_d_ts,\$report_temp,\$report_folder" \ + < "${KEYMAN_ROOT}/common/tools/api-extractor/api-extractor.template.json" \ + > "${THIS_SCRIPT_PATH}/api-extractor.json" + + export -n project_path index_d_ts keyman_root report_temp report_folder + + api-extractor run \ + --local \ + --verbose \ + --config "${THIS_SCRIPT_PATH}/api-extractor.json" + + rm "${THIS_SCRIPT_PATH}/tsdoc.json" + rm "${THIS_SCRIPT_PATH}/api-extractor.json" +} \ No newline at end of file diff --git a/resources/build/version/src/fixupHistory.ts b/resources/build/version/src/fixupHistory.ts index 4f34c0c13d5..ff98088571c 100644 --- a/resources/build/version/src/fixupHistory.ts +++ b/resources/build/version/src/fixupHistory.ts @@ -7,13 +7,9 @@ type GitHub = ReturnType; import { readFileSync, writeFileSync } from 'node:fs'; import { gt } from 'semver'; -import { reportHistory } from './reportHistory.js'; +import { reportHistory, PRInformation } from './reportHistory.js'; import { spawnChild } from './util/spawnAwait.js'; -interface PRInformation { - title: string; - number: number; -} // ------------------------------------------------------------------------------------ // splitPullsIntoHistory diff --git a/resources/build/version/src/reportHistory.ts b/resources/build/version/src/reportHistory.ts index 91bfb53d33d..8fbd8519b9d 100644 --- a/resources/build/version/src/reportHistory.ts +++ b/resources/build/version/src/reportHistory.ts @@ -33,7 +33,7 @@ const getPullRequestInformation = async ( return commit_id; }; -interface PRInformation { +export interface PRInformation { title: string; number: number; version?: string; diff --git a/resources/builder.inc.sh b/resources/builder.inc.sh index 6ec2fbcebff..2b81b7f52fd 100755 --- a/resources/builder.inc.sh +++ b/resources/builder.inc.sh @@ -425,7 +425,7 @@ _builder_failure_trap() { # _builder_cleanup_deps() { if ! builder_is_dep_build && ! builder_is_child_build && [[ ! -z ${_builder_deps_built+x} ]]; then - if $_builder_debug_internal; then + if builder_is_debug_internal; then builder_echo_debug "Dependencies that were built:" cat "$_builder_deps_built" fi @@ -1722,18 +1722,6 @@ _builder_parse_expanded_parameters() { _builder_add_chosen_action_target_dependencies fi - if $_builder_debug_internal; then - builder_echo_debug "Selected actions and targets:" - for e in "${_builder_chosen_action_targets[@]}"; do - builder_echo_debug "* $e" - done - builder_echo_debug - builder_echo_debug "Selected options:" - for e in "${_builder_chosen_options[@]}"; do - builder_echo_debug "* $e" - done - fi - if builder_is_dep_build; then _builder_verify_expected_sub_process_variables elif builder_is_child_build; then @@ -1768,11 +1756,13 @@ _builder_parse_expanded_parameters() { fi if builder_is_debug_build; then - BUILDER_CONFIGURATION=debug + export BUILDER_CONFIGURATION=debug else - BUILDER_CONFIGURATION=release + export BUILDER_CONFIGURATION=release fi + _builder_print_internal_debug_info + # Now that we've successfully parsed options adhering to the _builder spec, we may activate our # action_failure and action_hanging traps. (We don't want them active on scripts not yet using # said script.) @@ -2080,7 +2070,7 @@ _builder_do_build_deps() { $_builder_offline \ $_builder_build_deps \ --builder-dep-parent "$THIS_SCRIPT_IDENTIFIER" && ( - if $_builder_debug_internal; then + if builder_is_debug_internal; then builder_echo success "## Dependency $dep$dep_target for $_builder_matched_action_name completed successfully" fi ) || ( @@ -2149,6 +2139,9 @@ builder_is_full_dep_build() { # returns `0` if the current build script has at least one dependency. # builder_has_dependencies() { + if [[ ${_builder_deps:-false} == false ]]; then + return 1 + fi if [[ ${#_builder_deps[@]} -eq 0 ]]; then return 1 fi @@ -2245,17 +2238,29 @@ _builder_report_dependencies() { # returns `0` if we should be verbose in output # builder_verbose() { - if [[ $builder_verbose == --verbose ]]; then + if [[ ${builder_verbose:-} == --verbose ]]; then return 0 fi return 1 } # -# returns `0` if we are doing a debug build +# Returns `0` if we are doing a debug build. Not the same as +# `builder_is_debug_internal`. # builder_is_debug_build() { - if [[ $builder_debug == --debug ]]; then + if [[ ${builder_debug:-} == --debug ]]; then + return 0 + fi + return 1 +} + +# +# Returns `0` if builder internal debug reporting is enabled. This is not the +# same as a debug build. +# +builder_is_debug_internal() { + if $_builder_debug_internal; then return 0 fi return 1 @@ -2539,6 +2544,82 @@ _builder_get_operating_system() { readonly BUILDER_OS } +function _builder_echo_function_result() { + if $1; then + printf " %-40s%s\n" "$1:" true + else + printf " %-40s%s\n" "$1:" false + fi +} + +function _builder_print_internal_debug_info() { + if ! builder_is_debug_internal && ! builder_is_ci_build; then + return + fi + + if [[ ${_builder_internal_debug_info_printed:-false} == false ]]; then + builder_echo start builder_debug "Builder internal debug information" + + # For CI builds, we report this only once per build; for internal debug + # builds, it prints for every child/dep build also + echo -e "${COLOR_TEAL}Selected actions and targets${COLOR_RESET}" + for e in "${_builder_chosen_action_targets[@]}"; do + echo " $e" + done + + echo + echo -e "${COLOR_TEAL}Selected options${COLOR_RESET}" + for e in "${_builder_chosen_options[@]}"; do + echo " $e" + done + + echo + echo -e "${COLOR_TEAL}Builder configuration${COLOR_RESET}" + _builder_echo_function_result builder_is_debug_build + _builder_echo_function_result builder_verbose + + echo + echo -e "${COLOR_TEAL}Builder CI information${COLOR_RESET}" + _builder_echo_function_result builder_is_ci_build + _builder_echo_function_result builder_is_ci_release_build + _builder_echo_function_result builder_is_ci_test_build + _builder_echo_function_result builder_is_ci_build_level_release + _builder_echo_function_result builder_is_ci_build_level_build + + echo + echo -e "${COLOR_TEAL}Builder platform information${COLOR_RESET}" + _builder_echo_function_result builder_is_running_on_docker + _builder_echo_function_result builder_is_running_on_gha + _builder_echo_function_result builder_is_running_on_teamcity + _builder_echo_function_result builder_is_windows + _builder_echo_function_result builder_is_macos + _builder_echo_function_result builder_is_linux + fi + + if builder_is_debug_internal; then + # This may change in sub-project builds, so we print it every time, + # but we won't print it in CI to prevent logs exploding; note that + # the builder_debug header will not be printed each time; that is + # "by design" + echo + echo -e "${COLOR_TEAL}Builder dependency internal data${COLOR_RESET}" + _builder_echo_function_result builder_is_dep_build + _builder_echo_function_result builder_is_child_build + _builder_echo_function_result builder_is_quick_dep_build + _builder_echo_function_result builder_is_full_dep_build + _builder_echo_function_result builder_has_dependencies + fi + + if [[ ${_builder_internal_debug_info_printed:-false} == false ]]; then + builder_echo end builder_debug success "Builder internal debug information" + + if builder_is_ci_build; then + # In CI builds, we will only print the debug info once + export _builder_internal_debug_info_printed=true + fi + fi +} + ################################################################################ # Final initialization ################################################################################ @@ -2557,6 +2638,6 @@ if [ -z ${_builder_debug_internal+x} ]; then _builder_debug_internal=false fi -if $_builder_debug_internal; then - builder_echo_debug "Command line: $0 $@" +if builder_is_debug_internal; then + builder_echo_debug "Command line: $0 $*" fi diff --git a/resources/docker-images/run.sh b/resources/docker-images/run.sh index 534e84eebf4..a76d2bcb0ba 100755 --- a/resources/docker-images/run.sh +++ b/resources/docker-images/run.sh @@ -44,6 +44,9 @@ else fi run_android() { + if [[ -n ${RELEASE_OEM:-} ]] && [[ -n ${RELEASE_OEM_FIRSTVOICES:-} ]]; then + DOCKER_RUN_ARGS+=(-e "RELEASE_OEM=${RELEASE_OEM:-}" -e "RELEASE_OEM_FIRSTVOICES=${RELEASE_OEM_FIRSTVOICES:-}") + fi docker_wrapper run "${DOCKER_RUN_ARGS[@]}" -i --rm -v "${KEYMAN_ROOT}":/home/build/build \ -v "${KEYMAN_ROOT}/core/build/docker-core/${build_dir}":/home/build/build/core/build \ "${registry_slash}keymanapp/keyman-android-ci:${image_version}" \ diff --git a/resources/teamcity/android/keyman-android-test-samples.sh b/resources/teamcity/android/keyman-android-test-samples.sh index 675d339dcf2..c79612a3ca8 100755 --- a/resources/teamcity/android/keyman-android-test-samples.sh +++ b/resources/teamcity/android/keyman-android-test-samples.sh @@ -29,7 +29,7 @@ builder_parse "$@" cd "${KEYMAN_ROOT}/android" function do_build() { - builder_launch /android/build.sh configure,build:engine,sample1,sample2,keyboardharness + builder_launch /android/build.sh configure,build:sample1,sample2,keyboardharness } if builder_has_action all; then diff --git a/web/build.sh b/web/build.sh index ca253ab82c7..143a3b8aa93 100755 --- a/web/build.sh +++ b/web/build.sh @@ -114,6 +114,25 @@ build_tests_action() { cp "${KEYMAN_ROOT}/web/src/test/auto/dom/cases/attachment/textStoreForElement.tests.html" \ "${KEYMAN_ROOT}/web/build/test/dom/cases/attachment/" + + # Copy and update guide examples - for local, PR, and test builds we + # replace the CDN URL with the local build path, so that we can test + # against the current build + mkdir -p "${KEYMAN_ROOT}/web/build/docs/engine/guide" + cp -r "${KEYMAN_ROOT}/web/docs/engine/guide/examples" \ + "${KEYMAN_ROOT}/web/build/docs/engine/guide/" + + # shellcheck disable=SC2310 + if ! builder_is_ci_release_build; then + for f in "${KEYMAN_ROOT}/web/build/docs/engine/guide/examples"/*.html; do + # Replace CDN URL (https://s.keyman.com/kwm/engine/18.0.123) with + # local local build path (/build/publish/debug). We write to a temp + # file and then replace the original instead of modifying in-place. + # This is safer and more portable. + sed "s|https://s\.keyman\.com/kmw/engine/[0-9]*\.[0-9]*\.[0-9]*/|/build/publish/${config}/|g" \ + "${f}" > "${f}.tmp" && mv "${f}.tmp" "${f}" + done + fi } coverage_action() { diff --git a/web/docs/engine/guide/examples/__auto-control.html b/web/docs/engine/guide/examples/__auto-control.html index c2a8b472b09..76ff91cf98d 100644 --- a/web/docs/engine/guide/examples/__auto-control.html +++ b/web/docs/engine/guide/examples/__auto-control.html @@ -19,8 +19,8 @@

Automatic Mode Example

-

-

+

+

Back to Document diff --git a/web/docs/engine/guide/examples/__full-manual-control.html b/web/docs/engine/guide/examples/__full-manual-control.html index 6313c135184..b862184ff0e 100644 --- a/web/docs/engine/guide/examples/__full-manual-control.html +++ b/web/docs/engine/guide/examples/__full-manual-control.html @@ -24,13 +24,13 @@ } document.f.multilingual.focus(); - keyman.setActiveKeyboard('', ''); + await keyman.setActiveKeyboard('', ''); }); -function KWControlChange() { +async function KWControlChange() { var name = KWControl.value.substr(0, KWControl.value.indexOf("$$")); var languageCode = KWControl.value.substr(KWControl.value.indexOf("$$") + 2); - keyman.setActiveKeyboard(name, languageCode); + await keyman.setActiveKeyboard(name, languageCode); document.f.multilingual.focus(); } @@ -40,7 +40,7 @@

Manual Control - Custom Interface

-

Keyboard:

+

diff --git a/web/docs/engine/guide/examples/__manual-control.html b/web/docs/engine/guide/examples/__manual-control.html index a1cfa72c83c..f3c1b7a80e9 100644 --- a/web/docs/engine/guide/examples/__manual-control.html +++ b/web/docs/engine/guide/examples/__manual-control.html @@ -11,7 +11,7 @@ languages: { id: 'lo', name: 'Lao' }, filename: "./js/laokeys.js" }); - keyman.setActiveKeyboard('laokeys'); + await keyman.setActiveKeyboard('laokeys'); keyman.osk.hide(); }); @@ -30,8 +30,8 @@

Manual Mode Example

KeymanWeb

-

-

+

+

Back to Document diff --git a/web/docs/engine/guide/examples/full-manual-control.md b/web/docs/engine/guide/examples/full-manual-control.md index 39be4d34b3d..98d4236f4d7 100644 --- a/web/docs/engine/guide/examples/full-manual-control.md +++ b/web/docs/engine/guide/examples/full-manual-control.md @@ -28,15 +28,15 @@ Include the following script in the HEAD of your page: } document.f.multilingual.focus(); - keyman.setActiveKeyboard('', ''); + await keyman.setActiveKeyboard('', ''); }); /* KWControlChange: Called when user selects an item in the KWControl SELECT */ - function KWControlChange() { + async function KWControlChange() { /* Select the keyboard in KeymanWeb */ var name = KWControl.value.substr(0, KWControl.value.indexOf("$$")); - var languageCode = KWControl.value.substr(KWControl.value.indexOf("$$"+2)); - keyman.setActiveKeyboard(name, languageCode); + var languageCode = KWControl.value.substr(KWControl.value.indexOf("$$") + 2); + await keyman.setActiveKeyboard(name, languageCode); /* Focus onto the multilingual field in the form */ document.f.multilingual.focus(); } diff --git a/web/docs/engine/guide/examples/manual-control.md b/web/docs/engine/guide/examples/manual-control.md index d67db736e96..86759499365 100644 --- a/web/docs/engine/guide/examples/manual-control.md +++ b/web/docs/engine/guide/examples/manual-control.md @@ -2,7 +2,10 @@ title: Manual Mode Example --- -In this example, the web page designer specifies when KeymanWeb's on-screen keyboard may be displayed on non-mobile devices. They have also specified that the LaoKeys keyboard should be activated by default. This example continues to use the KeymanWeb default interface. Please click [this link](__manual-control.html) to open the test page. +In this example, the web page designer specifies when KeymanWeb's on-screen keyboard may be +displayed on non-mobile devices. They have also specified that the LaoKeys keyboard should be +activated by default. This example continues to use the KeymanWeb default interface. Please click +[this link](__manual-control.html) to open the test page. ## Code Walkthrough @@ -17,7 +20,7 @@ Include the following script in the HEAD of your page: languages:{id:'lo',name:'Lao'}, filename: "./js/laokeys.js" }); - keyman.setActiveKeyboard('laokeys'); + await keyman.setActiveKeyboard('laokeys'); keyman.osk.hide(); }); diff --git a/web/docs/engine/reference/compatibility/FocusLastActiveElement.md b/web/docs/engine/reference/compatibility/FocusLastActiveElement.md deleted file mode 100644 index 49cb6dfed40..00000000000 --- a/web/docs/engine/reference/compatibility/FocusLastActiveElement.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: FocusLastActiveElement ---- - -## Summary - -Restore the focus to the element active before input was moved to -KeymanWeb. - -## Syntax - -``` -keyman.FocusLastActiveElement() -``` - -### Parameters - -None. - -### Return Value - -`undefined` - -### Replaced By - -[`keyman.focusLastActiveElement()`](../core/focusLastActiveElement) diff --git a/web/docs/engine/reference/compatibility/GetLastActiveElement.md b/web/docs/engine/reference/compatibility/GetLastActiveElement.md deleted file mode 100644 index 32ca21c1035..00000000000 --- a/web/docs/engine/reference/compatibility/GetLastActiveElement.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: GetLastActiveElement ---- - -## Summary - -Returns the last active target element before the current KMW operation. - -## Syntax - -``` -keyman.GetLastActiveElement(); -``` - -### Parameters - -None. - -### Return Value - -`Element` -: The requested element. - -### Replaced By - -[`keyman.getLastActiveElement()`](../core/getLastActiveElement) diff --git a/web/docs/engine/reference/compatibility/HideHelp.md b/web/docs/engine/reference/compatibility/HideHelp.md deleted file mode 100644 index 4c916c02a06..00000000000 --- a/web/docs/engine/reference/compatibility/HideHelp.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: HideHelp ---- - -## Summary - -Hides the OSK (visual help). - -## Syntax - -``` -keyman.HideHelp(); -``` - -### Parameters - -None. - -### Return Value - -`undefined` - -### Replaced By - -[`keyman.osk.hide()`](../osk/hide) diff --git a/web/docs/engine/reference/compatibility/ShowPinnedHelp.md b/web/docs/engine/reference/compatibility/ShowPinnedHelp.md deleted file mode 100644 index 43b3e40946b..00000000000 --- a/web/docs/engine/reference/compatibility/ShowPinnedHelp.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: ShowPinnedHelp ---- - -## Summary - -Shows the on-screen keyboard and locks it at the resulting location. - -## Syntax - -``` -keyman.ShowPinnedHelp(x, y); -``` - -### Parameters - -None. - -### Return Value - -`undefined` - -### Replaced by - -[`keyman.osk.setRect()`](../osk/setRect), -[`keyman.osk.show()`](../osk/show) diff --git a/web/docs/engine/reference/compatibility/index.md b/web/docs/engine/reference/compatibility/index.md deleted file mode 100644 index 98f4e37813e..00000000000 --- a/web/docs/engine/reference/compatibility/index.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Compatibility Functions ---- - -The following *KeymanWeb* core functions -have been retained for compatibility with existing custom keyboards, but -should not be used in any new keyboards or user interfaces. Equivalent -new function calls are indicated. - -[`GetLastActiveElement` Function](GetLastActiveElement) -: [`keyman.getLastActiveElement()`](../core/getLastActiveElement) - - - -[`FocusLastActiveElement` Function](FocusLastActiveElement) -: [`keyman.focusLastActiveElement()`](../core/focusLastActiveElement) - - - -[`HideHelp` Function](HideHelp) -: [`keyman.osk.hide()`](../osk/hide) - - - -[`ShowHelp` Function](ShowHelp) -: [`keyman.osk.setPos()`](../osk/setPos), - [`keyman.osk.show()`](../osk/show) - - - -[`ShowPinnedHelp` Function](ShowPinnedHelp) -: [`keyman.osk.setRect()`](../osk/setRect), - [`keyman.osk.show()`](../osk/show) diff --git a/web/docs/engine/reference/core/index.md b/web/docs/engine/reference/core/index.md index 8ce45877fde..ff474d539bf 100644 --- a/web/docs/engine/reference/core/index.md +++ b/web/docs/engine/reference/core/index.md @@ -1,5 +1,5 @@ --- -title: Core Module +title: `keyman` - Core Module --- The KeymanWeb core module is exposed to the developer as `window.keyman`. diff --git a/web/docs/engine/reference/events/index.md b/web/docs/engine/reference/events/index.md index df7538c2c19..962c09d795f 100644 --- a/web/docs/engine/reference/events/index.md +++ b/web/docs/engine/reference/events/index.md @@ -7,8 +7,8 @@ interface to control the appearance and behavior of user interface elements. Standard event-processing requires all arguments to be passed as an array (object) with named member variables. -Two components of Keyman Engine for Web specify events: -* `keyman` object -- the main component +Two components of Keyman Engine for Web specify events: +* `keyman` object -- the core component * `keyman.osk` object -- the on-screen keyboard component Object events are handled in user code by passing the handler entry to diff --git a/web/docs/engine/reference/index.md b/web/docs/engine/reference/index.md index f6e5a3a8def..2445ec292a2 100644 --- a/web/docs/engine/reference/index.md +++ b/web/docs/engine/reference/index.md @@ -5,38 +5,38 @@ title: KeymanWeb Reference [KeymanWeb Overview](overview) : KeymanWeb is a cross-browser JavaScript input method solution. -[KeymanWeb Core Module](core) -: The KeymanWeb core module is exposed to the developer as `window.keyman`. +# API Reference -[On-Screen Keyboard Module](osk) -: The On-Screen Keyboard module is exposed to the developer as `window.keyman.osk`. +[`keyman`](core) +: Core APIs for KeymanWeb, including initialization, adding keyboards, focus and element control. -[KeymanWeb Utility Module](util) -: The KeymanWeb Utility Function module is exposed to the developer as `window.keyman.util`. +[`keyman.osk`](osk) +: On-Screen Keyboard module. -[KeymanWeb Output Functions](interface) -: KeymanWeb exposes its keyboard interface object through `window.keyman.interface`, providing a number of functions for low-level processing of input, context and output. +[`keyman.util`](util) +: Utility functions. -[KeymanWeb Compatibility Functions](compatibility) -: The following KeymanWeb core functions have been retained for compatibility with existing custom keyboards, but should not be used in any new keyboards or user interfaces. Equivalent new function calls are indicated. +[`keyman.interface` (also `KeymanWeb`)](interface) +: API surface for keyboards, providing a number of functions for low-level processing of input, context and output. -[Desktop User Interfaces](ui) +[`keyman.ui` - Desktop User Interfaces](ui) : Four different KeymanWeb user interfaces for desktop browsers are included, allowing users to select and enable keyboard mapping from a list of installed keyboards, and to control the visibility of the On-Screen Keyboard. -[KeymanWeb Initialization Options](core/init#init_options) -: The following options can be defined by the site designer in the initialization call to KeymanWeb. +# API Guides -[KeymanWeb Events](events) -: A number of KeymanWeb events are exposed to allow the designer of a user interface to control the appearance and behavior of user interface elements. Standard event-processing requires all arguments to be passed as an array (object) with named member variables. +[Initialization Options](core/init#init_options) +: The following options can be defined by the site designer in the initialization call to KeymanWeb. -[KeymanWeb Keyboard Properties](keyboard_properties) -: Most keyboards are generated automatically from the Keyman keyboard source by Keyman Developer and contain properties used by KeymanWeb during keyboard mapping. +[Events](events) +: A number of KeymanWeb events are exposed to allow the designer of a user interface to control the appearance and behavior of user interface elements. -[KeymanWeb Layout Designer](layouts) -: One of the main features of KeymanWeb is its ability to support distinct, user-customizable layouts for touch-screen keyboards on phones and tablets. +[Keyboard Properties](keyboard_properties) +: Standard properties for a Keyboard object returned from a keyboard .js file. -[KeymanWeb Layout Specifications](layoutspec) +[Touch Layout Specifications](layoutspec) : Touch-screen layouts for KeymanWeb are specified as JSON objects containing a member object for each specified device type. -[KeymanWeb Keyboard Development Reference](/developer/language/guide/multi-platform) +# See Also + +[Keyboard Development Reference](/developer/language/guide/multi-platform) : Information on how to explicitly support KeymanWeb in custom keyboards. diff --git a/web/docs/engine/reference/interface/FocusLastActiveElement.md b/web/docs/engine/reference/interface/FocusLastActiveElement.md new file mode 100644 index 00000000000..a06e8867810 --- /dev/null +++ b/web/docs/engine/reference/interface/FocusLastActiveElement.md @@ -0,0 +1,38 @@ +--- +title: FocusLastActiveElement (deprecated) +--- + +## Summary + +Restore the focus to the text store active before input was moved to KeymanWeb. + +Note that the name is misleading: this function move focus to the last active +`TextStore`, not the last active `HTMLElement` (in practice, this is equivalent +in a web browser context). + +## Syntax + +```js +keyman.interface.FocusLastActiveElement(); +keyman.interface.focusLastActiveElement(); +KeymanWeb.FocusLastActiveElement(); +KeymanWeb.focusLastActiveElement(); +``` + +### Parameters + +None. + +### Return Value + +`undefined` + +### Replaced By + +This function is deprecated; see instead: + +* [`keyman.interface.focusLastActiveTextStore()`](focusLastActiveTextStore) + +### See also + +* [`keyman.focusLastActiveElement()`](../core/focusLastActiveElement) diff --git a/web/docs/engine/reference/interface/GetLastActiveElement.md b/web/docs/engine/reference/interface/GetLastActiveElement.md new file mode 100644 index 00000000000..ee6eaf0e8d8 --- /dev/null +++ b/web/docs/engine/reference/interface/GetLastActiveElement.md @@ -0,0 +1,39 @@ +--- +title: GetLastActiveElement (deprecated) +--- + +## Summary + +Returns the last active target text store before the current KMW operation. + +Note that the name is misleading: this function returns the last active +`TextStore`, not the last active `HTMLElement`. Use instead +[`keyman.interface.getLastActiveTextStore()`](getLastActiveTextStore). + +## Syntax + +```js +keyman.interface.GetLastActiveElement(); +keyman.interface.getLastActiveElement(); +KeymanWeb.GetLastActiveElement(); +KeymanWeb.getLastActiveElement(); +``` + +### Parameters + +None. + +### Return Value + +`TextStore` +: The requested text store. + +### Replaced By + +This function is deprecated; see instead: + +* [`keyman.interface.getLastActiveTextStore()`](getLastActiveTextStore) + +### See also + +* [`keyman.getLastActiveElement()`](../core/getLastActiveElement) diff --git a/web/docs/engine/reference/interface/HideHelp.md b/web/docs/engine/reference/interface/HideHelp.md new file mode 100644 index 00000000000..f5f7a6c0a18 --- /dev/null +++ b/web/docs/engine/reference/interface/HideHelp.md @@ -0,0 +1,30 @@ +--- +title: HideHelp (deprecated) +--- + +## Summary + +Hides the OSK (visual help). + +## Syntax + +```js +keyman.interface.HideHelp(); +keyman.interface.hideHelp(); +KeymanWeb.HideHelp(); +KeymanWeb.hideHelp(); +``` + +### Parameters + +None. + +### Return Value + +`undefined` + +### Replaced By + +This function is deprecated; see instead: + +[`keyman.osk.hide()`](../osk/hide) diff --git a/web/docs/engine/reference/compatibility/ShowHelp.md b/web/docs/engine/reference/interface/ShowHelp.md similarity index 65% rename from web/docs/engine/reference/compatibility/ShowHelp.md rename to web/docs/engine/reference/interface/ShowHelp.md index 9647ec6b166..3ded765ff24 100644 --- a/web/docs/engine/reference/compatibility/ShowHelp.md +++ b/web/docs/engine/reference/interface/ShowHelp.md @@ -1,15 +1,18 @@ --- -title: ShowHelp +title: ShowHelp (deprecated) --- - + ## Summary Shows the on-screen keyboard at the requested (x, y) coordinates. ## Syntax -``` -keyman.ShowHelp(x, y); +```js +keyman.interface.ShowHelp(x, y); +keyman.interface.showHelp(x, y); +KeymanWeb.ShowHelp(x, y); +KeymanWeb.showHelp(x, y); ``` ### Parameters @@ -28,5 +31,7 @@ keyman.ShowHelp(x, y); ### Replaced by +This function is deprecated; see instead: + [`keyman.osk.setPos()`](../osk/setPos), [`keyman.osk.show()`](../osk/show) diff --git a/web/docs/engine/reference/interface/ShowPinnedHelp.md b/web/docs/engine/reference/interface/ShowPinnedHelp.md new file mode 100644 index 00000000000..f038fe5a6d0 --- /dev/null +++ b/web/docs/engine/reference/interface/ShowPinnedHelp.md @@ -0,0 +1,31 @@ +--- +title: ShowPinnedHelp (deprecated) +--- + +## Summary + +Shows the on-screen keyboard and locks it at the resulting location. + +## Syntax + +```js +keyman.interface.ShowPinnedHelp(); +keyman.interface.showPinnedHelp(); +KeymanWeb.ShowPinnedHelp(); +KeymanWeb.showPinnedHelp(); +``` + +### Parameters + +None. + +### Return Value + +`undefined` + +### Replaced by + +This function is deprecated; see instead: + +* [`keyman.osk.setRect()`](../osk/setRect), +* [`keyman.osk.show()`](../osk/show) diff --git a/web/docs/engine/reference/interface/focusLastActiveTextStore.md b/web/docs/engine/reference/interface/focusLastActiveTextStore.md new file mode 100644 index 00000000000..d0ab7127db7 --- /dev/null +++ b/web/docs/engine/reference/interface/focusLastActiveTextStore.md @@ -0,0 +1,24 @@ +--- +title: focusLastActiveTextStore +--- + +## Summary + +Set focus to the currently-focused or most recently focused text store. This is +for use, for example, by IME keyboards to return the focus to the text store +after the user clicks on an IME element, without resetting context. + +## Syntax + +```js +keyman.interface.focusLastActiveTextStore(); +KeymanWeb.focusLastActiveTextStore(); +``` + +### Parameters + +None. + +### Return Value + +`undefined` diff --git a/web/docs/engine/reference/interface/getLastActiveTextStore.md b/web/docs/engine/reference/interface/getLastActiveTextStore.md new file mode 100644 index 00000000000..7a0ab7a9b39 --- /dev/null +++ b/web/docs/engine/reference/interface/getLastActiveTextStore.md @@ -0,0 +1,26 @@ +--- +title: getLastActiveTextStore +--- + +## Summary + +Get the currently or most recently focused text store. This is for use by IME +keyboards, to allow for more complex focus interactions when clicking IME +elements. + +## Syntax + +```js +keyman.interface.getLastActiveTextStore(); +KeymanWeb.getLastActiveTextStore(); +``` + +### Parameters + +None. + +### Return Value + +`TextStore` +: The last active text store. + diff --git a/web/docs/engine/reference/interface/index.md b/web/docs/engine/reference/interface/index.md index d257b7e3e33..943fbb35e35 100644 --- a/web/docs/engine/reference/interface/index.md +++ b/web/docs/engine/reference/interface/index.md @@ -1,107 +1,147 @@ --- -title: Output Functions - the Keyboard API +title: `keyman.interface` - the Keyboard API --- -The KeymanWeb core object `window.keyman.interface` (with legacy-oriented, deprecated alias `KeymanWeb`) exposes a number of functions for low-level processing of input, context and output. These functions are designed for use by keyboards compiled through Keyman Developer in order to facilitate input text processing and will also work for custom-coded KeymanWeb keyboards. As such, most of these functions should only be called by keyboard code, and a good understanding of the [Keyman Keyboard Language](/developer/language) will prove extremely beneficial toward understanding the keyboard API functions enumerated in this section. +The KeymanWeb core object `window.keyman.interface` exposes a number of +functions for low-level processing of input, context and output. These functions +are designed for use by keyboards compiled through Keyman Developer in order to +facilitate input text processing and will also work for custom-coded KeymanWeb +keyboards. These functions should only be called by keyboard code, and a good +understanding of the [Keyman Keyboard Language](/developer/language) will prove +extremely beneficial toward understanding the keyboard API functions enumerated +in this section. -Custom user interfaces would not normally use these functions, but they are described here as some custom keyboards, such as IME-style keyboards, may need to interact with the user interface. +The KeymanWeb Keyboard API publishes a deprecated global `KeymanWeb` which is +the same as `keyman.interface`. Use `keyman.interface` in preference to +`KeymanWeb`. + +Custom user interfaces should not use these functions. These functions are intended +for use in a keyboard context, in particular during a keystroke event cycle. [`any` Function (Deprecated)](any) : Returns whether or not the char `ch` is found within the [`any`](/developer/language/reference/any)([`store`](/developer/language/reference/store)) string, setting an internally-tracked index for use in the `indexOutput` function. -: Shorthand name: `KeymanWeb.KA` +: Shorthand name: `keyman.interface.KA` [`beep` Function](beep) : Flash body or element as substitute for an audible feedback [`beep`](/developer/language/reference/beep). -: Shorthand name: `KeymanWeb.KB` +: Shorthand name: `keyman.interface.KB` [`beepReset` Function](beepReset) : Cancels a previous feedback [`beep`](/developer/language/reference/beep) operation on a page element. -: Shorthand name: `KeymanWeb.KBR` +: Shorthand name: `keyman.interface.KBR` [`context` Function (Deprecated)](context) : Gets [`context`](/developer/language/reference/context) for an ongoing keyboard operation relative to the caret's present position. -: Shorthand name: `KeymanWeb.KC` +: Shorthand name: `keyman.interface.KC` [`contextExOutput` Function](contextExOutput) : Emits the character or object at `contextOffset` from the current matched rule's context. -: Shorthand name: `KeymanWeb.KCXO` +: Shorthand name: `keyman.interface.KCXO` [`contextMatch` Function (Deprecated)](contextMatch) : Context matching: Returns `true` if the specified `context` call matches a provided string. -: Shorthand name: `KeymanWeb.KCM` +: Shorthand name: `keyman.interface.KCM` [`deadkeyMatch` Function (Deprecated)](deadkeyMatch) : Deadkey matching: Seeks to match the [`deadkey`](/developer/language/reference/deadkey) state `dk` at the relative caret position `n`. -: Shorthand name: `KeymanWeb.KDM` +: Shorthand name: `keyman.interface.KDM` [`deadkeyOutput` Function](deadkeyOutput) : Deadkey output: Associates the [`deadkey`](/developer/language/reference/deadkey) state `dk` with the element at the current caret position, after overwriting `nd` characters. -: Shorthand name: `KeymanWeb.KDO` +: Shorthand name: `keyman.interface.KDO` [`deleteContext` Function](deleteContext) : Context deletion - removes the specified number of deadkeys and characters from the left of the caret. -: Shorthand name: `KeymanWeb.KDC` +: Shorthand name: `keyman.interface.KDC` + +[`focusLastActiveTextStore` Function](focusLastActiveTextStore) +: Set focus to the currently-focused or most recently focused text store. [`fullContextMatch` Function](fullContextMatch) : Context matching: Returns `true` if the current context matches the specified rule context specification. -: Shorthand name: `KeymanWeb.KFCM` +: Shorthand name: `keyman.interface.KFCM` + +[`getLastActiveTextStore` Function](getLastActiveTextStore) +: Get the currently or most recently focused text store. This is for use by IME keyboards. [`ifStore` Function](ifStore) : `ifStore` compares the content of a [system `store`](/developer/language/guide/stores#toc-system-stores) with a string value. -: Shorthand name: `KeymanWeb.KIFS` +: Shorthand name: `keyman.interface.KIFS` [`indexOutput` Function](indexOutput) : Index-based output: Outputs a mapped character according to a previous selection from a `keyman.interface.any()` call upon a [`store`](/developer/language/reference/store) string, after deleting `nd` characters. -: Shorthand name: `KeymanWeb.KIO` +: Shorthand name: `keyman.interface.KIO` [`insertText` Function](insertText) : Inserts a text string and optional [`deadkey`](/developer/language/reference/deadkey) into the active output element. -: Shorthand name: `KeymanWeb.KT` +: Shorthand name: `keyman.interface.KT` [`isKeypress` Function](isKeypress) : Returns `true` if the input event corresponds to a keypress event resulting in character output. -: Shorthand name: `KeymanWeb.KIK` +: Shorthand name: `keyman.interface.KIK` [`keyInformation` Function](keyInformation) : Returns an object with extended information about a specified keystroke event. -: Shorthand name: `KeymanWeb.KKI` +: Shorthand name: `keyman.interface.KKI` [`keyMatch` Function](keyMatch) : Keystroke matching: Returns `true` if the event matches the rule's shift mask and key code. -: Shorthand name: `KeymanWeb.KKM` +: Shorthand name: `keyman.interface.KKM` [`loadStore` Function](loadStore) : Load an option [`store`](/developer/language/guide/stores) value from a cookie or default value if no prior stored value exists. -: Shorthand name: `KeymanWeb.KLOAD` +: Shorthand name: `keyman.interface.KLOAD` [`nul` Function](_nul) : [`nul`](/developer/language/reference/nul) context check: Returns `true` if the length of the [`context`](/developer/language/reference/context) is less than or equal to `n` characters. -: Shorthand name: `KeymanWeb.KN` +: Shorthand name: `keyman.interface.KN` [`output` Function](output) : Outputs the specified string to an element, overwriting characters before the caret if specified. -: Shorthand name: `KeymanWeb.KO` +: Shorthand name: `keyman.interface.KO` [`registerKeyboard` Function](registerKeyboard) : Register the keyboard stub and load the keyboard. -: Shorthand name: `KeymanWeb.KR` +: Shorthand name: `keyman.interface.KR` [`registerStub` Function](registerStub) : Register the keyboard stub, return true if already registered. -: Shorthand name: `KeymanWeb.KRS` +: Shorthand name: `keyman.interface.KRS` [`saveFocus` Function](saveFocus) : Save focus: Temporarily saves keyboard processing data for the currently-focused control. -: Shorthand name: `KeymanWeb.KSF` +: Shorthand name: `keyman.interface.KSF` [`saveStore` Function](saveStore) : Save an option [`store`](/developer/language/guide/stores) value to a cookie for the active keyboard. -: Shorthand name: `KeymanWeb.KSAVE` +: Shorthand name: `keyman.interface.KSAVE` [`setStore` Function](setStore) : `setStore` sets the value of a [system `store`](/developer/language/guide/stores#toc-system-stores) to a string. -: Shorthand name: `KeymanWeb.KSETS` +: Shorthand name: `keyman.interface.KSETS` [`stateMatch` Function](stateMatch) : State-key matching: Returns `true` if the event matches the rule's state-key requirements. -: Shorthand name: `KeymanWeb.KSM` +: Shorthand name: `keyman.interface.KSM` + +## Deprecated functions for IMEs + +The following functions have been retained for compatibility with existing IME +keyboards, but should not be used in any new keyboards or user interfaces. + +[`GetLastActiveElement` or `getLastActiveElement` Function (Deprecated)](GetLastActiveElement) +: Use [`keyman.interface.getLastActiveTextStore()`](getLastActiveTextStore) + +[`FocusLastActiveElement` or `focusLastActiveElement` Function (Deprecated)](FocusLastActiveElement) +: Use [`keyman.interface.focusLastActiveTextStore()`](focusLastActiveTextStore) + +[`HideHelp` or `hideHelp` Function (Deprecated)](HideHelp) +: Use [`keyman.osk.hide()`](../osk/hide) + +[`ShowHelp` or `showHelp` Function (Deprecated)](ShowHelp) +: Use [`keyman.osk.setPos()`](../osk/setPos) +: Use [`keyman.osk.show()`](../osk/show) + +[`ShowPinnedHelp` or `showPinnedHelp` Function (Deprecated)](ShowPinnedHelp) +: Use [`keyman.osk.setRect()`](../osk/setRect) +: Use [`keyman.osk.show()`](../osk/show) diff --git a/web/docs/engine/reference/layouts.md b/web/docs/engine/reference/layouts.md deleted file mode 100644 index 56d1e40d625..00000000000 --- a/web/docs/engine/reference/layouts.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Layout Designer ---- - -One of the main features of *KeymanWeb* is its ability to support distinct, user-customizable -layouts for touch-screen keyboards on phones and tablets. *Keyman Developer* includes a layout -designer to simplify the process of creating custom layouts for any keyboard. diff --git a/web/docs/engine/reference/osk/index.md b/web/docs/engine/reference/osk/index.md index 999a25c504c..d95aeaa5d0d 100644 --- a/web/docs/engine/reference/osk/index.md +++ b/web/docs/engine/reference/osk/index.md @@ -1,5 +1,5 @@ --- -title: On-Screen Keyboard Module +title: `keyman.osk` - On-Screen Keyboard Module --- The On-Screen Keyboard module is exposed to the developer as `window.keyman.osk`. diff --git a/web/docs/engine/reference/overview.md b/web/docs/engine/reference/overview.md index 712a20640bf..4fef7b48af4 100644 --- a/web/docs/engine/reference/overview.md +++ b/web/docs/engine/reference/overview.md @@ -11,7 +11,7 @@ the *On-Screen Keyboard* module, a *Utility function* library, or one of the sta Interface* modules. A *KeymanWeb* instance is automatically constructed when you include the compiled KeymanWeb -script (kmw-release.js) in your web page source. +script in your web page source. The *KeymanWeb API* comprises the following objects: @@ -26,3 +26,13 @@ The *KeymanWeb API* comprises the following objects: - *User Interface* : Exposed as [`keyman.ui`](ui). + +The *KeymanWeb Keyboard API* provides endpoints for KeymanWeb keyboards to +interact with KeymanWeb. This API should not be used by website code. + +- *Interface* +: Exposed as [`keyman.interface`](interface) + +The KeymanWeb Keyboard API publishes a deprecated global `KeymanWeb` which is +the same as `keyman.interface`. Use `keyman.interface` in preference to +`KeymanWeb`. \ No newline at end of file diff --git a/web/docs/engine/reference/ui/index.md b/web/docs/engine/reference/ui/index.md index 74788baf516..53a555996d5 100644 --- a/web/docs/engine/reference/ui/index.md +++ b/web/docs/engine/reference/ui/index.md @@ -1,7 +1,7 @@ --- -title: Desktop User Interfaces +title: `keyman.ui` - Desktop User Interfaces --- - + Four different KeymanWeb user interfaces for desktop browsers are included, allowing users to select and enable keyboard mapping from a list of installed keyboards, and to control the visibility of the diff --git a/web/docs/engine/reference/util/index.md b/web/docs/engine/reference/util/index.md index f599e3d5d6c..eb860adaa01 100644 --- a/web/docs/engine/reference/util/index.md +++ b/web/docs/engine/reference/util/index.md @@ -1,5 +1,5 @@ --- -title: Utility Module +title: `keyman.util` - Utility Module --- The KeymanWeb Utility Function module is exposed to the developer as `window.keyman.util`. diff --git a/web/index.html b/web/index.html index 7dc31756496..87a7fe5cb53 100644 --- a/web/index.html +++ b/web/index.html @@ -6,10 +6,6 @@ - - - - KeymanWeb Testing