diff --git a/HISTORY.md b/HISTORY.md index 53b5c68dcd..bf2f6887cc 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,13 @@ # Keyman Version History +## 19.0.35 alpha 2025-05-12 + +* maint(common): add two more functions to detect if running on CI (#13869) +* maint(common): use unique names for Keyman version variables (#13854) +* maint(common): use new `builder_is_running_on_*` functions (#13875) +* maint(common): remove obsolete code from docker-images `run.sh` (#13879) +* chore(mac): remove obsolete key processing code (#13848) + ## 19.0.34 alpha 2025-05-08 * docs(common): update `builder_echo` documentation for TC blocks (#13855) diff --git a/VERSION.md b/VERSION.md index d8676f6b6a..e99a6f6be4 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.35 \ No newline at end of file +19.0.36 \ No newline at end of file diff --git a/android/KMAPro/build.gradle b/android/KMAPro/build.gradle index 5614fcae6a..c644d44fca 100644 --- a/android/KMAPro/build.gradle +++ b/android/KMAPro/build.gradle @@ -8,10 +8,10 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.9.2' // sentry-android-gradle-plugin 2.1.5+ requires AGP 7.0 classpath 'io.sentry:sentry-android-gradle-plugin:4.6.0' - classpath 'name.remal:gradle-plugins:1.5.0' + classpath 'name.remal:gradle-plugins:1.9.2' // From jcenter() which was deprecated August 2024 // https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/ diff --git a/android/KMAPro/build.sh b/android/KMAPro/build.sh index aef29ae0a0..fd54215d1c 100755 --- a/android/KMAPro/build.sh +++ b/android/KMAPro/build.sh @@ -37,7 +37,7 @@ builder_parse "$@" if builder_is_debug_build; then builder_heading "### Debug config ####" CONFIG="debug" - BUILD_FLAGS="assembleDebug -x lint -x test" + BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug" TEST_FLAGS="-x assembleDebug lintDebug testDebug" fi diff --git a/android/KMAPro/gradle.properties b/android/KMAPro/gradle.properties index 93aaf1410c..f56c02ac58 100644 --- a/android/KMAPro/gradle.properties +++ b/android/KMAPro/gradle.properties @@ -1,4 +1,7 @@ android.enableJetifier=true android.useAndroidX=true +# AGP 8.0 generates R classes for resources defined in the current module only. +android.nonTransitiveRClass=false + org.gradle.jvmargs=-Xmx1024m diff --git a/android/KMAPro/gradle/wrapper/gradle-wrapper.properties b/android/KMAPro/gradle/wrapper/gradle-wrapper.properties index 3994438e22..d6e308a637 100644 --- a/android/KMAPro/gradle/wrapper/gradle-wrapper.properties +++ b/android/KMAPro/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index 46750a183f..864fb36ee9 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -20,6 +20,15 @@ android { noCompress "kmp" } + buildFeatures { + buildConfig = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + defaultConfig { applicationId "com.tavultesoft.kmapro" minSdkVersion 21 @@ -156,7 +165,7 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' implementation 'com.stepstone.stepper:material-stepper:4.3.1' api(name: 'keyman-engine', ext: 'aar') diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index fbe8475f09..3eec444fbd 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -440,41 +440,41 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_info: - showInfo(); - return true; - case R.id.action_share: - showShareDialog(); - return true; + // Android Gradle 8.0 no longer declares resources final, so can't use switch statement here + if (item.getItemId() == R.id.action_info) { + showInfo(); + return true; + } else if (item.getItemId() == R.id.action_share) { + showShareDialog(); + return true; /* Disable Web Browser to investigate Google sign-in - case R.id.action_web: + } else if ((item.getItemId() == R.id.action_web) { showWebBrowser(); return true;*/ - case R.id.action_text_size: - showTextSizeDialog(); - return true; - case R.id.action_clear_text: - showClearTextDialog(); - return true; - case R.id.action_get_started: - showGetStarted(); - return true; - case R.id.action_settings: - showSettings(); - return true; - case R.id.action_update_keyboards: - KMManager.getUpdateTool().executeOpenUpdates(); - // Dismiss icon - updateUpdateCountIndicator(0); - final MenuItem _keyboardupdate = menu.findItem(R.id.action_update_keyboards); - if (_keyboardupdate != null && _keyboardupdate.isVisible()) { - _keyboardupdate.setVisible(false); - } + } else if (item.getItemId() == R.id.action_text_size) { + showTextSizeDialog(); + return true; + } else if (item.getItemId() == R.id.action_clear_text){ + showClearTextDialog(); + return true; + } else if (item.getItemId() == R.id.action_get_started) { + showGetStarted(); + return true; + } else if (item.getItemId() == R.id.action_settings) { + showSettings(); + return true; + } else if (item.getItemId() == R.id.action_update_keyboards) { + KMManager.getUpdateTool().executeOpenUpdates(); + // Dismiss icon + updateUpdateCountIndicator(0); + final MenuItem _keyboardupdate = menu.findItem(R.id.action_update_keyboards); + if (_keyboardupdate != null && _keyboardupdate.isVisible()) { + _keyboardupdate.setVisible(false); + } - return true; - default: - return super.onOptionsItemSelected(item); + return true; + } else { + return super.onOptionsItemSelected(item); } } @@ -632,7 +632,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene Intent i = new Intent(this, InfoActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); startActivity(i); - overridePendingTransition(android.R.anim.fade_in, R.anim.hold); + overridePendingTransition(android.R.anim.fade_in, com.keyman.engine.R.anim.hold); } private void showWebBrowser() { @@ -643,7 +643,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene Intent i = new Intent(this, WebBrowserActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); startActivity(i); - overridePendingTransition(android.R.anim.fade_in, R.anim.hold); + overridePendingTransition(android.R.anim.fade_in, com.keyman.engine.R.anim.hold); } private void showShareDialog() { diff --git a/android/KMEA/app/build.gradle b/android/KMEA/app/build.gradle index c556e2588c..dd4251025b 100644 --- a/android/KMEA/app/build.gradle +++ b/android/KMEA/app/build.gradle @@ -1,4 +1,5 @@ plugins { + id 'base' id 'com.android.library' id 'name.remal.default-plugins' } @@ -6,10 +7,25 @@ plugins { ext.rootPath = '../../' apply from: "$rootPath/version.gradle" +base { + // Specify library filename keyman-engine-$CONFIG.aar + archivesName = "keyman-engine" +} + android { compileSdk 34 namespace "com.keyman.engine" + buildFeatures { + // needed for custom BuildConfigField values in defaultConfig + buildConfig = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + defaultConfig { minSdkVersion 21 targetSdkVersion 34 @@ -17,9 +33,6 @@ android { // KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle but Gradle removes them for libraries buildConfigField "String", "KEYMAN_ENGINE_VERSION_NAME", "\""+KEYMAN_VERSION_NAME+"\"" buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\"" - - // Sepcify library filename keyman-engine-$CONFIG.aar - setProperty("archivesBaseName", "keyman-engine") } buildTypes { @@ -36,10 +49,6 @@ android { } testOptions { - unitTests { - // Allows use of a simulated Android API for tests. (Thanks, roboelectric!) - includeAndroidResources = true - } unitTests.all { testLogging { // May also add "passed", "skipped". @@ -55,17 +64,17 @@ android { dependencies { // 1.6.0-rc01 needed to resolve https://issuetracker.google.com/issues/238425626 - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' // material:1.7.0 will need Gradle plugin 7.1.0+ implementation 'com.google.android.material:material:1.12.0' - implementation 'commons-io:commons-io:2.13.0' + implementation 'commons-io:commons-io:2.16.1' implementation 'io.sentry:sentry-android:7.8.0' implementation 'androidx.preference:preference:1.2.1' // Robolectric - testImplementation 'androidx.test:core:1.5.0' - testImplementation 'androidx.test.ext:junit:1.1.5' - testImplementation 'org.robolectric:robolectric:4.10.3' + testImplementation 'androidx.test.ext:junit:1.2.1' + testImplementation 'org.robolectric:robolectric:4.14.1' + testImplementation 'androidx.test:core:1.6.1' // Generate QR Codes implementation ('com.github.kenglxn.QRGen:android:3.0.1') { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java index bb8ee190b6..ce32f2beeb 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java @@ -1,5 +1,6 @@ package com.keyman.engine.cloud; +import android.annotation.SuppressLint; import android.app.DownloadManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -60,6 +61,7 @@ public class CloudDownloadMgr{ * Append downloadreceiver to the main context. * @param aContext the context */ + @SuppressLint("UnspecifiedRegisterReceiverFlag") public synchronized void initialize(Context aContext) { if(isInitialized) @@ -73,6 +75,7 @@ public class CloudDownloadMgr{ aContext.registerReceiver(completeListener, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_EXPORTED); } else { + // Needs @SuppressLint UnspecifiedRegisterReceiverFlag aContext.registerReceiver(completeListener, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); } } catch (IllegalArgumentException e) { diff --git a/android/KMEA/build.gradle b/android/KMEA/build.gradle index 9aa2d0dcc7..4ca999ea60 100644 --- a/android/KMEA/build.gradle +++ b/android/KMEA/build.gradle @@ -5,10 +5,10 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.9.2' // io.sentry:sentry-android-gradle-plugin not available for library project classpath 'io.sentry:sentry-android:7.8.0' - classpath 'name.remal:gradle-plugins:1.5.0' + classpath 'name.remal:gradle-plugins:1.9.2' } } diff --git a/android/KMEA/build.sh b/android/KMEA/build.sh index c8c79c13de..dcd4c9510a 100755 --- a/android/KMEA/build.sh +++ b/android/KMEA/build.sh @@ -37,7 +37,7 @@ builder_parse "$@" if builder_is_debug_build; then builder_heading "### Debug config ####" CONFIG="debug" - BUILD_FLAGS="assembleDebug -x lint -x test" + 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\']" fi diff --git a/android/KMEA/gradle/wrapper/gradle-wrapper.properties b/android/KMEA/gradle/wrapper/gradle-wrapper.properties index 3994438e22..8330c6a158 100644 --- a/android/KMEA/gradle/wrapper/gradle-wrapper.properties +++ b/android/KMEA/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Wed Apr 30 11:17:37 ICT 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/README.md b/android/README.md index 6abb94161d..db1eb592b0 100644 --- a/android/README.md +++ b/android/README.md @@ -141,6 +141,10 @@ android { aaptOptions { noCompress "kmp" } + + buildFeatures { + buildConfig = true + } ``` 5. After the `android {}` object, include the following: ````gradle @@ -154,7 +158,7 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' api (name:'keyman-engine', ext:'aar') implementation 'androidx.preference:preference:1.2.1' diff --git a/android/Samples/KMSample1/app/build.gradle b/android/Samples/KMSample1/app/build.gradle index 51490c5ab2..cc92dee298 100644 --- a/android/Samples/KMSample1/app/build.gradle +++ b/android/Samples/KMSample1/app/build.gradle @@ -36,7 +36,7 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' api(name: 'keyman-engine', ext: 'aar') implementation 'androidx.preference:preference:1.2.1' diff --git a/android/Samples/KMSample1/build.gradle b/android/Samples/KMSample1/build.gradle index 15824022e2..18d4c46cd5 100644 --- a/android/Samples/KMSample1/build.gradle +++ b/android/Samples/KMSample1/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.9.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/android/Samples/KMSample1/gradle.properties b/android/Samples/KMSample1/gradle.properties index 80f4cae5a8..f486cc756a 100644 --- a/android/Samples/KMSample1/gradle.properties +++ b/android/Samples/KMSample1/gradle.properties @@ -18,4 +18,8 @@ # org.gradle.parallel=true android.enableJetifier=true android.useAndroidX=true + +# AGP 8.0 generates R classes for resources defined in the current module only. +android.nonTransitiveRClass=false + org.gradle.jvmargs=-Xmx1024m diff --git a/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties b/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties index fb79de47c0..4fba274552 100644 --- a/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties +++ b/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/android/Samples/KMSample2/app/build.gradle b/android/Samples/KMSample2/app/build.gradle index 71ab1dab03..cd2f4d6225 100644 --- a/android/Samples/KMSample2/app/build.gradle +++ b/android/Samples/KMSample2/app/build.gradle @@ -35,7 +35,7 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' api (name:'keyman-engine', ext:'aar') implementation 'androidx.preference:preference:1.2.1' diff --git a/android/Samples/KMSample2/build.gradle b/android/Samples/KMSample2/build.gradle index 15824022e2..18d4c46cd5 100644 --- a/android/Samples/KMSample2/build.gradle +++ b/android/Samples/KMSample2/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.9.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/android/Samples/KMSample2/gradle.properties b/android/Samples/KMSample2/gradle.properties index 80f4cae5a8..f486cc756a 100644 --- a/android/Samples/KMSample2/gradle.properties +++ b/android/Samples/KMSample2/gradle.properties @@ -18,4 +18,8 @@ # org.gradle.parallel=true android.enableJetifier=true android.useAndroidX=true + +# AGP 8.0 generates R classes for resources defined in the current module only. +android.nonTransitiveRClass=false + org.gradle.jvmargs=-Xmx1024m diff --git a/android/Samples/KMSample2/gradle/wrapper/gradle-wrapper.properties b/android/Samples/KMSample2/gradle/wrapper/gradle-wrapper.properties index 4b0560a7a2..0c0a7aec7d 100644 --- a/android/Samples/KMSample2/gradle/wrapper/gradle-wrapper.properties +++ b/android/Samples/KMSample2/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/android/Tests/KeyboardHarness/app/build.gradle b/android/Tests/KeyboardHarness/app/build.gradle index 3eba70ec99..ab8066393b 100644 --- a/android/Tests/KeyboardHarness/app/build.gradle +++ b/android/Tests/KeyboardHarness/app/build.gradle @@ -14,6 +14,10 @@ android { noCompress "kmp" } + buildFeatures { + buildConfig = true + } + defaultConfig { applicationId "com.keyman.android.tests.keyboardHarness" minSdkVersion 21 @@ -42,7 +46,7 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' api (name:'keyman-engine', ext:'aar') implementation 'androidx.preference:preference:1.2.1' diff --git a/android/Tests/KeyboardHarness/build.gradle b/android/Tests/KeyboardHarness/build.gradle index ce0b936509..8fdf2aa422 100644 --- a/android/Tests/KeyboardHarness/build.gradle +++ b/android/Tests/KeyboardHarness/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.0' + classpath 'com.android.tools.build:gradle:8.9.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/android/Tests/KeyboardHarness/build.sh b/android/Tests/KeyboardHarness/build.sh index 65671872f8..c23f8ca0f5 100755 --- a/android/Tests/KeyboardHarness/build.sh +++ b/android/Tests/KeyboardHarness/build.sh @@ -33,7 +33,7 @@ ARTIFACT="app-release-unsigned.apk" if builder_is_debug_build; then builder_heading "### Debug config ####" CONFIG="debug" - BUILD_FLAGS="assembleDebug -x lint -x test" + BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug" TEST_FLAGS="-x assembleDebug lintDebug testDebug" ARTIFACT="app-$CONFIG.apk" fi diff --git a/android/Tests/KeyboardHarness/gradle.properties b/android/Tests/KeyboardHarness/gradle.properties index 80f4cae5a8..8bb139036f 100644 --- a/android/Tests/KeyboardHarness/gradle.properties +++ b/android/Tests/KeyboardHarness/gradle.properties @@ -19,3 +19,6 @@ android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx1024m + +# AGP 8.0 generates R classes for resources defined in the current module only. +android.nonTransitiveRClass=false diff --git a/android/Tests/KeyboardHarness/gradle/wrapper/gradle-wrapper.properties b/android/Tests/KeyboardHarness/gradle/wrapper/gradle-wrapper.properties index da62d22563..3b14c8260c 100644 --- a/android/Tests/KeyboardHarness/gradle/wrapper/gradle-wrapper.properties +++ b/android/Tests/KeyboardHarness/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/android/Tests/keycode/gradle/wrapper/gradle-wrapper.properties b/android/Tests/keycode/gradle/wrapper/gradle-wrapper.properties index 763217c50f..8357d84852 100644 --- a/android/Tests/keycode/gradle/wrapper/gradle-wrapper.properties +++ b/android/Tests/keycode/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/developer/src/common/web/utils/package.json b/developer/src/common/web/utils/package.json index caff2e23a5..20f71e6e9a 100644 --- a/developer/src/common/web/utils/package.json +++ b/developer/src/common/web/utils/package.json @@ -11,7 +11,7 @@ "dependencies": { "@keymanapp/common-types": "*", "eventemitter3": "^5.0.0", - "fast-xml-parser": "^5.0.9", + "fast-xml-parser": "^5.2.2", "path-browserify": "^1.0.1", "restructure": "^3.0.1", "sax": ">=0.6.0", diff --git a/developer/src/common/web/utils/src/compiler-callbacks.ts b/developer/src/common/web/utils/src/compiler-callbacks.ts index a7163175e8..dda2c1485b 100644 --- a/developer/src/common/web/utils/src/compiler-callbacks.ts +++ b/developer/src/common/web/utils/src/compiler-callbacks.ts @@ -7,6 +7,21 @@ import { CompilerEvent, CompilerCallbackOptions, CompilerErrorSeverity, CompilerError, CompilerMessageOverrideMap, CompilerErrorMask } from "./compiler-interfaces.js"; +/** + * The EventResolver implementation is able to expand error messages, + * particularly to change a byte offset into a line number. + */ +export interface EventResolver { + resolve(event: CompilerEvent): void; +} + +/** A do-nothing EventResolver. Implementations can use this as their default EventResolver. */ +export class NullEventResolver implements EventResolver { + resolve(event: CompilerEvent): void { + // do nothing, this is the null event resolver. + } +} + /** * Abstract interface for callbacks, to abstract out file i/o */ @@ -40,6 +55,10 @@ export interface CompilerCallbacks { */ resolveFilename(baseFilename: string, filename: string): string; + /** + * Report a message from the compiler back to the caller. + * @param event + */ reportMessage(event: CompilerEvent): void; debug(msg: string): void; @@ -106,14 +125,64 @@ export interface CompilerFileSystemAsyncCallbacks { resolveFilename(baseFilename: string, filename: string): string; } +/** a CompilerCallbacks implementation that delegates to a parent */ +export class DelegatingCompilerCallbacks implements CompilerCallbacks { + constructor(protected options: CompilerCallbackOptions, protected parent: CompilerCallbacks) { + } + + loadFile(filename: string): Uint8Array { + return this.parent.loadFile(filename); + } + + fileSize(filename: string): number { + return this.parent.fileSize(filename); + } + + isDirectory(filename: string): boolean { + return this.parent.isDirectory(filename); + } + + get path(): CompilerPathCallbacks { + return this.parent.path; + } + + get fs(): CompilerFileSystemCallbacks { + return this.parent.fs; + } + + get net(): CompilerNetAsyncCallbacks { + return this.parent.net; + } + + get fsAsync(): CompilerFileSystemAsyncCallbacks { + return this.parent.fsAsync; + } + + resolveFilename(baseFilename: string, filename: string): string { + return this.parent.resolveFilename(baseFilename, filename); + } + + debug(msg: string): void { + return this.parent.debug(msg); + } + + fileURLToPath(url: string | URL): string { + return this.parent.fileURLToPath(url); + } + + reportMessage(event: CompilerEvent) { + this.parent.reportMessage(event); + } +} /** * Wrapper class for CompilerCallbacks for a given input file */ -export class CompilerFileCallbacks implements CompilerCallbacks { +export class CompilerFileCallbacks extends DelegatingCompilerCallbacks { messages: CompilerEvent[] = []; - constructor(private filename: string, private options: CompilerCallbackOptions, private parent: CompilerCallbacks) { + constructor(private filename: string, options: CompilerCallbackOptions, parent: CompilerCallbacks) { + super(options, parent); } /** @@ -170,38 +239,6 @@ export class CompilerFileCallbacks implements CompilerCallbacks { this.messages = []; } - loadFile(filename: string): Uint8Array { - return this.parent.loadFile(filename); - } - - fileSize(filename: string): number { - return this.parent.fileSize(filename); - } - - isDirectory(filename: string): boolean { - return this.parent.isDirectory(filename); - } - - get path(): CompilerPathCallbacks { - return this.parent.path; - } - - get fs(): CompilerFileSystemCallbacks { - return this.parent.fs; - } - - get net(): CompilerNetAsyncCallbacks { - return this.parent.net; - } - - get fsAsync(): CompilerFileSystemAsyncCallbacks { - return this.parent.fsAsync; - } - - resolveFilename(baseFilename: string, filename: string): string { - return this.parent.resolveFilename(baseFilename, filename); - } - reportMessage(event: CompilerEvent): void { const disable = CompilerFileCallbacks.applyMessageOverridesToEvent(event, this.options.messageOverrides); this.messages.push(event); @@ -209,14 +246,6 @@ export class CompilerFileCallbacks implements CompilerCallbacks { this.parent.reportMessage({ filename: this.filename, ...event }); } } - - debug(msg: string): void { - return this.parent.debug(msg); - } - - fileURLToPath(url: string | URL): string { - return this.parent.fileURLToPath(url); - } } export class DefaultCompilerFileSystemAsyncCallbacks implements CompilerFileSystemAsyncCallbacks { @@ -242,3 +271,15 @@ export class DefaultCompilerFileSystemAsyncCallbacks implements CompilerFileSyst return this.owner.resolveFilename(baseFilename, filename); } } + +/** a CompilerCallbacks that applies the EventResolver to any reported message */ +export class ResolvingCompilerCallbacks extends DelegatingCompilerCallbacks { + constructor(private eventResolver: EventResolver, options: CompilerCallbackOptions, parent: CompilerCallbacks) { + super(options, parent); + } + + reportMessage(event: CompilerEvent) { + this.eventResolver.resolve(event); + this.parent.reportMessage(event); + } +} diff --git a/developer/src/common/web/utils/src/compiler-interfaces.ts b/developer/src/common/web/utils/src/compiler-interfaces.ts index 18b93d9c88..aaeffc8d8c 100644 --- a/developer/src/common/web/utils/src/compiler-interfaces.ts +++ b/developer/src/common/web/utils/src/compiler-interfaces.ts @@ -5,7 +5,17 @@ import { CompilerCallbacks } from "./compiler-callbacks.js"; */ export interface CompilerEvent { filename?: string; + /** line where a message applies */ line?: number; + /** + * column where a message applies. + */ + column?: number; + /** + * offset where a message applies. + * If set, encompasses line and column. + */ + offset?: number; code: number; message: string; /** diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index 7ed873905f..937cd98642 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -40,6 +40,10 @@ export { CompilerPathCallbacks, CompilerFileSystemCallbacksFolderEntry as FileSystemFolderEntry, DefaultCompilerFileSystemAsyncCallbacks, + EventResolver, + NullEventResolver, + DelegatingCompilerCallbacks, + ResolvingCompilerCallbacks, } from './compiler-callbacks.js'; export { defaultCompilerOptions, CompilerBaseOptions, CompilerOptions, CompilerEvent, CompilerErrorNamespace, @@ -63,8 +67,9 @@ export { UrlSubpathCompilerCallback } from './utils/UrlSubpathCompilerCallback.j export { DeveloperUtilsMessages } from './developer-utils-messages.js'; export * as SourceFilenamePatterns from './source-filename-patterns.js'; -export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js'; - +export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader, KeymanXMLMetadata, XML_FILENAME_SYMBOL } from './xml-utils.js'; +export { SymbolUtils } from './symbol-utils.js'; +export * as LineUtils from './line-utils.js'; export * as GitHubUrls from './github-urls.js'; export * as CloudUrls from './cloud-urls.js'; @@ -72,4 +77,4 @@ export { getFontFamily, getFontFamilySync } from './font-family.js'; export * as ValidIds from './valid-ids.js'; -export * as ProjectLoader from './project-loader.js'; \ No newline at end of file +export * as ProjectLoader from './project-loader.js'; diff --git a/developer/src/common/web/utils/src/line-utils.ts b/developer/src/common/web/utils/src/line-utils.ts new file mode 100644 index 0000000000..2e1cced292 --- /dev/null +++ b/developer/src/common/web/utils/src/line-utils.ts @@ -0,0 +1,124 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by srl on 2025-04-03 + * + * Abstraction for line number processing + */ + +import { CompilerEvent, EventResolver } from "./index.js"; + +/** line number with an optional column number */ +export interface LineColumn { + line: number; + column?: number; +} + +export type LineList = number[]; + +/** a LineFinder is able to convert from an offset to a line/column */ +export class LineFinder { + private list: LineList; + constructor(public text: string) { + } + + public getLineList(): LineList { + if (!this.list) { + this.list = LineFinder.textToLines(this.text); + } + return this.list; + } + + /** + * Convert an offset into line:column + * @param offset input offset into the text + * @returns line:column information + */ + public findOffset(offset: number): LineColumn { + return LineFinder.offsetToLineColumn(offset, this.getLineList()); + } + + /** + * Preprocess text to turn it into arrays of line lengths. + * This is effectively a 1-based line length, since line 0 has length of + * 0. + * Note that the fast-xml-parser converts \r\n to \n before processing, + * and this function does as well. So, any offsets passed in will need to + * have that taken into consideration. + */ + public static textToLines(text: string): LineList { + return [ + 0, // "line 0" is empty + ...text.replaceAll("\r\n", "\n").split("\n") + .map(l => l.length + 1) // line length (counting the trailing newline) + ]; + } + + /** + * convert a line number array to a line/column. + * Note that the fast-xml-parser converts \r\n to \n before processing, + * and this function does as well. So, any offsets passed in will need to + * have that taken into consideration. + */ + public static offsetToLineColumn(offset: number, list: LineList): LineColumn { + for (let line = 1; line < list.length; line++) { // 1-based (assume the first row is 0) + if (list[line] > offset) { + return { line, column: offset }; + } + offset = offset - (list[line]); // count newline at end + } + // default: line 0, error + return { line: 0 } + } +} + +/** + * Interface for a class which can receive file contents, + * keyed by the filename + */ +export interface FileConsumer { + /** + * @param filename name of the file + * @param contents string contents of the file + */ + addFile(filename: string, contents: string): void; +} + +/** Cache of LineFinder elements, organized by filename */ +export class LineFinderCache implements FileConsumer { + contentsCache: Map = new Map(); + + /** add or update a source file */ + addFile(filename: string, contents: string): void { + this.contentsCache.set(filename, new LineFinder(contents)); + } + + getByFilename(filename: string): LineFinder | undefined { + const lf = this.contentsCache.get(filename); + return lf; + } +} + +/** + * This EventResolver expands events that have a filename and offset set, + * by looking up the event.offset in the LineFinder list. + * The expanded event has the event.line and event.column fields set. + */ +export class LineFinderEventResolver implements EventResolver, FileConsumer { + private lfcache = new LineFinderCache(); + + addFile(filename: string, contents: string): void { + this.lfcache.addFile(filename, contents); + } + + /** resolve a CompilerEvent by expanding the line numbers */ + resolve(event: CompilerEvent) { + if (event.offset && !event.line && event.filename) { + const lf = this.lfcache.getByFilename(event.filename); + const loc = lf.findOffset(event.offset); + event.line = loc.line; + event.column = loc.column; + } + return event; + } +} diff --git a/developer/src/common/web/utils/src/symbol-utils.ts b/developer/src/common/web/utils/src/symbol-utils.ts new file mode 100644 index 0000000000..92cf07dd02 --- /dev/null +++ b/developer/src/common/web/utils/src/symbol-utils.ts @@ -0,0 +1,33 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by srl on 2025-03-27 + * + * Utilities for manipulating Symbol properties + */ + +export class SymbolUtils { + /** + * Copy symbols shallowly from 'from' onto 'onto' + * @param onto object to copy onto + * @param from source for symbols + * @returns the onto object + */ + public static copySymbols(onto: T, from: any): T { + const o = onto as any; + for (const sym of Object.getOwnPropertySymbols(from)) { + o[sym] = from[sym]; + } + return onto; + } + + /** use Object.entries to remove all symbols, recursively. */ + public static removeSymbols(from: T): T { + if (Array.isArray(from)) { + return from.map(o => SymbolUtils.removeSymbols(o)) as T; + } + if (typeof from !== "object") return from; + return Object.fromEntries(Object.entries(from).map(([k, v]) => ([k, SymbolUtils.removeSymbols(v)]))) as T; + } + +} diff --git a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts index c8bbbe40dd..0a31bc051b 100644 --- a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -5,12 +5,14 @@ */ import { SchemaValidators, util } from '@keymanapp/common-types'; import { DeveloperUtilsMessages } from '../../developer-utils-messages.js'; -import { CompilerCallbacks } from "../../compiler-callbacks.js"; +import { CompilerCallbacks, EventResolver } from "../../compiler-callbacks.js"; +import { CompilerEvent } from "../../compiler-interfaces.js"; import { LDMLKeyboardXMLSourceFile, LKImport, ImportStatus } from './ldml-keyboard-xml.js'; import { constants } from '@keymanapp/ldml-keyboard-constants'; import { LDMLKeyboardTestDataXMLSourceFile, LKTTest, LKTTests } from './ldml-keyboard-testdata-xml.js'; -import { KeymanXMLReader } from '@keymanapp/developer-utils'; import boxXmlArray = util.boxXmlArray; +import { LineFinderEventResolver } from '../../line-utils.js'; +import { XML_FILENAME_SYMBOL, KeymanXMLReader } from '../../xml-utils.js'; interface NameAndProps { '$'?: any; // content @@ -25,28 +27,44 @@ export class LDMLKeyboardXMLSourceFileReaderOptions { localImportsPaths: string[]; }; -export class LDMLKeyboardXMLSourceFileReader { +export class LDMLKeyboardXMLSourceFileReader implements EventResolver { + /** for resolving messages involving line numbers */ + private eventResolver: LineFinderEventResolver = new LineFinderEventResolver(); + constructor(private options: LDMLKeyboardXMLSourceFileReaderOptions, private callbacks : CompilerCallbacks) { } + resolve(event: CompilerEvent): void { + this.eventResolver.resolve(event); + } static get defaultImportsURL(): [string,string] { return ['../import/', import.meta.url]; } - readImportFile(version: string, subpath: string): Uint8Array { - const importPath = this.callbacks.resolveFilename(this.options.cldrImportsPath, `${version}/${subpath}`); - return this.callbacks.loadFile(importPath); + /** bottleneck for reading keyboard XML files */ + readFile(path: string): Uint8Array { + const data = this.callbacks.loadFile(path); + if (data) { + this.eventResolver.addFile(path, new TextDecoder().decode(data)); + } + return data; } - readLocalImportFile(path: string): Uint8Array { + /** @returns [data, filename] */ + readImportFile(version: string, subpath: string): [Uint8Array, string] { + const importPath = this.callbacks.resolveFilename(this.options.cldrImportsPath, `${version}/${subpath}`); + return [this.readFile(importPath), importPath]; + } + + readLocalImportFile(path: string): [Uint8Array, string] { // try each of the local imports paths for (const localPath of this.options.localImportsPaths) { const importPath = this.callbacks.path.join(localPath, path); if(this.callbacks.fs.existsSync(importPath)) { - return this.callbacks.loadFile(importPath); + return [this.readFile(importPath), importPath]; } } - return null; // was not able to load from any of the paths + return [null, null]; // was not able to load from any of the paths } /** @@ -223,6 +241,7 @@ export class LDMLKeyboardXMLSourceFileReader { return false; } let importData: Uint8Array; + let importPath: string; if (base === constants.cldr_import_base) { // CLDR import @@ -235,10 +254,10 @@ export class LDMLKeyboardXMLSourceFileReader { /** There's no data or DTD change in 45, 46, 46.1, 47 so map them all to 46 at present. */ paths[0] = constants.cldr_version_latest; } - importData = this.readImportFile(paths[0], paths[1]); + [importData, importPath] = this.readImportFile(paths[0], paths[1]); } else { // local import - importData = this.readLocalImportFile(path); + [importData, importPath] = this.readLocalImportFile(path); } if (!importData || !importData.length) { this.callbacks.reportMessage(DeveloperUtilsMessages.Error_ImportReadFail({base, path, subtag})); @@ -246,6 +265,7 @@ export class LDMLKeyboardXMLSourceFileReader { } const importXml: any = this.loadUnboxed(importData); // TODO-LDML: have to load as any because it is an arbitrary part const importRootNode = importXml[subtag]; // e.g. + this.eventResolver.addFile(importPath, new TextDecoder().decode(importData)); // TODO: double decode // importXml will have one property: the root element. if (!importRootNode) { @@ -263,7 +283,11 @@ export class LDMLKeyboardXMLSourceFileReader { return false; } // Mark all children as an import - subsubval.forEach(o => o[ImportStatus.import] = basePath); + subsubval.forEach(o => { + o[ImportStatus.import] = basePath; + KeymanXMLReader.setMetaData(o, {[XML_FILENAME_SYMBOL as any]: importPath}); // mark overriding import path + }); + if (implied) { // mark all children as an implied import subsubval.forEach(o => o[ImportStatus.impliedImport] = basePath); diff --git a/developer/src/common/web/utils/src/xml-utils.ts b/developer/src/common/web/utils/src/xml-utils.ts index 22adcd7c22..f83ca75dc7 100644 --- a/developer/src/common/web/utils/src/xml-utils.ts +++ b/developer/src/common/web/utils/src/xml-utils.ts @@ -6,7 +6,13 @@ * Abstraction for XML reading and writing */ -import { XMLParser, XMLBuilder, XmlBuilderOptions, X2jOptions } from 'fast-xml-parser'; +import { XMLParser, XMLBuilder, XMLMetaData, X2jOptions, XmlBuilderOptions } from 'fast-xml-parser'; +import { SymbolUtils } from "./symbol-utils.js"; + +/** Symbol giving the start offset, in chars, of the node */ +const XML_META_DATA_SYMBOL = XMLParser.getMetaDataSymbol(); +/** Symbol giving an override which file a node came from */ +export const XML_FILENAME_SYMBOL = Symbol("XML Filename"); export type KeymanXMLType = 'keyboard3' // LDML @@ -48,6 +54,7 @@ const PARSER_OPTIONS: KeymanXMLParserOptionsBag = { // if we do need elements in the future, we'd check the preserve-space attribute here. return tagValue?.trim(); }, + captureMetaData: true, trimValues: false, // preserve spaces, but see tagValueProcessor }, 'keyboardTest3': { @@ -108,11 +115,54 @@ const GENERATOR_OPTIONS: KeymanXMLGeneratorOptionsBag = { }, }; +export interface KeymanXMLMetadata extends XMLMetaData { + /** override of name of XML file */ + [XML_FILENAME_SYMBOL]?: string; +} + /** wrapper for XML parsing support */ export class KeymanXMLReader { public constructor(public type: KeymanXMLType) { } + /** Get metadata on a node if not already set */ + static getMetaData(o: any) : KeymanXMLMetadata { + if(!o) return o; + const metadata : KeymanXMLMetadata = o[XML_META_DATA_SYMBOL as any]; + return metadata; + } + + /** Set metadata if not already set */ + public static setMetaData(o: any, metadata: KeymanXMLMetadata) : KeymanXMLMetadata { + let m : KeymanXMLMetadata = KeymanXMLReader.getMetaData(o); + if (!m) { + m = {}; + } + // copy non-symbols + m = {...metadata, ...m}; + // copy symbols + SymbolUtils.copySymbols(m, metadata); + o[XML_META_DATA_SYMBOL as any] = m; + return m; + } + + /** set metadata on this and children with the default filename - if not already set */ + public static setDefaultFilename(data: any, filename: string) { + if (!data || !filename) return; + if (typeof data === 'object') { + const m = KeymanXMLReader.getMetaData(data) || {}; + if (!m[XML_FILENAME_SYMBOL]) { + (m as any)[XML_FILENAME_SYMBOL] = filename; + KeymanXMLReader.setMetaData(data, m); + } + if (Array.isArray(data)) { + data.forEach(e => KeymanXMLReader.setDefaultFilename(e, filename)); + } else for(const k of Object.keys(data)) { + KeymanXMLReader.setDefaultFilename(data[k], filename); + } + } + } + /** move `{ $abc: 4 }` into `{ $: { abc: 4 } }` */ private static fixupDollarAttributes(data: any) : any { if (typeof data === 'object') { @@ -131,9 +181,9 @@ export class KeymanXMLReader { } }); if (attrs.length) { - e.push(['$', Object.fromEntries(attrs)]); + e.push(['$', SymbolUtils.copySymbols(Object.fromEntries(attrs), data)]); } - return Object.fromEntries(e); + return SymbolUtils.copySymbols(Object.fromEntries(e), data); } else { return data; } @@ -169,7 +219,7 @@ export class KeymanXMLReader { } } }); - return Object.fromEntries(e); + return SymbolUtils.copySymbols(Object.fromEntries(e), data); } else { return data; } @@ -249,12 +299,11 @@ export class KeymanXMLReader { } public parser() { - let options = PARSER_OPTIONS[this.type]; + const options = PARSER_OPTIONS[this.type]; if (!options) { /* c8 ignore next 1 */ throw Error(`Internal error: unhandled XML type ${this.type}`); } - options = Object.assign({}, options); // TODO: xml2js likes to mutate the options here. Shallow clone the object. return new XMLParser(options); } } diff --git a/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts b/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts index 700d223406..d1a7cdddb1 100644 --- a/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts +++ b/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts @@ -6,6 +6,7 @@ import { KPJFileReader } from "../../src/types/kpj/kpj-file-reader.js"; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { KPJFileWriter } from '../../src/types/kpj/kpj-file-writer.js'; import { KeymanDeveloperProjectOptions } from '../../src/types/kpj/keyman-developer-project.js'; +import { SymbolUtils } from '../../src/symbol-utils.js'; const callbacks = new TestCompilerCallbacks(); @@ -21,12 +22,12 @@ describe('kpj-file-writer', function () { const writer = new KPJFileWriter(); const output = writer.write(project); - const outputKpj = reader.read(new TextEncoder().encode(output)); + // Remove XML metadata symbols to reduce clutter for testing purposes + const outputKpj = SymbolUtils.removeSymbols(reader.read(new TextEncoder().encode(output))); // The outputKpj may not contain all the fields from the inputKpj, only the // essential fields. Many of the fields in .kpj are deprecated, when they // relate to file content (e.g. parented files, file details) - assert.deepEqual(outputKpj.KeymanDeveloperProject.Options, { "BuildPath": "$PROJECTPATH\\build", "CompilerWarningsAsErrors": "True", @@ -61,7 +62,8 @@ describe('kpj-file-writer', function () { const writer = new KPJFileWriter(); const output = writer.write(project); - const outputKpj = reader.read(new TextEncoder().encode(output)); + // Remove XML metadata symbols to reduce clutter for testing purposes + const outputKpj = SymbolUtils.removeSymbols(reader.read(new TextEncoder().encode(output))); // The outputKpj may not contain all the fields from the inputKpj, only the // essential fields. Many of the fields in .kpj are deprecated, when they diff --git a/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts b/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts index 28396a3ca0..117a48a281 100644 --- a/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts +++ b/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts @@ -14,6 +14,7 @@ import { makePathToFixture } from '../helpers/index.js'; import { KpsFileReader } from "../../src/types/kps/kps-file-reader.js"; import { KpsFileWriter } from '../../src/types/kps/kps-file-writer.js'; +import { SymbolUtils } from '../../src/symbol-utils.js'; import { DeveloperUtilsMessages } from '../../src/developer-utils-messages.js'; const callbacks = new TestCompilerCallbacks(); @@ -59,13 +60,15 @@ describe('kps-file-reader', function () { it('kps-file-reader should round-trip with kps-file-writer', function() { const input = fs.readFileSync(makePathToFixture('kps', 'khmer_angkor.kps')); const reader = new KpsFileReader(callbacks); - const kps = reader.read(input); + // Remove XML metadata symbols to reduce clutter for testing purposes + const kps = SymbolUtils.removeSymbols(reader.read(input)); const writer = new KpsFileWriter(); const output = writer.write(kps); // Round Trip - const kps2 = reader.read(new TextEncoder().encode(output)); + // Remove XML metadata symbols to reduce clutter for testing purposes + const kps2 = SymbolUtils.removeSymbols(reader.read(new TextEncoder().encode(output))); assert.deepEqual(kps2, kps); }); diff --git a/developer/src/common/web/utils/test/kvks/kvks-file.tests.ts b/developer/src/common/web/utils/test/kvks/kvks-file.tests.ts index 422a2e4a40..7e60119d28 100644 --- a/developer/src/common/web/utils/test/kvks/kvks-file.tests.ts +++ b/developer/src/common/web/utils/test/kvks/kvks-file.tests.ts @@ -5,6 +5,7 @@ import KvksFileReader from "../../src/types/kvks/kvks-file-reader.js"; import KvksFileWriter from "../../src/types/kvks/kvks-file-writer.js"; import { verify_khmer_angkor, verify_balochi_inpage } from './kvk-utils.tests.js'; import { assert } from 'chai'; +import { SymbolUtils } from '../../src/symbol-utils.js'; describe('kvks-file-reader', function() { it('should read a valid file', function() { @@ -63,7 +64,8 @@ describe('kvks-file-writer', function() { const input = fs.readFileSync(path); const reader = new KvksFileReader(); - const kvksExpected = reader.read(input); + // Remove XML metadata symbols to reduce clutter for testing purposes + const kvksExpected = SymbolUtils.removeSymbols(reader.read(input)); const invalidVkeys: string[] = []; const vk = reader.transform(kvksExpected, invalidVkeys); assert.isEmpty(invalidVkeys); @@ -74,6 +76,7 @@ describe('kvks-file-writer', function() { // We compare the (re)loaded data, because there may be // minor, irrelevant formatting differences in the emitted xml const kvks = reader.read(Buffer.from(output, 'utf8')); - assert.deepEqual(kvks, kvksExpected); + // Remove XML metadata symbols to reduce clutter for testing purposes + assert.deepEqual(SymbolUtils.removeSymbols(kvks), kvksExpected); }); }); diff --git a/developer/src/common/web/utils/test/ldml/ldml-keyboard-xml-reader.tests.ts b/developer/src/common/web/utils/test/ldml/ldml-keyboard-xml-reader.tests.ts index d2dfb86370..d09c1dfc41 100644 --- a/developer/src/common/web/utils/test/ldml/ldml-keyboard-xml-reader.tests.ts +++ b/developer/src/common/web/utils/test/ldml/ldml-keyboard-xml-reader.tests.ts @@ -8,6 +8,7 @@ import { testReaderCases } from '../helpers/reader-callback-test.js'; import CLDRScanToVkey = Constants.CLDRScanToVkey; import CLDRScanToKeyMap = Constants.CLDRScanToKeyMap; import USVirtualKeyCodes = Constants.USVirtualKeyCodes; +import { KeymanXMLReader, XML_FILENAME_SYMBOL } from '../../src/xml-utils.js'; function pluckKeysFromKeybag(keys: LKKey[], ids: string[]) { return keys.filter(({id}) => ids.indexOf(id) !== -1); @@ -140,10 +141,18 @@ describe('ldml keyboard xml reader tests', function () { { id: 'interrobang', output: '‽' }, { id: 'snail', output: '@' }, ]); + const snailKey = source?.keyboard3?.keys.key.find(({ id }) => id === 'snail'); // all of the keys are implied imports here - assert.isFalse(ImportStatus.isImpliedImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail'))); - assert.isTrue(ImportStatus.isImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail'))); - assert.isTrue(ImportStatus.isLocalImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail'))); + assert.isFalse(ImportStatus.isImpliedImport(snailKey)); + assert.isTrue(ImportStatus.isImport(snailKey)); + assert.isTrue(ImportStatus.isLocalImport(snailKey)); + // get the actual filename of where the import was located + const metadata = KeymanXMLReader.getMetaData(snailKey); + assert.ok(metadata); + const snailFilename = (metadata)[XML_FILENAME_SYMBOL]; + assert.ok(snailFilename); + assert.ok(/keys-Zyyy-morepunctuation.xml$/.test(snailFilename) + , `snail key filename is ${snailFilename}`); }, }, { diff --git a/developer/src/common/web/utils/test/line-utils.tests.ts b/developer/src/common/web/utils/test/line-utils.tests.ts new file mode 100644 index 0000000000..ffbdba5b0e --- /dev/null +++ b/developer/src/common/web/utils/test/line-utils.tests.ts @@ -0,0 +1,28 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by srl on 2025-04-03 + * + * Test for LineFinder + */ + +import { assert } from 'chai'; +import { readFileSync } from 'node:fs'; +import 'mocha'; +import { LineFinder } from '../src/line-utils.js'; +import { makePathToFixture } from './helpers/index.js'; + +describe(`LineFinder test`, () => { + const path = 'tran_fail-empty.xml'; + + it(`Should be able to process ${path}`, () => { + const xmlPath = makePathToFixture('xml', `${path}`); + const data = readFileSync(xmlPath, 'utf-8'); + assert.ok(data); + const lf = new LineFinder(data); + assert.ok(lf); + assert.deepEqual(lf.findOffset(40), { line: 3, column: 0 }); + assert.deepEqual(lf.findOffset(136), { line: 4, column: 2 }); + assert.deepEqual(lf.findOffset(186), { line: 8, column: 2 }); + }); +}); diff --git a/developer/src/common/web/utils/test/xml-utils.tests.ts b/developer/src/common/web/utils/test/xml-utils.tests.ts index 1cc9724b27..4b3df736b6 100644 --- a/developer/src/common/web/utils/test/xml-utils.tests.ts +++ b/developer/src/common/web/utils/test/xml-utils.tests.ts @@ -13,7 +13,9 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { KeymanXMLType, KeymanXMLReader, KeymanXMLWriter } from '../src/xml-utils.js'; +import { LineFinder } from '../src/line-utils.js'; import { makePathToFixture } from './helpers/index.js'; +import { SymbolUtils } from '../src/symbol-utils.js'; // if true, attempt to WRITE the fixtures const { GEN_XML_FIXTURES } = env; @@ -119,7 +121,7 @@ describe(`XML Reader Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => writeJson(jsonPath, actual); } else { assert.ok(expect, `Could not read ${jsonPath} - run with env GEN_XML_FIXTURES=1 to update.`); - assert.deepEqual(actual, expect, `Mismatch of ${xmlPath} vs ${jsonPath}`); + assert.deepEqual(SymbolUtils.removeSymbols(actual), expect, `Mismatch of ${xmlPath} vs ${jsonPath}`); } }); } @@ -160,3 +162,41 @@ describe(`XML Writer Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => }); } }); + +describe(`XML Reader line number test`, () => { + const path = 'tran_fail-empty.xml'; + const xmlPath = makePathToFixture('xml', `${path}`); + const type: KeymanXMLType = 'keyboard3'; + it(`Should report line numbers on parse of ${type} ${path}`, () => { + let xml = readData(xmlPath); + assert.ok(xml, `Could not read ${xmlPath}`); + xml = xml.replace(/\r\n/g, '\n'); + const reader = new KeymanXMLReader(type); + assert.ok(reader); + + // now, parse. subsitute endings for Win + const actual = reader.parse(xml); + const lines = LineFinder.textToLines(xml); + assert.ok(actual, `Parser failed on ${xmlPath}`); + + // now, assert char offset + const getMetaData = KeymanXMLReader.getMetaData; + assert.ok(getMetaData(actual.keyboard3)); + assert.equal( + getMetaData(actual.keyboard3)?.startIndex, 40); // index of element + assert.equal( + getMetaData(actual.keyboard3.info)?.startIndex, 136); // index of etc + assert.equal( + getMetaData(actual.keyboard3.transforms)?.startIndex, 186); + assert.deepEqual( + LineFinder.offsetToLineColumn( + getMetaData(actual.keyboard3).startIndex, lines), { line: 3, column: 0 }); + assert.deepEqual( + LineFinder.offsetToLineColumn( + getMetaData(actual.keyboard3.info).startIndex, lines), { line: 4, column: 2 }); + assert.deepEqual( + LineFinder.offsetToLineColumn( + getMetaData(actual.keyboard3.transforms).startIndex, lines), { line: 8, column: 2 }); + }); +}); + diff --git a/developer/src/kmc-ldml/src/compiler/compiler.ts b/developer/src/kmc-ldml/src/compiler/compiler.ts index 76dcb85635..b69e984728 100644 --- a/developer/src/kmc-ldml/src/compiler/compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/compiler.ts @@ -8,7 +8,9 @@ import { CompilerCallbacks, KeymanCompiler, KeymanCompilerResult, KeymanCompilerArtifacts, defaultCompilerOptions, LDMLKeyboardXMLSourceFileReader, LDMLKeyboard, LDMLKeyboardTestDataXMLSourceFile, KMXBuilder, - KeymanCompilerArtifactOptional + KeymanCompilerArtifactOptional, + ResolvingCompilerCallbacks, + KeymanXMLReader, } from "@keymanapp/developer-utils"; import { LdmlCompilerOptions } from './ldml-compiler-options.js'; import { LdmlCompilerMessages } from './ldml-compiler-messages.js'; @@ -94,6 +96,7 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { // uset parser private usetparser?: LdmlKeyboardTypes.UnicodeSetParser = undefined; + private reader?: LDMLKeyboardXMLSourceFileReader; /** * Initialize the compiler, including loading the WASM host for uset parsing. @@ -105,7 +108,9 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { */ async init(callbacks: CompilerCallbacks, options: LdmlCompilerOptions): Promise { this.options = { ...options }; - this.callbacks = callbacks; + this.reader = new LDMLKeyboardXMLSourceFileReader(this.options.readerOptions, callbacks); + // wrap the callbacks so that the eventresolver is called + this.callbacks = new ResolvingCompilerCallbacks(this.reader, this.options, callbacks); return true; } @@ -238,9 +243,9 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { * @returns the source file, or null if invalid */ public load(filename: string): LDMLKeyboardXMLSourceFile | null { - const reader = new LDMLKeyboardXMLSourceFileReader(this.options.readerOptions, this.callbacks); + const reader = this.reader; // load the file from disk into a string - const data = this.callbacks.loadFile(filename); + const data = reader.readFile(filename); if (!data) { this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidFile({ errorText: 'Unable to read XML file' })); return null; @@ -261,6 +266,9 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { return null; } + // record the default filename - for error reporting. + KeymanXMLReader.setDefaultFilename(source, filename); + return source; } @@ -272,7 +280,7 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { * @returns the source file, or null if invalid */ public loadTestData(filename: string): LDMLKeyboardTestDataXMLSourceFile | null { - const reader = new LDMLKeyboardXMLSourceFileReader(this.options.readerOptions, this.callbacks); + const reader = this.reader; const data = this.callbacks.loadFile(filename); if (!data) { this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidFile({ errorText: 'Unable to read XML file' })); diff --git a/developer/src/kmc-ldml/src/compiler/keys.ts b/developer/src/kmc-ldml/src/compiler/keys.ts index ceafb51902..7b79a3be83 100644 --- a/developer/src/kmc-ldml/src/compiler/keys.ts +++ b/developer/src/kmc-ldml/src/compiler/keys.ts @@ -446,7 +446,7 @@ export class KeysCompiler extends SectionCompiler { if (layer.row.length > keymap.length) { this.callbacks.reportMessage( - LdmlCompilerMessages.Error_HardwareLayerHasTooManyRows() + LdmlCompilerMessages.Error_HardwareLayerHasTooManyRows(layer) ); valid = false; } @@ -485,7 +485,10 @@ export class KeysCompiler extends SectionCompiler { } if (!keydef.output && !keydef.gap && !keydef.layerId) { this.callbacks.reportMessage( - LdmlCompilerMessages.Error_KeyMissingToGapOrSwitch({ keyId: key }) + LdmlCompilerMessages.Error_KeyMissingToGapOrSwitch( + { keyId: key }, + keydef, + ) ); valid = false; continue; diff --git a/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts index 7a925a33c5..28c5b52e1f 100644 --- a/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts +++ b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts @@ -1,5 +1,5 @@ import { util } from "@keymanapp/common-types"; -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def } from '@keymanapp/developer-utils'; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, XML_FILENAME_SYMBOL, CompilerEvent, KeymanXMLReader } from '@keymanapp/developer-utils'; // const SevInfo = CompilerErrorSeverity.Info | CompilerErrorNamespace.LdmlKeyboardCompiler; const SevHint = CompilerErrorSeverity.Hint | CompilerErrorNamespace.LdmlKeyboardCompiler; const SevWarn = CompilerErrorSeverity.Warn | CompilerErrorNamespace.LdmlKeyboardCompiler; @@ -9,6 +9,32 @@ const SevError = CompilerErrorSeverity.Error | CompilerErrorNamespace.LdmlKeyboa // sub-numberspace for transform errors const SevErrorTransform = SevError | 0xF00; +/** + * Any object with metadata, for line number errs. + * Could be for example an LKKeys or KMXPlus.KeysKeys object. + * Defined as 'any' here to reduce noise on the client side. + * @see {@link KeymanXMLReader.getMetaData()} + */ +type ObjectWithMetadata = any; + +/** + * Convenience function for constructing CompilerEvents with line numbers + * @param code Unique numeric value of the event + * @param message A short description of the error presented to the user + * @param x Object to be used as a source for line number information + * @param detail Detailed Markdown-formatted description of the error + * including references to documentation, remediation options. + * @see CompilerMessageSpec + * @returns + */ +function CompilerMessageObjectSpec(code: number, message: string, x: ObjectWithMetadata, detail?: string): CompilerEvent { + let evt = m(code, message, detail); // raw message + evt = LdmlCompilerMessages.offset(evt, x); // with offset + return evt; +}; + +const mx = CompilerMessageObjectSpec; + /** * @internal */ @@ -20,7 +46,11 @@ export class LdmlCompilerMessages { static Error_InvalidLocale = (o:{tag: string}) => m(this.ERROR_InvalidLocale, `Invalid BCP 47 locale form '${def(o.tag)}'`); static ERROR_HardwareLayerHasTooManyRows = SevError | 0x0003; - static Error_HardwareLayerHasTooManyRows = () => m(this.ERROR_HardwareLayerHasTooManyRows, `'hardware' layer has too many rows`); + static Error_HardwareLayerHasTooManyRows = (x: any) => mx( + this.ERROR_HardwareLayerHasTooManyRows, + `'hardware' layer has too many rows`, + x, + ); static ERROR_RowOnHardwareLayerHasTooManyKeys = SevError | 0x0004; static Error_RowOnHardwareLayerHasTooManyKeys = (o:{row: number, hardware: string, modifiers: string}) => m(this.ERROR_RowOnHardwareLayerHasTooManyKeys, `Row #${def(o.row)} on 'hardware' ${def(o.hardware)} layer for modifier ${o.modifiers || 'none'} has too many keys`); @@ -97,8 +127,11 @@ export class LdmlCompilerMessages { m(this.ERROR_DisplayIsRepeated, `display ${LdmlCompilerMessages.outputOrKeyId(o)} has more than one display entry.`); static ERROR_KeyMissingToGapOrSwitch = SevError | 0x0011; - static Error_KeyMissingToGapOrSwitch = (o:{keyId: string}) => - m(this.ERROR_KeyMissingToGapOrSwitch, `key id='${def(o.keyId)}' must have either output=, gap=, or layerId=.`); + static Error_KeyMissingToGapOrSwitch = (o:{keyId: string}, x: ObjectWithMetadata) => mx( + this.ERROR_KeyMissingToGapOrSwitch, + `key id='${def(o.keyId)}' must have either output=, gap=, or layerId=.`, + x, + ); static ERROR_ExcessHardware = SevError | 0x0012; static Error_ExcessHardware = (o:{formId: string}) => m(this.ERROR_ExcessHardware, @@ -274,5 +307,18 @@ export class LdmlCompilerMessages { `Invalid transform to="${def(o.to)}": "${def(o.message)}"`, ); - + /** + * Get an offset from o and set e's offset field + * @param event a compiler event, such as from functions in this class + * @param x any object parsed from XML or with the XML_META_DATA_SYMBOL symbol copied over + * @returns modified event object + */ + static offset(event: CompilerEvent, x?: any): CompilerEvent { + if(x) { + const metadata = KeymanXMLReader.getMetaData(x) || {}; + event.offset = metadata?.startIndex; + event.filename = event.filename || metadata[XML_FILENAME_SYMBOL]; + } + return event; + } } diff --git a/developer/src/kmc-ldml/test/helpers/compareXml.ts b/developer/src/kmc-ldml/test/helpers/compareXml.ts index f245da51a9..057986bdb2 100644 --- a/developer/src/kmc-ldml/test/helpers/compareXml.ts +++ b/developer/src/kmc-ldml/test/helpers/compareXml.ts @@ -1,6 +1,6 @@ import {assert} from 'chai'; import {readFileSync} from 'node:fs'; -import { KeymanXMLReader } from "@keymanapp/developer-utils"; +import { KeymanXMLReader, SymbolUtils } from "@keymanapp/developer-utils"; /** * @@ -21,5 +21,5 @@ export function compareXml(actual : string, expect: string, mutator?: (input: an const actualParsed = mutator(reader.parse(actualStr)); const expectParsed = mutator(reader.parse(expectStr)); - assert.deepEqual(actualParsed, expectParsed); + assert.deepEqual(SymbolUtils.removeSymbols(actualParsed), SymbolUtils.removeSymbols(expectParsed)); } diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index 4fc6d443ab..8cda5f5d4e 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import { fileURLToPath } from 'url'; import { SectionCompiler, SectionCompilerNew } from '../../src/compiler/section-compiler.js'; import { util, KMXPlus, LdmlKeyboardTypes } from '@keymanapp/common-types'; -import { CompilerEvent, compilerEventFormat, CompilerCallbacks, LDMLKeyboardXMLSourceFileReader, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboard, } from "@keymanapp/developer-utils"; +import { CompilerEvent, compilerEventFormat, CompilerCallbacks, LDMLKeyboardXMLSourceFileReader, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboard, KeymanXMLMetadata, KeymanXMLReader } from "@keymanapp/developer-utils"; import { LdmlKeyboardCompiler } from '../../src/main.js'; // make sure main.js compiles import { assert } from 'chai'; import { KMXPlusMetadataCompiler } from '../../src/compiler/metadata-compiler.js'; @@ -290,7 +290,7 @@ export function testCompilationCases(compiler: SectionCompilerNew, cases : Compi // no warnings, so expect zero messages assert.sameDeepMembers(callbacks.messages, [], 'expected zero messages but got ' + callbacks.messages); } - + if (expectFailure) { assert.isNull(section, 'expected compilation result failure (null)'); } else { @@ -327,3 +327,13 @@ const dontEscape = /[a-zA-Z0-9\.${}\[\]-]/; export function hex_str(s?: string) : string { return [...s].map(ch => dontEscape.test(ch) ? ch : util.escapeRegexChar(ch)).join(''); } + +/** return an object simulating an XML object with a column number */ +export function withColumn(c: number) : KeymanXMLMetadata { + // set metadata on an empty object + const o = {}; + KeymanXMLReader.setMetaData(o, { + startIndex: c + }); + return o; +} diff --git a/developer/src/kmc-ldml/test/keys.tests.ts b/developer/src/kmc-ldml/test/keys.tests.ts index 92799ea97e..3db4388a35 100644 --- a/developer/src/kmc-ldml/test/keys.tests.ts +++ b/developer/src/kmc-ldml/test/keys.tests.ts @@ -1,7 +1,7 @@ import 'mocha'; import { assert } from 'chai'; import { KeysCompiler } from '../src/compiler/keys.js'; -import { assertCodePoints, compilerTestCallbacks, loadSectionFixture, testCompilationCases } from './helpers/index.js'; +import { assertCodePoints, compilerTestCallbacks, loadSectionFixture, testCompilationCases, withColumn } from './helpers/index.js'; import { KMXPlus, Constants, LdmlKeyboardTypes } from '@keymanapp/common-types'; import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; import { constants } from '@keymanapp/ldml-keyboard-constants'; @@ -442,7 +442,7 @@ describe('keys.kmap', function () { assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_HardwareLayerHasTooManyRows()); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_HardwareLayerHasTooManyRows(withColumn(276))); }); it('should reject layouts with too many hardware keys', async function() { @@ -464,7 +464,10 @@ describe('keys.kmap', function () { const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-key-missing-attrs.xml', compilerTestCallbacks, keysDependencies) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_KeyMissingToGapOrSwitch({keyId: 'Q'})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_KeyMissingToGapOrSwitch( + {keyId: 'Q'}, + withColumn(188) + )); }); it('should accept layouts with gap/switch keys', async function() { const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/gap-switch.xml', compilerTestCallbacks, keysDependencies) as Keys; diff --git a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts index 98b62a8bad..a92314aa5f 100644 --- a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts +++ b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts @@ -45,6 +45,8 @@ export class NodeCompilerCallbacks implements CompilerCallbacks { messages: CompilerEvent[] = []; messageCount = 0; messageFilename: string = ''; + /** cache of the contentes of the text */ + messageFiletext: string = ''; maxLogMessages = MaxMessagesDefault; constructor(private options: CompilerCallbackOptions) { @@ -55,6 +57,7 @@ export class NodeCompilerCallbacks implements CompilerCallbacks { this.messages = []; this.messageCount = 0; this.messageFilename = ''; + this.messageFiletext = ''; } /** @@ -154,6 +157,7 @@ export class NodeCompilerCallbacks implements CompilerCallbacks { // Reset max message limit when a new file is being processed this.messageFilename = event.filename; this.messageCount = 0; + this.messageFiletext = ''; } const disable = CompilerFileCallbacks.applyMessageOverridesToEvent(event, this.options.messageOverrides); diff --git a/docs/minimum-versions.md b/docs/minimum-versions.md index 0bd31eebac..b01ca18b51 100644 --- a/docs/minimum-versions.md +++ b/docs/minimum-versions.md @@ -67,10 +67,10 @@ https://help.keyman.com/developer/engine/android/latest-version/ | KEYMAN_MIN_VERSION_NPM | 10.5.1 | | KEYMAN_MIN_VERSION_VISUAL_STUDIO | 2019 | | KEYMAN_VERSION_CLDR | 46 | -| KEYMAN_VERSION_GRADLE | 7.6.4 | +| KEYMAN_VERSION_GRADLE | 8.12 | | KEYMAN_VERSION_ICU | 73.1 | | KEYMAN_VERSION_ISO639_3 | 2024-05-22 | -| KEYMAN_VERSION_JAVA | 11 | +| KEYMAN_VERSION_JAVA | 21 | | KEYMAN_VERSION_LANGTAGS | 2025-02-18 | | KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY | 2025-03-10 | | KEYMAN_VERSION_UNICODE | 16.0.0 | diff --git a/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m b/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m index f6bf6c81fe..b873a34290 100644 --- a/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m +++ b/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m @@ -196,14 +196,6 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [self clearContext]; NSString *kmxPath = [self.kmxList objectAtIndex:self.kbComboBox.indexOfSelectedItem-1]; KMXFile *kmx = [[KMXFile alloc] initWithFilePath:kmxPath]; - if (![kmx isValid]) { - NSAlert *alert = [[NSAlert alloc] init]; - [alert addButtonWithTitle:@"OK"]; - [alert setMessageText:@"Invalid KMX file"]; - [alert setInformativeText:@"This KMX file contains some invalid code!"]; - [alert setAlertStyle:NSAlertStyleWarning]; - [alert runModal]; - } NSString *packagePath = [kmxPath stringByDeletingLastPathComponent]; NSDictionary *infoDict = [KMXFile keyboardInfoFromKmxFile:kmxPath]; @@ -217,12 +209,6 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [self.kbData setObject:kmx forKey:kKMXFileKey]; self.imgView.image = kmx.bitmap; - - int index = 0; - for (NSObject *kmStore in kmx.store) { - NSLog(@"%d: %@", index, kmStore); - index++; - } } } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m index 2b06db4cad..8c12e64580 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m @@ -337,7 +337,7 @@ NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType]; NSMutableArray *kmpFiles = nil; for (NSString *filename in filenames) { - if ([[filename lastNChars:4] isEqualTo:@".kmp"]) { + if ([filename hasSuffix:@".kmp"]) { if (kmpFiles == nil) kmpFiles = [NSMutableArray arrayWithObject:filename]; else diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 168c0400fa..9bac3e9c89 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -740,11 +740,8 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef return _contextBuffer; } -- (void)setContextBuffer:(NSMutableString *)contextBuffer { - _contextBuffer = [contextBuffer mutableCopy]; - if (_contextBuffer.length) - [_contextBuffer replaceOccurrencesOfString:@"\0" withString:[NSString nullChar] options:0 range:NSMakeRange(0, 1)]; - [self.kme setCoreContextIfNeeded:self.contextBuffer]; +- (void)clearContextBuffer { + [self.kme clearCoreContext]; } - (void)awakeFromNib { @@ -880,8 +877,8 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef NSString *keyboardName = [self.activeKeyboards objectAtIndex:0]; [self setSelectedKeyboard:keyboardName inMenuItem:menuItem]; [self setSelectedKeyboard:keyboardName]; - [self setContextBuffer:nil]; - + [self clearContextBuffer]; + [KMSentryHelper addInfoBreadCrumb:@"startup" message:[NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName]]; } @@ -893,7 +890,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [self setKvk:nil]; [self setKeyboardName:nil]; [self setKeyboardIcon:nil]; - [self setContextBuffer:nil]; + [self clearContextBuffer]; [self setSelectedKeyboard:@""]; } @@ -932,7 +929,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef os_log_info([KMLogs keyboardLog], "Selected keyboard from menu: %{public}@", keyboardName); [self setKeyboardName:keyboardName]; [self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; - [self setContextBuffer:nil]; + [self clearContextBuffer]; [self setSelectedKeyboard:path]; [self applyPersistedOptions]; } diff --git a/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.m b/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.m index c2078c95ea..5733aa8a6a 100644 --- a/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.m +++ b/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.m @@ -35,13 +35,6 @@ return _contextBuffer; } -- (void)setContextBuffer:(NSMutableString *)contextBuffer { - _contextBuffer = [contextBuffer mutableCopy]; - if (_contextBuffer.length) - [_contextBuffer replaceOccurrencesOfString:@"\0" withString:[NSString nullChar] options:0 range:NSMakeRange(0, 1)]; - [self.kme setCoreContextIfNeeded:self.contextBuffer]; -} - -(NSEvent *)keyStrokeEventForCharacter:(NSString *)character keyCode:(unsigned short) keyCode { return [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSZeroPoint modifierFlags:0 timestamp:NSTimeIntervalSince1970 windowNumber:0 context:nil characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode]; } diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj b/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj index aafac72d49..916087e100 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj @@ -51,13 +51,7 @@ 984C2AD81A79C98D0023F89D /* KeymanEngine4Mac.h in Headers */ = {isa = PBXBuildFile; fileRef = 984C2AD71A79C98D0023F89D /* KeymanEngine4Mac.h */; settings = {ATTRIBUTES = (Public, ); }; }; 984C2ADE1A79C98D0023F89D /* KeymanEngine4Mac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 984C2AD21A79C98D0023F89D /* KeymanEngine4Mac.framework */; }; 984C2AE51A79C98D0023F89D /* KMXFileTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 984C2AE41A79C98D0023F89D /* KMXFileTests.m */; }; - 984C2B061A79CA4F0023F89D /* NSString+XString.h in Headers */ = {isa = PBXBuildFile; fileRef = 984C2AF21A79CA4F0023F89D /* NSString+XString.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 984C2B071A79CA4F0023F89D /* NSString+XString.m in Sources */ = {isa = PBXBuildFile; fileRef = 984C2AF31A79CA4F0023F89D /* NSString+XString.m */; }; 984C2B091A79CA4F0023F89D /* KMBinaryFileFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = 984C2AF51A79CA4F0023F89D /* KMBinaryFileFormat.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 984C2B0C1A79CA4F0023F89D /* KMCompGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = 984C2AF81A79CA4F0023F89D /* KMCompGroup.h */; }; - 984C2B0D1A79CA4F0023F89D /* KMCompGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 984C2AF91A79CA4F0023F89D /* KMCompGroup.m */; }; - 984C2B0E1A79CA4F0023F89D /* KMCompKey.h in Headers */ = {isa = PBXBuildFile; fileRef = 984C2AFA1A79CA4F0023F89D /* KMCompKey.h */; }; - 984C2B0F1A79CA4F0023F89D /* KMCompKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 984C2AFB1A79CA4F0023F89D /* KMCompKey.m */; }; 984C2B101A79CA4F0023F89D /* KMCompStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 984C2AFC1A79CA4F0023F89D /* KMCompStore.h */; }; 984C2B111A79CA4F0023F89D /* KMCompStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 984C2AFD1A79CA4F0023F89D /* KMCompStore.m */; }; 984C2B121A79CA4F0023F89D /* KMEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = 984C2AFE1A79CA4F0023F89D /* KMEngine.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -134,13 +128,7 @@ 984C2ADD1A79C98D0023F89D /* KeymanEngine4MacTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KeymanEngine4MacTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 984C2AE31A79C98D0023F89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 984C2AE41A79C98D0023F89D /* KMXFileTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMXFileTests.m; sourceTree = ""; }; - 984C2AF21A79CA4F0023F89D /* NSString+XString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+XString.h"; sourceTree = ""; }; - 984C2AF31A79CA4F0023F89D /* NSString+XString.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+XString.m"; sourceTree = ""; }; 984C2AF51A79CA4F0023F89D /* KMBinaryFileFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMBinaryFileFormat.h; sourceTree = ""; }; - 984C2AF81A79CA4F0023F89D /* KMCompGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMCompGroup.h; sourceTree = ""; }; - 984C2AF91A79CA4F0023F89D /* KMCompGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMCompGroup.m; sourceTree = ""; }; - 984C2AFA1A79CA4F0023F89D /* KMCompKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMCompKey.h; sourceTree = ""; }; - 984C2AFB1A79CA4F0023F89D /* KMCompKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMCompKey.m; sourceTree = ""; }; 984C2AFC1A79CA4F0023F89D /* KMCompStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMCompStore.h; sourceTree = ""; }; 984C2AFD1A79CA4F0023F89D /* KMCompStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMCompStore.m; sourceTree = ""; }; 984C2AFE1A79CA4F0023F89D /* KMEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMEngine.h; sourceTree = ""; }; @@ -307,10 +295,6 @@ 981880641BC1EA5800A1FBA5 /* OnScreenKeyboard */, 984C2AEF1A79CA4F0023F89D /* Categories */, 984C2AF51A79CA4F0023F89D /* KMBinaryFileFormat.h */, - 984C2AF81A79CA4F0023F89D /* KMCompGroup.h */, - 984C2AF91A79CA4F0023F89D /* KMCompGroup.m */, - 984C2AFA1A79CA4F0023F89D /* KMCompKey.h */, - 984C2AFB1A79CA4F0023F89D /* KMCompKey.m */, 984C2AFC1A79CA4F0023F89D /* KMCompStore.h */, 984C2AFD1A79CA4F0023F89D /* KMCompStore.m */, 984C2AFE1A79CA4F0023F89D /* KMEngine.h */, @@ -332,8 +316,6 @@ 984C2AEF1A79CA4F0023F89D /* Categories */ = { isa = PBXGroup; children = ( - 984C2AF21A79CA4F0023F89D /* NSString+XString.h */, - 984C2AF31A79CA4F0023F89D /* NSString+XString.m */, ); path = Categories; sourceTree = ""; @@ -351,7 +333,6 @@ 984C2B141A79CA4F0023F89D /* KMXFile.h in Headers */, 981880771BC1EA5800A1FBA5 /* OSKView.h in Headers */, 984C2B161A79CA4F0023F89D /* KVKFile.h in Headers */, - 984C2B061A79CA4F0023F89D /* NSString+XString.h in Headers */, 984C2AD81A79C98D0023F89D /* KeymanEngine4Mac.h in Headers */, 2993C33429B1D92E00FB5E95 /* CoreAction.h in Headers */, 980053351B37C9B50088DDDD /* NFont.h in Headers */, @@ -360,9 +341,7 @@ 29CBB2C62CA137A50097556B /* CoreKey.h in Headers */, 984C2B191A79CB020023F89D /* WindowsVKCodes.h in Headers */, 29E50EB12CA121BA00B59959 /* CoreKeyboardInfo.h in Headers */, - 984C2B0C1A79CA4F0023F89D /* KMCompGroup.h in Headers */, 984C2B101A79CA4F0023F89D /* KMCompStore.h in Headers */, - 984C2B0E1A79CA4F0023F89D /* KMCompKey.h in Headers */, 980053371B37C9B50088DDDD /* NKey.h in Headers */, 9840F5ED1B43B02500410ABA /* MacVKCodes.h in Headers */, 981880711BC1EA5800A1FBA5 /* KeyLabelCell.h in Headers */, @@ -525,7 +504,6 @@ 29B4A0D92BFAE8A400682049 /* KMELogs.m in Sources */, 981880781BC1EA5800A1FBA5 /* OSKView.m in Sources */, 2993C32E29B1D92E00FB5E95 /* CoreWrapper.m in Sources */, - 984C2B0F1A79CA4F0023F89D /* KMCompKey.m in Sources */, 984C2B131A79CA4F0023F89D /* KMEngine.m in Sources */, 984C2B171A79CA4F0023F89D /* KVKFile.m in Sources */, 29A1971F2AF091E600512A37 /* CoreKeyOutput.m in Sources */, @@ -533,12 +511,10 @@ 981880761BC1EA5800A1FBA5 /* OSKKey.m in Sources */, 29BE6F042C9D700B00635B1A /* CoreKey.m in Sources */, 2993C32F29B1D92E00FB5E95 /* CoreHelper.m in Sources */, - 984C2B0D1A79CA4F0023F89D /* KMCompGroup.m in Sources */, 984C2B111A79CA4F0023F89D /* KMCompStore.m in Sources */, 2993C32C29B1D92E00FB5E95 /* CoreAction.m in Sources */, 984C2B151A79CA4F0023F89D /* KMXFile.m in Sources */, 980053361B37C9B50088DDDD /* NFont.m in Sources */, - 984C2B071A79CA4F0023F89D /* NSString+XString.m in Sources */, 29E50EB02CA121BA00B59959 /* CoreKeyboardInfo.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/Categories/NSString+XString.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/Categories/NSString+XString.h deleted file mode 100644 index 3647ea2c3f..0000000000 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/Categories/NSString+XString.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// NSString+XString.h -// KeymanEngine4Mac -// -// Created by Serkan Kurt on 8/01/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import -#import "KMBinaryFileFormat.h" - -@interface NSString (XString) - -- (NSUInteger)xLength; -- (NSUInteger)xLengthIncludingIf; -- (NSUInteger)deadKeyCount; -- (NSString *)lastNChars:(NSUInteger)n; -- (NSString *)codeString; -- (BOOL)isValidCode; -+ (NSString *)nullChar; - -@end - -@interface NSMutableString (XString) - -- (void)appendDeadkey:(NSUInteger)index; -- (void)appendNullChar; -- (NSUInteger)deleteLastDeadkeys; -- (NSUInteger)deleteLastNullChars; -- (NSUInteger)removeAllNullChars; -- (void)deleteLastNChars:(NSUInteger)n; - -@end diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/Categories/NSString+XString.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/Categories/NSString+XString.m deleted file mode 100644 index f7d5c8c882..0000000000 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/Categories/NSString+XString.m +++ /dev/null @@ -1,489 +0,0 @@ -// -// NSString+XString.m -// KeymanEngine4Mac -// -// Created by Serkan Kurt on 8/01/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import "NSString+XString.h" - -@implementation NSString (XString) - -// For calculating offset including system stores -- (NSUInteger)xLengthIncludingIf { - NSUInteger cx = 0; - for (int i = 0; i < self.length;) { - unichar c = [self characterAtIndex:i]; - if (c == UC_SENTINEL) { - c = [self characterAtIndex:i+1]; - switch (c) { - case CODE_ANY: - case CODE_NOTANY: - case CODE_CONTEXTEX: - case CODE_DEADKEY: - case CODE_NULLCHAR: - cx++; - i+=3; - break; - case CODE_INDEX: - cx++; - i+=4; - break; - case CODE_IFOPT: - case CODE_IFSYSTEMSTORE: - cx++; - i+=5; - break; - case CODE_NUL: - default: - i+=2; - break; - } - } - else { - cx++; - i++; - } - } - - return cx; -} - -- (NSUInteger)xLength { - NSUInteger cx = 0; - for (int i = 0; i < self.length;) { - unichar c = [self characterAtIndex:i]; - if (c == UC_SENTINEL) { - c = [self characterAtIndex:i+1]; - switch (c) { - case CODE_ANY: - case CODE_NOTANY: - case CODE_CONTEXTEX: - case CODE_DEADKEY: - case CODE_NULLCHAR: - cx++; - i+=3; - break; - case CODE_INDEX: - cx++; - i+=4; - break; - case CODE_IFOPT: - case CODE_IFSYSTEMSTORE: - i+=5; - break; - case CODE_NUL: - default: - i+=2; - break; - } - } - else { - cx++; - i++; - } - } - - return cx; -} - -- (NSUInteger)deadKeyCount { - NSUInteger dk = 0; - for (int i = 0; i < self.length;) { - unichar c = [self characterAtIndex:i]; - if (c == UC_SENTINEL) { - c = [self characterAtIndex:i+1]; - switch (c) { - case CODE_DEADKEY: - dk++; - i+=3; - break; - case CODE_NULLCHAR: - case CODE_ANY: - case CODE_NOTANY: - case CODE_CONTEXTEX: - i+=3; - break; - case CODE_INDEX: - i+=4; - break; - case CODE_IFOPT: - case CODE_IFSYSTEMSTORE: - i+=5; - break; - case CODE_NUL: - default: - i+=2; - break; - } - } - else { - i++; - } - } - - return dk; -} - -- (NSString *)lastNChars:(NSUInteger)n { - if (n == 0) - return @""; - - if (self.length >= 3) { - NSInteger len = 0; - NSInteger chars = 0; - for (NSInteger i = self.length - 1; i >= 0;) { - NSInteger index = i - 2; - if (index >= 0) { - unichar c1 = [self characterAtIndex:index]; - unichar c2 = [self characterAtIndex:index+1]; - if (c1 == UC_SENTINEL && (c2 == CODE_DEADKEY || c2 == CODE_NULLCHAR)) { - i -= 3; - len += 3; - } - else { - i--; - len++; - } - } - else { - i--; - len++; - } - - chars++; - if (chars == n) - break; - } - - return [NSString stringWithString:[self substringFromIndex:self.length - len]]; - } - else { - if (n >= self.length) - return [NSString stringWithString:self]; - else - return [NSString stringWithString:[self substringFromIndex:self.length - n]]; - } -} - -- (NSString *)codeString { - if (self == nil) - return nil; - - NSMutableString *mStr = [NSMutableString stringWithString:@""]; - for (int i=0; i < self.length;) { - unichar c = [self characterAtIndex:i]; - if (c == UC_SENTINEL) { - if ((i+1) < self.length) - c = [self characterAtIndex:i+1]; - else { - [mStr appendString:@"sentinel(INVALID)"]; - return mStr; - } - - switch (c) { - case CODE_ANY: { - if ((i+2) < self.length) { - DWORD index = [self characterAtIndex:i+2]; - [mStr appendString:[NSString stringWithFormat:@"any(%d)", index]]; - } - else { - [mStr appendString:@"any(INVALID)"]; - } - - i+=3; - break; - } - case CODE_NOTANY: { - if ((i+2) < self.length) { - DWORD index = [self characterAtIndex:i+2]; - [mStr appendString:[NSString stringWithFormat:@"notany(%d)", index]]; - } - else { - [mStr appendString:@"notany(INVALID)"]; - } - - i+=3; - break; - } - case CODE_INDEX: { - if ((i+3) < self.length) { - DWORD x1 = [self characterAtIndex:i+2]; - DWORD x2 = [self characterAtIndex:i+3]; - [mStr appendString:[NSString stringWithFormat:@"index(%d,%d)", x1, x2]]; - } - else { - [mStr appendString:@"index(INVALID)"]; - } - - i+=4; - break; - } - case CODE_NUL: - [mStr appendString:@"nul()"]; - i+=2; - break; - case CODE_DEADKEY: { - if ((i+2) < self.length) { - c = [self characterAtIndex:i+2]; - [mStr appendString:[NSString stringWithFormat:@"dk(%d)", c]]; - } - else { - [mStr appendString:@"dk(INVALID)"]; - } - - i+=3; - break; - } - case CODE_NULLCHAR: { - if ((i+2) < self.length) { - c = [self characterAtIndex:i+2]; - [mStr appendString:[NSString stringWithFormat:@"nc(%d)", c]]; - } - else { - [mStr appendString:@"nc(INVALID)"]; - } - - i+=3; - break; - } - case CODE_CONTEXT: { - [mStr appendString:[NSString stringWithFormat:@"ctx()"]]; - i+=2; - break; - } - case CODE_CONTEXTEX: { - if ((i+2) < self.length) { - c = [self characterAtIndex:i+2]; - [mStr appendString:[NSString stringWithFormat:@"ctxex(%d)", c]]; - } - else { - [mStr appendString:@"ctxex(INVALID)"]; - } - - i+=3; - break; - } - case CODE_USE: { - if ((i+2) < self.length) { - c = [self characterAtIndex:i+2]; - [mStr appendString:[NSString stringWithFormat:@"use(%d)", c]]; - } - else { - [mStr appendString:@"use(INVALID)"]; - } - - i+=3; - break; - } - case CODE_BEEP: { - [mStr appendString:[NSString stringWithFormat:@"beep()"]]; - i+=2; - break; - } - case CODE_RETURN: { - [mStr appendString:[NSString stringWithFormat:@"return()"]]; - i+=2; - break; - } - case CODE_IFOPT: { - if ((i+4) < self.length) { - DWORD x1 = [self characterAtIndex:i+2]; - DWORD x2 = [self characterAtIndex:i+3]; - DWORD x3 = [self characterAtIndex:i+4]; - [mStr appendString:[NSString stringWithFormat:@"ifopt(%d %d %d)", x1, x2, x3]]; - } - else { - [mStr appendString:@"ifopt(INVALID)"]; - } - - i+=5; - break; - } - case CODE_IFSYSTEMSTORE: { - if ((i+4) < self.length) { - DWORD x1 = [self characterAtIndex:i+2]; - DWORD x2 = [self characterAtIndex:i+3]; - DWORD x3 = [self characterAtIndex:i+4]; - [mStr appendString:[NSString stringWithFormat:@"ifsys(%d %d %d)", x1, x2, x3]]; - } - else { - [mStr appendString:@"ifsys(INVALID)"]; - } - - i+=5; - break; - } - default: - [mStr appendString:@"other()"]; - i+=2; - break; - } - } - else { - [mStr appendString:[NSString stringWithFormat:@"U+%x", c]]; - i++; - } - } - - return mStr; -} - -- (BOOL)isValidCode { - if (self == nil) - return NO; - - for (int i=0; i < self.length;) { - unichar c = [self characterAtIndex:i]; - if (c == UC_SENTINEL) { - if ((i+1) < self.length) - c = [self characterAtIndex:i+1]; - else - return NO; - - switch (c) { - case CODE_ANY: - case CODE_NOTANY: - case CODE_DEADKEY: - case CODE_NULLCHAR: - case CODE_CONTEXTEX: - case CODE_USE: { - if ((i+2) >= self.length) - return NO; - - i+=3; - break; - } - case CODE_INDEX: { - if ((i+3) >= self.length) - return NO; - - i+=4; - break; - } - case CODE_IFOPT: - case CODE_IFSYSTEMSTORE: { - if ((i+4) >= self.length) - return NO; - - i+=5; - break; - } - case CODE_NUL: - case CODE_CONTEXT: - case CODE_BEEP: - case CODE_RETURN: - default: - i+=2; - break; - } - } - else { - i++; - } - } - - return YES; -} - -+ (NSString *)nullChar { - return [NSString stringWithFormat:@"%C%C%d", UC_SENTINEL, CODE_NULLCHAR, 1]; -} - -@end - -@implementation NSMutableString (XString) - -- (void)appendDeadkey:(NSUInteger)index { - NSString *dk = [NSString stringWithFormat:@"%C%C%C", UC_SENTINEL, CODE_DEADKEY, index]; - [self appendString:dk]; -} - -- (void)appendNullChar { - [self appendString:[[self class] nullChar]]; -} - -- (NSUInteger)deleteLastDeadkeys { - NSUInteger dkc = 0; - if (self.length >= 3) { - unichar c1 = [self characterAtIndex:self.length - 3]; - unichar c2 = [self characterAtIndex:self.length - 2]; - while (c1 == UC_SENTINEL && c2 == CODE_DEADKEY) { - NSRange range = NSMakeRange(self.length - 3, 3); - [self deleteCharactersInRange:range]; - dkc++; - if (self.length < 3) - break; - c1 = [self characterAtIndex:self.length - 3]; - c2 = [self characterAtIndex:self.length - 2]; - } - } - - return dkc; -} - -- (NSUInteger)deleteLastNullChars { - NSUInteger dsc = 0; - if (self.length >= 3) { - unichar c1 = [self characterAtIndex:self.length - 3]; - unichar c2 = [self characterAtIndex:self.length - 2]; - while (c1 == UC_SENTINEL && c2 == CODE_NULLCHAR) { - NSRange range = NSMakeRange(self.length - 3, 3); - [self deleteCharactersInRange:range]; - dsc++; - if (self.length < 3) - break; - c1 = [self characterAtIndex:self.length - 3]; - c2 = [self characterAtIndex:self.length - 2]; - } - } - - return dsc; -} - -- (NSUInteger)removeAllNullChars { - return [self replaceOccurrencesOfString:[[self class] nullChar] withString:@"" options:NSLiteralSearch range:NSMakeRange(0, self.length)]; -} - -- (void)deleteLastNChars:(NSUInteger)n { - if (n > 0) { - if (self.length >= 3) { - NSInteger len = 0; - NSInteger chars = 0; - for (NSInteger i = self.length - 1; i >= 0;) { - NSInteger index = i - 2; - if (index >= 0) { - unichar c1 = [self characterAtIndex:index]; - unichar c2 = [self characterAtIndex:index+1]; - if (c1 == UC_SENTINEL && (c2 == CODE_DEADKEY || c2 == CODE_NULLCHAR)) { - i -= 3; - len += 3; - } - else { - i--; - len++; - } - } - else { - i--; - len++; - } - - chars++; - if (chars == n) - break; - } - - [self deleteCharactersInRange:NSMakeRange(self.length - len, len)]; - } - else { - if (self.length >= n) - [self deleteCharactersInRange:NSMakeRange(self.length - n, n)]; - else - [self deleteCharactersInRange:NSMakeRange(0, self.length)]; - } - } -} - -@end diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h index 894f0f4b9c..3951d5831d 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h @@ -6,12 +6,18 @@ // Copyright (c) 2017 SIL International. All rights reserved. // +/** + * Note that many of the constants defined here are also defined in the common header`kmx_file.h` + * Keyman for Mac only reads the kmx file to pass the contents to Keyman Core and for meta data. + * If the meta data can instead be returned by Keyman Core in the future, then the duplicate definitions + * found here can be removed. + */ + #ifndef KMBinaryFileFormat_h #define KMBinaryFileFormat_h typedef uint16_t WORD; typedef uint32_t DWORD; -//typedef wchar_t* PWSTR; struct COMP_KEYBOARD { DWORD dwIdentifier; // 0000 Keyman compiled keyboard id @@ -49,15 +55,6 @@ struct COMP_KEYBOARD { #define VERSION_MIN VERSION_50 #define VERSION_MAX VERSION_170 -struct COMP_GROUP { - DWORD dpName; // string (only debug) - DWORD dpKeyArray; // [LPCOMP_KEY] address of first item in key array - DWORD dpMatch; // extended string - DWORD dpNoMatch; // extended string - DWORD cxKeyArray; // in array entries - BOOL fUsingKeys; // group(xx) [using keys] <-- specified or not -}; - struct COMP_STORE { DWORD dwSystemID; DWORD dpName; // string (only debug) @@ -125,78 +122,6 @@ struct COMP_STORE { #define TSS__MAX 43 -// ShiftFlags -#define LCTRLFLAG 0x0001 // Left Control flag -#define RCTRLFLAG 0x0002 // Right Control flag -#define LALTFLAG 0x0004 // Left Alt flag -#define RALTFLAG 0x0008 // Right Alt flag -#define K_SHIFTFLAG 0x0010 // Either shift flag -#define K_CTRLFLAG 0x0020 // Either ctrl flag -#define K_ALTFLAG 0x0040 // Either alt flag -//#define K_METAFLAG 0x0080 // Either Meta-key flag (tentative). Not usable in keyboard rules; -// Used internally (currently, only by KMW) to ensure Meta-key -// shortcuts safely bypass rules -// Meta key = Command key on macOS, Windows key on Windows -#define CAPITALFLAG 0x0100 // Caps lock on -#define NOTCAPITALFLAG 0x0200 // Caps lock NOT on -#define NUMLOCKFLAG 0x0400 // Num lock on -#define NOTNUMLOCKFLAG 0x0800 // Num lock NOT on -#define SCROLLFLAG 0x1000 // Scroll lock on -#define NOTSCROLLFLAG 0x2000 // Scroll lock NOT on -#define ISVIRTUALKEY 0x4000 // It is a Virtual Key Sequence -#define VIRTUALCHARKEY 0x8000 // Keyman 6.0: Virtual Key Cap Sequence -#define K_MODIFIERFLAG 0x007F -#define K_CAPITALMASK (CAPITALFLAG|NOTCAPITALFLAG) - -struct COMP_KEY { - WORD Key; // Windows VK code or character value (VIRTUALCHARKEY, ISVIRTUALKEY) - DWORD Line; - DWORD ShiftFlags; - DWORD dpOutput; // extended string - DWORD dpContext; // extended string -}; - -#define UC_SENTINEL 0xFFFF -#define UC_SENTINEL_EXTENDEDEND 0x10 - -#define CODE_ANY 0x01 -#define CODE_INDEX 0x02 -#define CODE_CONTEXT 0x03 -#define CODE_NUL 0x04 -#define CODE_USE 0x05 -#define CODE_RETURN 0x06 -#define CODE_BEEP 0x07 -#define CODE_DEADKEY 0x08 -// 0x09 = bkspace.-- we don't need to keep this separate though with UC_SENTINEL -#define CODE_EXTENDED 0x0A -//#define CODE_EXTENDEDEND 0x0B deprecated -#define CODE_SWITCH 0x0C -#define CODE_KEY 0x0D -#define CODE_CLEARCONTEXT 0x0E -#define CODE_CALL 0x0F -// UC_SENTINEL_EXTENDEDEND 0x10 -#define CODE_CONTEXTEX 0x11 -#define CODE_NOTANY 0x12 -#define CODE_KEYMAN70_LASTCODE 0x12 - -#define CODE_SETOPT 0x13 -#define CODE_IFOPT 0x14 -#define CODE_SAVEOPT 0x15 -#define CODE_RESETOPT 0x16 -#define CODE_KEYMAN80_LASTCODE 0x16 - -/* Keyman 9.0 codes */ -#define CODE_IFSYSTEMSTORE 0x17 -#define CODE_SETSYSTEMSTORE 0x18 -#define CODE_LASTCODE 0x18 - -/* Keyman4Mac codes */ -#define CODE_NULLCHAR 0x20 - - -#define NOT_EQUAL 0 -#define EQUAL 1 - /* KVK file format definitions */ // KVK header flags diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompGroup.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompGroup.h deleted file mode 100644 index 2bc3fc0725..0000000000 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompGroup.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// KMCompGroup.h -// KeymanEngine4Mac -// -// Created by Serkan Kurt on 28/11/2014. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#ifndef KMCompGroup_h -#define KMCompGroup_h - -#import -#import -#import "KMBinaryFileFormat.h" - -@interface KMCompGroup : NSObject - -@property (strong, nonatomic) NSString *name; -@property (strong, nonatomic) NSMutableArray *keys; -@property (strong, nonatomic) NSString *match; -@property (strong, nonatomic) NSString *noMatch; -@property (assign, nonatomic) BOOL fUsingKeys; - -@end - -#endif /* KMCompGroup_h */ diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompGroup.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompGroup.m deleted file mode 100644 index 1aaa376084..0000000000 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompGroup.m +++ /dev/null @@ -1,33 +0,0 @@ -// -// KMCompGroup.m -// KeymanEngine4Mac -// -// Created by Serkan Kurt on 28/11/2014. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import "KMCompGroup.h" -#import "NSString+XString.h" - -@implementation KMCompGroup - -- (id)init { - self = [super init]; - if (self) { - _name = @""; - _keys = [[NSMutableArray alloc] initWithCapacity:0]; - _match = @""; - _noMatch = @""; - } - - return self; -} - -- (NSString *)description { - NSString *format = @"<%@:%p, Name:%@ Match:%@ NoMatch:%@ fUsingKeys:%d Keys:%@>"; - NSString *str = [NSString stringWithFormat:format, - [self className], self, _name, [_match codeString], [_noMatch codeString], _fUsingKeys, _keys]; - return str; -} - -@end diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompKey.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompKey.h deleted file mode 100644 index 20dcf001b3..0000000000 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompKey.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// KMCompKey.h -// KeymanEngine4Mac -// -// Created by Serkan Kurt on 28/11/2014. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#ifndef KMCompKey_h -#define KMCompKey_h - -#import -#import "KMBinaryFileFormat.h" - -@interface KMCompKey : NSObject - -@property (assign, nonatomic) WORD key; -@property (assign, nonatomic) DWORD line; -@property (assign, nonatomic) DWORD shiftFlags; -@property (strong, nonatomic) NSString *output; -@property (strong, nonatomic) NSString *context; - -@end - -#endif /* KMCompKey_h */ diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompKey.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompKey.m deleted file mode 100644 index f8508dbf4c..0000000000 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMCompKey.m +++ /dev/null @@ -1,31 +0,0 @@ -// -// KMCompKey.m -// KeymanEngine4Mac -// -// Created by Serkan Kurt on 28/11/2014. -// Copyright (c) 2017 SIL International. All rights reserved. -// - -#import "KMCompKey.h" -#import "NSString+XString.h" - -@implementation KMCompKey - -- (id)init { - self = [super init]; - if (self) { - _output = @""; - _context = @""; - } - - return self; -} - -- (NSString *)description { - NSString *format = @"<%@:%p K:0x%X L:0x%X F:0x%X O:%@ C:%@>"; - NSString *str = [NSString stringWithFormat:format, - self.className, self, self.key, self.line, self.shiftFlags, [self.output codeString], [self.context codeString]]; - return str; -} - -@end diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m index 080c7023c3..fd59c48b62 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m @@ -8,10 +8,6 @@ #import "KMEngine.h" #import "KMBinaryFileFormat.h" -#import "KMCompStore.h" -#import "KMCompGroup.h" -#import "KMCompKey.h" -#import "NSString+XString.h" #import "WindowsVKCodes.h" #import "MacVKCodes.h" #import "CoreWrapper.h" diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMXFile.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMXFile.h index 8eec8bb4e3..9ef03a0891 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMXFile.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMXFile.h @@ -27,17 +27,10 @@ extern NSString *const kKMVisualKeyboardKey; @property (assign, nonatomic, readonly) BOOL isRegistered; @property (assign, nonatomic, readonly) BOOL isMnemonic; @property (assign, nonatomic, readonly) DWORD version; -@property (strong, nonatomic, readonly) NSArray *store; -@property (strong, nonatomic, readonly) NSArray *storeSaved; -@property (strong, nonatomic, readonly) NSArray *group; -@property (strong, nonatomic, readonly) NSArray *startGroup; -@property (assign, nonatomic, readonly) DWORD flags; -@property (assign, nonatomic, readonly) DWORD hotKey; @property (strong, nonatomic, readonly) NSImage *bitmap; @property (strong, nonatomic, readonly) NSString *filePath; - (id)initWithFilePath:(NSString *)path; -- (BOOL)isValid; /*! * Provides an info dictionary from a KMX file which includes keyboard name, version, copyright information and keyboard icon. diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMXFile.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMXFile.m index 2ce2fbfa89..27a2848426 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMXFile.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMXFile.m @@ -8,9 +8,6 @@ #import "KMXFile.h" #import "KMCompStore.h" -#import "KMCompGroup.h" -#import "KMCompKey.h" -#import "NSString+XString.h" #import "KMELogs.h" NSString *const kKMKeyboardNameKey = @"KMKeyboardNameKey"; @@ -22,6 +19,7 @@ NSString *const kKMVisualKeyboardKey = @"KMVisualKeyboardKey"; @implementation KMXFile - (id)initWithFilePath:(NSString *)path { + os_log_info([KMELogs configLog], "initWithFilePath, path: %{public}@", path); self = [super init]; if (self) { if (path == nil) { @@ -29,7 +27,6 @@ NSString *const kKMVisualKeyboardKey = @"KMVisualKeyboardKey"; return nil; } - NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:path]; if (file == nil) { os_log_error([KMELogs configLog], "Failed to open kmx file"); @@ -56,22 +53,45 @@ NSString *const kKMVisualKeyboardKey = @"KMVisualKeyboardKey"; _keyboardID = cmp_kb.KeyboardID; _isRegistered = (cmp_kb.IsRegistered == 0?NO:YES); _version = cmp_kb.version; - NSMutableArray *mStartGroup = [[NSMutableArray alloc] initWithCapacity:2]; - [mStartGroup addObject:[NSNumber numberWithInt:cmp_kb.StartGroup[0]]]; - [mStartGroup addObject:[NSNumber numberWithInt:cmp_kb.StartGroup[1]]]; - _startGroup = [[NSArray alloc] initWithArray:mStartGroup]; - _flags = cmp_kb.dwFlags; - _hotKey = cmp_kb.dwHotKey; + + // stores not used at init time + //NSArray *systemStores = [self readSystemStores:cmp_kb file: file]; + + [file seekToFileOffset:cmp_kb.dpBitmapOffset]; + NSData *bitmapData = [file readDataOfLength:cmp_kb.dwBitmapSize]; + NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:bitmapData]; + _bitmap = [[NSImage alloc] initWithCGImage:imageRep.CGImage size:NSMakeSize(CGImageGetWidth(imageRep.CGImage), CGImageGetHeight(imageRep.CGImage))]; - struct COMP_STORE cmp_str[cmp_kb.cxStoreArray]; - [file seekToFileOffset:cmp_kb.dpStoreArray]; - size = sizeof(cmp_str); - dataBuffer = [file readDataOfLength:size]; - [dataBuffer getBytes:cmp_str length:size]; - NSMutableArray *mStore = [[NSMutableArray alloc] initWithCapacity:cmp_kb.cxStoreArray]; - for (int i = 0; i < cmp_kb.cxStoreArray; i++) { - KMCompStore *kmStore = [[KMCompStore alloc] init]; - kmStore.dwSystemID = cmp_str[i].dwSystemID; + [file closeFile]; + } + + return self; +} + +/** + * Read the system stores from the kmx file into an array. + * Skips over all the normal stores as Keyman for Mac does not process keys but only needs the + * meta data contained in system stores. + */ +- (NSArray *) readSystemStores:(struct COMP_KEYBOARD)cmp_kb file: (NSFileHandle *) file { + struct COMP_STORE cmp_str[cmp_kb.cxStoreArray]; + [file seekToFileOffset:cmp_kb.dpStoreArray]; + size_t size = sizeof(cmp_str); + NSData *dataBuffer = [file readDataOfLength:size]; + [dataBuffer getBytes:cmp_str length:size]; + + NSMutableArray *systemStoreArray = [[NSMutableArray alloc] initWithCapacity:cmp_kb.cxStoreArray]; + + os_log_info([KMELogs configLog], "COMP_STORE, number of entries: %u", cmp_kb.cxStoreArray); + for (int i = 0; i < cmp_kb.cxStoreArray; i++) { + KMCompStore *kmStore = [[KMCompStore alloc] init]; + kmStore.dwSystemID = cmp_str[i].dwSystemID; + + /* Keyman for Mac is not processing keys, so no need to save normal stores. + If dwSystemID is not zero, then this is a system store and may contain + useful metadata */ + + if (kmStore.dwSystemID != 0) { kmStore.name = [KMXFile UTF16StringWithPointer:cmp_str[i].dpName inFile:file]; if (kmStore.dwSystemID == TSS_VERSION) @@ -87,79 +107,23 @@ NSString *const kKMVisualKeyboardKey = @"KMVisualKeyboardKey"; else kmStore.string = [KMXFile UTF16StringWithPointer:cmp_str[i].dpString inFile:file]; - [mStore addObject:kmStore]; + [systemStoreArray addObject:kmStore]; + os_log_info([KMELogs configLog], "loaded store, systemId: %u name: %{public}@ string: %{public}@", kmStore.dwSystemID, kmStore.name, kmStore.string); } - - _store = [[NSArray alloc] initWithArray:mStore]; - _storeSaved = [[NSArray alloc] initWithArray:mStore copyItems:YES]; - - struct COMP_GROUP cmp_grp[cmp_kb.cxGroupArray]; - [file seekToFileOffset:cmp_kb.dpGroupArray]; - size = sizeof(cmp_grp); - dataBuffer = [file readDataOfLength:size]; - [dataBuffer getBytes:cmp_grp length:size]; - - NSMutableArray *mGroup = [[NSMutableArray alloc] initWithCapacity:cmp_kb.cxGroupArray]; - for (int i = 0; i < cmp_kb.cxGroupArray; i++) { - KMCompGroup *kmGrp = [[KMCompGroup alloc] init]; - kmGrp.name = [KMXFile UTF16StringWithPointer:cmp_grp[i].dpName inFile:file]; - kmGrp.match = [KMXFile UTF16StringWithPointer:cmp_grp[i].dpMatch inFile:file]; - kmGrp.noMatch = [KMXFile UTF16StringWithPointer:cmp_grp[i].dpNoMatch inFile:file]; - kmGrp.fUsingKeys = cmp_grp[i].fUsingKeys; - - struct COMP_KEY cmp_keys[cmp_grp[i].cxKeyArray]; - [file seekToFileOffset:cmp_grp[i].dpKeyArray]; - size = sizeof(cmp_keys); - dataBuffer = [file readDataOfLength:size]; - [dataBuffer getBytes:cmp_keys length:size]; - - for (int j = 0; j < cmp_grp[i].cxKeyArray; j++) { - KMCompKey *kmKey = [[KMCompKey alloc] init]; - kmKey.key = cmp_keys[j].Key; - kmKey.line = cmp_keys[j].Line; - kmKey.shiftFlags = cmp_keys[j].ShiftFlags; - kmKey.output = [KMXFile UTF16StringWithPointer:cmp_keys[j].dpOutput inFile:file]; - kmKey.context = [KMXFile UTF16StringWithPointer:cmp_keys[j].dpContext inFile:file]; - [kmGrp.keys addObject:kmKey]; - } - - [mGroup addObject:kmGrp]; - } - _group = [[NSArray alloc] initWithArray:mGroup]; - - [file seekToFileOffset:cmp_kb.dpBitmapOffset]; - NSData *bitmapData = [file readDataOfLength:cmp_kb.dwBitmapSize]; - NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:bitmapData]; - _bitmap = [[NSImage alloc] initWithCGImage:imageRep.CGImage size:NSMakeSize(CGImageGetWidth(imageRep.CGImage), CGImageGetHeight(imageRep.CGImage))]; - - [file closeFile]; } - - return self; + + return systemStoreArray; } - (NSString *)description { - NSString *format = @"<%@: %p, File version: 0x%X Flags: 0x%X Store: %@ Group: %@>"; + NSString *format = @"<%@: %p, File version: 0x%X>"; NSString *str = [NSString stringWithFormat:format, - self.className, self, self.fileVersion, self.flags, self.store, self.group]; + self.className, self, self.fileVersion]; return str; } -- (BOOL)isValid { - for (KMCompGroup *gp in self.group) { - if ((gp.match && ![gp.match isValidCode]) || (gp.noMatch && ![gp.noMatch isValidCode])) - return NO; - - for (KMCompKey *kmKey in gp.keys) { - if (![kmKey.context isValidCode] || ![kmKey.output isValidCode]) - return NO; - } - } - - return YES; -} - + (NSDictionary *)keyboardInfoFromKmxFile:(NSString *)path { + os_log_info([KMELogs configLog], "keyboardInfoFromKmxFile, path: %{public}@", path); NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:path]; if (file == nil) { diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h index 6addfc4b47..197f256851 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h @@ -24,7 +24,6 @@ FOUNDATION_EXPORT const unsigned char KeymanEngine4MacVersionString[]; #import #import #import -#import #import #import #import diff --git a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMXFileTests.m b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMXFileTests.m index 6b13479d87..6d6a696252 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMXFileTests.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMXFileTests.m @@ -36,10 +36,4 @@ XCTAssert(kmxFile == nil, @"Expected nil file"); } -- (void)testinitWithFilePath_Valid_ReturnsInitializedKmxFile { - KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; - XCTAssert(kmxFile != nil, @"Expected non-nil file"); - XCTAssert([kmxFile isValid], @"Expected valid KMX file."); -} - @end diff --git a/oem/firstvoices/android/app/build.gradle b/oem/firstvoices/android/app/build.gradle index 5c014021a6..8a6558aa3d 100644 --- a/oem/firstvoices/android/app/build.gradle +++ b/oem/firstvoices/android/app/build.gradle @@ -11,6 +11,10 @@ android { compileSdk 34 namespace="com.firstvoices.keyboards" + buildFeatures { + buildConfig = true + } + defaultConfig { applicationId "com.firstvoices.keyboards" minSdkVersion 21 @@ -66,12 +70,6 @@ android { } } -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(11)) - } -} - // how to configure the sentry android gradle plugin task publishSentry { doLast { @@ -124,10 +122,10 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' api(name: 'keyman-engine', ext: 'aar') - implementation 'io.sentry:sentry-android:6.9.2' + implementation 'io.sentry:sentry-android:7.8.0' implementation 'androidx.preference:preference:1.2.1' } diff --git a/oem/firstvoices/android/build.gradle b/oem/firstvoices/android/build.gradle index 4e544b7663..b4bbee2968 100644 --- a/oem/firstvoices/android/build.gradle +++ b/oem/firstvoices/android/build.gradle @@ -6,8 +6,8 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - classpath 'io.sentry:sentry-android-gradle-plugin:2.1.2' + classpath 'com.android.tools.build:gradle:8.9.2' + classpath 'io.sentry:sentry-android-gradle-plugin:4.6.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/oem/firstvoices/android/build.sh b/oem/firstvoices/android/build.sh index 5d521c775a..2e10dd5b83 100755 --- a/oem/firstvoices/android/build.sh +++ b/oem/firstvoices/android/build.sh @@ -34,7 +34,7 @@ builder_parse "$@" if builder_is_debug_build; then builder_heading "### Debug config ####" CONFIG="debug" - BUILD_FLAGS="assembleDebug -x lint -x test" + BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug" TEST_FLAGS="-x assembleDebug lintDebug testDebug" fi diff --git a/oem/firstvoices/android/gradle.properties b/oem/firstvoices/android/gradle.properties index c8e906dff4..6831edcea1 100644 --- a/oem/firstvoices/android/gradle.properties +++ b/oem/firstvoices/android/gradle.properties @@ -19,4 +19,8 @@ android.enableJetifier=true android.useAndroidX=true + +# AGP 8.0 generates R classes for resources defined in the current module only. +android.nonTransitiveRClass=false + org.gradle.jvmargs=-Xmx1024m diff --git a/oem/firstvoices/android/gradle/wrapper/gradle-wrapper.properties b/oem/firstvoices/android/gradle/wrapper/gradle-wrapper.properties index 3994438e22..d6e308a637 100644 --- a/oem/firstvoices/android/gradle/wrapper/gradle-wrapper.properties +++ b/oem/firstvoices/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/package-lock.json b/package-lock.json index dc78fe38ba..4c811b578e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -341,7 +341,7 @@ "dependencies": { "@keymanapp/common-types": "*", "eventemitter3": "^5.0.0", - "fast-xml-parser": "^5.0.9", + "fast-xml-parser": "^5.2.2", "path-browserify": "^1.0.1", "restructure": "^3.0.1", "sax": ">=0.6.0", @@ -9124,9 +9124,9 @@ "dev": true }, "node_modules/fast-xml-parser": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.0.9.tgz", - "integrity": "sha512-2mBwCiuW3ycKQQ6SOesSB8WeF+fIGb6I/GG5vU5/XEptwFFhp9PE8b9O7fbs2dpq9fXn4ULR3UsfydNUCntf5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.2.tgz", + "integrity": "sha512-ZaCmslH75Jkfowo/x44Uq8KT5SutC5BFxHmY61nmTXPccw11PVuIXKUqC2hembMkJ3nPwTkQESXiUlsKutCbMg==", "funding": [ { "type": "github", @@ -9135,7 +9135,7 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.0.5" + "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -13993,9 +13993,9 @@ "link": true }, "node_modules/strnum": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz", - "integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.0.tgz", + "integrity": "sha512-w0S//9BqZZGw0L0Y8uLSelFGnDJgTyyNQLmSlPnVz43zPAiqu3w4t8J8sDqqANOGeZIZ/9jWuPguYcEnsoHv4A==", "funding": [ { "type": "github", diff --git a/resources/build/build-utils.sh b/resources/build/build-utils.sh index 515f958752..4996845c5a 100755 --- a/resources/build/build-utils.sh +++ b/resources/build/build-utils.sh @@ -330,11 +330,22 @@ set_keyman_standard_build_path() { PATH="$KEYMAN_ROOT/node_modules/.bin:$PATH" } -# For CI compatbility of building Keyman for Android 16.0 with OpenJDK 8, -# this overrides JAVA_HOME for the builder script to use OpenJDK 11. +# For CI compatibility of building Keyman for Android 18.0 with OpenJDK 11, +# this overrides JAVA_HOME for the builder script to use OpenJDK 21. set_java_home() { + set_java_home_21 +} + +set_java_home_21() { + if [[ ! -z ${JAVA_HOME_21+x} ]]; then + builder_echo "Setting JAVA_HOME to JAVA_HOME_21 (${JAVA_HOME_21})" + export JAVA_HOME="${JAVA_HOME_21}" + fi +} + +set_java_home_11() { if [[ ! -z ${JAVA_HOME_11+x} ]]; then - builder_echo "Setting JAVA_HOME to JAVA_HOME_11 ($JAVA_HOME_11)" + builder_echo "Setting JAVA_HOME to JAVA_HOME_11 (${JAVA_HOME_11})" export JAVA_HOME="${JAVA_HOME_11}" fi } diff --git a/resources/build/minimum-versions.inc.sh b/resources/build/minimum-versions.inc.sh index eadd2442cf..eed047e39d 100644 --- a/resources/build/minimum-versions.inc.sh +++ b/resources/build/minimum-versions.inc.sh @@ -31,13 +31,13 @@ KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.58 # Use KEYMAN_USE_EMSDK to automati KEYMAN_MIN_VERSION_VISUAL_STUDIO=2019 KEYMAN_MIN_VERSION_MESON=1.0.0 -KEYMAN_VERSION_GRADLE=7.6.4 # See /android/KMEA/gradle/wrapper/gradle-wrapper.properties +KEYMAN_VERSION_GRADLE=8.12 # See /android/KMEA/gradle/wrapper/gradle-wrapper.properties # Dependencies with fixed versions KEYMAN_VERSION_ICU=73.1 # See /core/subprojects/icu-minimal.wrap # Language and runtime versions -KEYMAN_VERSION_JAVA=11 # We're using Java/OpenJDK 11 +KEYMAN_VERSION_JAVA=21 # We're using Java/OpenJDK 21 KEYMAN_MIN_VERSION_CPP=17 # C++17 KEYMAN_MIN_VERSION_ANDROID_SDK=21 diff --git a/resources/docker-images/run.sh b/resources/docker-images/run.sh index c71047e22f..05124768e9 100755 --- a/resources/docker-images/run.sh +++ b/resources/docker-images/run.sh @@ -67,14 +67,6 @@ run_web() { "${builder_extra_params[@]}" } -if [[ -z "${DISTRO_VERSION:-}" ]]; then - image_version=default - build_dir=default -else - image_version="${DISTRO:-}-${DISTRO_VERSION}-java${KEYMAN_VERSION_JAVA}-node$(_print_expected_node_version)-emsdk${KEYMAN_MIN_VERSION_EMSCRIPTEN}" - build_dir="${DISTRO:-}-${DISTRO_VERSION}" -fi - mkdir -p "${KEYMAN_ROOT}/core/build/docker-core/${build_dir}" builder_run_action android run_android