diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml
index 52c485aa65..bf1bfca2fa 100644
--- a/.github/workflows/deb-packaging.yml
+++ b/.github/workflows/deb-packaging.yml
@@ -116,7 +116,7 @@ jobs:
strategy:
fail-fast: true
matrix:
- dist: [focal, jammy, noble, oracular]
+ dist: [jammy, noble, oracular]
steps:
- name: Checkout
diff --git a/HISTORY.md b/HISTORY.md
index 1815302cac..544a275dea 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,5 +1,84 @@
# Keyman Version History
+## 19.0.2 alpha 2025-02-12
+
+* chore(linux): remove support of Ubuntu 20.04 Focal (#13203)
+
+## 19.0.1 alpha 2025-02-11
+
+* refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168)
+* chore: increment to alpha 19.0 (#13187)
+
+## 18.0.188 alpha 2025-02-10
+
+* fix(windows): check the params status flag equals ucrsUpdateReady before attempting to download the keyman setup file (#13154)
+* feat(windows): background updates go from downloading to waiting for a restart except if the `apply now` flag is set (#13159)
+* feat(developer): verify package version number format in kmc-package (#13118)
+* fix(developer): support non-US base keyboard layouts in debuggers (#13131)
+* feat(developer): improve compiler messages and user interface (#13156)
+* feat(developer): verify that packages do not contain themselves in kmc-package (#13157)
+* fix(developer): link welcome.htm in package for new projects; use v17 project format for new models (#13161)
+* fix(windows): use Automatically check for updates and download for the english xml (#13162)
+* change(linux): Implement ordered output without patched ibus (#11535)
+
+## 18.0.187 alpha 2025-02-07
+
+* docs(web): relocate gesture docs to web/docs/internal (#13139)
+* chore(mac): remove empty options tab from configuration (#13160)
+* chore(linux): Update debian changelog (#13143)
+
+## 18.0.186 alpha 2025-02-06
+
+* feat(windows): check updates and automatic update merged (#13115)
+
+## 18.0.185 alpha 2025-02-05
+
+* fix(developer): make kmc log options consistent across all commands (#13075)
+* feat(developer): add user interface for `kmc copy` to TIKE (#13076)
+* chore(developer): add verbose logs for project copier (#13080)
+* chore(mac, ios): build and run with xcode 16 (#13077)
+
+## 18.0.184 alpha 2025-02-04
+
+* fix(common): handle undefined input in `@keymanapp/langtags` lookups (#13117)
+* chore: update docs to reference node 20.0 (#13124)
+* feat(developer): include command line in kmc sentry reports (#13113)
+* fix(common/web): replace invalid StrsItem identity checks with isEqual() (#13122)
+
+## 18.0.183 alpha 2025-02-03
+
+* change(ios): set host-app bundle identifier in embedded Web debug-report (#13105)
+* fix(ios): prevent message-handler collision (#13037)
+* change(web): add context tracker bksp handling, alignment-offset calculations (#12911)
+* fix(web): correct edit-path ordering at source (#12925)
+* fix(web): correctly track context transitions caused by applying suggestions (#12927)
+* chore(linux): preserve log files when running tests under Docker (#13107)
+
+## 18.0.182 alpha 2025-01-31
+
+* feat(windows): report on processor type in tsysinfo (#13084)
+* fix(common): push branch when creating PR in version history writer (#13082)
+* refactor: move langtags to @keymanapp/langtags npm module (#13046)
+* chore(developer): use langtags.json for kmc-generate language names (#13047)
+* feat(developer): support relocation of external files in kmc-copy (#13061)
+* feat(windows): kmshell switch handling for the installing state (#12956)
+* feat(windows): add boot switch (#12995)
+* docs(web): remove `.md` in help links (#13051)
+* chore: add android platform to titles (#13033)
+* chore: add the windows platform to titles (#13026)
+* fix(mac): properly manage Input Method lifecycle (#13006)
+* chore(developer): handle forward slashes in paths in package editor (#13081)
+* chore(developer,core,resources): LDML v46 update and remove workaround for import base (#13099)
+
+## 18.0.181 alpha 2025-01-30
+
+* feat(windows): add localized button captions and form labels (#13025)
+* fix(web): disabled correction state should prevent predictive corrections (#13066)
+* fix(web): disabled autoaccept should not autohighlight suggestions (#13068)
+* chore(common): add 17.0.334 to version history (#13065)
+* fix(windows): add api GetContext and TIPProcessKeyEx with IsTextSelected bool (#13023)
+* fix(web): keyboard query error states should always provide informative Error objects, reports (#13048)
+
## 18.0.180 alpha 2025-01-29
* chore(common): Update crowdin strings for Portuguese (#13056)
@@ -1242,6 +1321,18 @@
* chore(common): move to 18.0 alpha (#10713)
* chore: move to 18.0 alpha
+## 17.0.335 alpha 2025-02-06
+
+* fix(android): improve resource-update tool handling of host Activity's closure (#13057)
+* fix(ios): prevent message-handler collision (#13058)
+* chore(linux): improve compatibility with Gentoo Linux (#12889)
+* chore(linux): Update debian changelog (#13062)
+* fix(web): keyboard query error states should always provide informative Error objects, reports (#13079)
+* chore: push the branch when creating PR in version history writer (#13085)
+* fix(web): disabled correction state should prevent predictive corrections (#13074)
+* change(ios): set host-app bundle identifier in embedded Web debug-report (#13112)
+* chore(mac, ios): build and run with xcode 16 (#13121)
+
## 17.0.334 stable 2025-01-27
* chore(linux): update copyright year (#12919)
diff --git a/VERSION.md b/VERSION.md
index 3428a04a07..dd7d796646 100644
--- a/VERSION.md
+++ b/VERSION.md
@@ -1 +1 @@
-18.0.181
\ No newline at end of file
+19.0.3
\ No newline at end of file
diff --git a/android/docs/engine/guides/in-app/index.md b/android/docs/engine/guides/in-app/index.md
index bfa37d4622..538094ddbb 100644
--- a/android/docs/engine/guides/in-app/index.md
+++ b/android/docs/engine/guides/in-app/index.md
@@ -1,13 +1,13 @@
---
-title: Guide: build an in-app keyboard
+title: Guide: build an in-app keyboard for Android with Keyman Engine
---
Keyman Engine for Android allows you to use any Keyman touch keyboard in your Android app, or even to create your own
system keyboard app for purchase in the Play Store.
This guide will walk you through the steps for creating your first Android app with Keyman Engine for Android.
-If you are not familiar with Android development, you will find the
-[Android Developer online training](https://developer.android.com/training/index.html) an invaluable
+If you are not familiar with Android development, you will find the
+[Android Developer online training](https://developer.android.com/training/index.html) an invaluable
resource, and working through some of their tutorials first will help you with the rest of this guide.
### 1. Install Free Tools
@@ -131,5 +131,5 @@ And there you have it: your first Keyman Engine for Android app!
## See Also
* [Guide: Build a system keyboard app](../system-keyboard/)
* [Keyman Developer Documentation](/developer/17.0/)
-* [Keyman Engine for Android Documentation](/developer/engine/android/18.0/)
+* [Keyman Engine for Android Documentation](/developer/engine/android/19.0/)
* [Android Developer Home](https://developer.android.com/index.html)
diff --git a/android/docs/engine/guides/system-keyboard/index.md b/android/docs/engine/guides/system-keyboard/index.md
index 227bb4f33c..7a4901e166 100644
--- a/android/docs/engine/guides/system-keyboard/index.md
+++ b/android/docs/engine/guides/system-keyboard/index.md
@@ -1,5 +1,5 @@
---
-title: Guide: build a system keyboard app
+title: Guide: build a system keyboard app for Android with Keyman Engine
---
In [part 1](../in-app/) of this series, we looked at the steps involved
diff --git a/android/docs/engine/index.md b/android/docs/engine/index.md
index fa549e4b34..38f255592b 100644
--- a/android/docs/engine/index.md
+++ b/android/docs/engine/index.md
@@ -4,7 +4,7 @@ title: Keyman Engine for Android
## Overview
-Keyman Engine for Android 18.0 is a Java library for Android 5.0 and later versions which enables a fully customisable keyboard layout, both within an app and system-wide.
+Keyman Engine for Android 19.0 is a Java library for Android 5.0 and later versions which enables a fully customisable keyboard layout, both within an app and system-wide.
Keyboard layouts for Keyman Engine can be created with [Keyman Developer](/developer/17.0), and a [library of existing keyboard layouts](http://keyman.com/developer/keymanweb/keyboards)
is also available.
diff --git a/android/docs/engine/whatsnew.md b/android/docs/engine/whatsnew.md
index af3f8932d6..5d6f717f8a 100644
--- a/android/docs/engine/whatsnew.md
+++ b/android/docs/engine/whatsnew.md
@@ -1,11 +1,6 @@
---
-title: What's New in Keyman Engine 18.0 for Android
+title: What's New in Keyman Engine 19.0 for Android
---
-## Keyman Engine for Android Breaking Changes: ##
-
-* Change package name from `com.tavultesoft.kmea` to `com.keyman.engine` #7881
-* Update to Java 11 #8543
-
## See Also
* [Keyman Engine for Android Documentation](index)
\ No newline at end of file
diff --git a/android/docs/help/about/index.md b/android/docs/help/about/index.md
index 787c0c9d4d..4428a4bb75 100644
--- a/android/docs/help/about/index.md
+++ b/android/docs/help/about/index.md
@@ -1,5 +1,5 @@
---
-title: About Keyman
+title: About Keyman for Android
---
* [Welcome to Keyman](welcome)
diff --git a/android/docs/help/about/whatsnew.md b/android/docs/help/about/whatsnew.md
index 0f0e891e42..d0ca85e93f 100644
--- a/android/docs/help/about/whatsnew.md
+++ b/android/docs/help/about/whatsnew.md
@@ -1,9 +1,6 @@
---
-title: What's New in Keyman 18.0 for Android
+title: What's New in Keyman 19.0 for Android
---
-Here are some of the new features we have added to Keyman 18.0 for Android:
+Here are some of the new features we have added to Keyman 19.0 for Android:
-* New menu to adjust longpress delay time (#12170, #12185)
-* Support localizations for right-to-left languages (#12215)
-* Handle additional actions for ENTER key (#12125, #12315)
diff --git a/android/docs/help/basic/index.md b/android/docs/help/basic/index.md
index 02d942ca0e..4a0d7aafe0 100644
--- a/android/docs/help/basic/index.md
+++ b/android/docs/help/basic/index.md
@@ -1,5 +1,5 @@
---
-title: Basic Help
+title: Basic Help - Keyman for Android
---
* [Using the Banner on the Keyboard](using-the-banner)
diff --git a/android/docs/help/context/gestures.md b/android/docs/help/context/gestures.md
index d46ab541f6..ec3fc690bd 100644
--- a/android/docs/help/context/gestures.md
+++ b/android/docs/help/context/gestures.md
@@ -1,5 +1,5 @@
---
-title: Gesture Hints and Tips
+title: Gesture Hints and Tips - Keyman for Android
---
## Using Gestures on the Keyboard Keys
diff --git a/android/docs/help/index.md b/android/docs/help/index.md
index 62df93195b..360af91e6d 100644
--- a/android/docs/help/index.md
+++ b/android/docs/help/index.md
@@ -1,5 +1,5 @@
---
-title: Keyman for Android 18.0 Help
+title: Keyman for Android 19.0 Help
---
## [About Keyman](about/)
diff --git a/android/docs/help/start/enabling-system-keyboard.md b/android/docs/help/start/enabling-system-keyboard.md
index 381fbb3e27..dcf16b97fb 100644
--- a/android/docs/help/start/enabling-system-keyboard.md
+++ b/android/docs/help/start/enabling-system-keyboard.md
@@ -1,5 +1,5 @@
---
-title: How To - Enabling Keyman as a System-Wide Keyboard
+title: How To - Enabling Keyman for Android as a System-Wide Keyboard
---
## Enabling System Wide Keyboards
diff --git a/android/docs/help/start/index.md b/android/docs/help/start/index.md
index 4787ce9f9b..7d46903c58 100644
--- a/android/docs/help/start/index.md
+++ b/android/docs/help/start/index.md
@@ -1,5 +1,5 @@
---
-title: Getting Started
+title: Getting Started with Keyman for Android
---
* [How To - Download and Install a Keyman Keyboard](installing-keyboards)
diff --git a/android/docs/help/troubleshooting/grant-storage-permission.md b/android/docs/help/troubleshooting/grant-storage-permission.md
index 7b47cdf4ea..82f1e0d1aa 100644
--- a/android/docs/help/troubleshooting/grant-storage-permission.md
+++ b/android/docs/help/troubleshooting/grant-storage-permission.md
@@ -1,5 +1,5 @@
---
-title: How To - Granting Storage Permission
+title: How To - Granting Storage Permission on Keyman for Android
---
## Granting Storage Permission
diff --git a/common/web/build.sh b/common/web/build.sh
index 798db3b231..2370e3ecf4 100755
--- a/common/web/build.sh
+++ b/common/web/build.sh
@@ -10,6 +10,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
builder_describe "Keyman common web modules" \
:keyman-version \
+ :langtags \
:types \
clean \
configure \
diff --git a/common/web/langtags/.gitignore b/common/web/langtags/.gitignore
new file mode 100644
index 0000000000..a0d996af40
--- /dev/null
+++ b/common/web/langtags/.gitignore
@@ -0,0 +1 @@
+src/imports/
diff --git a/common/web/langtags/README.md b/common/web/langtags/README.md
new file mode 100644
index 0000000000..d55707a510
--- /dev/null
+++ b/common/web/langtags/README.md
@@ -0,0 +1,38 @@
+# Keyman langtags
+
+This package provides a wrapper for the
+[langtags.json](https://ldml.api.sil.org/langtags.json) dataset from
+https://github.com/silnrsi/langtags.
+
+This package is published in sync with Keyman, so changes in langtags may not
+propagate immediately.
+
+## Usage
+
+* `getLangtagByTag(tag)`: Find a language tag from a given tag; matches on tag,
+ tags, full properties, with a case-insensitive search. Returns unmodified
+ object from langtags.json on match.
+
+* `metadata.conformance()`: Returns script and region conformance data from
+ langtags.json
+
+* `metadata.globalvar()`: Returns global variant data from langtags.json
+
+* `metadata.phonvar()`: Returns phonetic variant data from langtags.json
+
+* `metadata.version()`: Returns version data from langtags.json
+
+* `langtags`: Raw access to the langtags.json array of objects
+
+## Background
+
+Key documents for further reading (from langtags repository):
+
+* [Overview of language tags and how this project works with them](https://github.com/silnrsi/langtags/blob/master/doc/tagging.md)
+
+* [How to use langtags.json](https://github.com/silnrsi/langtags/blob/master/doc/langtags.md)
+
+## License
+
+* Copyright ©️ 2025 SIL Global.
+* MIT License ([License text](https://github.com/keymanapp/keyman/tree/master/LICENSE.md))
diff --git a/common/web/langtags/build.sh b/common/web/langtags/build.sh
new file mode 100755
index 0000000000..f125448d4a
--- /dev/null
+++ b/common/web/langtags/build.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+## START STANDARD BUILD SCRIPT INCLUDE
+# adjust relative paths as necessary
+THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
+. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
+## END STANDARD BUILD SCRIPT INCLUDE
+
+. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh"
+. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh"
+
+builder_describe "Build Keyman langtags.js common module" \
+ "clean" \
+ "configure" \
+ "build" \
+ "test" \
+ "publish publish to npm" \
+ "--npm-publish+ For publish, do a npm publish, not npm pack (only for CI)" \
+ "--dry-run,-n don't actually publish, just dry run"
+
+builder_describe_outputs \
+ configure /common/web/langtags/src/imports/langtags.js \
+ build /common/web/langtags/build/src/main.js
+
+builder_parse "$@"
+
+#-------------------------------------------------------------------------------------------------------------------
+
+function compile_langtags() {
+ mkdir -p src/imports
+ echo 'export default ' > src/imports/langtags.js
+ cat "$KEYMAN_ROOT/resources/standards-data/langtags/langtags.json" >> src/imports/langtags.js
+}
+
+function do_configure() {
+ verify_npm_setup
+ compile_langtags
+}
+
+#-------------------------------------------------------------------------------------------------------------------
+
+builder_run_action clean rm -rf ./build ./src/imports ./node_modules
+builder_run_action configure do_configure
+builder_run_action build tsc --build
+builder_run_action test echo 'no tests for langtags'
+builder_run_action publish builder_publish_npm
diff --git a/common/web/langtags/package.json b/common/web/langtags/package.json
new file mode 100644
index 0000000000..f2848f587f
--- /dev/null
+++ b/common/web/langtags/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@keymanapp/langtags",
+ "description": "Keyman bundling of language tags from langtags.json",
+ "keywords": [
+ "keyboard",
+ "keyman",
+ "ldml",
+ "unicode"
+ ],
+ "type": "module",
+ "types": "./build/src/main.d.ts",
+ "exports": {
+ ".": {
+ "es6-bundling": "./src/main.ts",
+ "types": "./build/src/main.d.ts",
+ "default": "./build/src/main.js"
+ }
+ },
+ "files": [
+ "/build/src/"
+ ],
+ "scripts": {
+ "build": "tsc -b",
+ "test": "echo no tests for langtags"
+ },
+ "author": "Marc Durdin (https://github.com/mcdurdin)",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/keymanapp/keyman/issues"
+ },
+ "devDependencies": {
+ "typescript": "^5.4.5"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/keymanapp/keyman.git"
+ },
+ "sideEffects": false
+}
diff --git a/common/web/langtags/src/main.ts b/common/web/langtags/src/main.ts
new file mode 100644
index 0000000000..410bae313a
--- /dev/null
+++ b/common/web/langtags/src/main.ts
@@ -0,0 +1,122 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Wraps langtags.json as a Javascript object
+ */
+
+import langtags from './imports/langtags.js';
+
+// These interfaces and types correspond to https://github.com/silnrsi/langtags/blob/master/source/langtags_schema.json
+
+export type LangTagBcp47 = string; // BCP 47
+export type LangTagBcp47Variant = string; // BCP 47 variant
+export type LangTagIso639_3 = string; // ISO639-3
+export type LangTagIso3166_1 = string; // region
+export type LangTagIso15924 = string; // script
+
+
+export interface LangTag {
+ full: string; // should be LangTagBcp47?
+ iana?: string[];
+ iso639_3?: LangTagIso639_3;
+ latnnames?: string[];
+ localname?: string;
+ localnames?: string;
+ macrolang?: LangTagBcp47;
+ name?: string;
+ names?: string[];
+ nophonvars?: boolean;
+ obsolete?: boolean;
+ region?: LangTagIso3166_1;
+ regionname?: string;
+ regions?: LangTagIso3166_1[];
+ rod?: string;
+ script?: LangTagIso15924;
+ sldr?: boolean;
+ suppress?: boolean;
+ tag?: LangTagBcp47;
+ tags?: LangTagBcp47[];
+ unwritten?: boolean;
+ variants?: LangTagBcp47Variant[];
+ windows?: LangTagBcp47;
+};
+
+export interface LangTagConformance {
+ regions: LangTagIso3166_1[];
+ scripts: LangTagIso15924[];
+ tag: '_conformance';
+};
+
+export interface LangTagGlobalVar {
+ variants: LangTagBcp47Variant[];
+ tag: '_globalvar';
+};
+
+export interface LangTagPhonVar {
+ variants: LangTagBcp47Variant[];
+ tag: '_phonvar';
+};
+
+export interface LangTagVersion {
+ api: string;
+ date: string;
+ tag: '_version';
+};
+
+/**
+ * A map of all language tags, lower cased
+ */
+const langtagsByTag: Map = new Map();
+
+/* metadata variables */
+let conformance: LangTagConformance = undefined;
+let globalvar: LangTagGlobalVar = undefined;
+let phonvar: LangTagPhonVar = undefined;
+let version: LangTagVersion = undefined;
+
+const metadata = {
+ conformance: () => ({ regions: [...conformance.regions], scripts: [...conformance.scripts] }),
+ globalvar: () => ({ variants: [...globalvar.variants] }),
+ phonvar: () => ({ variants: [...phonvar.variants] }),
+ version: () => ({ api: version.api, date: version.date })
+};
+
+/**
+ * Build a dictionary of language tags from langtags.json. This takes under 10ms
+ * on a reasonable laptop.
+ */
+function preinit(): void {
+ for(const tag of langtags) {
+ if(tag.tag == '_conformance') {
+ conformance = tag;
+ } else if(tag.tag == '_globalvar') {
+ globalvar = tag;
+ } else if(tag.tag == '_phonvar') {
+ phonvar = tag;
+ } else if(tag.tag == '_version') {
+ version = tag;
+ } else if(tag.full && tag.tag) {
+ langtagsByTag.set(tag.tag.toLowerCase(), tag);
+ langtagsByTag.set(tag.full.toLowerCase(), tag);
+ if(tag.tags) {
+ for(const t of tag.tags) {
+ langtagsByTag.set(t.toLowerCase(), tag);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Find a LangTag from a given tag; matches on tag, tags, full properties,
+ * with a case-insensitive search.
+ * @param tag
+ * @returns LangTag or undefined if not found
+ */
+function getLangtagByTag(tag: string): LangTag {
+ return langtagsByTag.get(tag?.toLowerCase());
+}
+
+preinit();
+
+export { langtags, getLangtagByTag, metadata };
diff --git a/common/web/langtags/tsconfig.json b/common/web/langtags/tsconfig.json
new file mode 100644
index 0000000000..c09293aeb0
--- /dev/null
+++ b/common/web/langtags/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../tsconfig.base.json",
+
+ "compilerOptions": {
+ "outDir": "build/src/",
+ "rootDir": "src/",
+ "baseUrl": ".",
+ /*
+ following two settings allow us to import langtags.js. It cannot be a .ts
+ because the type is too complex for TypeScript to infer.
+ */
+ "allowJs": true,
+ "noImplicitAny": false,
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/imports/*.js", // Import langtags.json
+ ],
+}
diff --git a/common/web/types/build.sh b/common/web/types/build.sh
index 597962aeee..a392ec022c 100755
--- a/common/web/types/build.sh
+++ b/common/web/types/build.sh
@@ -30,8 +30,8 @@ builder_parse "$@"
function compile_schemas() {
# We need the schema files at runtime and bundled, so always copy it for all actions except `clean`
local schemas=(
- "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/45/ldml-keyboard3.schema.json"
- "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/45/ldml-keyboardtest3.schema.json"
+ "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/46/ldml-keyboard3.schema.json"
+ "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/46/ldml-keyboardtest3.schema.json"
"$KEYMAN_ROOT/common/schemas/kvks/kvks.schema.json"
"$KEYMAN_ROOT/common/schemas/kpj/kpj.schema.json"
"$KEYMAN_ROOT/common/schemas/kpj-9.0/kpj-9.0.schema.json"
diff --git a/common/web/types/tests/kmx/kmx-plus/kmx-plus.tests.ts b/common/web/types/tests/kmx/kmx-plus/kmx-plus.tests.ts
new file mode 100644
index 0000000000..e1ce5b456c
--- /dev/null
+++ b/common/web/types/tests/kmx/kmx-plus/kmx-plus.tests.ts
@@ -0,0 +1,55 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Created by Dr Mark C. Sinclair on 2025-02-03
+ *
+ * Test code for kmx-plus.ts
+ */
+
+import 'mocha';
+import { assert } from 'chai';
+import { Strs, StrsItem, StrsOptions, DependencySections } from '../../../src/kmx/kmx-plus/kmx-plus.js';
+
+const GOTHIC_A = new StrsItem("𐌰", 0x10330);
+
+describe('Test of KMX Plus file', () => {
+ describe('Test of Strs', () => {
+ describe('Test of implicit constructor', () => {
+ it('can construct a Strs', () => {
+ const strs = new Strs();
+ assert.isNotNull(strs);
+ assert.deepEqual(strs.strings, [new StrsItem('')]);
+ assert.deepEqual(strs.allProcessedStrings, new Set());
+ });
+ });
+ describe('Test of allocString()', () => {
+ it('can allocate a one-character string', () => {
+ const strs = new Strs();
+ strs['processString'] = stubStrsProcessString;
+ const csi = strs.allocString("𐌰", { singleOk: true });
+ assert.deepEqual(csi, GOTHIC_A);
+ });
+ it('can allocate an unknown string', () => {
+ const strs = new Strs();
+ strs['processString'] = stubStrsProcessString;
+ const si = strs.allocString("𐌰𐌱𐌲");
+ assert.deepEqual(si, new StrsItem("𐌰𐌱𐌲"));
+ assert.deepEqual(strs.strings, [new StrsItem(''), new StrsItem("𐌰𐌱𐌲")]);
+ });
+ it('can allocate a known string', () => {
+ const strs = new Strs();
+ strs['processString'] = stubStrsProcessString;
+ const one = strs.allocString("𐌰𐌱𐌲");
+ assert.deepEqual(one, new StrsItem("𐌰𐌱𐌲"));
+ assert.deepEqual(strs.strings, [new StrsItem(''), new StrsItem("𐌰𐌱𐌲")]);
+ const two = strs.allocString("𐌰𐌱𐌲");
+ assert.isTrue(two === one);
+ assert.deepEqual(strs.strings, [new StrsItem(''), new StrsItem("𐌰𐌱𐌲")]);
+ });
+ });
+ });
+});
+
+function stubStrsProcessString(s: string, opts: StrsOptions, sections: DependencySections) {
+ return s;
+}
diff --git a/common/windows/delphi/general/RegistryKeys.pas b/common/windows/delphi/general/RegistryKeys.pas
index bacca938c6..b40c309ddd 100644
--- a/common/windows/delphi/general/RegistryKeys.pas
+++ b/common/windows/delphi/general/RegistryKeys.pas
@@ -314,7 +314,6 @@ const
SRegValue_ActiveProject_Filename = 'project filename';
SRegValue_ActiveProject_SourcePath = 'source path';
- SRegValue_AutomaticUpdates = 'automatic updates'; //CU
SRegValue_CheckForUpdates = 'check for updates'; // CU
SRegValue_LastUpdateCheckTime = 'last update check time'; // CU
SRegValue_ApplyNow = 'apply now'; // CU Start the install now even though it will require an restart
diff --git a/common/windows/delphi/general/Upload_Settings.pas b/common/windows/delphi/general/Upload_Settings.pas
index d5347f4fb9..e1e8ce4294 100644
--- a/common/windows/delphi/general/Upload_Settings.pas
+++ b/common/windows/delphi/general/Upload_Settings.pas
@@ -58,6 +58,8 @@ const
URLPath_KeymanDeveloper_KeymanForAndroidDownload = '/go/developer/'+SKeymanVersion+'/android-app';
URLPath_KeymanDeveloper_KeymanForIosDownload = '/go/developer/'+SKeymanVersion+'/ios-app';
+ URLPath_KeymanDeveloper_KeyboardSearchForCloneKeymanCloud = '/go/developer/'+SKeymanVersion+'/clone-keyboard';
+
URLPath_Support = '/go/'+SKeymanVersion+'/support';
URLPath_Privacy = '/go/'+SKeymanVersion+'/privacy';
URLPath_Privacy_Presentation = '/privacy';
@@ -71,6 +73,10 @@ const
UrlPath_RegEx_MatchKeyboardsGo = '^http(?:s)?://keyman(?:-staging)?\.com(?:\.local)?/go/windows/[^/]+/download-keyboards';
// e.g. https://keyman-staging.com/go/windows/14.0/download-keyboards?version=14.0.146.0
+ // Cloning keyboards - Keyman Developer
+ URLSubPath_KeymanDeveloper_Clone_Keyboards = '/keyboards/';
+ URLSubPath_KeymanDeveloper_Clone_Keyboards_Custom = '/keyboards/h/';
+
function URLPath_PackageDownload(const PackageID, BCP47: string; IsUpdate: Boolean): string;
function API_Protocol: string; // = 'https';
@@ -86,6 +92,8 @@ function MakeAPIURL(path: string): string;
function MakeKeymanURL(const path: string): string;
+function URL_KmcMessage(const id: string): string;
+
implementation
uses
@@ -102,6 +110,7 @@ const
S_UserAgent_Developer = 'Keyman Developer';
S_UserAgent_Diagnostics = 'Keyman for Windows Diagnostics';
+ S_Host_KmnSh = 'https://kmn.sh';
S_KeymanCom = 'https://keyman.com';
S_APIProtocol = 'https';
S_APIServer = 'api.keyman.com';
@@ -114,6 +123,7 @@ const
const
URLPath_PackageDownload_Format = '/go/package/download/%0:s?platform=windows&tier=%1:s&bcp47=%2:s&update=%3:d';
+ URL_KeymanDeveloper_HelpKmcMessage_Format = S_Host_KmnSh+'/%0:s';
function API_UserAgent: string;
begin
@@ -130,7 +140,6 @@ begin
Result := S_UserAgent_Diagnostics + '/' + GetVersionString;
end;
-
function MakeKeymanURL(const path: string): string;
begin
Result := KeymanCom_Protocol_Server + path;
@@ -173,4 +182,9 @@ begin
Result := Format(URLPath_PackageDownload_Format, [URLEncode(PackageID), URLEncode(CKeymanVersionInfo.Tier), URLEncode(BCP47), IsUpdateInt]);
end;
+function URL_KmcMessage(const id: string): string;
+begin
+ Result := Format(URL_KeymanDeveloper_HelpKmcMessage_Format, [id.ToLower]);
+end;
+
end.
diff --git a/common/windows/delphi/general/VersionInfo.pas b/common/windows/delphi/general/VersionInfo.pas
index 7567279196..3e399f2f0c 100644
--- a/common/windows/delphi/general/VersionInfo.pas
+++ b/common/windows/delphi/general/VersionInfo.pas
@@ -27,6 +27,7 @@ uses
Winapi.Windows;
function GetVersionString: WideString;
+function GetMajorMinorVersionString: string;
procedure GetVersionBuild(var vMajor, vMinor: Integer);
function GetVersionCopyright: WideString;
function GetFileVersionString(const FileName: WideString): WideString;
@@ -54,6 +55,12 @@ begin
IntToStr(LoWord(VersionMinor));
end;
+function GetMajorMinorVersionString: string;
+begin
+ Result := IntToStr(HiWord(VersionMajor)) + '.' + // I3310
+ IntToStr(LoWord(VersionMajor));
+end;
+
procedure GetVersionBuild(var vMajor, vMinor: Integer);
begin
vMajor := VersionMajor;
diff --git a/common/windows/delphi/general/utilfiletypes.pas b/common/windows/delphi/general/utilfiletypes.pas
index 104c10f411..f34be0933d 100644
--- a/common/windows/delphi/general/utilfiletypes.pas
+++ b/common/windows/delphi/general/utilfiletypes.pas
@@ -1,18 +1,18 @@
(*
Name: utilfiletypes
Copyright: Copyright (C) SIL International.
- Documentation:
- Description:
+ Documentation:
+ Description:
Create Date: 20 Jun 2006
Modified Date: 4 Dec 2006
Authors: mcdurdin
- Related Files:
- Dependencies:
+ Related Files:
+ Dependencies:
- Bugs:
- Todo:
- Notes:
+ Bugs:
+ Todo:
+ Notes:
History: 20 Jun 2006 - mcdurdin - Initial version
01 Aug 2006 - mcdurdin - Refactor util functions into multiple units
04 Dec 2006 - mcdurdin - Add XML and HTML filetypes
@@ -100,7 +100,15 @@ type
end;
type
- TKeymanProjectType = (kptUnknown, kptBasic, kptLDMLKeyboard, kptImportWindowsKeyboard, kptWordlistLexicalModel);
+ TKeymanProjectType = (
+ kptUnknown,
+ kptBasic,
+ kptLDMLKeyboard,
+ kptImportWindowsKeyboard,
+ kptWordlistLexicalModel,
+ kptCloneLocalProject,
+ kptCloneKeymanCloudProject,
+ kptCloneGitHubProject);
implementation
diff --git a/core/include/ldml/keyman_core_ldml.h b/core/include/ldml/keyman_core_ldml.h
index 1d8aaa2575..189b37fc96 100644
--- a/core/include/ldml/keyman_core_ldml.h
+++ b/core/include/ldml/keyman_core_ldml.h
@@ -16,11 +16,11 @@
#pragma once
#define LDML_BKSP_FLAGS_ERROR 0x1
-#define LDML_CLDR_IMPLIED_FORMS_IMPORT "45/scanCodes-implied.xml"
-#define LDML_CLDR_IMPLIED_KEYS_IMPORT "45/keys-Latn-implied.xml"
+#define LDML_CLDR_IMPLIED_FORMS_IMPORT "46/scanCodes-implied.xml"
+#define LDML_CLDR_IMPLIED_KEYS_IMPORT "46/keys-Latn-implied.xml"
#define LDML_CLDR_IMPORT_BASE "cldr"
#define LDML_CLDR_TEST_VERSION_LATEST "techpreview"
-#define LDML_CLDR_VERSION_LATEST "45"
+#define LDML_CLDR_VERSION_LATEST "46"
#define LDML_ELEM_FLAGS_ORDER_BITSHIFT 0x10
#define LDML_ELEM_FLAGS_ORDER_MASK 0xFF0000
#define LDML_ELEM_FLAGS_PREBASE 0x8
diff --git a/core/include/ldml/keyman_core_ldml.ts b/core/include/ldml/keyman_core_ldml.ts
index b649d9424e..093ad839c0 100644
--- a/core/include/ldml/keyman_core_ldml.ts
+++ b/core/include/ldml/keyman_core_ldml.ts
@@ -59,7 +59,7 @@ class Constants {
/**
* The current CLDR version
*/
- readonly cldr_version_latest = '45';
+ readonly cldr_version_latest = '46';
/**
* The version for testdata files
*/
@@ -564,8 +564,17 @@ class Constants {
/** maximum count of markers (not including 'any') */
readonly marker_max_count = this.marker_max_index - this.marker_min_index + 1;
+ /**
+ * List of versions to treat as if they were the 'latest' version.
+ */
+ treatAsLatest(version: string): boolean {
+ return cldrTreatAsLatest.has(version);
+ }
};
+/** There's no data or DTD change in 45, 46, 46.1, 47 so map them all to 46 at present. */
+const cldrTreatAsLatest: Set = new Set(['45', '46.1', '47']);
+
export const constants = new Constants();
// }
diff --git a/core/src/ldml/C7532_ldml_updating.md b/core/src/ldml/C7532_ldml_updating.md
index da6bbcb80c..10d7708ada 100644
--- a/core/src/ldml/C7532_ldml_updating.md
+++ b/core/src/ldml/C7532_ldml_updating.md
@@ -22,7 +22,7 @@ working on ‘layr’, using ‘disp’ as a model from https://github.com/keyma
## XML changes
-- `resources/standards-data/ldml-keyboards/45/` : update / reimport / fix fixup script if needed
+- `resources/standards-data/ldml-keyboards/46/` : update / reimport / fix fixup script if needed
- E/C: `common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts`
- add to `LKKeyboard` and subproperties as needed to support the structure on the XML side
- Now would be a good time to stop and make sure everything compiles. It didn’t, there was an unrelated issue with snprintf!
diff --git a/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml b/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml
index 827dd54dfa..5494e8f41e 100644
--- a/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml
+++ b/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml b/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml
index c0157f8828..56aa8d8d01 100644
--- a/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml
+++ b/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml b/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml
index f4024275e8..efef244c8b 100644
--- a/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml
+++ b/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml
index 52323ea071..e146f5afae 100644
--- a/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml
+++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/k_009_transform_nfc-test.xml b/core/tests/unit/ldml/keyboards/k_009_transform_nfc-test.xml
index 8d63e86e02..0006f14e27 100644
--- a/core/tests/unit/ldml/keyboards/k_009_transform_nfc-test.xml
+++ b/core/tests/unit/ldml/keyboards/k_009_transform_nfc-test.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/k_020_fr-test.xml b/core/tests/unit/ldml/keyboards/k_020_fr-test.xml
index 22fce78538..662f3ca665 100644
--- a/core/tests/unit/ldml/keyboards/k_020_fr-test.xml
+++ b/core/tests/unit/ldml/keyboards/k_020_fr-test.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml b/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml
index d23e596bbb..b1eea391af 100644
--- a/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml
+++ b/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/k_201_reorder_esk-test.xml b/core/tests/unit/ldml/keyboards/k_201_reorder_esk-test.xml
index fbb3427878..3268aa733e 100644
--- a/core/tests/unit/ldml/keyboards/k_201_reorder_esk-test.xml
+++ b/core/tests/unit/ldml/keyboards/k_201_reorder_esk-test.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml
index 22865b12c5..57769f1c18 100644
--- a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml
+++ b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/k_211_marker_escape-test.xml b/core/tests/unit/ldml/keyboards/k_211_marker_escape-test.xml
index 5fb09e7fc5..f1786fb974 100644
--- a/core/tests/unit/ldml/keyboards/k_211_marker_escape-test.xml
+++ b/core/tests/unit/ldml/keyboards/k_211_marker_escape-test.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/k_212_marker_11057-test.xml b/core/tests/unit/ldml/keyboards/k_212_marker_11057-test.xml
index 92e2abbf3f..69749bc516 100644
--- a/core/tests/unit/ldml/keyboards/k_212_marker_11057-test.xml
+++ b/core/tests/unit/ldml/keyboards/k_212_marker_11057-test.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/ldml_test-test.xml b/core/tests/unit/ldml/keyboards/ldml_test-test.xml
index 2c08741232..0e569ff8c5 100644
--- a/core/tests/unit/ldml/keyboards/ldml_test-test.xml
+++ b/core/tests/unit/ldml/keyboards/ldml_test-test.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/core/tests/unit/ldml/keyboards/meson.build b/core/tests/unit/ldml/keyboards/meson.build
index 394b8a6811..04a288710c 100644
--- a/core/tests/unit/ldml/keyboards/meson.build
+++ b/core/tests/unit/ldml/keyboards/meson.build
@@ -5,8 +5,8 @@
#
-# keyboards in resources/standards-data/ldml-keyboards/45/3.0/
-# tests in resources/standards-data/ldml-keyboards/45/test/
+# keyboards in resources/standards-data/ldml-keyboards/46/3.0/
+# tests in resources/standards-data/ldml-keyboards/46/test/
tests_from_cldr = [
'ja-Latn',
'pt-t-k0-abnt2',
@@ -57,7 +57,7 @@ tests += tests_from_cldr
# Setup kmc
kmc_root = meson.global_source_root() / '../developer/src/kmc'
-ldml_root = meson.global_source_root() / '../resources/standards-data/ldml-keyboards/45'
+ldml_root = meson.global_source_root() / '../resources/standards-data/ldml-keyboards/46'
ldml_data = join_paths(ldml_root, '3.0')
ldml_testdata = join_paths(ldml_root, 'test')
kmc_cmd = [node, '--enable-source-maps', kmc_root]
diff --git a/developer/docs/help/context/clone-github-project-parameters.md b/developer/docs/help/context/clone-github-project-parameters.md
new file mode 100644
index 0000000000..d2ec5c7aba
--- /dev/null
+++ b/developer/docs/help/context/clone-github-project-parameters.md
@@ -0,0 +1,34 @@
+---
+title: Clone GitHub Project Parameters Dialog
+---
+
+
+
+Allows you to clone a project from a GitHub repository, giving the project a
+new id, and organizing project files following the
+[file layout](/developer/keyboards/) used in the
+[Keyman keyboards repository](https://github.com/keymanapp/keyboards).
+
+Keyboard and lexical model projects can be cloned.
+
+Projects can also be cloned from the command line with [`kmc copy`](kmc).
+
+### Parameters
+
+GitHub URL:
+: The full URL to a source project .kpj file to copy, on github.com.
+
+Destination path
+: Specifies the base path where the project folder will be created.
+ The project folder name will be the keyboard ID. If the folder
+ already exists, then you will be prompted before Keyman Developer
+ overwrites files inside it.
+
+New project ID
+: The base filename of the keyboard/model, project and package. This must
+ conform to the Keyman keyboard or lexical model identifier rules.
+
+Relocate external files into new project folder
+: When selected, if the project references files that are not inside the same
+ folder as the .kpj file, move these files into an `external` subfolder
+
diff --git a/developer/docs/help/context/clone-keyman-cloud-project-parameters.md b/developer/docs/help/context/clone-keyman-cloud-project-parameters.md
new file mode 100644
index 0000000000..71d1cf8a3a
--- /dev/null
+++ b/developer/docs/help/context/clone-keyman-cloud-project-parameters.md
@@ -0,0 +1,37 @@
+---
+title: Clone Keyman Cloud Project Parameters Dialog
+---
+
+
+
+Allows you to clone a project from Keyman Cloud, giving the project a
+new id, and organizing project files following the
+[file layout](/developer/keyboards/) used in the
+[Keyman keyboards repository](https://github.com/keymanapp/keyboards).
+
+Projects can also be cloned from the command line with [`kmc copy`](kmc).
+
+Only keyboard projects can be cloned from Keyman Cloud at this time via this
+dialog. Lexical model projects can be cloned from Keyman Cloud via the command
+line.
+
+Search for a keyboard using the standard keyman.com keyboard search in the upper
+pane. Once you select a keyboard, and fill in the New Project Details, the OK
+button will be enabled to allow you to finish the cloning process.
+
+### Parameters
+
+Destination path
+: Specifies the base path where the project folder will be created.
+ The project folder name will be the keyboard ID. If the folder
+ already exists, then you will be prompted before Keyman Developer
+ overwrites files inside it.
+
+New project ID
+: The base filename of the keyboard, project and package. This must
+ conform to the Keyman keyboard identifier rules.
+
+Relocate external files into new project folder
+: When selected, if the project references files that are not inside the same
+ folder as the .kpj file, move these files into an `external` subfolder
+
diff --git a/developer/docs/help/context/clone-local-project-parameters.md b/developer/docs/help/context/clone-local-project-parameters.md
new file mode 100644
index 0000000000..3b1b5bc2c9
--- /dev/null
+++ b/developer/docs/help/context/clone-local-project-parameters.md
@@ -0,0 +1,34 @@
+---
+title: Clone Local Project Parameters Dialog
+---
+
+
+
+Allows you to clone a project from your local filesystem, giving the project a
+new id, and organizing project files following the
+[file layout](/developer/keyboards/) used in the
+[Keyman keyboards repository](https://github.com/keymanapp/keyboards).
+
+Keyboard and lexical model projects can be cloned.
+
+Projects can also be cloned from the command line with [`kmc copy`](kmc).
+
+### Parameters
+
+Source project filename
+: The full path of the source project .kpj file to copy.
+
+Destination path
+: Specifies the base path where the project folder will be created.
+ The project folder name will be the keyboard ID. If the folder
+ already exists, then you will be prompted before Keyman Developer
+ overwrites files inside it.
+
+New project ID
+: The base filename of the keyboard/model, project and package. This must
+ conform to the Keyman keyboard or lexical model identifier rules.
+
+Relocate external files into new project folder
+: When selected, if the project references files that are not inside the same
+ folder as the .kpj file, move these files into an `external` subfolder
+
diff --git a/developer/docs/help/context/new-project.md b/developer/docs/help/context/new-project.md
index 1fd83fcd6f..8af883788e 100644
--- a/developer/docs/help/context/new-project.md
+++ b/developer/docs/help/context/new-project.md
@@ -21,4 +21,28 @@ Creates a keyboard project that matches the folder structure used in the Keyman
Creates a lexical model project that matches the folder structure used in the Keyman [lexical models repository](https://github.com/keymanapp/lexical-models) and includes all basic files needed for a lexical model project. Clicking OK will open the [New Lexical Model Project Parameters dialog](new-lm-project-parameters).
-The **Basic** and **Import Windows Keyboard** modes can also be driven by command line with [KMConvert](kmconvert).
\ No newline at end of file
+## Clone local project
+
+Copies a Keyman keyboard or lexical model project on your local filesystem into
+a new folder, giving the project and included files a new name. Clicking OK will
+open the [Clone Local Project Parameters dialog](clone-local-project-parameters).
+
+## Clone project from Keyman Cloud
+
+Copies a Keyman keyboard or lexical model project from keyman.com into a new
+folder, giving the project and included files a new name. Clicking OK will
+open the [Clone Keyman Cloud Project Parameters dialog](clone-keyman-cloud-project-parameters).
+
+## Clone project from GitHub
+
+Copies a Keyman keyboard or lexical model project from a GitHub repository into
+a new folder, giving the project and included files a new name. Clicking OK will
+open the [Clone GitHub Project Parameters dialog](clone-github-project-parameters).
+
+# Command line access
+
+[`kmc generate`](kmc) can be used to generate new Keyman keyboards, LDML
+keyboards, and Wordlist lexical models from the command line.
+
+[KMConvert](kmconvert) can also be used to import Windows keyboards from the
+command line.
diff --git a/developer/docs/help/images/ui/frmCloneGitHubProjectParameters.png b/developer/docs/help/images/ui/frmCloneGitHubProjectParameters.png
new file mode 100644
index 0000000000..ce7d1c99c3
Binary files /dev/null and b/developer/docs/help/images/ui/frmCloneGitHubProjectParameters.png differ
diff --git a/developer/docs/help/images/ui/frmCloneKeymanCloudProjectParameters.png b/developer/docs/help/images/ui/frmCloneKeymanCloudProjectParameters.png
new file mode 100644
index 0000000000..a71fd7ed98
Binary files /dev/null and b/developer/docs/help/images/ui/frmCloneKeymanCloudProjectParameters.png differ
diff --git a/developer/docs/help/images/ui/frmCloneLocalProjectParameters.png b/developer/docs/help/images/ui/frmCloneLocalProjectParameters.png
new file mode 100644
index 0000000000..705ada692e
Binary files /dev/null and b/developer/docs/help/images/ui/frmCloneLocalProjectParameters.png differ
diff --git a/developer/docs/help/images/ui/frmNewProject.png b/developer/docs/help/images/ui/frmNewProject.png
index 8fc13cd763..1c41529b1d 100644
Binary files a/developer/docs/help/images/ui/frmNewProject.png and b/developer/docs/help/images/ui/frmNewProject.png differ
diff --git a/developer/docs/help/index.md b/developer/docs/help/index.md
index 9e9c250abb..d7fbc4098d 100644
--- a/developer/docs/help/index.md
+++ b/developer/docs/help/index.md
@@ -1,5 +1,5 @@
---
-title: Keyman Developer 18.0 User Guide
+title: Keyman Developer 19.0 User Guide
---
Need help using Keyman Developer to create your keyboard layouts? You'll
@@ -8,7 +8,7 @@ and tutorials, and full reference information.
## Guides and Tutorials
- [What is Keyman Developer?](guides/intro)
-- [What's new in 18.0](whats-new)
+- [What's new in 19.0](whats-new)
- [Developing Keyman keyboard layouts](guides/develop)
- [Testing Keyman keyboards](guides/test)
- [Distributing Keyman keyboards](guides/distribute)
diff --git a/developer/docs/help/reference/kmc/cli/get-started.md b/developer/docs/help/reference/kmc/cli/get-started.md
index 9147eebda2..cb3e9073e2 100644
--- a/developer/docs/help/reference/kmc/cli/get-started.md
+++ b/developer/docs/help/reference/kmc/cli/get-started.md
@@ -26,7 +26,7 @@ configuration or installation is required.
kmc is also available as an npm package,
[@keymanapp/kmc](https://npmjs.com/package/@keymanapp/kmc).
-You'll need [node.js](https://nodejs.org/), version 18.0 or later.
+You'll need [node.js](https://nodejs.org/), version 20.0 or later.
```shell
npm install -g @keymanapp/kmc
diff --git a/developer/docs/help/reference/kmc/cli/reference.md b/developer/docs/help/reference/kmc/cli/reference.md
index fdaed58faa..1d5ed9fc20 100644
--- a/developer/docs/help/reference/kmc/cli/reference.md
+++ b/developer/docs/help/reference/kmc/cli/reference.md
@@ -90,14 +90,33 @@ The following parameters are available:
: Prints the version number of kmc
-`--no-error-reporting`, `--error-reporting`
+`--color`, `--no-color`
-: Enable or disable error reporting to keyman.com, overriding [user
- settings](../../user-settings). Error reporting is for fatal errors in the
- compiler, and not errors in compiled files. No user data is sent in error
+: Controls colorization for log messages, using ANSI color controls. If both of
+ these settings are omitted, kmc will attempt to detect from console, and will
+ use colorization for interactive terminals, and no colorization when
+ redirection is being used.
+
+`--error-reporting`, `--no-error-reporting`
+
+: Enable or disable error reporting to keyman.com, overriding
+ [user settings](../../user-settings). Error reporting is for fatal errors in
+ the compiler, and not errors in compiled files. No user data is sent in error
reports, although some filenames and paths may be present in the diagnostic
data attached to the report.
+`--log-format `
+
+: Output log format. The available options are:
+ * `formatted` (default): emits log messages in a human-readable format
+ * `tsv`: emits log messages in UTF-8 tab-separated format. This format will be
+ stable across versions of kmc. The format has the following fields:
+ * filename
+ * line number
+ * severity
+ * code
+ * message
+
`-l `, `--log-level `
: Controls the level of logging to console for messages relating to the
@@ -108,9 +127,12 @@ The following parameters are available:
* `hint`: emits compilation errors, warnings, and hints
* `info` (default): emits compilation errors, warnings, hints, and
informational messages
+ * `verbose`: emits all compilation messages, including verbose
+ informational messages
* `debug`: emits all compilation messages, plus internal debug messages
- Logging of specific [messages](messages) can be controlled with `--message`, `-m`.
+ Logging of specific [messages](messages) can be controlled with `--message`,
+ `-m` (`build` command only).
Note that when warnings are treated as errors
(`--compiler-warnings-as-errors`, `-w`), they will still be logged as
@@ -118,13 +140,6 @@ The following parameters are available:
## `kmc build` options
-`--color`, `--no-color`
-
-: Controls colorization for log messages, using ANSI color controls. If both of
- these settings are omitted, kmc will attempt to detect from console, and will
- use colorization for interactive terminals, and no colorization when
- redirection is being used.
-
`-d`, `--debug`
: Include debug information in output files. Debug information is used for
@@ -167,17 +182,6 @@ The following parameters are available:
: Turns off warnings (CWARN_HeaderStatementIsDeprecated,
CWARN_LanguageHeadersDeprecatedInKeyman10) for deprecated code styles
-`--log-format `
-
-: Output log format. The available options are:
- * `formatted` (default): emits log messages in a human-readable format
- * `tsv`: emits log messages in UTF-8 tab-separated format. This format will be
- stable across versions of kmc. The format has the following fields:
- * filename
- * line number
- * severity
- * code
- * message
`-o `, `--out-file `
@@ -509,6 +513,12 @@ following sources:
: Show what would happen, without making changes
+`-r, --relocate-external`
+
+: Copy referenced files that are outside the project folder into an 'external'
+ folder within the new project, rather than just referencing them in their
+ current location.
+
### File copying, renaming, and structure rules
The **origin** project folder is the one that contains the .kpj file. When a
diff --git a/developer/docs/help/whats-new.md b/developer/docs/help/whats-new.md
index d1dce092c3..1005476c3d 100644
--- a/developer/docs/help/whats-new.md
+++ b/developer/docs/help/whats-new.md
@@ -1,43 +1,5 @@
---
-title: What's new in Keyman Developer 18.0
+title: What's new in Keyman Developer 19.0
---
-Keyman Developer 17 has a number of significant changes:
-
-* `kmcomp` has been removed and replaced with `kmc`. Learn more in our [kmcomp migration guide](reference/kmc/cli/kmcomp-migration).
-
-* Windows installer packages should now be built with
- `kmc build windows-package-installer` and can no longer be built within
- the Keyman Developer IDE. More details on how to use this are in the
- [`kmc` reference documentation](reference/kmc/cli/reference#toc-kmc-build-windows-package-installer-additional-options).
-
-* `.keyboard_info` files are now generated entirely from the package and
- keyboard files. Extra fields are available in packages for license file,
- welcome file, typing examples, related packages (including deprecated
- packages), and additional font files. Keyboards in the Keyman Cloud
- keyboard repository have already been updated; if you are an author of
- one of these keyboards, you should pull the changes from the repository
- before submitting future updates.
-
-* Keyboard project files (.kpj) now have a new format which does not list
- individual files, but instead includes all files in the same folder and
- subfolders relative to the project file. This prevents projects from
- including unrelated files, and reduces the number of files that change
- when keyboards are updated.
-
-* Keyman Developer will now open each project in a new window, allowing
- the user to rapidly switch between multiple projects at the same time.
-
-* The Keyman Keyboard and Lexical Model Cloud repositories have new build
- scripts which run `kmc`. These run much, much faster than previously!
-
-* The [`&displayMap`](/developer/language/reference/displaymap) store allows
- keyboard developers to specify a font mapping to PUA for the On Screen
- Keyboard and Touch Layout to resolve diacritic rendering issues.
-
-* Keyman Developer has been updated to support Unicode 15.1.
-
-* Additional non-printing characters have been added to the Touch Layout Editor.
-
-* Virtual keys in output of rules have never worked properly or been officially
- supported; Keyman Developer will now warn if you attempt to use this pattern.
+Keyman Developer 19 has the following significant changes:
diff --git a/developer/src/build.sh b/developer/src/build.sh
index dd4f5264e5..d01c6caeb8 100755
--- a/developer/src/build.sh
+++ b/developer/src/build.sh
@@ -156,6 +156,7 @@ function do_publish() {
./common/web/utils/build.sh publish $DRY_RUN $NPM_PUBLISH
../../common/web/keyman-version/build.sh publish $DRY_RUN $NPM_PUBLISH
+ ../../common/web/langtags/build.sh publish $DRY_RUN $NPM_PUBLISH
../../common/web/types/build.sh publish $DRY_RUN $NPM_PUBLISH
../../core/include/ldml/build.sh publish $DRY_RUN $NPM_PUBLISH
# end TODO
diff --git a/developer/src/common/delphi/lexicalmodels/Keyman.System.LexicalModelUtils.pas b/developer/src/common/delphi/lexicalmodels/Keyman.System.LexicalModelUtils.pas
index c367331eee..44120f9cf9 100644
--- a/developer/src/common/delphi/lexicalmodels/Keyman.System.LexicalModelUtils.pas
+++ b/developer/src/common/delphi/lexicalmodels/Keyman.System.LexicalModelUtils.pas
@@ -4,7 +4,6 @@ interface
type
TLexicalModelUtils = class
- private
public
const SPackageNameDoesNotFollowLexicalModelConventions_Message: string =
'The package file %0:s does not follow the recommended model filename conventions. The name should be all lower case, '+
@@ -31,6 +30,8 @@ type
class function CleanLexicalModelIDComponent(const Name: string): string; static;
class function IsCleanLexicalModelIDComponent(const Name: string): Boolean; static;
+ class function IsValidLexicalModelID(ID: string; EnforceCaseAndWhitespace: Boolean): Boolean; static;
+
class function ExtractFileExt(filename: string): string; static;
class function RemoveFileExt(filename: string): string; static;
class function ChangeFileExt(filename, ext: string): string; static;
@@ -51,11 +52,20 @@ const
// The model ID SHOULD adhere to this pattern (see also developer/src/kmc-model):
// author .bcp47 .uniq
+ MODEL_ID_PATTERN = '^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*$';
MODEL_ID_PATTERN_JS = '^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.js$';
MODEL_ID_PATTERN_TS = '^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.ts$';
MODEL_ID_PATTERN_PACKAGE = '^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.(kps|kmp)$';
MODEL_ID_PATTERN_PROJECT = '^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.kpj$';
+class function TLexicalModelUtils.IsValidLexicalModelID(ID: string;
+ EnforceCaseAndWhitespace: Boolean): Boolean;
+begin
+ if not EnforceCaseAndWhitespace then
+ ID := LowerCase(Trim(ID));
+ Result := TRegEx.IsMatch(ID, MODEL_ID_PATTERN);
+end;
+
class function TLexicalModelUtils.DoesJSFilenameFollowLexicalModelConventions(
const Name: string): Boolean;
begin
diff --git a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts
index 8a16de6bf1..6c004d101c 100644
--- a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts
+++ b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts
@@ -1,4 +1,4 @@
-import { CompilerError, CompilerErrorMask } from '@keymanapp/developer-utils';
+import { CompilerError, CompilerErrorMask, CompilerEvent, dedentCompilerMessageDetail } from '@keymanapp/developer-utils';
import {assert, expect} from 'chai';
//
@@ -37,8 +37,24 @@ export function verifyCompilerMessagesObject(source: Record, namespa
const c = o[1].toUpperCase() + '_' + o[2];
expect(m[c]).to.be.a('number', `Expected constant name ${c} to exist`);
- const v = m[key]('','','','','','','','','','','','' /* ignore arguments*/);
+ const v: CompilerEvent = m[key]('','','','','','','','','','','','' /* ignore arguments*/);
expect(v.code).to.equal(m[c], `Function ${key} returns the wrong code`);
+
+ assert.isNotNull(v.message, `Function ${key} returns an invalid message: '${v.message}'`);
+ assert.isNotEmpty(v.message, `Function ${key} returns an empty message: '${v.message}'`);
+
+ // Verify layout of compiler detail lines
+
+ if(v.detail) {
+ const detail = dedentCompilerMessageDetail(v).split('\n');
+ for(const line of detail) {
+ // Prevent lines over 80 characters -- we don't rewrap at this stage
+ // Don't check lines with URLs because they may be forced to go long
+ if(!line.includes('http')) {
+ assert.isAtMost(line.length, 80, `Function ${key} returns a message detail with at least one line longer than 80 characters: '${line}'`);
+ }
+ }
+ }
}
else if(typeof m[key] == 'number') {
const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key);
diff --git a/developer/src/common/web/utils/src/compiler-interfaces.ts b/developer/src/common/web/utils/src/compiler-interfaces.ts
index 4cb9ae3c22..869e0165da 100644
--- a/developer/src/common/web/utils/src/compiler-interfaces.ts
+++ b/developer/src/common/web/utils/src/compiler-interfaces.ts
@@ -291,10 +291,8 @@ export interface CompilerMessageOverride {
level: CompilerErrorSeverityOverride;
};
-export interface CompilerCallbackOptions {
- logLevel?: CompilerLogLevel;
- logFormat?: CompilerLogFormat;
- color?: boolean; // null or undefined == use console default
+export interface CompilerCallbackOptions extends CompilerBaseOptions {
+ // TODO: these overlap with CompilerOptions, should refactor
compilerWarningsAsErrors?: boolean;
messageOverrides?: CompilerMessageOverrideMap;
};
@@ -401,6 +399,19 @@ export const CompilerMessageSpec = (code: number, message: string, detail?: stri
detail,
});
+/**
+ * Remove initial whitespace from compiler detail messages, to enable
+ * indented formatting of message detail strings inside the message
+ * definitions
+ * @param event
+ * @returns dedented event detail
+ */
+export function dedentCompilerMessageDetail(event: CompilerEvent) {
+ // TODO(lowpri): dedent may be too naive -- should use least
+ // non-zero whitespace line as amount to dedent
+ return (event.detail ?? '').replace(/^[ ]+/gm, '');
+}
+
export const CompilerMessageDef = (param: any) => String(param ?? ``);
export const CompilerMessageSpecWithException = (code: number, message: string, exceptionVar: any, detail?: string) : CompilerEvent => ({
diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts
index f8ee6246da..f3e23e79f2 100644
--- a/developer/src/common/web/utils/src/index.ts
+++ b/developer/src/common/web/utils/src/index.ts
@@ -52,7 +52,7 @@ export { defaultCompilerOptions, CompilerBaseOptions, CompilerOptions, CompilerE
ALL_COMPILER_LOG_FORMATS, CompilerLogFormat,
CompilerMessageOverride,
CompilerMessageOverrideMap,
-
+ dedentCompilerMessageDetail,
KeymanCompilerArtifact,
KeymanCompilerArtifactOptional,
KeymanCompilerArtifacts,
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 928bb94bef..0a5abf6355 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
@@ -231,6 +231,10 @@ export class LDMLKeyboardXMLSourceFileReader {
this.callbacks.reportMessage(CommonTypesMessages.Error_ImportInvalidPath({base, path, subtag}));
return false;
}
+ if (constants.treatAsLatest(paths[0])) {
+ /** 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]);
} else {
// local import
diff --git a/developer/src/common/web/utils/src/utils/KeymanSentry.ts b/developer/src/common/web/utils/src/utils/KeymanSentry.ts
index 94409eafc8..dfe62bfa3d 100644
--- a/developer/src/common/web/utils/src/utils/KeymanSentry.ts
+++ b/developer/src/common/web/utils/src/utils/KeymanSentry.ts
@@ -64,11 +64,17 @@ export class KeymanSentry {
}
}
- static async captureException(e: any): Promise {
+ /**
+ * capture an exception for Sentry; note that in local environments, this will normally
+ * throw the exception rather than sending to Sentry
+ * @param e
+ * @param force if true, reports to Sentry even in local environments
+ */
+ static async captureException(e: any, force?: boolean): Promise {
if(isInit) {
// For local development, we don't want to bury the trace; we need the cast to avoid
// TS2367 (comparison appears to be unintentional)
- if((KEYMAN_VERSION.VERSION_ENVIRONMENT as string) == 'local') {
+ if(!force && (KEYMAN_VERSION.VERSION_ENVIRONMENT as string) == 'local') {
throw e;
}
diff --git a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml
index b3cc7cabdf..01bd9f9a43 100644
--- a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml
+++ b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml
@@ -2,6 +2,6 @@
-
+
diff --git a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml
index e8ac9dc964..bb3927a873 100644
--- a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml
+++ b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml
@@ -2,6 +2,6 @@
-
+
diff --git a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/test-fr.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/test-fr.xml
index b92ba41360..fc0224fcb8 100644
--- a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/test-fr.xml
+++ b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/test-fr.xml
@@ -1,5 +1,5 @@
-
+
-
+
diff --git a/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts b/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts
index e3e6fc1e9f..fcb3628144 100644
--- a/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts
+++ b/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts
@@ -32,7 +32,7 @@ describe('ldml keyboard xml reader tests', function () {
instancePath: '/keyboard3/conformsTo',
keyword: 'enum',
message: `must be equal to one of the allowed values`,
- params: 'allowedValues="45"',
+ params: 'allowedValues="45,46"', // this has to be kept in sync with the DTD
})],
},
{
diff --git a/developer/src/inst/kmdev.wxs b/developer/src/inst/kmdev.wxs
index 832d8c55de..1602f624ff 100644
--- a/developer/src/inst/kmdev.wxs
+++ b/developer/src/inst/kmdev.wxs
@@ -59,7 +59,7 @@
-
+
@@ -198,7 +198,7 @@
-
+
diff --git a/developer/src/inst/node/dist/Keyman_README.md b/developer/src/inst/node/dist/Keyman_README.md
index faf95051f5..6d4ce63744 100644
--- a/developer/src/inst/node/dist/Keyman_README.md
+++ b/developer/src/inst/node/dist/Keyman_README.md
@@ -1,6 +1,6 @@
# Bundled node.js
-We are bundling node 18.14.1 here as of Keyman 17.0.52.
+We are bundling node 20.16.1 here as of Keyman 18.0.156-alpha
Note that if we change the node version, we will need to
rebuild the binary modules in /developer/src/server/src/win32/
diff --git a/developer/src/kmc-copy/src/KeymanProjectCopier.ts b/developer/src/kmc-copy/src/KeymanProjectCopier.ts
index 9e8d175991..a751e46731 100644
--- a/developer/src/kmc-copy/src/KeymanProjectCopier.ts
+++ b/developer/src/kmc-copy/src/KeymanProjectCopier.ts
@@ -4,7 +4,7 @@
* Copy a keyboard or lexical model project
*/
-import { CloudUrls, GitHubUrls, CompilerCallbacks, CompilerLogLevel, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter, ValidIds } from "@keymanapp/developer-utils";
+import { CloudUrls, GitHubUrls, CompilerCallbacks, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter, ValidIds, CompilerBaseOptions } from "@keymanapp/developer-utils";
import { KeymanFileTypes } from "@keymanapp/common-types";
import { CopierMessages } from "./copier-messages.js";
@@ -19,12 +19,7 @@ type CopierFunction = (
* @public
* Options for the Keyman Developer project copier
*/
-export interface CopierOptions /* not inheriting from CompilerBaseOptions */ {
- /**
- * Reporting level to console, used by NodeCompilerCallbacks (not used in
- * compiler modules; all messages are still reported to the internal log)
- */
- logLevel?: CompilerLogLevel;
+export interface CopierOptions extends CompilerBaseOptions {
/**
* output path where project folder will be created
*/
@@ -33,6 +28,12 @@ export interface CopierOptions /* not inheriting from CompilerBaseOptions */ {
* dryRun: show what would happen
*/
dryRun: boolean;
+ /**
+ * Copy referenced files that are outside the project folder into an
+ * 'external' folder within the new project, rather than just referencing them
+ * in their current location
+ */
+ relocateExternalFiles?: boolean;
};
/**
@@ -78,7 +79,6 @@ export class KeymanProjectCopier implements KeymanCompiler {
isLocalOrigin(): boolean {
return this.githubRef == undefined;
}
- relocateExternalFiles: boolean = false; // TODO-COPY: support
public async init(callbacks: CompilerCallbacks, options: CopierOptions): Promise {
if(!callbacks || !options) {
@@ -357,6 +357,7 @@ export class KeymanProjectCopier implements KeymanCompiler {
const subFilename = this.normalizePath(this.resolveFilename(project.projectFilename, normalizedFilePath));
const copier = this.copiers[ file.fileType] ?? this.copyGenericFile.bind(this);
// Ignore errors because we will continue to do a best effort
+ this.callbacks.reportMessage(CopierMessages.Verbose_CopyingFile({from: normalizedFilePath, to: subFilename}));
await copier(project, subFilename, subOutputPath, source, result);
}
@@ -559,20 +560,25 @@ export class KeymanProjectCopier implements KeymanCompiler {
let subOutputPath: string;
if(this.callbacks.path.isAbsolute(subFilenameRelative) || subFilenameRelative.startsWith('..')) {
- if(this.isLocalOrigin() && !this.relocateExternalFiles) {
+ if(this.isLocalOrigin() && !this.options.relocateExternalFiles) {
// Reference outside the project structure, do not attempt to normalize or copy,
// but we do need to update references for the new output path
- return this.normalizePath(this.callbacks.path.relative(result.artifacts[parentFilename].filename, subFilenameAbsolute));
+ const destinationFilename =
+ this.normalizePath(this.callbacks.path.relative(result.artifacts[parentFilename].filename, subFilenameAbsolute));
+ this.callbacks.reportMessage(CopierMessages.Verbose_CopyingFile({from: originalSubfilename, to: destinationFilename}));
+ return destinationFilename;
} else {
// Reference outside the project structure, we will copy into a subfolder of the project;
// should be relative to root of remote repo, e.g. release/k/khmer_angkor references release/shared/fonts/...,
// so should go external/release/shared/fonts/...
- subOutputPath = this.normalizePath(this.callbacks.path.join(this.options.outPath, 'external', this.callbacks.path.dirname(subFilenameAbsolute)));
+ const relativePath = this.callbacks.path.dirname(subFilenameRelative).replace(/\.\.[/\\]/g, '');
+ subOutputPath = this.normalizePath(this.callbacks.path.join(this.options.outPath, 'external', relativePath));
}
} else {
subOutputPath = this.normalizePath(this.callbacks.path.join(outputPath, this.callbacks.path.dirname(originalSubfilename)));
}
+ this.callbacks.reportMessage(CopierMessages.Verbose_CopyingFile({from: originalSubfilename, to: subFilename}));
await this.copyGenericFile(project, subFilename, subOutputPath, source, result);
// Even if the subfile is missing, we'll still continue the overall copy
diff --git a/developer/src/kmc-copy/src/copier-messages.ts b/developer/src/kmc-copy/src/copier-messages.ts
index 8afff79149..d4751474b9 100644
--- a/developer/src/kmc-copy/src/copier-messages.ts
+++ b/developer/src/kmc-copy/src/copier-messages.ts
@@ -19,9 +19,10 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
*/
export class CopierMessages {
static FATAL_UnexpectedException = SevFatal | 0x0001;
- static Fatal_UnexpectedException = (o:{e: any}) => m(
+ static Fatal_UnexpectedException = (o:{e: any}) => CompilerMessageSpecWithException(
this.FATAL_UnexpectedException,
- null, o.e ?? 'unknown error'
+ null,
+ o.e ?? 'unknown error'
);
static INFO_CopyingProject = SevInfo | 0x0002;
@@ -123,81 +124,90 @@ export class CopierMessages {
static ERROR_CannotDownloadFolderFromGitHub = SevError | 0x0012;
static Error_CannotDownloadFolderFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m(
this.ERROR_CannotDownloadFolderFromGitHub,
- `The folder '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`,
- `An error was encountered attempting to download a folder from GitHub API. Check the
- provided error details for the cause.`
- );
+ `The folder '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`, `
+ An error was encountered attempting to download a folder from GitHub API. Check
+ the provided error details for the cause.
+ `);
static ERROR_FolderDownloadedFromGitHubIsNotAValidFolder = SevError | 0x0013;
static Error_FolderDownloadedFromGitHubIsNotAValidFolder = (o:{ref: string}) => m(
this.ERROR_FolderDownloadedFromGitHubIsNotAValidFolder,
- `The path '${def(o.ref)}' does not appear to be a folder on GitHub`,
- `The provided path may be a file or may not exist. Check the reference
- before trying again.`
- );
+ `The path '${def(o.ref)}' does not appear to be a folder on GitHub`, `
+ The provided path may be a file or may not exist. Check the reference
+ before trying again.
+ `);
static WARN_CannotDownloadFileFromGitHub = SevWarn | 0x0014;
static Warn_CannotDownloadFileFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m(
this.WARN_CannotDownloadFileFromGitHub,
- `The file '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`,
- `An error was encountered attempting to download a file from GitHub. Check the
- provided error details for the cause.`
- );
+ `The file '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`, `
+ An error was encountered attempting to download a file from GitHub. Check the
+ provided error details for the cause.
+ `);
static ERROR_InvalidCloudKeyboardId = SevError | 0x0015;
static Error_InvalidCloudKeyboardId = (o:{id: string}) => m(
this.ERROR_InvalidCloudKeyboardId,
- `The keyboard identifier '${def(o.id)}' is not a valid keyboard identifier`,
- `Keyboard identifiers on Keyman Cloud can only use the characters a-z, 0-9, and _.`
- );
+ `The keyboard identifier '${def(o.id)}' is not a valid keyboard identifier`, `
+ Keyboard identifiers on Keyman Cloud can only use the characters
+ \`a\`-\`z\`, \`0\`-\`9\`, and \`_\`.
+ `);
static ERROR_CouldNotRetrieveFromCloud = SevError | 0x0016;
static Error_CouldNotRetrieveFromCloud = (o:{id: string, message?: string, cause?: string}) => m(
this.ERROR_CouldNotRetrieveFromCloud,
- `Details for keyboard or model identified by '${def(o.id)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`,
- `An error was encountered attempting to retrieve keyboard or model details from Keyman Cloud API. Check the
- provided error details for the cause.`
- );
+ `Details for keyboard or model identified by '${def(o.id)}' could not be `+
+ `downloaded: ${def(o.message)} ${def(o.cause)}`, `
+ An error was encountered attempting to retrieve keyboard or model details from
+ Keyman Cloud API. Check the provided error details for the cause.
+ `);
static ERROR_KeymanCloudReturnedInvalidData = SevError | 0x0017;
static Error_KeymanCloudReturnedInvalidData = (o:{id: string}) => m(
this.ERROR_KeymanCloudReturnedInvalidData,
- `Keyman Cloud API returned invalid data for keyboard or model identified by '${def(o.id)}'`,
- `There may be a network error or a server error. Retry your request later or contact
- Keyman Support for assistance.`
- );
+ `Keyman Cloud API returned invalid data for keyboard or model identified by '${def(o.id)}'`, `
+ There may be a network error or a server error. Retry your request later or
+ contact Keyman Support for assistance.
+ `);
static ERROR_CloudDoesNotHaveSource = SevError | 0x0018;
static Error_CloudDoesNotHaveSource = (o:{id: string}) => m(
this.ERROR_CloudDoesNotHaveSource,
- `The keyboard or model identified by '${def(o.id)}' does not have source available`,
- `Legacy keyboards in Keyman Cloud do not have source available. Check the Keyman keyboard catalog
- at keyman.com for further details. Some new keyboards or models may be available as binary-only.`
- );
+ `The keyboard or model identified by '${def(o.id)}' does not have source available`, `
+ Legacy keyboards in Keyman Cloud do not have source available. Check the Keyman
+ keyboard catalog at https://keyman.com/keyboards for further details. Some new
+ keyboards or models may be available as binary-only.
+ `);
static ERROR_CannotDownloadRepoFromGitHub = SevError | 0x0019 ;
static Error_CannotDownloadRepoFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m(
this.ERROR_CannotDownloadRepoFromGitHub,
- `The repository at '${def(o.ref)}' could not be accessed: ${def(o.message)} ${def(o.cause)}`,
- `An error was encountered attempting to download details about a repository from GitHub API.
- Check the provided error details for the cause.`
- );
+ `The repository at '${def(o.ref)}' could not be accessed: ${def(o.message)} ${def(o.cause)}`, `
+ An error was encountered attempting to download details about a repository
+ from GitHub API. Check the provided error details for the cause.
+ `);
+
+ //------------------------------------------------------------------------------|
+ // max length of detail message lines (checked by verifyCompilerMessagesObject) |
+ //------------------------------------------------------------------------------|
static ERROR_CouldNotFindDefaultBranchOnGitHub = SevError | 0x001A;
static Error_CouldNotFindDefaultBranchOnGitHub = (o:{ref: string}) => m(
this.ERROR_CouldNotFindDefaultBranchOnGitHub,
- `The default branch could not be found for the GitHub repository '${def(o.ref)}'`,
- `The repository may be private, or you may have a typo in the owner or repository name.`
- );
+ `The default branch could not be found for the GitHub repository '${def(o.ref)}'`, `
+ The repository may be private, or you may have a typo in the owner or
+ repository name.
+ `);
static INFO_CannotDownloadBinaryFileFromGitHub = SevInfo | 0x001B;
static Info_CannotDownloadBinaryFileFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m(
this.INFO_CannotDownloadBinaryFileFromGitHub,
- `The Keyman binary file '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}. This is not normally a problem`,
- `In most repositories, Keyman binary files such as .kmx, .kmp, .js are not included.
- This is not normally a problem, as the files can be built from the source. Check
- the provided error details for more details.`
- );
+ `The Keyman binary file '${def(o.ref)}' could not be downloaded: ${def(o.message)} `+
+ `${def(o.cause)}. This is not normally a problem`, `
+ In most repositories, Keyman binary files such as .kmx, .kmp, .js are not
+ included. This is not normally a problem, as the files can be built from the
+ source. Check the provided error details for more details.
+ `);
static VERBOSE_DownloadingFile = SevVerbose | 0x001C;
static Verbose_DownloadingFile = (o:{filename: string, url: string}) => m(
@@ -231,4 +241,10 @@ export class CopierMessages {
`The output file '${def(o.filename)}' has two different possible source files.`,
);
+ static VERBOSE_CopyingFile = SevVerbose | 0x0021;
+ static Verbose_CopyingFile = (o:{from: string, to: string}) => m(
+ this.VERBOSE_CopyingFile,
+ `Copying file '${def(o.from)}' to '${def(o.to)}'`,
+ );
+
};
diff --git a/developer/src/kmc-copy/test/copier.tests.ts b/developer/src/kmc-copy/test/copier.tests.ts
index 6a2a8cbb1e..697e58e0b5 100644
--- a/developer/src/kmc-copy/test/copier.tests.ts
+++ b/developer/src/kmc-copy/test/copier.tests.ts
@@ -13,6 +13,7 @@ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import { KeymanProjectCopier } from '../src/KeymanProjectCopier.js';
import { makePathToFixture } from './helpers/index.js';
import { GitHubRef } from './cloud.js';
+import { KpsFileReader } from '@keymanapp/developer-utils';
const { TEST_SAVE_ARTIFACTS, TEST_SAVE_FIXTURES } = env;
let outputRoot: string = '/an/imaginary/root/';
@@ -269,8 +270,8 @@ describe('KeymanProjectCopier', function() {
"/release/j/jawa/source/jawa2.bmp": "source/jawa2.bmp",
"/release/j/jawa/source/readme.htm": "source/readme.htm",
"/release/j/jawa/source/welcome/welcome.htm": "source/welcome/welcome.htm",
- "/release/shared/fonts/noto/Java/NotoSansJavanese-Bold.ttf": "external/release/shared/fonts/noto/Java/NotoSansJavanese-Bold.ttf",
- "/release/shared/fonts/noto/Java/NotoSansJavanese-Regular.ttf": "external/release/shared/fonts/noto/Java/NotoSansJavanese-Regular.ttf",
+ "/release/shared/fonts/noto/Java/NotoSansJavanese-Bold.ttf": "external/shared/fonts/noto/Java/NotoSansJavanese-Bold.ttf",
+ "/release/shared/fonts/noto/Java/NotoSansJavanese-Regular.ttf": "external/shared/fonts/noto/Java/NotoSansJavanese-Regular.ttf",
// NOTE: the following files are not included because this is a 1.0
// project and they are not referenced by any files:
// * HISTORY.md
@@ -523,6 +524,103 @@ describe('KeymanProjectCopier', function() {
]);
});
+ it('should copy external files into an "external" folder when relocateExternalFiles is true', async function() {
+ const copier = new KeymanProjectCopier();
+ assert.isTrue(await copier.init(callbacks, {
+ dryRun: false,
+ outPath: 'organized',
+ relocateExternalFiles: true,
+ }));
+
+ const result = await copier.run(makePathToFixture('projects','external','alephwithbeth','alephwithbeth.kpj'));
+ assert.sameMembers(Object.keys(result.artifacts).map(k => result.artifacts[k].filename), [
+ 'organized', // kmc-copy:outputPath
+ 'organized/source/organized.keyman-touch-layout',
+ 'organized/source/organized.kmn',
+ 'organized/source/organized.ico',
+ 'organized/source/organized.kvks',
+ 'organized/source/organized.kps',
+ 'organized/source/readme.htm',
+ // files in 'another-folder/' get moved to 'external/'
+ 'organized/external/another-folder/welcome/welcome.htm',
+ 'organized/external/another-folder/welcome/KeyboardLayout.png',
+ 'organized/external/another-folder/welcome/KeyboardLayoutAlt.png',
+ 'organized/external/another-folder/welcome/KeyboardLayoutShift.png',
+ // build artifact files get moved to build/
+ 'organized/build/organized.js',
+ 'organized/build/organized.kvk',
+ 'organized/build/organized.kmx',
+ // 'organized/build/organized.kmp', // TODO-COPY: do we need to copy this file as well?
+ 'organized/LICENSE.md',
+ 'organized/organized.kpj',
+ 'organized/HISTORY.md',
+ 'organized/README.md',
+ 'organized/source/help/organized.php',
+ 'organized/source/help/KeyboardLayout.png',
+ 'organized/source/help/KeyboardLayoutAlt.png',
+ 'organized/source/help/KeyboardLayoutShift.png',
+ ]);
+
+ // Verify that .kps has the appropriate paths
+ const reader = new KpsFileReader(callbacks);
+ const kpsFilename = Object.keys(result.artifacts).find(k => result.artifacts[k].filename == 'organized/source/organized.kps')!;
+ const kps = reader.read(result.artifacts[kpsFilename].data);
+ assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../external/another-folder/welcome/welcome.htm'));
+ assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../external/another-folder/welcome/KeyboardLayout.png'));
+ assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../external/another-folder/welcome/KeyboardLayoutAlt.png'));
+ assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../external/another-folder/welcome/KeyboardLayoutShift.png'));
+ });
+
+ it('should leave external files in their original location when relocateExternalFiles is false', async function() {
+ const copier = new KeymanProjectCopier();
+ assert.isTrue(await copier.init(callbacks, {
+ dryRun: false,
+ outPath: 'organized',
+ relocateExternalFiles: false,
+ }));
+
+ const result = await copier.run(makePathToFixture('projects','external','alephwithbeth','alephwithbeth.kpj'));
+ assert.sameMembers(Object.keys(result.artifacts).map(k => result.artifacts[k].filename), [
+ 'organized', // kmc-copy:outputPath
+ 'organized/source/organized.keyman-touch-layout',
+ 'organized/source/organized.kmn',
+ 'organized/source/organized.ico',
+ 'organized/source/organized.kvks',
+ 'organized/source/organized.kps',
+ 'organized/source/readme.htm',
+
+ // files in 'another-folder/' stay there, so they are not referenced
+ // 'organized/external/welcome.htm',
+ // 'organized/external/KeyboardLayout.png',
+ // 'organized/external/KeyboardLayoutAlt.png',
+ // 'organized/external/KeyboardLayoutShift.png',
+
+ // build artifact files get moved to build/
+ 'organized/build/organized.js',
+ 'organized/build/organized.kvk',
+ 'organized/build/organized.kmx',
+ // 'organized/build/organized.kmp', // TODO-COPY: do we need to copy this file as well?
+ 'organized/LICENSE.md',
+ 'organized/organized.kpj',
+ 'organized/HISTORY.md',
+ 'organized/README.md',
+ 'organized/source/help/organized.php',
+ 'organized/source/help/KeyboardLayout.png',
+ 'organized/source/help/KeyboardLayoutAlt.png',
+ 'organized/source/help/KeyboardLayoutShift.png',
+ ]);
+
+ // we'll check the .kps for its references, these are relative to the test/organized/source? folder
+ const reader = new KpsFileReader(callbacks);
+ const kpsFilename = Object.keys(result.artifacts).find(k => result.artifacts[k].filename == 'organized/source/organized.kps')!;
+ const kps = reader.read(result.artifacts[kpsFilename].data);
+
+ assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../../../test/fixtures/projects/external/another-folder/welcome/welcome.htm'));
+ assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../../../test/fixtures/projects/external/another-folder/welcome/KeyboardLayout.png'));
+ assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../../../test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutAlt.png'));
+ assert.isDefined(kps.Package.Files?.File.find(file => file.Name == '../../../test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutShift.png'));
+ });
+
// TODO-COPY: additional tests
it.skip('should copy a standalone .kmn into a new project', async function() {});
it.skip('should copy a standalone .kmn and .kps into a new project', async function() {});
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/HISTORY.md b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/HISTORY.md
new file mode 100644
index 0000000000..65dd7e13c7
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/HISTORY.md
@@ -0,0 +1,35 @@
+alephwithbeth Change History
+====================
+
+1.0.6 (2024-06-11)
+------------------
+* Remove source files from package
+
+1.0.5 (2023-02-28)
+------------------
+* replaced hyphen with maqef
+* added a touch-optimized keyboard layout for phone
+
+1.0.4 (2023-02-13)
+------------------
+* Removed unnecessary version numbers.
+* added description
+
+1.0.3 (2023-02-11)
+------------------
+* Removed the faulty auto correct feature.
+* Captialized all words of the title.
+* Added title to .kmn
+
+1.0.2 (2023-02-09)
+------------------
+* Renamed keyboard to "Aleph with Beth"
+
+1.0.1 (2023-02-08)
+----------------
+* Updated author and website (in kps)
+* Added copyrights where needed.
+
+1.0 (2023-01-28)
+----------------
+* Created by Cameron Briggs
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/LICENSE.md b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/LICENSE.md
new file mode 100644
index 0000000000..7f84a306c0
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright © 2023-2024 Cameron Briggs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/README.md b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/README.md
new file mode 100644
index 0000000000..16edd8db20
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/README.md
@@ -0,0 +1,25 @@
+alephwithbeth keyboard
+==============
+
+Description
+-----------
+A Hebrew keyboard designed to intuitively match the standard QWERTY layout.
+
+Copyright
+---------
+See [LICENSE.md](LICENSE.md)
+
+Supported Platforms
+-------------------
+ * Windows
+ * macOS
+ * Linux
+ * Web
+ * iPhone
+ * iPad
+ * Android phone
+ * Android tablet
+ * Mobile devices
+ * Desktop devices
+ * Tablet devices
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/alephwithbeth.kpj b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/alephwithbeth.kpj
new file mode 100644
index 0000000000..e3309add20
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/alephwithbeth.kpj
@@ -0,0 +1,9 @@
+
+
+
+ 2.0
+ $PROJECTPATH\build
+ True
+ True
+
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.js b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.js
new file mode 100644
index 0000000000..a17168b28d
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.js
@@ -0,0 +1 @@
+KeymanWeb.KR(new Keyboard_alephwithbeth());function Keyboard_alephwithbeth(){this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9;this.KI="Keyboard_alephwithbeth";this.KN="Aleph With Beth";this.KMINVER="14.0";this.KV={F:' 1em "Arial"',K102:0};this.KV.KLS={"shift": ["","!","֘","֨","֜","֞","֔","֬","֝","(",")","_","+","","","","קּ","וּ","ֵ","רּ","תּ","יּ","ֻ","","וֹ","פּ","{","}","|","","","","ָ","סּ","דּ","טּ","גּ","הּ","וַיּ","כּ","לּ","״","\"","","","","","","","זּ","צּ","שּׂ","שּׁ","בּ","נּ","מּ","","ּ","?","","","","","",""],"default": ["֑","1","2","3","4","5","6","7","8","9","0","־","=","","","","ק","ו","ֶ","ר","ת","י","ע","ִ","ֹ","פ","[","]","\\","","","","ַ","ס","ד","ט","ג","ה","ח","כ","ל","ְ","'","","","","","","","ז","צ","שׂ","שׁ","ב","נ","מ","א",".","/","","","","","",""],"alt": ["","֗","֮","֙","֠","֩","֟","֡","֕","֓","֯","ֿ","◌","","","","","","ֱ","","","","","ֽ","ֳ","ף","֔","֒","֫","","","","ֲ","","","","•","̊","","ך","","׃","ׄ","","","","","","","","ץ","","","","ן","ם","֫","̈","״","","","","","",""]};this.KV.BK=(function(x){var e=Array.apply(null,Array(65)).map(String.prototype.valueOf,""),r=[],v,i,m=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt'];for(i=m.length-1;i>=0;i--)if((v=x[m[i]])||r.length)r=(v?v:e).slice().concat(r);return r})(this.KV.KLS);this.KDU=0;this.KH="Aleph With Beth";this.KM=0;this.KBVER="1.0.6";this.KMBM=0x70;this.KRTL=1;this.KVKL={"tablet":{"displayUnderlying":false,"layer":[{"id":"default","row":[{"id":"1","key":[{"id":"K_1","text":"1"},{"id":"K_2","text":"2"},{"id":"K_3","text":"3"},{"id":"K_4","text":"4"},{"id":"K_5","text":"5"},{"id":"K_6","text":"6"},{"id":"K_7","text":"7"},{"id":"K_8","text":"8"},{"id":"K_9","text":"9"},{"id":"K_0","text":"0"},{"id":"K_HYPHEN","text":"־"},{"id":"K_EQUAL","text":"="},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"ק","pad":"75"},{"id":"K_W","text":"ו"},{"id":"K_E","text":"ֶ"},{"id":"K_R","text":"ר"},{"id":"K_T","text":"ת"},{"id":"K_Y","text":"י"},{"id":"K_U","text":"ע"},{"id":"K_I","text":"ִ"},{"id":"K_O","text":"ֹ"},{"id":"K_P","text":"פ"},{"id":"K_LBRKT","text":"["},{"id":"K_RBRKT","text":"]"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE","text":"֑"},{"id":"K_A","text":"ַ"},{"id":"K_S","text":"ס"},{"id":"K_D","text":"ד"},{"id":"K_F","text":"ט"},{"id":"K_G","text":"ג"},{"id":"K_H","text":"ה"},{"id":"K_J","text":"ח"},{"id":"K_K","text":"כ"},{"id":"K_L","text":"ל"},{"id":"K_COLON","text":"ְ"},{"id":"K_QUOTE","text":"'"},{"id":"K_BKSLASH","text":"\\"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift"},{"id":"K_oE2"},{"id":"K_Z","text":"ז"},{"id":"K_X","text":"צ"},{"id":"K_C","text":"שׂ"},{"id":"K_V","text":"שׁ"},{"id":"K_B","text":"ב"},{"id":"K_N","text":"נ"},{"id":"K_M","text":"מ"},{"id":"K_COMMA","text":"א"},{"id":"K_PERIOD","text":"."},{"id":"K_SLASH","text":"/"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"shift","row":[{"id":"1","key":[{"id":"K_1","text":"!"},{"id":"K_2","text":"֘"},{"id":"K_3","text":"֨"},{"id":"K_4","text":"֜"},{"id":"K_5","text":"֞"},{"id":"K_6","text":"֔"},{"id":"K_7","text":"֬"},{"id":"K_8","text":"֝"},{"id":"K_9","text":"("},{"id":"K_0","text":")"},{"id":"K_HYPHEN","text":"_"},{"id":"K_EQUAL","text":"+"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"קּ","pad":"75"},{"id":"K_W","text":"וּ"},{"id":"K_E","text":"ֵ"},{"id":"K_R","text":"רּ"},{"id":"K_T","text":"תּ"},{"id":"K_Y","text":"יּ"},{"id":"K_U","text":"ֻ"},{"id":"K_I"},{"id":"K_O","text":"וֹ"},{"id":"K_P","text":"פּ"},{"id":"K_LBRKT","text":"{"},{"id":"K_RBRKT","text":"}"},{"id":"T_new_362","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ָ"},{"id":"K_S","text":"סּ"},{"id":"K_D","text":"דּ"},{"id":"K_F","text":"טּ"},{"id":"K_G","text":"גּ"},{"id":"K_H","text":"הּ"},{"id":"K_J","text":"וַיּ"},{"id":"K_K","text":"כּ"},{"id":"K_L","text":"לּ"},{"id":"K_COLON","text":"״"},{"id":"K_QUOTE","text":"\""},{"id":"K_BKSLASH","text":"|"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"default"},{"id":"K_oE2"},{"id":"K_Z","text":"זּ"},{"id":"K_X","text":"צּ"},{"id":"K_C","text":"שּׂ"},{"id":"K_V","text":"שּׁ"},{"id":"K_B","text":"בּ"},{"id":"K_N","text":"נּ"},{"id":"K_M","text":"מּ"},{"id":"K_COMMA"},{"id":"K_PERIOD","text":"ּ"},{"id":"K_SLASH","text":"?"},{"id":"T_new_388","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"alt","row":[{"id":"1","key":[{"id":"K_1","text":"֗"},{"id":"K_2","text":"֮"},{"id":"K_3","text":"֙"},{"id":"K_4","text":"֠"},{"id":"K_5","text":"֩"},{"id":"K_6","text":"֟"},{"id":"K_7","text":"֡"},{"id":"K_8","text":"֕"},{"id":"K_9","text":"֓"},{"id":"K_0","text":"֯"},{"id":"K_HYPHEN","text":"ֿ"},{"id":"K_EQUAL","text":"◌"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","pad":"75"},{"id":"K_W"},{"id":"K_E","text":"ֱ"},{"id":"K_R"},{"id":"K_T"},{"id":"K_Y"},{"id":"K_U"},{"id":"K_I","text":"ֽ"},{"id":"K_O","text":"ֳ"},{"id":"K_P","text":"ף"},{"id":"K_LBRKT","text":"֔"},{"id":"K_RBRKT","text":"֒"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ֲ"},{"id":"K_S"},{"id":"K_D"},{"id":"K_F"},{"id":"K_G","text":"•"},{"id":"K_H","text":"̊"},{"id":"K_J"},{"id":"K_K","text":"ך"},{"id":"K_L"},{"id":"K_COLON","text":"׃"},{"id":"K_QUOTE","text":"ׄ"},{"id":"K_BKSLASH","text":"֫"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift","layer":"shift"},{"id":"K_oE2"},{"id":"K_Z"},{"id":"K_X","text":"ץ"},{"id":"K_C"},{"id":"K_V"},{"id":"K_B"},{"id":"K_N","text":"ן"},{"id":"K_M","text":"ם"},{"id":"K_COMMA","text":"֫"},{"id":"K_PERIOD","text":"̈"},{"id":"K_SLASH","text":"״"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"default","width":"130","sp":"1","nextlayer":"default"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]}]},"phone":{"displayUnderlying":false,"layer":[{"id":"default","row":[{"id":"1","key":[{"id":"K_1","text":"1"},{"id":"K_2","text":"2"},{"id":"K_3","text":"3"},{"id":"K_4","text":"4"},{"id":"K_5","text":"5"},{"id":"K_6","text":"6"},{"id":"K_7","text":"7"},{"id":"K_8","text":"8"},{"id":"K_9","text":"9"},{"id":"K_0","text":"0"},{"id":"K_HYPHEN","text":"־"},{"id":"K_EQUAL","text":"="},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"ק","pad":"75"},{"id":"K_W","text":"ו"},{"id":"K_E","text":"ֶ"},{"id":"K_R","text":"ר"},{"id":"K_T","text":"ת"},{"id":"K_Y","text":"י"},{"id":"K_U","text":"ע"},{"id":"K_I","text":"ִ"},{"id":"K_O","text":"ֹ"},{"id":"K_P","text":"פ"},{"id":"K_LBRKT","text":"["},{"id":"K_RBRKT","text":"]"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE","text":"֑"},{"id":"K_A","text":"ַ"},{"id":"K_S","text":"ס"},{"id":"K_D","text":"ד"},{"id":"K_F","text":"ט"},{"id":"K_G","text":"ג"},{"id":"K_H","text":"ה"},{"id":"K_J","text":"ח"},{"id":"K_K","text":"כ"},{"id":"K_L","text":"ל"},{"id":"K_COLON","text":"ְ"},{"id":"K_QUOTE","text":"'"},{"id":"K_BKSLASH","text":"\\"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift"},{"id":"K_oE2"},{"id":"K_Z","text":"ז"},{"id":"K_X","text":"צ"},{"id":"K_C","text":"שׂ"},{"id":"K_V","text":"שׁ"},{"id":"K_B","text":"ב"},{"id":"K_N","text":"נ"},{"id":"K_M","text":"מ"},{"id":"K_COMMA","text":"א"},{"id":"K_PERIOD","text":"."},{"id":"K_SLASH","text":"/"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"shift","row":[{"id":"1","key":[{"id":"K_1","text":"!"},{"id":"K_2","text":"֘"},{"id":"K_3","text":"֨"},{"id":"K_4","text":"֜"},{"id":"K_5","text":"֞"},{"id":"K_6","text":"֔"},{"id":"K_7","text":"֬"},{"id":"K_8","text":"֝"},{"id":"K_9","text":"("},{"id":"K_0","text":")"},{"id":"K_HYPHEN","text":"_"},{"id":"K_EQUAL","text":"+"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"קּ","pad":"75"},{"id":"K_W","text":"וּ"},{"id":"K_E","text":"ֵ"},{"id":"K_R","text":"רּ"},{"id":"K_T","text":"תּ"},{"id":"K_Y","text":"יּ"},{"id":"K_U","text":"ֻ"},{"id":"K_I"},{"id":"K_O","text":"וֹ"},{"id":"K_P","text":"פּ"},{"id":"K_LBRKT","text":"{"},{"id":"K_RBRKT","text":"}"},{"id":"T_new_362","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ָ"},{"id":"K_S","text":"סּ"},{"id":"K_D","text":"דּ"},{"id":"K_F","text":"טּ"},{"id":"K_G","text":"גּ"},{"id":"K_H","text":"הּ"},{"id":"K_J","text":"וַיּ"},{"id":"K_K","text":"כּ"},{"id":"K_L","text":"לּ"},{"id":"K_COLON","text":"״"},{"id":"K_QUOTE","text":"\""},{"id":"K_BKSLASH","text":"|"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"default"},{"id":"K_oE2"},{"id":"K_Z","text":"זּ"},{"id":"K_X","text":"צּ"},{"id":"K_C","text":"שּׂ"},{"id":"K_V","text":"שּׁ"},{"id":"K_B","text":"בּ"},{"id":"K_N","text":"נּ"},{"id":"K_M","text":"מּ"},{"id":"K_COMMA"},{"id":"K_PERIOD","text":"ּ"},{"id":"K_SLASH","text":"?"},{"id":"T_new_388","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"alt","row":[{"id":"1","key":[{"id":"K_1","text":"֗"},{"id":"K_2","text":"֮"},{"id":"K_3","text":"֙"},{"id":"K_4","text":"֠"},{"id":"K_5","text":"֩"},{"id":"K_6","text":"֟"},{"id":"K_7","text":"֡"},{"id":"K_8","text":"֕"},{"id":"K_9","text":"֓"},{"id":"K_0","text":"֯"},{"id":"K_HYPHEN","text":"ֿ"},{"id":"K_EQUAL","text":"◌"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","pad":"75"},{"id":"K_W"},{"id":"K_E","text":"ֱ"},{"id":"K_R"},{"id":"K_T"},{"id":"K_Y"},{"id":"K_U"},{"id":"K_I","text":"ֽ"},{"id":"K_O","text":"ֳ"},{"id":"K_P","text":"ף"},{"id":"K_LBRKT","text":"֔"},{"id":"K_RBRKT","text":"֒"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ֲ"},{"id":"K_S"},{"id":"K_D"},{"id":"K_F"},{"id":"K_G","text":"•"},{"id":"K_H","text":"̊"},{"id":"K_J"},{"id":"K_K","text":"ך"},{"id":"K_L"},{"id":"K_COLON","text":"׃"},{"id":"K_QUOTE","text":"ׄ"},{"id":"K_BKSLASH","text":"֫"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift","layer":"shift"},{"id":"K_oE2"},{"id":"K_Z"},{"id":"K_X","text":"ץ"},{"id":"K_C"},{"id":"K_V"},{"id":"K_B"},{"id":"K_N","text":"ן"},{"id":"K_M","text":"ם"},{"id":"K_COMMA","text":"֫"},{"id":"K_PERIOD","text":"̈"},{"id":"K_SLASH","text":"״"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"default","width":"130","sp":"1","nextlayer":"default"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]}]}};this.s7="כמנפצ";this.s8="ךםןףץ";this.s9="כּפּ";this.s10="ךּףּ";this.s11="ּ";this.s12="מּנּצּ";this.s13="םןץ";this.s14="ֽ֑֖֛֢֣֤֥֦֧֪֚֭֮֒֓֔֕֗֘֙֜֝֞֟֠֡֨֩֫֬֯־ֿ׀ׄ";this.s15="...";this.KVER="18.0.175.0";this.KVS=[];this.gs=function(t,e) {return this.g0(t,e);};this.gs=function(t,e) {return this.g0(t,e);};this.g0=function(t,e) {var k=KeymanWeb,r=0,m=0;if(k.KKM(e,16384,9)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}}else if(k.KKM(e,16384,13)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}}else if(k.KKM(e,16384,32)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}else if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16432,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16400,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16448,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16400,49)) {if(1){r=m=1;k.KO(0,t,"!");}}else if(k.KKM(e,16400,222)) {if(1){r=m=1;k.KO(0,t,"\"");}}else if(k.KKM(e,16400,51)) {if(1){r=m=1;k.KO(0,t,"֨");}}else if(k.KKM(e,16400,52)) {if(1){r=m=1;k.KO(0,t,"֜");}}else if(k.KKM(e,16400,53)) {if(1){r=m=1;k.KO(0,t,"֞");}}else if(k.KKM(e,16400,55)) {if(1){r=m=1;k.KO(0,t,"֬");}}else if(k.KKM(e,16384,222)) {if(1){r=m=1;k.KO(0,t,"'");}}else if(k.KKM(e,16448,222)) {if(1){r=m=1;k.KO(0,t,"ׄ");}}else if(k.KKM(e,16400,57)) {if(1){r=m=1;k.KO(0,t,"(");}}else if(k.KKM(e,16400,48)) {if(1){r=m=1;k.KO(0,t,")");}}else if(k.KKM(e,16400,56)) {if(1){r=m=1;k.KO(0,t,"֝");}}else if(k.KKM(e,16400,187)) {if(1){r=m=1;k.KO(0,t,"+");}}else if(k.KKM(e,16448,188)) {if(1){r=m=1;k.KO(0,t,"֫");}}else if(k.KKM(e,16384,188)) {if(1){r=m=1;k.KO(0,t,"א");}}else if(k.KKM(e,16384,189)) {if(1){r=m=1;k.KO(0,t,"־");}}else if(k.KKM(e,16448,189)) {if(1){r=m=1;k.KO(0,t,"ֿ");}}else if(k.KKM(e,16384,190)) {if(1){r=m=1;k.KO(0,t,".");}}else if(k.KKM(e,16448,190)) {if(1){r=m=1;k.KO(0,t,"̈");}}else if(k.KKM(e,16384,191)) {if(1){r=m=1;k.KO(0,t,"/");}}else if(k.KKM(e,16448,191)) {if(1){r=m=1;k.KO(0,t,"״");}}else if(k.KKM(e,16384,48)) {if(1){r=m=1;k.KO(0,t,"0");}}else if(k.KKM(e,16448,48)) {if(1){r=m=1;k.KO(0,t,"֯");}}else if(k.KKM(e,16384,49)) {if(1){r=m=1;k.KO(0,t,"1");}}else if(k.KKM(e,16448,49)) {if(1){r=m=1;k.KO(0,t,"֗");}}else if(k.KKM(e,16384,50)) {if(1){r=m=1;k.KO(0,t,"2");}}else if(k.KKM(e,16448,50)) {if(1){r=m=1;k.KO(0,t,"֮");}}else if(k.KKM(e,16384,51)) {if(1){r=m=1;k.KO(0,t,"3");}}else if(k.KKM(e,16448,51)) {if(1){r=m=1;k.KO(0,t,"֙");}}else if(k.KKM(e,16384,52)) {if(1){r=m=1;k.KO(0,t,"4");}}else if(k.KKM(e,16448,52)) {if(1){r=m=1;k.KO(0,t,"֠");}}else if(k.KKM(e,16384,53)) {if(1){r=m=1;k.KO(0,t,"5");}}else if(k.KKM(e,16448,53)) {if(1){r=m=1;k.KO(0,t,"֩");}}else if(k.KKM(e,16384,54)) {if(1){r=m=1;k.KO(0,t,"6");}}else if(k.KKM(e,16448,54)) {if(1){r=m=1;k.KO(0,t,"֟");}}else if(k.KKM(e,16384,55)) {if(1){r=m=1;k.KO(0,t,"7");}}else if(k.KKM(e,16448,55)) {if(1){r=m=1;k.KO(0,t,"֡");}}else if(k.KKM(e,16384,56)) {if(1){r=m=1;k.KO(0,t,"8");}}else if(k.KKM(e,16448,56)) {if(1){r=m=1;k.KO(0,t,"֕");}}else if(k.KKM(e,16384,57)) {if(1){r=m=1;k.KO(0,t,"9");}}else if(k.KKM(e,16448,57)) {if(1){r=m=1;k.KO(0,t,"֓");}}else if(k.KKM(e,16400,186)) {if(1){r=m=1;k.KO(0,t,"״");}}else if(k.KKM(e,16448,186)) {if(1){r=m=1;k.KO(0,t,"׃");}}else if(k.KKM(e,16384,186)) {if(1){r=m=1;k.KO(0,t,"ְ");}}else if(k.KKM(e,16400,188)) {if(1){r=m=1;}}else if(k.KKM(e,16384,187)) {if(1){r=m=1;k.KO(0,t,"=");}}else if(k.KKM(e,16448,187)) {if(1){r=m=1;k.KO(0,t,"◌");}}else if(k.KKM(e,16400,190)) {if(1){r=m=1;k.KO(0,t,"ּ");}}else if(k.KKM(e,16400,191)) {if(1){r=m=1;k.KO(0,t,"?");}}else if(k.KKM(e,16400,50)) {if(1){r=m=1;k.KO(0,t,"֘");}}else if(k.KKM(e,16400,65)) {if(1){r=m=1;k.KO(0,t,"ָ");}}else if(k.KKM(e,16400,66)) {if(1){r=m=1;k.KO(0,t,"בּ");}}else if(k.KKM(e,16400,67)) {if(1){r=m=1;k.KO(0,t,"שּׂ");}}else if(k.KKM(e,16400,68)) {if(1){r=m=1;k.KO(0,t,"דּ");}}else if(k.KKM(e,16400,69)) {if(1){r=m=1;k.KO(0,t,"ֵ");}}else if(k.KKM(e,16400,70)) {if(1){r=m=1;k.KO(0,t,"טּ");}}else if(k.KKM(e,16400,71)) {if(1){r=m=1;k.KO(0,t,"גּ");}}else if(k.KKM(e,16400,72)) {if(1){r=m=1;k.KO(0,t,"הּ");}}else if(k.KKM(e,16400,73)) {if(1){r=m=1;}}else if(k.KKM(e,16400,74)) {if(1){r=m=1;k.KO(0,t,"וַיּ");}}else if(k.KKM(e,16400,75)) {if(1){r=m=1;k.KO(0,t,"כּ");}}else if(k.KKM(e,16400,76)) {if(1){r=m=1;k.KO(0,t,"לּ");}}else if(k.KKM(e,16400,77)) {if(1){r=m=1;k.KO(0,t,"מּ");}}else if(k.KKM(e,16400,78)) {if(1){r=m=1;k.KO(0,t,"נּ");}}else if(k.KKM(e,16400,79)) {if(1){r=m=1;k.KO(0,t,"וֹ");}}else if(k.KKM(e,16400,80)) {if(1){r=m=1;k.KO(0,t,"פּ");}}else if(k.KKM(e,16400,81)) {if(1){r=m=1;k.KO(0,t,"קּ");}}else if(k.KKM(e,16400,82)) {if(1){r=m=1;k.KO(0,t,"רּ");}}else if(k.KKM(e,16400,83)) {if(1){r=m=1;k.KO(0,t,"סּ");}}else if(k.KKM(e,16400,84)) {if(1){r=m=1;k.KO(0,t,"תּ");}}else if(k.KKM(e,16400,85)) {if(1){r=m=1;k.KO(0,t,"ֻ");}}else if(k.KKM(e,16400,86)) {if(1){r=m=1;k.KO(0,t,"שּׁ");}}else if(k.KKM(e,16400,87)) {if(1){r=m=1;k.KO(0,t,"וּ");}}else if(k.KKM(e,16400,88)) {if(1){r=m=1;k.KO(0,t,"צּ");}}else if(k.KKM(e,16400,89)) {if(1){r=m=1;k.KO(0,t,"יּ");}}else if(k.KKM(e,16400,90)) {if(1){r=m=1;k.KO(0,t,"זּ");}}else if(k.KKM(e,16384,219)) {if(1){r=m=1;k.KO(0,t,"[");}}else if(k.KKM(e,16448,219)) {if(1){r=m=1;k.KO(0,t,"֔");}}else if(k.KKM(e,16384,220)) {if(1){r=m=1;k.KO(0,t,"\\");}}else if(k.KKM(e,16448,220)) {if(1){r=m=1;k.KO(0,t,"֫");}}else if(k.KKM(e,16384,221)) {if(1){r=m=1;k.KO(0,t,"]");}}else if(k.KKM(e,16448,221)) {if(1){r=m=1;k.KO(0,t,"֒");}}else if(k.KKM(e,16400,54)) {if(1){r=m=1;k.KO(0,t,"֔");}}else if(k.KKM(e,16400,189)) {if(1){r=m=1;k.KO(0,t,"_");}}else if(k.KKM(e,16448,192)) {if(1){r=m=1;}}else if(k.KKM(e,16384,192)) {if(1){r=m=1;k.KO(0,t,"֑");}}else if(k.KKM(e,16448,65)) {if(1){r=m=1;k.KO(0,t,"ֲ");}}else if(k.KKM(e,16384,65)) {if(1){r=m=1;k.KO(0,t,"ַ");}}else if(k.KKM(e,16448,66)) {if(1){r=m=1;}}else if(k.KKM(e,16384,66)) {if(1){r=m=1;k.KO(0,t,"ב");}}else if(k.KKM(e,16448,67)) {if(1){r=m=1;}}else if(k.KKM(e,16384,67)) {if(1){r=m=1;k.KO(0,t,"שׂ");}}else if(k.KKM(e,16448,68)) {if(1){r=m=1;}}else if(k.KKM(e,16384,68)) {if(1){r=m=1;k.KO(0,t,"ד");}}else if(k.KKM(e,16448,69)) {if(1){r=m=1;k.KO(0,t,"ֱ");}}if(m) {}else if(k.KKM(e,16384,69)) {if(1){r=m=1;k.KO(0,t,"ֶ");}}else if(k.KKM(e,16448,70)) {if(1){r=m=1;}}else if(k.KKM(e,16384,70)) {if(1){r=m=1;k.KO(0,t,"ט");}}else if(k.KKM(e,16448,71)) {if(1){r=m=1;k.KO(0,t,"•");}}else if(k.KKM(e,16384,71)) {if(1){r=m=1;k.KO(0,t,"ג");}}else if(k.KKM(e,16448,72)) {if(1){r=m=1;k.KO(0,t,"̊");}}else if(k.KKM(e,16384,72)) {if(1){r=m=1;k.KO(0,t,"ה");}}else if(k.KKM(e,16448,73)) {if(1){r=m=1;k.KO(0,t,"ֽ");}}else if(k.KKM(e,16384,73)) {if(1){r=m=1;k.KO(0,t,"ִ");}}else if(k.KKM(e,16448,74)) {if(1){r=m=1;}}else if(k.KKM(e,16384,74)) {if(1){r=m=1;k.KO(0,t,"ח");}}else if(k.KKM(e,16448,75)) {if(1){r=m=1;k.KO(0,t,"ך");}}else if(k.KKM(e,16384,75)) {if(1){r=m=1;k.KO(0,t,"כ");}}else if(k.KKM(e,16448,76)) {if(1){r=m=1;}}else if(k.KKM(e,16384,76)) {if(1){r=m=1;k.KO(0,t,"ל");}}else if(k.KKM(e,16448,77)) {if(1){r=m=1;k.KO(0,t,"ם");}}else if(k.KKM(e,16384,77)) {if(1){r=m=1;k.KO(0,t,"מ");}}else if(k.KKM(e,16448,78)) {if(1){r=m=1;k.KO(0,t,"ן");}}else if(k.KKM(e,16384,78)) {if(1){r=m=1;k.KO(0,t,"נ");}}else if(k.KKM(e,16448,79)) {if(1){r=m=1;k.KO(0,t,"ֳ");}}else if(k.KKM(e,16384,79)) {if(1){r=m=1;k.KO(0,t,"ֹ");}}else if(k.KKM(e,16448,80)) {if(1){r=m=1;k.KO(0,t,"ף");}}else if(k.KKM(e,16384,80)) {if(1){r=m=1;k.KO(0,t,"פ");}}else if(k.KKM(e,16448,81)) {if(1){r=m=1;}}else if(k.KKM(e,16384,81)) {if(1){r=m=1;k.KO(0,t,"ק");}}else if(k.KKM(e,16448,82)) {if(1){r=m=1;}}else if(k.KKM(e,16384,82)) {if(1){r=m=1;k.KO(0,t,"ר");}}else if(k.KKM(e,16448,83)) {if(1){r=m=1;}}else if(k.KKM(e,16384,83)) {if(1){r=m=1;k.KO(0,t,"ס");}}else if(k.KKM(e,16448,84)) {if(1){r=m=1;}}else if(k.KKM(e,16384,84)) {if(1){r=m=1;k.KO(0,t,"ת");}}else if(k.KKM(e,16448,85)) {if(1){r=m=1;}}else if(k.KKM(e,16384,85)) {if(1){r=m=1;k.KO(0,t,"ע");}}else if(k.KKM(e,16448,86)) {if(1){r=m=1;}}else if(k.KKM(e,16384,86)) {if(1){r=m=1;k.KO(0,t,"שׁ");}}else if(k.KKM(e,16448,87)) {if(1){r=m=1;}}else if(k.KKM(e,16384,87)) {if(1){r=m=1;k.KO(0,t,"ו");}}else if(k.KKM(e,16448,88)) {if(1){r=m=1;k.KO(0,t,"ץ");}}else if(k.KKM(e,16384,88)) {if(1){r=m=1;k.KO(0,t,"צ");}}else if(k.KKM(e,16448,89)) {if(1){r=m=1;}}else if(k.KKM(e,16384,89)) {if(1){r=m=1;k.KO(0,t,"י");}}else if(k.KKM(e,16448,90)) {if(1){r=m=1;}}else if(k.KKM(e,16384,90)) {if(1){r=m=1;k.KO(0,t,"ז");}}else if(k.KKM(e,16400,219)) {if(1){r=m=1;k.KO(0,t,"{");}}else if(k.KKM(e,16400,220)) {if(1){r=m=1;k.KO(0,t,"|");}}else if(k.KKM(e,16400,221)) {if(1){r=m=1;k.KO(0,t,"}");}}else if(k.KKM(e,16400,192)) {if(1){r=m=1;}}return r;};}
\ No newline at end of file
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.keyboard_info b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.keyboard_info
new file mode 100644
index 0000000000..e96344dbbb
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.keyboard_info
@@ -0,0 +1,40 @@
+{
+ "id": "alephwithbeth",
+ "name": "Aleph With Beth",
+ "license": "mit",
+ "isRTL": true,
+ "authorName": "Cameron Briggs",
+ "description": "alephwithbeth keyboard
",
+ "languages": {
+ "he": {
+ "examples": [],
+ "languageName": "Hebrew",
+ "displayName": "Hebrew"
+ }
+ },
+ "lastModifiedDate": "2025-01-27T23:04:56.765Z",
+ "packageFilename": "alephwithbeth.kmp",
+ "packageFileSize": 244476,
+ "jsFilename": "alephwithbeth.js",
+ "jsFileSize": 23534,
+ "packageIncludes": [
+ "documentation",
+ "welcome",
+ "visualKeyboard"
+ ],
+ "version": "1.0.6",
+ "encodings": [
+ "unicode"
+ ],
+ "platformSupport": {
+ "windows": "full",
+ "macos": "full",
+ "linux": "full",
+ "desktopWeb": "full",
+ "ios": "full",
+ "android": "full",
+ "mobileWeb": "full"
+ },
+ "minKeymanVersion": "14.0",
+ "helpLink": "https://help.keyman.com/keyboard/alephwithbeth"
+}
\ No newline at end of file
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmp b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmp
new file mode 100644
index 0000000000..540f4ca3ec
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmp differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmx b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmx
new file mode 100644
index 0000000000..27579d2002
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kmx differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kvk b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kvk
new file mode 100644
index 0000000000..7772d84d9e
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/build/alephwithbeth.kvk differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.ico b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.ico
new file mode 100644
index 0000000000..84b57c5aeb
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.ico differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.keyman-touch-layout b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.keyman-touch-layout
new file mode 100644
index 0000000000..05db57c7f9
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.keyman-touch-layout
@@ -0,0 +1,1646 @@
+{
+ "tablet": {
+ "displayUnderlying": false,
+ "layer": [
+ {
+ "id": "default",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "1"
+ },
+ {
+ "id": "K_2",
+ "text": "2"
+ },
+ {
+ "id": "K_3",
+ "text": "3"
+ },
+ {
+ "id": "K_4",
+ "text": "4"
+ },
+ {
+ "id": "K_5",
+ "text": "5"
+ },
+ {
+ "id": "K_6",
+ "text": "6"
+ },
+ {
+ "id": "K_7",
+ "text": "7"
+ },
+ {
+ "id": "K_8",
+ "text": "8"
+ },
+ {
+ "id": "K_9",
+ "text": "9"
+ },
+ {
+ "id": "K_0",
+ "text": "0"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "־"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "="
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "ק",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": "ו"
+ },
+ {
+ "id": "K_E",
+ "text": "ֶ"
+ },
+ {
+ "id": "K_R",
+ "text": "ר"
+ },
+ {
+ "id": "K_T",
+ "text": "ת"
+ },
+ {
+ "id": "K_Y",
+ "text": "י"
+ },
+ {
+ "id": "K_U",
+ "text": "ע"
+ },
+ {
+ "id": "K_I",
+ "text": "ִ"
+ },
+ {
+ "id": "K_O",
+ "text": "ֹ"
+ },
+ {
+ "id": "K_P",
+ "text": "פ"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "["
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "]"
+ },
+ {
+ "id": "T_new_138",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": "֑"
+ },
+ {
+ "id": "K_A",
+ "text": "ַ"
+ },
+ {
+ "id": "K_S",
+ "text": "ס"
+ },
+ {
+ "id": "K_D",
+ "text": "ד"
+ },
+ {
+ "id": "K_F",
+ "text": "ט"
+ },
+ {
+ "id": "K_G",
+ "text": "ג"
+ },
+ {
+ "id": "K_H",
+ "text": "ה"
+ },
+ {
+ "id": "K_J",
+ "text": "ח"
+ },
+ {
+ "id": "K_K",
+ "text": "כ"
+ },
+ {
+ "id": "K_L",
+ "text": "ל"
+ },
+ {
+ "id": "K_COLON",
+ "text": "ְ"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "'"
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "\\"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "shift"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": "ז"
+ },
+ {
+ "id": "K_X",
+ "text": "צ"
+ },
+ {
+ "id": "K_C",
+ "text": "שׂ"
+ },
+ {
+ "id": "K_V",
+ "text": "שׁ"
+ },
+ {
+ "id": "K_B",
+ "text": "ב"
+ },
+ {
+ "id": "K_N",
+ "text": "נ"
+ },
+ {
+ "id": "K_M",
+ "text": "מ"
+ },
+ {
+ "id": "K_COMMA",
+ "text": "א"
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "."
+ },
+ {
+ "id": "K_SLASH",
+ "text": "/"
+ },
+ {
+ "id": "T_new_164",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "alt",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "alt"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "shift",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "!"
+ },
+ {
+ "id": "K_2",
+ "text": "֘"
+ },
+ {
+ "id": "K_3",
+ "text": "֨"
+ },
+ {
+ "id": "K_4",
+ "text": "֜"
+ },
+ {
+ "id": "K_5",
+ "text": "֞"
+ },
+ {
+ "id": "K_6",
+ "text": "֔"
+ },
+ {
+ "id": "K_7",
+ "text": "֬"
+ },
+ {
+ "id": "K_8",
+ "text": "֝"
+ },
+ {
+ "id": "K_9",
+ "text": "("
+ },
+ {
+ "id": "K_0",
+ "text": ")"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "_"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "+"
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "קּ",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": "וּ"
+ },
+ {
+ "id": "K_E",
+ "text": "ֵ"
+ },
+ {
+ "id": "K_R",
+ "text": "רּ"
+ },
+ {
+ "id": "K_T",
+ "text": "תּ"
+ },
+ {
+ "id": "K_Y",
+ "text": "יּ"
+ },
+ {
+ "id": "K_U",
+ "text": "ֻ"
+ },
+ {
+ "id": "K_I",
+ "text": ""
+ },
+ {
+ "id": "K_O",
+ "text": "וֹ"
+ },
+ {
+ "id": "K_P",
+ "text": "פּ"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "{"
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "}"
+ },
+ {
+ "id": "T_new_362",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": ""
+ },
+ {
+ "id": "K_A",
+ "text": "ָ"
+ },
+ {
+ "id": "K_S",
+ "text": "סּ"
+ },
+ {
+ "id": "K_D",
+ "text": "דּ"
+ },
+ {
+ "id": "K_F",
+ "text": "טּ"
+ },
+ {
+ "id": "K_G",
+ "text": "גּ"
+ },
+ {
+ "id": "K_H",
+ "text": "הּ"
+ },
+ {
+ "id": "K_J",
+ "text": "וַיּ"
+ },
+ {
+ "id": "K_K",
+ "text": "כּ"
+ },
+ {
+ "id": "K_L",
+ "text": "לּ"
+ },
+ {
+ "id": "K_COLON",
+ "text": "״"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "\""
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "|"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "default"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": "זּ"
+ },
+ {
+ "id": "K_X",
+ "text": "צּ"
+ },
+ {
+ "id": "K_C",
+ "text": "שּׂ"
+ },
+ {
+ "id": "K_V",
+ "text": "שּׁ"
+ },
+ {
+ "id": "K_B",
+ "text": "בּ"
+ },
+ {
+ "id": "K_N",
+ "text": "נּ"
+ },
+ {
+ "id": "K_M",
+ "text": "מּ"
+ },
+ {
+ "id": "K_COMMA",
+ "text": ""
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "ּ"
+ },
+ {
+ "id": "K_SLASH",
+ "text": "?"
+ },
+ {
+ "id": "T_new_388",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "alt",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "alt"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "alt",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "֗"
+ },
+ {
+ "id": "K_2",
+ "text": "֮"
+ },
+ {
+ "id": "K_3",
+ "text": "֙"
+ },
+ {
+ "id": "K_4",
+ "text": "֠"
+ },
+ {
+ "id": "K_5",
+ "text": "֩"
+ },
+ {
+ "id": "K_6",
+ "text": "֟"
+ },
+ {
+ "id": "K_7",
+ "text": "֡"
+ },
+ {
+ "id": "K_8",
+ "text": "֕"
+ },
+ {
+ "id": "K_9",
+ "text": "֓"
+ },
+ {
+ "id": "K_0",
+ "text": "֯"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "ֿ"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "◌"
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": ""
+ },
+ {
+ "id": "K_E",
+ "text": "ֱ"
+ },
+ {
+ "id": "K_R",
+ "text": ""
+ },
+ {
+ "id": "K_T",
+ "text": ""
+ },
+ {
+ "id": "K_Y",
+ "text": ""
+ },
+ {
+ "id": "K_U",
+ "text": ""
+ },
+ {
+ "id": "K_I",
+ "text": "ֽ"
+ },
+ {
+ "id": "K_O",
+ "text": "ֳ"
+ },
+ {
+ "id": "K_P",
+ "text": "ף"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "֔"
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "֒"
+ },
+ {
+ "id": "T_new_138",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": ""
+ },
+ {
+ "id": "K_A",
+ "text": "ֲ"
+ },
+ {
+ "id": "K_S",
+ "text": ""
+ },
+ {
+ "id": "K_D",
+ "text": ""
+ },
+ {
+ "id": "K_F",
+ "text": ""
+ },
+ {
+ "id": "K_G",
+ "text": "•"
+ },
+ {
+ "id": "K_H",
+ "text": "̊"
+ },
+ {
+ "id": "K_J",
+ "text": ""
+ },
+ {
+ "id": "K_K",
+ "text": "ך"
+ },
+ {
+ "id": "K_L",
+ "text": ""
+ },
+ {
+ "id": "K_COLON",
+ "text": "׃"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "ׄ"
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "֫"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "shift",
+ "layer": "shift"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": ""
+ },
+ {
+ "id": "K_X",
+ "text": "ץ"
+ },
+ {
+ "id": "K_C",
+ "text": ""
+ },
+ {
+ "id": "K_V",
+ "text": ""
+ },
+ {
+ "id": "K_B",
+ "text": ""
+ },
+ {
+ "id": "K_N",
+ "text": "ן"
+ },
+ {
+ "id": "K_M",
+ "text": "ם"
+ },
+ {
+ "id": "K_COMMA",
+ "text": "֫"
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "̈"
+ },
+ {
+ "id": "K_SLASH",
+ "text": "״"
+ },
+ {
+ "id": "T_new_164",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "default",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "default"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "phone": {
+ "displayUnderlying": false,
+ "layer": [
+ {
+ "id": "default",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "1"
+ },
+ {
+ "id": "K_2",
+ "text": "2"
+ },
+ {
+ "id": "K_3",
+ "text": "3"
+ },
+ {
+ "id": "K_4",
+ "text": "4"
+ },
+ {
+ "id": "K_5",
+ "text": "5"
+ },
+ {
+ "id": "K_6",
+ "text": "6"
+ },
+ {
+ "id": "K_7",
+ "text": "7"
+ },
+ {
+ "id": "K_8",
+ "text": "8"
+ },
+ {
+ "id": "K_9",
+ "text": "9"
+ },
+ {
+ "id": "K_0",
+ "text": "0"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "־"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "="
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "ק",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": "ו"
+ },
+ {
+ "id": "K_E",
+ "text": "ֶ"
+ },
+ {
+ "id": "K_R",
+ "text": "ר"
+ },
+ {
+ "id": "K_T",
+ "text": "ת"
+ },
+ {
+ "id": "K_Y",
+ "text": "י"
+ },
+ {
+ "id": "K_U",
+ "text": "ע"
+ },
+ {
+ "id": "K_I",
+ "text": "ִ"
+ },
+ {
+ "id": "K_O",
+ "text": "ֹ"
+ },
+ {
+ "id": "K_P",
+ "text": "פ"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "["
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "]"
+ },
+ {
+ "id": "T_new_138",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": "֑"
+ },
+ {
+ "id": "K_A",
+ "text": "ַ"
+ },
+ {
+ "id": "K_S",
+ "text": "ס"
+ },
+ {
+ "id": "K_D",
+ "text": "ד"
+ },
+ {
+ "id": "K_F",
+ "text": "ט"
+ },
+ {
+ "id": "K_G",
+ "text": "ג"
+ },
+ {
+ "id": "K_H",
+ "text": "ה"
+ },
+ {
+ "id": "K_J",
+ "text": "ח"
+ },
+ {
+ "id": "K_K",
+ "text": "כ"
+ },
+ {
+ "id": "K_L",
+ "text": "ל"
+ },
+ {
+ "id": "K_COLON",
+ "text": "ְ"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "'"
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "\\"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "shift"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": "ז"
+ },
+ {
+ "id": "K_X",
+ "text": "צ"
+ },
+ {
+ "id": "K_C",
+ "text": "שׂ"
+ },
+ {
+ "id": "K_V",
+ "text": "שׁ"
+ },
+ {
+ "id": "K_B",
+ "text": "ב"
+ },
+ {
+ "id": "K_N",
+ "text": "נ"
+ },
+ {
+ "id": "K_M",
+ "text": "מ"
+ },
+ {
+ "id": "K_COMMA",
+ "text": "א"
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "."
+ },
+ {
+ "id": "K_SLASH",
+ "text": "/"
+ },
+ {
+ "id": "T_new_164",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "alt",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "alt"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "shift",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "!"
+ },
+ {
+ "id": "K_2",
+ "text": "֘"
+ },
+ {
+ "id": "K_3",
+ "text": "֨"
+ },
+ {
+ "id": "K_4",
+ "text": "֜"
+ },
+ {
+ "id": "K_5",
+ "text": "֞"
+ },
+ {
+ "id": "K_6",
+ "text": "֔"
+ },
+ {
+ "id": "K_7",
+ "text": "֬"
+ },
+ {
+ "id": "K_8",
+ "text": "֝"
+ },
+ {
+ "id": "K_9",
+ "text": "("
+ },
+ {
+ "id": "K_0",
+ "text": ")"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "_"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "+"
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "קּ",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": "וּ"
+ },
+ {
+ "id": "K_E",
+ "text": "ֵ"
+ },
+ {
+ "id": "K_R",
+ "text": "רּ"
+ },
+ {
+ "id": "K_T",
+ "text": "תּ"
+ },
+ {
+ "id": "K_Y",
+ "text": "יּ"
+ },
+ {
+ "id": "K_U",
+ "text": "ֻ"
+ },
+ {
+ "id": "K_I",
+ "text": ""
+ },
+ {
+ "id": "K_O",
+ "text": "וֹ"
+ },
+ {
+ "id": "K_P",
+ "text": "פּ"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "{"
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "}"
+ },
+ {
+ "id": "T_new_362",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": ""
+ },
+ {
+ "id": "K_A",
+ "text": "ָ"
+ },
+ {
+ "id": "K_S",
+ "text": "סּ"
+ },
+ {
+ "id": "K_D",
+ "text": "דּ"
+ },
+ {
+ "id": "K_F",
+ "text": "טּ"
+ },
+ {
+ "id": "K_G",
+ "text": "גּ"
+ },
+ {
+ "id": "K_H",
+ "text": "הּ"
+ },
+ {
+ "id": "K_J",
+ "text": "וַיּ"
+ },
+ {
+ "id": "K_K",
+ "text": "כּ"
+ },
+ {
+ "id": "K_L",
+ "text": "לּ"
+ },
+ {
+ "id": "K_COLON",
+ "text": "״"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "\""
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "|"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "default"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": "זּ"
+ },
+ {
+ "id": "K_X",
+ "text": "צּ"
+ },
+ {
+ "id": "K_C",
+ "text": "שּׂ"
+ },
+ {
+ "id": "K_V",
+ "text": "שּׁ"
+ },
+ {
+ "id": "K_B",
+ "text": "בּ"
+ },
+ {
+ "id": "K_N",
+ "text": "נּ"
+ },
+ {
+ "id": "K_M",
+ "text": "מּ"
+ },
+ {
+ "id": "K_COMMA",
+ "text": ""
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "ּ"
+ },
+ {
+ "id": "K_SLASH",
+ "text": "?"
+ },
+ {
+ "id": "T_new_388",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "alt",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "alt"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "alt",
+ "row": [
+ {
+ "id": 1,
+ "key": [
+ {
+ "id": "K_1",
+ "text": "֗"
+ },
+ {
+ "id": "K_2",
+ "text": "֮"
+ },
+ {
+ "id": "K_3",
+ "text": "֙"
+ },
+ {
+ "id": "K_4",
+ "text": "֠"
+ },
+ {
+ "id": "K_5",
+ "text": "֩"
+ },
+ {
+ "id": "K_6",
+ "text": "֟"
+ },
+ {
+ "id": "K_7",
+ "text": "֡"
+ },
+ {
+ "id": "K_8",
+ "text": "֕"
+ },
+ {
+ "id": "K_9",
+ "text": "֓"
+ },
+ {
+ "id": "K_0",
+ "text": "֯"
+ },
+ {
+ "id": "K_HYPHEN",
+ "text": "ֿ"
+ },
+ {
+ "id": "K_EQUAL",
+ "text": "◌"
+ },
+ {
+ "id": "K_BKSP",
+ "text": "*BkSp*",
+ "width": "100",
+ "sp": "1"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "key": [
+ {
+ "id": "K_Q",
+ "text": "",
+ "pad": "75"
+ },
+ {
+ "id": "K_W",
+ "text": ""
+ },
+ {
+ "id": "K_E",
+ "text": "ֱ"
+ },
+ {
+ "id": "K_R",
+ "text": ""
+ },
+ {
+ "id": "K_T",
+ "text": ""
+ },
+ {
+ "id": "K_Y",
+ "text": ""
+ },
+ {
+ "id": "K_U",
+ "text": ""
+ },
+ {
+ "id": "K_I",
+ "text": "ֽ"
+ },
+ {
+ "id": "K_O",
+ "text": "ֳ"
+ },
+ {
+ "id": "K_P",
+ "text": "ף"
+ },
+ {
+ "id": "K_LBRKT",
+ "text": "֔"
+ },
+ {
+ "id": "K_RBRKT",
+ "text": "֒"
+ },
+ {
+ "id": "T_new_138",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "key": [
+ {
+ "id": "K_BKQUOTE",
+ "text": ""
+ },
+ {
+ "id": "K_A",
+ "text": "ֲ"
+ },
+ {
+ "id": "K_S",
+ "text": ""
+ },
+ {
+ "id": "K_D",
+ "text": ""
+ },
+ {
+ "id": "K_F",
+ "text": ""
+ },
+ {
+ "id": "K_G",
+ "text": "•"
+ },
+ {
+ "id": "K_H",
+ "text": "̊"
+ },
+ {
+ "id": "K_J",
+ "text": ""
+ },
+ {
+ "id": "K_K",
+ "text": "ך"
+ },
+ {
+ "id": "K_L",
+ "text": ""
+ },
+ {
+ "id": "K_COLON",
+ "text": "׃"
+ },
+ {
+ "id": "K_QUOTE",
+ "text": "ׄ"
+ },
+ {
+ "id": "K_BKSLASH",
+ "text": "֫"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "key": [
+ {
+ "id": "K_SHIFT",
+ "text": "*Shift*",
+ "width": "160",
+ "sp": "1",
+ "nextlayer": "shift",
+ "layer": "shift"
+ },
+ {
+ "id": "K_oE2",
+ "text": ""
+ },
+ {
+ "id": "K_Z",
+ "text": ""
+ },
+ {
+ "id": "K_X",
+ "text": "ץ"
+ },
+ {
+ "id": "K_C",
+ "text": ""
+ },
+ {
+ "id": "K_V",
+ "text": ""
+ },
+ {
+ "id": "K_B",
+ "text": ""
+ },
+ {
+ "id": "K_N",
+ "text": "ן"
+ },
+ {
+ "id": "K_M",
+ "text": "ם"
+ },
+ {
+ "id": "K_COMMA",
+ "text": "֫"
+ },
+ {
+ "id": "K_PERIOD",
+ "text": "̈"
+ },
+ {
+ "id": "K_SLASH",
+ "text": "״"
+ },
+ {
+ "id": "T_new_164",
+ "text": "",
+ "width": "10",
+ "sp": "10"
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "key": [
+ {
+ "id": "K_LCONTROL",
+ "text": "default",
+ "width": "130",
+ "sp": "1",
+ "nextlayer": "default"
+ },
+ {
+ "id": "K_LOPT",
+ "text": "*Menu*",
+ "width": "140",
+ "sp": "1"
+ },
+ {
+ "id": "K_SPACE",
+ "text": "",
+ "width": "930"
+ },
+ {
+ "id": "K_ENTER",
+ "text": "*Enter*",
+ "width": "145",
+ "sp": "1"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kmn b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kmn
new file mode 100644
index 0000000000..d4a2b9a993
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kmn
@@ -0,0 +1,215 @@
+store(&TARGETS) 'any'
+store(&NAME) 'Aleph With Beth'
+store(&BITMAP) 'alephwithbeth.ico'
+store(&VISUALKEYBOARD) 'alephwithbeth.kvks'
+
+store(letters-with-final-forms) "כמנפצ"
+store(sofit-forms-of-letters) "ךםןףץ"
+store(presentation-dageshed-letters-with-final-forms) "כּפּ"
+store(presentation-dageshed-sofit-forms-of-letters) "ךּףּ"
+store(dagesh) 'ּ'
+c store(vowels) "ְֱֲֳִֵֶַׇׇָֹֻׅ"
+
+store(normal-dageshed-letters-with-final-forms) "מּנּצּ"
+store(normal-undageshed-sofit-forms-of-letters) "םןץ"
+
+store(punctuation) U+0591 .. U+05AF U+05BD .. U+05BF U+05C0 U+05C4
+store(whitespace) [K_SPACE] [K_ENTER] [K_TAB]
+
+store(&KMW_RTL) '1'
+store(&LAYOUTFILE) 'alephwithbeth.keyman-touch-layout'
+store(©RIGHT) '© Cameron Briggs'
+store(&KEYBOARDVERSION) '1.0.6'
+store(&KMW_HELPTEXT) 'Aleph With Beth'
+
+begin Unicode > use(main)
+
+group(main) using keys
++ [SHIFT K_I] > nul
++ [ALT K_BKQUOTE] > nul
++ [ALT K_B] > nul
++ [ALT K_V] > nul
++ [ALT K_C] > nul
++ [ALT K_Z] > nul
++ [ALT K_L] > nul
++ [ALT K_J] > nul
++ [ALT K_F] > nul
++ [ALT K_D] > nul
++ [ALT K_S] > nul
++ [ALT K_U] > nul
++ [ALT K_Y] > nul
++ [ALT K_T] > nul
++ [ALT K_R] > nul
++ [ALT K_W] > nul
++ [ALT K_Q] > nul
++ [SHIFT K_BKSLASH] > '|'
++ [SHIFT K_EQUAL] > '+'
++ [SHIFT K_HYPHEN] > '_'
++ [SHIFT K_0] > ')'
++ [SHIFT K_9] > '('
++ [SHIFT K_SLASH] > '?'
++ [SHIFT K_QUOTE] > '"'
++ [SHIFT K_BKQUOTE] > nul
++ [K_EQUAL] > '='
++ [K_HYPHEN] > '־'
++ [K_0] > '0'
++ [K_9] > '9'
++ [K_8] > '8'
++ [K_7] > '7'
++ [K_6] > '6'
++ [K_5] > '5'
++ [K_4] > '4'
++ [K_3] > '3'
++ [K_2] > '2'
++ [K_1] > '1'
++ [K_PERIOD] > '.'
++ [K_SLASH] > '/'
++ [K_QUOTE] > U+0027
++ [SHIFT K_COLON] > '״'
++ [ALT K_A] > 'ֲ'
++ [SHIFT K_RBRKT] > '}'
++ [SHIFT K_LBRKT] > '{'
++ [K_BKSLASH] > '\'
++ [K_RBRKT] > ']'
++ [K_LBRKT] > '['
++ [SHIFT K_R] > 'רּ'
++ [SHIFT CTRL K_SPACE] > ' '
++ [SHIFT K_SPACE] > ' '
++ [K_SPACE] > ' '
++ [ALT K_SPACE] > ' '
++ [SHIFT K_8] > '֝'
++ [SHIFT K_7] > '֬'
++ [SHIFT K_6] > '֔'
++ [SHIFT K_5] > '֞'
++ [SHIFT K_4] > '֜'
++ [SHIFT K_3] > '֨'
++ [SHIFT K_2] > '֘'
++ [SHIFT K_1] > '!'
++ [ALT K_SLASH] > '״'
++ [ALT K_PERIOD] > '̈'
++ [ALT K_COMMA] > '֫'
++ [ALT K_QUOTE] > 'ׄ'
++ [ALT K_COLON] > '׃'
++ [ALT K_H] > '̊'
++ [ALT K_G] > '•'
++ [ALT K_BKSLASH] > '֫'
++ [ALT K_RBRKT] > '֒'
++ [ALT K_LBRKT] > '֔'
++ [ALT K_I] > 'ֽ'
++ [ALT K_O] > 'ֳ'
++ [ALT K_E] > 'ֱ'
++ [ALT K_EQUAL] > '◌'
++ [ALT K_HYPHEN] > 'ֿ'
++ [ALT K_0] > '֯'
++ [ALT K_9] > '֓'
++ [ALT K_8] > '֕'
++ [ALT K_7] > '֡'
++ [ALT K_6] > '֟'
++ [ALT K_5] > '֩'
++ [ALT K_4] > '֠'
++ [ALT K_3] > '֙'
++ [ALT K_2] > '֮'
++ [ALT K_1] > '֗'
++ [ALT K_N] > 'ן'
++ [ALT K_P] > 'ף'
++ [ALT K_K] > 'ך'
++ [ALT K_M] > 'ם'
++ [ALT K_X] > 'ץ'
++ [SHIFT K_PERIOD] > 'ּ'
++ [SHIFT K_COMMA] > nul
++ [SHIFT K_M] > 'מּ'
++ [SHIFT K_N] > 'נּ'
++ [SHIFT K_B] > 'בּ'
++ [SHIFT K_V] > 'שּׁ'
++ [SHIFT K_C] > 'שּׂ'
++ [SHIFT K_X] > 'צּ'
++ [SHIFT K_Z] > 'זּ'
++ [SHIFT K_L] > 'לּ'
++ [SHIFT K_K] > 'כּ'
++ [SHIFT K_J] > 'וַיּ'
++ [SHIFT K_H] > 'הּ'
++ [SHIFT K_G] > 'גּ'
++ [SHIFT K_F] > 'טּ'
++ [SHIFT K_D] > 'דּ'
++ [SHIFT K_S] > 'סּ'
++ [SHIFT K_A] > 'ָ'
++ [SHIFT K_P] > 'פּ'
++ [SHIFT K_O] > 'וֹ'
++ [SHIFT K_U] > 'ֻ'
++ [SHIFT K_Y] > 'יּ'
++ [SHIFT K_T] > 'תּ'
++ [SHIFT K_E] > 'ֵ'
++ [SHIFT K_W] > 'וּ'
++ [SHIFT K_Q] > 'קּ'
++ [K_BKQUOTE] > '֑'
++ [K_COMMA] > 'א'
++ [K_M] > 'מ'
++ [K_N] > 'נ'
++ [K_B] > 'ב'
++ [K_V] > 'שׁ'
++ [K_C] > 'שׂ'
++ [K_X] > 'צ'
++ [K_Z] > 'ז'
++ [K_COLON] > 'ְ'
++ [K_L] > 'ל'
++ [K_K] > 'כ'
++ [K_J] > 'ח'
++ [K_H] > 'ה'
++ [K_G] > 'ג'
++ [K_F] > 'ט'
++ [K_D] > 'ד'
++ [K_S] > 'ס'
++ [K_A] > 'ַ'
++ [K_P] > 'פ'
++ [K_O] > 'ֹ'
++ [K_I] > 'ִ'
++ [K_U] > 'ע'
++ [K_Y] > 'י'
++ [K_T] > 'ת'
++ [K_R] > 'ר'
++ [K_E] > 'ֶ'
++ [K_W] > 'ו'
++ [K_Q] > 'ק'
+c Transform final letters
+any(letters-with-final-forms) + any(whitespace) > index(sofit-forms-of-letters,1)
+
+c Transform final letters with a dagesh
+any(letters-with-final-forms) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh
+any(presentation-dageshed-letters-with-final-forms) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1)
+any(normal-dageshed-letters-with-final-forms) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh
+
+c Transform final letters with a vowel
+c any(letters-with-final-forms) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2)
+
+c Transform final letters with a vowel and a dagesh
+c any(letters-with-final-forms) any(vowels) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) $dagesh
+c any(letters-with-final-forms) $dagesh any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(vowels,3)
+c any(presentation-dageshed-letters-with-final-forms) any(vowels) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(vowels,2)
+c any(normal-dageshed-letters-with-final-forms) any(vowels) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(vowels,2)
+
+c Transform final letters with punctuation
+any(letters-with-final-forms) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2)
+
+c Transform final letters with punctuation and a dagesh
+any(letters-with-final-forms) any(punctuation) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) $dagesh
+any(letters-with-final-forms) $dagesh any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(punctuation,3)
+any(presentation-dageshed-letters-with-final-forms) any(punctuation) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(punctuation,2)
+any(normal-dageshed-letters-with-final-forms) any(punctuation) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(punctuation,2)
+
+c Transfrom final letters with a punctuation and a vowel
+c any(letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3)
+c any(letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3)
+
+c Transform final letters with punctuation, a vowel, and a dagesh (phew!)
+c any(letters-with-final-forms) any(punctuation) any(vowels) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3) $dagesh
+c any(letters-with-final-forms) any(punctuation) $dagesh any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) $dagesh index(vowels,4)
+c any(letters-with-final-forms) any(vowels) any(punctuation) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3) $dagesh
+c any(letters-with-final-forms) any(vowels) $dagesh any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) $dagesh index(punctuation,4)
+c any(letters-with-final-forms) $dagesh any(punctuation) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(punctuation,3) index(vowels,4)
+c any(letters-with-final-forms) $dagesh any(vowels) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(vowels,3) index(punctuation,4)
+
+
+c any(presentation-dageshed-letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3)
+c any(presentation-dageshed-letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3)
+c any(normal-dageshed-letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(punctuation,2) index(vowels,3)
+c any(normal-dageshed-letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(vowels,2) index(punctuation,3)
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kps b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kps
new file mode 100644
index 0000000000..9c2b0a1251
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kps
@@ -0,0 +1,95 @@
+
+
+
+ 17.0.325.0
+ 7.0
+
+
+
+ readme.htm
+ ..\LICENSE.md
+ ..\..\another-folder\welcome\welcome.htm
+
+
+
+
+
+
+
+
+
+
+ Aleph With Beth
+ © Cameron Briggs
+ Cameron Briggs
+ https://freehebrew.online
+ alephwithbeth keyboard
+
+
+
+ readme.htm
+ File readme.htm
+ 0
+ .htm
+
+
+ ..\..\another-folder\welcome\welcome.htm
+ File welcome.htm
+ 0
+ .htm
+
+
+ ..\build\alephwithbeth.js
+ File alephwithbeth.js
+ 0
+ .js
+
+
+ ..\build\alephwithbeth.kvk
+ File alephwithbeth.kvk
+ 0
+ .kvk
+
+
+ ..\build\alephwithbeth.kmx
+ Keyboard alephwithbeth
+ 0
+ .kmx
+
+
+ ..\..\another-folder\welcome\KeyboardLayout.png
+ File KeyboardLayout.png
+ 0
+ .png
+
+
+ ..\..\another-folder\welcome\KeyboardLayoutAlt.png
+ File KeyboardLayoutAlt.png
+ 0
+ .png
+
+
+ ..\..\another-folder\welcome\KeyboardLayoutShift.png
+ File KeyboardLayoutShift.png
+ 0
+ .png
+
+
+ ..\LICENSE.md
+
+ 0
+ .md
+
+
+
+
+ Aleph With Beth
+ alephwithbeth
+ 1.0.4
+
+ Hebrew
+
+
+
+
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kvks b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kvks
new file mode 100644
index 0000000000..6b3cf3eeb9
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/alephwithbeth.kvks
@@ -0,0 +1,138 @@
+
+
+
+ 10.0
+ alephwithbeth
+
+
+
+
+ !
+ "
+ ֨
+ ֜
+ ֞
+ ֬
+ (
+ )
+ ֝
+ +
+ ״
+ ּ
+ ?
+ ֘
+ ָ
+ בּ
+ שּׂ
+ דּ
+ ֵ
+ טּ
+ גּ
+ הּ
+ וַיּ
+ כּ
+ לּ
+ מּ
+ נּ
+ וֹ
+ פּ
+ קּ
+ רּ
+ סּ
+ תּ
+ ֻ
+ שּׁ
+ וּ
+ צּ
+ יּ
+ זּ
+ ֔
+ _
+ {
+ |
+ }
+
+
+ '
+ א
+ .
+ /
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ ְ
+ =
+ [
+ \
+ ]
+ ֑
+ ַ
+ ב
+ שׂ
+ ד
+ ֶ
+ ט
+ ג
+ ה
+ ִ
+ ח
+ כ
+ ל
+ מ
+ נ
+ ֹ
+ פ
+ ק
+ ר
+ ס
+ ת
+ ע
+ שׁ
+ ו
+ צ
+ י
+ ז
+ ־
+ 0
+
+
+ ׄ
+ ֫
+ ֿ
+ ̈
+ ״
+ ֯
+ ֗
+ ֮
+ ֙
+ ֠
+ ֩
+ ֟
+ ֡
+ ֕
+ ֓
+ ׃
+ ◌
+ ֔
+ ֫
+ ֒
+ ֲ
+ ֱ
+ •
+ ̊
+ ֽ
+ ך
+ ם
+ ן
+ ֳ
+ ף
+ ץ
+
+
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayout.png b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayout.png
new file mode 100644
index 0000000000..d0998e288b
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayout.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutAlt.png b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutAlt.png
new file mode 100644
index 0000000000..f740eec1e3
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutAlt.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutShift.png b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutShift.png
new file mode 100644
index 0000000000..9b243d4dd2
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/KeyboardLayoutShift.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/alephwithbeth.php b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/alephwithbeth.php
new file mode 100644
index 0000000000..48a2c2e7c8
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/help/alephwithbeth.php
@@ -0,0 +1,22 @@
+
+
+
+Start Using Aleph with Beth
+
+Desktop Layout
+
+Base layout
+
+Layout when shift is pressed
+
+Layout when alt is pressed
+
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/readme.htm b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/readme.htm
new file mode 100644
index 0000000000..f187388c1e
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/alephwithbeth/source/readme.htm
@@ -0,0 +1,22 @@
+
+
+
+
+
+ alephwithbeth
+
+
+
+
+Aleph With Beth
+
+A Hebrew keyboard designed to intuitively match the standard QWERTY layout.
+
+© Cameron Briggs
+
+
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayout.png b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayout.png
new file mode 100644
index 0000000000..d0998e288b
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayout.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutAlt.png b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutAlt.png
new file mode 100644
index 0000000000..f740eec1e3
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutAlt.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutShift.png b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutShift.png
new file mode 100644
index 0000000000..9b243d4dd2
Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/KeyboardLayoutShift.png differ
diff --git a/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/welcome.htm b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/welcome.htm
new file mode 100644
index 0000000000..717e7bedeb
--- /dev/null
+++ b/developer/src/kmc-copy/test/fixtures/projects/external/another-folder/welcome/welcome.htm
@@ -0,0 +1,28 @@
+
+
+
+
+
+ Start Using alephwithbeth
+
+
+
+
+Start Using Aleph With Beth
+
+Keyboard Layouts
+
+Base layout
+
+Layout when shift is pressed
+
+Layout when alt is pressed
+
+
+
+
diff --git a/developer/src/kmc-generate/build.sh b/developer/src/kmc-generate/build.sh
index e99e8288c2..7db46acd56 100755
--- a/developer/src/kmc-generate/build.sh
+++ b/developer/src/kmc-generate/build.sh
@@ -13,6 +13,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
builder_describe "Build Keyman kmc-generate module" \
"@/common/web/keyman-version" \
+ "@/common/web/langtags" \
"@/common/web/types" \
"@/developer/src/common/web/test-helpers" \
"@/developer/src/common/web/utils" \
diff --git a/developer/src/kmc-generate/package.json b/developer/src/kmc-generate/package.json
index ba488ff575..daf4f92922 100644
--- a/developer/src/kmc-generate/package.json
+++ b/developer/src/kmc-generate/package.json
@@ -27,7 +27,8 @@
"dependencies": {
"@keymanapp/common-types": "*",
"@keymanapp/developer-utils": "*",
- "@keymanapp/keyman-version": "*"
+ "@keymanapp/keyman-version": "*",
+ "@keymanapp/langtags": "*"
},
"devDependencies": {
"@keymanapp/developer-test-helpers": "*",
diff --git a/developer/src/kmc-generate/src/abstract-generator.ts b/developer/src/kmc-generate/src/abstract-generator.ts
index 5a6ed46cc0..3c56c00e07 100644
--- a/developer/src/kmc-generate/src/abstract-generator.ts
+++ b/developer/src/kmc-generate/src/abstract-generator.ts
@@ -4,7 +4,7 @@
* Base interfaces and classes for generating Keyman Developer source files
*/
-import { CompilerCallbacks, CompilerLogLevel, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult } from "@keymanapp/developer-utils";
+import { CompilerBaseOptions, CompilerCallbacks, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult } from "@keymanapp/developer-utils";
import { GeneratorMessages } from './generator-messages.js';
import { KeymanTargets } from "@keymanapp/common-types";
@@ -13,12 +13,7 @@ import { KeymanTargets } from "@keymanapp/common-types";
* @public
* Options for the Keyman Developer project generator
*/
-export interface GeneratorOptions /* not inheriting from CompilerBaseOptions */ {
- /**
- * Reporting level to console, used by NodeCompilerCallbacks (not used in
- * compiler modules; all messages are still reported to the internal log)
- */
- logLevel?: CompilerLogLevel;
+export interface GeneratorOptions extends CompilerBaseOptions {
/**
* identifier (basename) of the keyboard or model
*/
diff --git a/developer/src/kmc-generate/src/basic-generator.ts b/developer/src/kmc-generate/src/basic-generator.ts
index fe5a5cd304..9975615523 100644
--- a/developer/src/kmc-generate/src/basic-generator.ts
+++ b/developer/src/kmc-generate/src/basic-generator.ts
@@ -6,6 +6,7 @@
import { KeymanTargets } from "@keymanapp/common-types";
import KEYMAN_VERSION from "@keymanapp/keyman-version";
+import { getLangtagByTag } from "@keymanapp/langtags";
import { AbstractGenerator, GeneratorArtifacts } from "./abstract-generator.js";
import { GeneratorMessages } from "./generator-messages.js";
@@ -26,13 +27,13 @@ export class BasicGenerator extends AbstractGenerator {
protected preGenerate() {
const dt = new Date();
- this.languageTags = this.options.languageTags.length
+ this.languageTags = this.options.languageTags?.length
? this.options.languageTags.map(tag => new Intl.Locale(tag).minimize().toString())
: [this.DEFAULT_LOCALE];
// Verify that targets passed in are valid
let failed = false;
- for(const target of this.options.targets) {
+ for(const target of this.options.targets ?? []) {
if(!KeymanTargets.AllKeymanTargets.includes(target)) {
this.callbacks.reportMessage(GeneratorMessages.Error_InvalidTarget({target}));
failed = true;
@@ -42,7 +43,7 @@ export class BasicGenerator extends AbstractGenerator {
return false;
}
- if(this.options.targets.includes(KeymanTargets.KeymanTarget.any) || this.options.targets.length == 0) {
+ if(this.options.targets?.includes(KeymanTargets.KeymanTarget.any) || !this.options.targets?.length) {
this.resolvedTargets = KeymanTargets.AllKeymanTargets.filter(target => target != KeymanTargets.KeymanTarget.any);
} else {
this.resolvedTargets = [].concat(this.options.targets);
@@ -72,8 +73,12 @@ export class BasicGenerator extends AbstractGenerator {
}
private getLanguageName(tag: string) {
- // TODO-GENERATE: probably need to use langtags.json
- return new Intl.Locale(tag).language;
+ const langtag = getLangtagByTag(tag);
+ if(!langtag) {
+ // Fallback to Intl.Locale, probably will be undefined
+ return new Intl.Locale(tag).language;
+ }
+ return langtag.name;
}
private generateLanguageListForPackage(): string {
diff --git a/developer/src/kmc-generate/src/generator-messages.ts b/developer/src/kmc-generate/src/generator-messages.ts
index 6c9ccfa7d8..185f68cbe6 100644
--- a/developer/src/kmc-generate/src/generator-messages.ts
+++ b/developer/src/kmc-generate/src/generator-messages.ts
@@ -18,8 +18,9 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
*/
export class GeneratorMessages {
static FATAL_UnexpectedException = SevFatal | 0x0001;
- static Fatal_UnexpectedException = (o:{e: any}) => m(
- this.FATAL_UnexpectedException, null, o.e ?? 'unknown error'
+ static Fatal_UnexpectedException = (o:{e: any}) => CompilerMessageSpecWithException(
+ this.FATAL_UnexpectedException, null,
+ o.e ?? 'unknown error',
);
static INFO_GeneratingProject = SevInfo | 0x0002;
diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps
index 62931d3bcf..1e05660d2a 100644
--- a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps
+++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps
@@ -39,7 +39,7 @@
sample
1.0
- en
+ English
diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps
index 62931d3bcf..1e05660d2a 100644
--- a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps
+++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps
@@ -39,7 +39,7 @@
sample
1.0
- en
+ English
diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.kps b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.kps
index 5020fd3eb0..76e9069e72 100644
--- a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.kps
+++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.kps
@@ -36,7 +36,7 @@
Sample Project
sample.en.sample
- en
+ English
diff --git a/developer/src/kmc-generate/tsconfig.json b/developer/src/kmc-generate/tsconfig.json
index 1fc0a249be..bf711ca14a 100644
--- a/developer/src/kmc-generate/tsconfig.json
+++ b/developer/src/kmc-generate/tsconfig.json
@@ -8,6 +8,7 @@
"paths": {
"@keymanapp/keyman-version": ["../../../common/web/keyman-version/keyman-version.mts"],
+ "@keymanapp/langtags": ["../../../common/web/langtags/src/main"],
"@keymanapp/common-types": ["../../../common/web/types/src/main"],
},
@@ -20,6 +21,7 @@
],
"references": [
{ "path": "../../../common/web/keyman-version" },
+ { "path": "../../../common/web/langtags" },
{ "path": "../../../common/web/types/" },
]
}
diff --git a/developer/src/kmc-keyboard-info/.gitignore b/developer/src/kmc-keyboard-info/.gitignore
index f118f231af..abbfb195cb 100644
--- a/developer/src/kmc-keyboard-info/.gitignore
+++ b/developer/src/kmc-keyboard-info/.gitignore
@@ -1,2 +1 @@
-src/imports/
test/fixtures/khmer_angkor/build/actual.keyboard_info
\ No newline at end of file
diff --git a/developer/src/kmc-keyboard-info/build.sh b/developer/src/kmc-keyboard-info/build.sh
index acd6bd65de..87902b6876 100755
--- a/developer/src/kmc-keyboard-info/build.sh
+++ b/developer/src/kmc-keyboard-info/build.sh
@@ -8,6 +8,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh"
builder_describe "Build Keyman kmc keyboard-info Compiler module" \
+ "@/common/web/langtags" \
"@/common/web/types" \
"@/developer/src/common/web/utils" \
"@/developer/src/kmc-package" \
@@ -21,23 +22,16 @@ builder_describe "Build Keyman kmc keyboard-info Compiler module" \
"--dry-run,-n don't actually publish, just dry run"
builder_describe_outputs \
- configure /developer/src/kmc-keyboard-info/src/imports/langtags.js \
+ configure /node_modules \
build /developer/src/kmc-keyboard-info/build/src/index.js \
api /developer/build/api/kmc-keyboard-info.api.json
builder_parse "$@"
-function do_configure() {
- verify_npm_setup
- mkdir -p src/imports
- echo 'export default ' > src/imports/langtags.js
- cat "$KEYMAN_ROOT/resources/standards-data/langtags/langtags.json" >> src/imports/langtags.js
-}
-
#-------------------------------------------------------------------------------------------------------------------
builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo
-builder_run_action configure do_configure
+builder_run_action configure verify_npm_setup
builder_run_action build tsc --build
builder_run_action api api-extractor run --local --verbose
builder_run_action test builder_do_typescript_tests
diff --git a/developer/src/kmc-keyboard-info/package.json b/developer/src/kmc-keyboard-info/package.json
index 45e1001501..b97cdcc22a 100644
--- a/developer/src/kmc-keyboard-info/package.json
+++ b/developer/src/kmc-keyboard-info/package.json
@@ -25,7 +25,8 @@
"dependencies": {
"@keymanapp/common-types": "*",
"@keymanapp/developer-utils": "*",
- "@keymanapp/kmc-package": "*"
+ "@keymanapp/kmc-package": "*",
+ "@keymanapp/langtags": "*"
},
"devDependencies": {
"@types/mocha": "^5.2.7",
diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts
index 3e1db45aed..49da3d6684 100644
--- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts
+++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts
@@ -7,7 +7,7 @@ import { minKeymanVersion } from "./min-keyman-version.js";
import { KeyboardInfoFile, KeyboardInfoFileIncludes, KeyboardInfoFileLanguageFont, KeyboardInfoFilePlatform } from "./keyboard-info-file.js";
import { KeymanFileTypes, KmpJsonFile, KmxFileReader, KMX, KeymanTargets } from "@keymanapp/common-types";
import { KeyboardInfoCompilerMessages } from "./keyboard-info-compiler-messages.js";
-import langtags from "./imports/langtags.js";
+import { getLangtagByTag } from "@keymanapp/langtags";
import { CompilerCallbacks, KeymanCompiler, CompilerOptions, KeymanCompilerResult, KeymanCompilerArtifacts, KeymanCompilerArtifact, KeymanUrls, isValidEmail, validateMITLicense } from "@keymanapp/developer-utils";
import { KmpCompiler } from "@keymanapp/kmc-package";
@@ -16,29 +16,6 @@ import { getFontFamily } from "@keymanapp/developer-utils";
const regionNames = new Intl.DisplayNames(['en'], { type: "region" });
const scriptNames = new Intl.DisplayNames(['en'], { type: "script" });
-const langtagsByTag = {};
-
-/**
- * Build a dictionary of language tags from langtags.json
- */
-
-function preinit(): void {
- if(langtagsByTag['en']) {
- // Already initialized, we can reasonably assume that 'en' will always be in
- // langtags.json.
- return;
- }
-
- for(const tag of langtags) {
- langtagsByTag[tag.tag] = tag;
- langtagsByTag[tag.full] = tag;
- if(tag.tags) {
- for(const t of tag.tags) {
- langtagsByTag[t] = tag;
- }
- }
- }
-}
/**
* @public
@@ -108,10 +85,6 @@ export class KeyboardInfoCompiler implements KeymanCompiler {
private callbacks: CompilerCallbacks;
private options: KeyboardInfoCompilerOptions;
- constructor() {
- preinit();
- }
-
/**
* Initialize the compiler.
* Copies options.
@@ -571,7 +544,7 @@ export class KeyboardInfoCompiler implements KeymanCompiler {
}
}
}
- const tag = langtagsByTag[bcp47] ?? langtagsByTag[locale.language];
+ const tag = getLangtagByTag(bcp47) ?? getLangtagByTag(locale.language);
language.languageName = tag ? tag.name : bcp47;
language.regionName = mapName(locale.region, regionNames);
language.scriptName = mapName(locale.script, scriptNames);
@@ -586,7 +559,7 @@ export class KeyboardInfoCompiler implements KeymanCompiler {
''
);
- const resolvedScript = locale.script ?? langtagsByTag[bcp47]?.script ?? langtagsByTag[locale.language]?.script ?? undefined;
+ const resolvedScript = locale.script ?? getLangtagByTag(bcp47)?.script ?? getLangtagByTag(locale.language)?.script ?? undefined;
if(commonScript === null) {
commonScript = resolvedScript;
} else {
@@ -648,10 +621,3 @@ export class KeyboardInfoCompiler implements KeymanCompiler {
}
}
-
-/**
- * these are exported only for unit tests, do not use
- */
-export const unitTestEndpoints = {
- langtagsByTag,
-};
\ No newline at end of file
diff --git a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts
index 99c244be2d..7b62b012ed 100644
--- a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts
+++ b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts
@@ -3,8 +3,8 @@ import { assert } from 'chai';
import 'mocha';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import { makePathToFixture } from './helpers/index.js';
-import { KeyboardInfoCompiler, KeyboardInfoCompilerResult, unitTestEndpoints } from '../src/keyboard-info-compiler.js';
-import langtags from "../src/imports/langtags.js";
+import { KeyboardInfoCompiler, KeyboardInfoCompilerResult } from '../src/keyboard-info-compiler.js';
+import { getLangtagByTag, langtags } from "@keymanapp/langtags";
import { KmpCompiler, KmpCompilerOptions } from '@keymanapp/kmc-package';
import { KMX, KeymanFileTypes, KeymanTargets, KmpJsonFile } from '@keymanapp/common-types';
import { CompilerCallbacks } from '@keymanapp/developer-utils';
@@ -114,10 +114,10 @@ describe('keyboard-info-compiler', function () {
const compiler = new KeyboardInfoCompiler(); // indirectly call preinit()
assert.isNotNull(compiler);
const en_langtag = langtags.find(({ tag }) => tag === 'en');
- assert.deepEqual((unitTestEndpoints.langtagsByTag)['en'], en_langtag);
- assert.deepEqual((unitTestEndpoints.langtagsByTag)['en-Latn-US'], en_langtag);
- assert.deepEqual((unitTestEndpoints.langtagsByTag)['en-Latn'], en_langtag);
- assert.deepEqual((unitTestEndpoints.langtagsByTag)['en-US'], en_langtag);
+ assert.deepEqual(getLangtagByTag('en'), en_langtag);
+ assert.deepEqual(getLangtagByTag('en-latn-us'), en_langtag);
+ assert.deepEqual(getLangtagByTag('en-latn'), en_langtag);
+ assert.deepEqual(getLangtagByTag('en-us'), en_langtag);
});
it('check init initialises KeyboardInfoCompiler correctly', async function() {
diff --git a/developer/src/kmc-keyboard-info/tsconfig.json b/developer/src/kmc-keyboard-info/tsconfig.json
index 124c812ecf..2791b7542b 100644
--- a/developer/src/kmc-keyboard-info/tsconfig.json
+++ b/developer/src/kmc-keyboard-info/tsconfig.json
@@ -4,18 +4,12 @@
"compilerOptions": {
"outDir": "build/src/",
"rootDir": "src/",
- /*
- following two settings allow us to import langtags.js. It cannot be a .ts
- because the type is too complex for TypeScript to infer.
- */
- "allowJs": true,
- "noImplicitAny": false,
},
"include": [
"src/**/*.ts",
- "src/imports/langtags.js",
],
"references": [
+ { "path": "../../../common/web/langtags" },
{ "path": "../../../common/web/types" },
{ "path": "../kmc-package/" },
{ "path": "../common/web/utils/" },
diff --git a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts
index 8d8b8ca6ed..e58b5fb9bf 100644
--- a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts
+++ b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts
@@ -1,6 +1,5 @@
import { keyAddress, KmnCompilerMessages } from "../compiler/kmn-compiler-messages.js";
-import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageDef as def, CompilerMessageSpec } from "@keymanapp/developer-utils";
-import { kmnfile } from "./compiler-globals.js";
+import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m } from "@keymanapp/developer-utils";
import { KeyAddress } from "./validate-layout-file.js";
const Namespace = CompilerErrorNamespace.KmwCompiler;
@@ -10,12 +9,6 @@ const SevHint = CompilerErrorSeverity.Hint | Namespace;
const SevError = CompilerErrorSeverity.Error | Namespace;
// const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
-const m = (code: number, message: string, o?: {filename?: string, line?: number}) : CompilerEvent => ({
- ...CompilerMessageSpec(code, message),
- filename: o?.filename ?? kmnfile,
- line: o?.line,
-});
-
/**
* @internal
* Error messages reported by the KeymanWeb .kmn compiler.
@@ -34,25 +27,62 @@ export class KmwCompilerMessages extends KmnCompilerMessages {
// 0x0001 - Reserved for removed identifier ERROR_NotAnyRequiresVersion14 =
// SevError | 0x0001, added in 17.0, removed in 18.0
+ //------------------------------------------------------------------------------|
+ // max length of detail message lines (checked by verifyCompilerMessagesObject) |
+ //------------------------------------------------------------------------------|
+
static ERROR_TouchLayoutIdentifierRequires15 = SevError | 0x0002;
- static Error_TouchLayoutIdentifierRequires15 = (o:{keyId:string, platformName:string, layerId:string, address:KeyAddress}) => m(this.ERROR_TouchLayoutIdentifierRequires15,
- `Key "${def(o.keyId)}" on "${def(o.platformName)}", layer "${def(o.layerId)}" (${keyAddress(o.address)}) has a multi-part identifier which requires version 15.0 or newer.`);
+ static Error_TouchLayoutIdentifierRequires15 = (o:{keyId:string, platformName:string, layerId:string, address:KeyAddress}) => m(
+ this.ERROR_TouchLayoutIdentifierRequires15,
+ `Key "${def(o.keyId)}" on "${def(o.platformName)}", layer "${def(o.layerId)}" (${keyAddress(o.address)}) has a `+
+ `multi-part identifier which requires version 15.0 or newer`, `
+ The Unicode key format \`U_xxxx_yyyy\` is supported in Keyman 15.0 and later
+ versions.
+
+ For example, \`U_0041_0300\` means the key will by default emit
+ \`U+0041 U+0300\` (À); earlier versions allow only a single Unicode value in
+ the identifier, e.g. \`U_0300\`.
+ `);
static ERROR_InvalidTouchLayoutFileFormat = SevError | 0x0003;
- static Error_InvalidTouchLayoutFileFormat = (o:{msg: string}) => m(this.ERROR_InvalidTouchLayoutFileFormat,
- `Invalid touch layout file: ${def(o.msg)}`);
+ static Error_InvalidTouchLayoutFileFormat = (o:{msg: string}) => m(
+ this.ERROR_InvalidTouchLayoutFileFormat,
+ `Invalid touch layout file: ${def(o.msg)}`, `
+ The referenced .keyman-touch-layout file contained invalid JSON content. The
+ touch layout file format is documented at
+ https://help.keyman.com/developer/current-version/reference/file-types/keyman-touch-layout.
+ `);
static ERROR_TouchLayoutFileDoesNotExist = SevError | 0x0004;
- static Error_TouchLayoutFileDoesNotExist = (o:{filename:string}) => m(this.ERROR_TouchLayoutFileDoesNotExist,
- `Touch layout file ${def(o.filename)} does not exist`);
+ static Error_TouchLayoutFileDoesNotExist = (o:{filename:string}) => m(
+ this.ERROR_TouchLayoutFileDoesNotExist,
+ `Touch layout file ${def(o.filename)} does not exist`, `
+ The compiler was unable to load the referenced .keyman-touch-layout file.
+ Verify that the referenced file does exist and is accessible to the compiler.
+ `);
static HINT_TouchLayoutUsesUnsupportedGesturesDownlevel = SevHint | 0x0005;
- static Hint_TouchLayoutUsesUnsupportedGesturesDownlevel = (o:{keyId:string}) => m(this.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel,
- `The touch layout uses a flick or multi-tap gesture on key ${def(o.keyId)}, which is only available on version 17.0+ of Keyman`);
+ static Hint_TouchLayoutUsesUnsupportedGesturesDownlevel = (o:{keyId:string}) => m(
+ this.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel,
+ `The touch layout uses a flick or multi-tap gesture on key ${def(o.keyId)}, which `+
+ `is only available on version 17.0+ of Keyman`, `
+ Flick and multi-tap gesture support was added to Keyman mobile platforms in
+ version 17.0. Keyboards which include these gestures can still work in earlier
+ versions of Keyman, but any flick and multi-tap gestures will not be available,
+ which may make some characters inaccesssible.
+ `);
static INFO_MinimumWebEngineVersion = SevInfo | 0x0006;
static Info_MinimumWebEngineVersion = (o:{version:string}) => m(
this.INFO_MinimumWebEngineVersion,
- `The compiler has assigned a minimum web engine version of ${o.version} based on features used in this keyboard`
- );
+ `The compiler has assigned a minimum web engine version of ${o.version} based on `+
+ `features used in this keyboard`, `
+ If the [\`&version\` store](https://help.keyman.com/developer/language/reference/version)
+ is not present in the keyboard source, the compiler attempts to assign the
+ lowest possible Keyman version that can support the features found in the
+ keyboard. Details on the history of language features and supported versions
+ can be found at https://help.keyman.com/developer/language/guide/history.
+ `);
+
+ //------------------------------------------------------------------------------|
};
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 5df0da4b73..3577a2e110 100644
--- a/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts
+++ b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts
@@ -217,33 +217,45 @@ export class LdmlCompilerMessages {
static Error_UnparseableTransformFrom = (o: { from: string, message: string }) =>
m(this.ERROR_UnparseableTransformFrom, `Invalid transform from="${def(o.from)}": "${def(o.message)}"`);
+ //------------------------------------------------------------------------------|
+ // max length of detail message lines (checked by verifyCompilerMessagesObject) |
+ //------------------------------------------------------------------------------|
+
static ERROR_IllegalTransformDollarsign = SevErrorTransform | 0x01;
static Error_IllegalTransformDollarsign = (o: { from: string }) => m(
this.ERROR_IllegalTransformDollarsign,
- `Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`,
- `**Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a variable name, `+
- `the variable name may not be valid (A-Z, a-z, 0-9, _, 32 character maximum).`
- );
+ `Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, `
+ **Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a
+ variable name, the variable name may not be valid (A-Z, a-z, 0-9, _, 32
+ character maximum).
+ `);
static ERROR_TransformFromMatchesNothing = SevErrorTransform | 0x02;
- static Error_TransformFromMatchesNothing = (o: { from: string }) =>
- m(this.ERROR_TransformFromMatchesNothing, `Invalid transfom from="${def(o.from)}": Matches an empty string.`);
+ static Error_TransformFromMatchesNothing = (o: { from: string }) => m(
+ this.ERROR_TransformFromMatchesNothing,
+ `Invalid transfom from="${def(o.from)}": Matches an empty string.`
+ );
static ERROR_IllegalTransformPlus = SevErrorTransform | 0x03;
- static Error_IllegalTransformPlus = (o: { from: string }) =>
- m(this.ERROR_IllegalTransformPlus, `Invalid transform from="${def(o.from)}": Unescaped plus (+) is not valid transform syntax.`,
- '**Hint**: Use `\\+` to match a literal plus.');
+ static Error_IllegalTransformPlus = (o: { from: string }) => m(
+ this.ERROR_IllegalTransformPlus,
+ `Invalid transform from="${def(o.from)}": Unescaped plus (+) is not valid transform syntax.`, `
+ **Hint**: Use \`\\+\` to match a literal plus.
+ `);
static ERROR_IllegalTransformAsterisk = SevErrorTransform | 0x04;
- static Error_IllegalTransformAsterisk = (o: { from: string }) =>
- m(this.ERROR_IllegalTransformAsterisk, `Invalid transform from="${def(o.from)}": Unescaped asterisk (*) is not valid transform syntax.`,
- '**Hint**: Use `\\*` to match a literal asterisk.');
+ static Error_IllegalTransformAsterisk = (o: { from: string }) =>m(
+ this.ERROR_IllegalTransformAsterisk,
+ `Invalid transform from="${def(o.from)}": Unescaped asterisk (*) is not valid transform syntax.`, `
+ **Hint**: Use \`\\*\` to match a literal asterisk.
+ `);
static ERROR_IllegalTransformToUset = SevErrorTransform | 0x05;
static Error_IllegalTransformToUset = (o: { to: string }) => m(
this.ERROR_IllegalTransformToUset,
- `Invalid transform to="${def(o.to)}": Set variable (\\$[…]) cannot be used in 'to=' unless part of a map.`,
- '**Hint**: If a map was meant, must use the form ``.'
- );
+ `Invalid transform to="${def(o.to)}": Set variable (\\$[…]) cannot be used in 'to=' unless part of a map.`, `
+ **Hint**: If a map was meant, must use the form
+ \`\`.
+ `);
}
diff --git a/developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml b/developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml
index d97d711301..c3ef2b3ef6 100644
--- a/developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml
+++ b/developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml
@@ -4,7 +4,7 @@
-
+
diff --git a/developer/src/kmc-ldml/test/fixtures/test-fr.xml b/developer/src/kmc-ldml/test/fixtures/test-fr.xml
index a150915901..401a86bdbf 100644
--- a/developer/src/kmc-ldml/test/fixtures/test-fr.xml
+++ b/developer/src/kmc-ldml/test/fixtures/test-fr.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts
index cd6cf6df36..04eae4afd8 100644
--- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts
+++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts
@@ -105,12 +105,14 @@ export class KmpCompiler implements KeymanCompiler {
return null;
}
+ outputFilename = outputFilename ?? inputFilename.replace(/\.kps$/, '.kmp');
+
//
// Validate the package file
//
const validation = new PackageValidation(this.callbacks, this.options);
- if(!validation.validate(inputFilename, kmpJsonData)) {
+ if(!validation.validate(inputFilename, outputFilename, kmpJsonData)) {
return null;
}
@@ -127,7 +129,7 @@ export class KmpCompiler implements KeymanCompiler {
artifacts: {
kmp: {
data,
- filename: outputFilename ?? inputFilename.replace(/\.kps$/, '.kmp')
+ filename: outputFilename
}
}
}
diff --git a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts
index 6545b7d963..5a088be128 100644
--- a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts
+++ b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts
@@ -184,5 +184,31 @@ export class PackageCompilerMessages {
static Warn_VisualKeyboardFileIsInvalid = (o:{filename: string}) => m(
this.WARN_VisualKeyboardFileIsInvalid, `Visual keyboard file '${def(o.filename)}' is invalid.`
);
+
+ static WARN_PackageVersionIsUnrecognizedFormat = SevWarn | 0x0027;
+ static Warn_PackageVersionIsUnrecognizedFormat = (o:{version: string}) => m(
+ this.WARN_PackageVersionIsUnrecognizedFormat, `Package version '${def(o.version)}' has an unrecognized format.`,
+ `The format for version numbers should be number[.number[.number]]. Each
+ number component should be an integer, without leading zeroes.`
+ );
+
+ static ERROR_PackageMustNotContainItself = SevError | 0x0028;
+ static Error_PackageMustNotContainItself = (o:{outputFilename: string}) => m(
+ this.ERROR_PackageMustNotContainItself, `The package may not include a .kmp file of the same name '${def(o.outputFilename)}'.`, `
+ While it is possible for a package file to contain other .kmp package files,
+ it is an error for a package file to contain a package with the same name as
+ itself.
+
+ **Note:** it is not recommended to include other package files within a
+ package, as the user experience for installation and uninstallation is
+ complex, and not consistent across all platforms.
+
+ **Note**: Nested packages are not checked for validity or whether or not they
+ may violate this rule transitively.
+ `);
+
+ //------------------------------------------------------------------------------|
+ // max length of detail message lines (checked by verifyCompilerMessagesObject) |
+ //------------------------------------------------------------------------------|
}
diff --git a/developer/src/kmc-package/src/compiler/package-validation.ts b/developer/src/kmc-package/src/compiler/package-validation.ts
index 3f98f7d830..ae5fd331f5 100644
--- a/developer/src/kmc-package/src/compiler/package-validation.ts
+++ b/developer/src/kmc-package/src/compiler/package-validation.ts
@@ -12,7 +12,7 @@ export class PackageValidation {
constructor(private callbacks: CompilerCallbacks, private options: CompilerOptions) {
}
- public validate(filename: string, kmpJson: KmpJsonFile.KmpJsonFile) {
+ public validate(filename: string, outputFilename: string, kmpJson: KmpJsonFile.KmpJsonFile) {
if(!this.checkForModelsAndKeyboardsInSamePackage(kmpJson)) {
return false;
}
@@ -22,7 +22,7 @@ export class PackageValidation {
if(!this.checkLexicalModels(filename, kmpJson)) {
return false;
}
- if(!this.checkContentFiles(kmpJson)) {
+ if(!this.checkContentFiles(kmpJson, outputFilename)) {
return false;
}
if(!this.checkPackageInfo(kmpJson)) {
@@ -130,9 +130,9 @@ export class PackageValidation {
return true;
}
- private checkContentFiles(kmpJson: KmpJsonFile.KmpJsonFile): boolean {
+ private checkContentFiles(kmpJson: KmpJsonFile.KmpJsonFile, outputFilename: string): boolean {
for(let file of kmpJson.files) {
- if(!this.checkContentFile(file)) {
+ if(!this.checkContentFile(file, outputFilename)) {
return false;
}
}
@@ -140,7 +140,7 @@ export class PackageValidation {
return true;
}
- private checkContentFile(file: KmpJsonFile.KmpJsonFileContentFile): boolean {
+ private checkContentFile(file: KmpJsonFile.KmpJsonFileContentFile, outputFilename: string): boolean {
const filename = this.callbacks.path.basename(file.name);
const ext = this.callbacks.path.extname(filename);
const base = filename.substring(0, filename.length-ext.length);
@@ -155,9 +155,20 @@ export class PackageValidation {
return false;
}
+ if(this.doesContentFileEqualOutputFilename(file, outputFilename)) {
+ this.callbacks.reportMessage(PackageCompilerMessages.Error_PackageMustNotContainItself({outputFilename}));
+ return false;
+ }
+
return true;
}
+ private doesContentFileEqualOutputFilename(file: KmpJsonFile.KmpJsonFileContentFile, outputFilename: string): boolean {
+ // case-insensitive comparison because Windows has case-insensitive filenames
+ return this.callbacks.path.basename(file.name).toLowerCase() ==
+ this.callbacks.path.basename(outputFilename).toLowerCase();
+ }
+
private checkIfContentFileIsDangerous(file: KmpJsonFile.KmpJsonFileContentFile): boolean {
const filename = this.callbacks.path.basename(file.name).toLowerCase();
diff --git a/developer/src/kmc-package/src/compiler/package-version-validator.ts b/developer/src/kmc-package/src/compiler/package-version-validator.ts
index b20e086aec..7250b6bef4 100644
--- a/developer/src/kmc-package/src/compiler/package-version-validator.ts
+++ b/developer/src/kmc-package/src/compiler/package-version-validator.ts
@@ -91,6 +91,11 @@ export class PackageVersionValidator {
kmp.info.version = {description: kmp.keyboards[0].version};
}
+ if(result && !isValidVersionNumber(kmp.info.version.description)) {
+ this.callbacks.reportMessage(PackageCompilerMessages.Warn_PackageVersionIsUnrecognizedFormat({version: kmp.info.version.description}));
+ return false;
+ }
+
return result;
}
@@ -109,3 +114,11 @@ export class PackageVersionValidator {
return true;
}
}
+
+function isValidVersionNumber(version: string) {
+ return /^(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*)){0,2}$/.test(version);
+}
+
+export const unitTestEndpoints = {
+ isValidVersionNumber,
+};
diff --git a/developer/src/kmc-package/test/fixtures/invalid/error_package_must_not_contain_itself.kps b/developer/src/kmc-package/test/fixtures/invalid/error_package_must_not_contain_itself.kps
new file mode 100644
index 0000000000..430ccd3b71
--- /dev/null
+++ b/developer/src/kmc-package/test/fixtures/invalid/error_package_must_not_contain_itself.kps
@@ -0,0 +1,37 @@
+
+
+
+ 15.0.266.0
+ 7.0
+
+
+ error_package_must_not_contain_itself
+ © 2019 National Research Council Canada
+ Eddie Antonio Santos
+ 1.0
+
+
+
+ error_package_must_not_contain_itself.kmp
+ error_package_must_not_contain_itself
+ 0
+ .kmp
+
+
+ basic.kmx
+ Keyboard Basic
+ 0
+ .kmx
+
+
+
+
+ Basic
+ basic
+ 1.0
+
+ Central Khmer (Khmer, Cambodia)
+
+
+
+
diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_package_version_is_unrecognized_format.kps b/developer/src/kmc-package/test/fixtures/invalid/warn_package_version_is_unrecognized_format.kps
new file mode 100644
index 0000000000..30e310bc6e
--- /dev/null
+++ b/developer/src/kmc-package/test/fixtures/invalid/warn_package_version_is_unrecognized_format.kps
@@ -0,0 +1,32 @@
+
+
+
+ 15.0.266.0
+ 7.0
+
+
+
+ warn_package_version_is_unrecognized_format
+ © 2019 National Research Council Canada
+ Eddie Antonio Santos
+ 1.0-invalid
+
+
+
+ basic.kmx
+ Keyboard Basic
+ 0
+ .kmx
+
+
+
+
+ Basic
+ basic
+ 1.0
+
+ Central Khmer (Khmer, Cambodia)
+
+
+
+
diff --git a/developer/src/kmc-package/test/messages.tests.ts b/developer/src/kmc-package/test/messages.tests.ts
index ba5ec56b08..21f1e0e4a2 100644
--- a/developer/src/kmc-package/test/messages.tests.ts
+++ b/developer/src/kmc-package/test/messages.tests.ts
@@ -241,4 +241,11 @@ describe('PackageCompilerMessages', function () {
await testForMessage(this, ['invalid', 'error_package_file_has_empty_version.kps'],
PackageCompilerMessages.ERROR_PackageFileHasEmptyVersion);
});
+
+ it('should generate Warn_PackageVersionIsUnrecognizedFormat if package version field is not a valid format', async function() {
+ await testForMessage(this, ['invalid', 'warn_package_version_is_unrecognized_format.kps'],
+ PackageCompilerMessages.WARN_PackageVersionIsUnrecognizedFormat);
+ });
+
+ // ERROR_PackageMustNotContainItself test in package-compiler.tests.ts
});
diff --git a/developer/src/kmc-package/test/package-compiler.tests.ts b/developer/src/kmc-package/test/package-compiler.tests.ts
index 16c523bda9..a9777a2a08 100644
--- a/developer/src/kmc-package/test/package-compiler.tests.ts
+++ b/developer/src/kmc-package/test/package-compiler.tests.ts
@@ -294,18 +294,30 @@ describe('KmpCompiler', function () {
it(`should handle a range of valid BCP47 tags`, function () {
const inputFilename = makePathToFixture('bcp47', 'valid_bcp47.kps');
+ const outputFilename = makePathToFixture('bcp47', 'valid_bcp47.kmp');
const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename);
assert.isNotNull(kmpJson);
const validation = new PackageValidation(callbacks, {});
- assert.isTrue(validation.validate(inputFilename, kmpJson));
+ assert.isTrue(validation.validate(inputFilename, outputFilename, kmpJson));
});
it(`should reject an invalid BCP47 tag`, function () {
const inputFilename = makePathToFixture('bcp47', 'invalid_bcp47_1.kps');
+ const outputFilename = makePathToFixture('bcp47', 'invalid_bcp47_1.kmp');
const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename);
assert.isNotNull(kmpJson);
const validation = new PackageValidation(callbacks, {});
- assert.isFalse(validation.validate(inputFilename, kmpJson));
+ assert.isFalse(validation.validate(inputFilename, outputFilename, kmpJson));
+ });
+
+ it(`should reject an package that contains itself`, function () {
+ const inputFilename = makePathToFixture('invalid', 'error_package_must_not_contain_itself.kps');
+ const outputFilename = makePathToFixture('invalid', 'error_package_must_not_contain_itself.kmp');
+ const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename);
+ assert.isNotNull(kmpJson);
+ const validation = new PackageValidation(callbacks, {});
+ assert.isFalse(validation.validate(inputFilename, outputFilename, kmpJson));
+ assert.equal(callbacks.messages[0].code, PackageCompilerMessages.ERROR_PackageMustNotContainItself);
});
});
diff --git a/developer/src/kmc-package/test/versioning.tests.ts b/developer/src/kmc-package/test/versioning.tests.ts
index 98d00b853c..adcba30019 100644
--- a/developer/src/kmc-package/test/versioning.tests.ts
+++ b/developer/src/kmc-package/test/versioning.tests.ts
@@ -1,9 +1,10 @@
import 'mocha';
import { assert } from 'chai';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
-import { makePathToFixture } from './helpers/index.js';
-import { KmpCompiler } from '../src/compiler/kmp-compiler.js';
import { KmpJsonFile } from '@keymanapp/common-types';
+import { KmpCompiler } from '../src/compiler/kmp-compiler.js';
+import { unitTestEndpoints } from '../src/compiler/package-version-validator.js';
+import { makePathToFixture } from './helpers/index.js';
// This unit test was translated from a Delphi test
// Keyman.Test.System.CompilePackageVersioningTest, but note the difference in
@@ -41,4 +42,34 @@ describe('package versioning', function () {
assert.isTrue(kmpJson !== null);
});
}
+
+ it('should validate recognized version numbers', function() {
+ const goodVersions = [
+ '1.0',
+ '1',
+ '1.0.2',
+ '2.2.3',
+ '9',
+ '0',
+ ], badVersions = [
+ '0001',
+ '0.1m',
+ '2.3.4.5',
+ '1.03.4',
+ 'test',
+ '1.0-beta',
+ '',
+ null,
+ undefined,
+ 'xxx',
+ ];
+
+ for(const v of goodVersions) {
+ assert.isTrue(unitTestEndpoints.isValidVersionNumber(v), `'${v}' should be recognized as a valid version number`);
+ }
+
+ for(const v of badVersions) {
+ assert.isFalse(unitTestEndpoints.isValidVersionNumber(v), `'${v}' should be recognized as an invalid version number`);
+ }
+ });
});
diff --git a/developer/src/kmc/README.md b/developer/src/kmc/README.md
index 5e5183bff5..cd63fbb7c5 100644
--- a/developer/src/kmc/README.md
+++ b/developer/src/kmc/README.md
@@ -25,7 +25,7 @@ configuration or installation is required.
kmc is also available as an npm package,
[@keymanapp/kmc](https://npmjs.com/package/@keymanapp/kmc).
-You'll need [node.js](https://nodejs.org/), version 18.0 or later.
+You'll need [node.js](https://nodejs.org/), version 20.0 or later.
```shell
npm install -g @keymanapp/kmc
diff --git a/developer/src/kmc/src/commands/analyze.ts b/developer/src/kmc/src/commands/analyze.ts
index 65119c30d9..b0699977c3 100644
--- a/developer/src/kmc/src/commands/analyze.ts
+++ b/developer/src/kmc/src/commands/analyze.ts
@@ -3,18 +3,14 @@ import * as path from 'path';
import { Command, Option } from 'commander';
import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js';
import { InfrastructureMessages } from '../messages/infrastructureMessages.js';
-import { CompilerCallbacks, CompilerLogLevel } from '@keymanapp/developer-utils';
+import { CompilerBaseOptions, CompilerCallbacks } from '@keymanapp/developer-utils';
import { AnalyzeOskCharacterUse, AnalyzeOskRewritePua } from '@keymanapp/kmc-analyze';
import { BaseOptions } from '../util/baseOptions.js';
import { runOnFiles } from '../util/projectRunner.js';
import { exitProcess } from '../util/sysexits.js';
+import { commanderOptionsToBaseOptions } from '../util/extendedCompilerOptions.js';
-interface AnalysisActivityOptions /* not inheriting from CompilerBaseOptions */ {
- /**
- * Reporting level to console, used by NodeCompilerCallbacks (not used in compiler modules;
- * all messages are still reported to the internal log)
- */
- logLevel?: CompilerLogLevel;
+interface AnalysisActivityOptions extends CompilerBaseOptions {
base?: string;
stripDottedCircle?: boolean;
includeCounts?: boolean;
@@ -30,7 +26,7 @@ export function declareAnalyze(program: Command) {
function declareOskCharUse(command: Command) {
let subCommand = command.command('osk-char-use [infile...]');
- BaseOptions.addLogLevel(subCommand);
+ BaseOptions.addAll(subCommand);
subCommand
.description('Analyze On Screen Keyboards for character usage')
.option('-b, --base', 'First PUA codepoint to use, in hexadecimal (default F100)', 'F100')
@@ -75,9 +71,24 @@ function declareOskRewrite(command: Command) {
});
}
+function commanderOptionsToAnalyzeOptions(options: any): AnalysisActivityOptions {
+ // TODO: split options per sub-command
+ const result: AnalysisActivityOptions = {
+ ...commanderOptionsToBaseOptions(options),
+ // AnalysisActivityOptions
+ base: options.base,
+ includeCounts: options.includeCounts ?? false,
+ inputMappingFile: options.inputMappingFile,
+ mappingFile: options.mappingFile,
+ stripDottedCircle: options.stripDottedCircle ?? false,
+ };
+ return result;
+}
+
async function analyze(action: (callbacks: CompilerCallbacks, filenames: string[], options: AnalysisActivityOptions)=>Promise,
- filenames: string[], options: AnalysisActivityOptions): Promise {
- let callbacks = new NodeCompilerCallbacks({logLevel: options.logLevel});
+ filenames: string[], commanderOptions: any): Promise {
+ const options = commanderOptionsToAnalyzeOptions(commanderOptions);
+ let callbacks = new NodeCompilerCallbacks(options);
try {
return await action(callbacks, filenames, options);
} catch(e) {
diff --git a/developer/src/kmc/src/commands/build.ts b/developer/src/kmc/src/commands/build.ts
index 996e767a77..39e0e20578 100644
--- a/developer/src/kmc/src/commands/build.ts
+++ b/developer/src/kmc/src/commands/build.ts
@@ -7,20 +7,18 @@ import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js';
import { InfrastructureMessages } from '../messages/infrastructureMessages.js';
import { KeymanFileTypes } from '@keymanapp/common-types';
import { CompilerOptions, CompilerFileCallbacks } from '@keymanapp/developer-utils';
-import { BaseOptions } from '../util/baseOptions.js';
+import { BuildBaseOptions } from '../util/baseOptions.js';
import { expandFileLists } from '../util/fileLists.js';
import { isProject } from '../util/projectLoader.js';
import { buildTestData } from './buildTestData/index.js';
import { buildWindowsPackageInstaller } from './buildWindowsPackageInstaller/index.js';
-import { commandOptionsToCompilerOptions } from '../util/extendedCompilerOptions.js';
+import { commanderOptionsToCompilerOptions } from '../util/extendedCompilerOptions.js';
import { exitProcess } from '../util/sysexits.js';
export function declareBuild(program: Command) {
// TODO: localization?
const buildCommand = program
.command('build')
- .option('--color', 'Force colorization for log messages')
- .option('--no-color', 'No colorization for log messages; if both omitted, detects from console')
// These options are only used with build file but are included here so that
// they are visible in `kmc build --help`
@@ -32,7 +30,7 @@ export function declareBuild(program: Command) {
.option('--no-compiler-version', 'Exclude compiler version metadata from output')
.option('--no-warn-deprecated-code', 'Turn off warnings for deprecated code styles');
- BaseOptions.addAll(buildCommand);
+ BuildBaseOptions.addAll(buildCommand);
buildCommand.command('file [infile...]', {isDefault: true})
.description(`Compile one or more source files or projects ('file' subcommand is default).`)
@@ -74,7 +72,7 @@ function initialize(commanderOptions: any) {
// We use a default callback instance when validating command line, but throw
// it away once we have completed initialization
const initializationCallbacks = new NodeCompilerCallbacks({});
- const options = commandOptionsToCompilerOptions(commanderOptions, initializationCallbacks);
+ const options = commanderOptionsToCompilerOptions(commanderOptions, initializationCallbacks);
return options;
}
diff --git a/developer/src/kmc/src/commands/buildTestData/index.ts b/developer/src/kmc/src/commands/buildTestData/index.ts
index 5343c81702..220bc4b26f 100644
--- a/developer/src/kmc/src/commands/buildTestData/index.ts
+++ b/developer/src/kmc/src/commands/buildTestData/index.ts
@@ -4,13 +4,13 @@ import * as kmcLdml from '@keymanapp/kmc-ldml';
import { CompilerCallbacks, defaultCompilerOptions, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboardXMLSourceFileReader } from '@keymanapp/developer-utils';
import { NodeCompilerCallbacks } from '../../util/NodeCompilerCallbacks.js';
import { fileURLToPath } from 'url';
-import { CommandLineBaseOptions } from 'src/util/baseOptions.js';
+import { CommandLineBuildBaseOptions } from 'src/util/baseOptions.js';
import { exitProcess } from '../../util/sysexits.js';
import { InfrastructureMessages } from '../../messages/infrastructureMessages.js';
import { dirname } from 'node:path';
export async function buildTestData(infile: string, _options: any, commander: any): Promise {
- const options: CommandLineBaseOptions = commander.optsWithGlobals();
+ const options: CommandLineBuildBaseOptions = commander.optsWithGlobals();
let compilerOptions: kmcLdml.LdmlCompilerOptions = {
...defaultCompilerOptions,
diff --git a/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts b/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts
index 06076db929..4dddfac1f2 100644
--- a/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts
+++ b/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts
@@ -3,10 +3,10 @@ import * as path from 'path';
import { CompilerCallbacks, defaultCompilerOptions } from '@keymanapp/developer-utils';
import { NodeCompilerCallbacks } from '../../util/NodeCompilerCallbacks.js';
import { WindowsPackageInstallerCompiler, WindowsPackageInstallerSources } from '@keymanapp/kmc-package';
-import { CommandLineBaseOptions } from 'src/util/baseOptions.js';
+import { CommandLineBuildBaseOptions } from 'src/util/baseOptions.js';
import { exitProcess } from '../../util/sysexits.js';
-interface WindowsPackageInstallerCommandLineOptions extends CommandLineBaseOptions {
+interface WindowsPackageInstallerCommandLineOptions extends CommandLineBuildBaseOptions {
msi: string;
exe: string;
license: string;
diff --git a/developer/src/kmc/src/commands/copy.ts b/developer/src/kmc/src/commands/copy.ts
index 1d4ab94dcf..b7d0aef12f 100644
--- a/developer/src/kmc/src/commands/copy.ts
+++ b/developer/src/kmc/src/commands/copy.ts
@@ -10,16 +10,18 @@ import { InfrastructureMessages } from '../messages/infrastructureMessages.js';
import { BaseOptions } from '../util/baseOptions.js';
import { exitProcess } from '../util/sysexits.js';
import { CopierOptions, KeymanProjectCopier } from '@keymanapp/kmc-copy';
+import { commanderOptionsToBaseOptions } from '../util/extendedCompilerOptions.js';
/* c8 ignore start */
export function declareCopy(program: Command) {
const command = program.command('copy ');
- BaseOptions.addLogLevel(command);
+ BaseOptions.addAll(command);
command
.description('Copy a Keyman keyboard or lexical model project')
.option('-o, --out-path ', 'New name and path for project')
.option('-n, --dry-run', 'Show what would happen, without making changes')
+ .option('-r, --relocate-external', 'Copy external files into "external" folder')
.action(copyProject)
.addHelpText('before', `
can be:
@@ -34,8 +36,11 @@ export function declareCopy(program: Command) {
function commanderOptionsToCopierOptions(options: any): CopierOptions {
const result: CopierOptions = {
+ ...commanderOptionsToBaseOptions(options),
+ // CopierOptions
outPath: options.outPath,
dryRun: options.dryRun ?? false,
+ relocateExternalFiles: options.relocateExternal ?? false,
};
return result;
}
@@ -44,15 +49,16 @@ const MaxDryRunCopyLogMessages = 10000;
async function copyProject(ids: string | string[], _options: any, commander: any): Promise {
const commanderOptions = commander.optsWithGlobals();
- const callbacks = new NodeCompilerCallbacks({logLevel: commanderOptions.logLevel ?? 'info'});
- if(!await doCopy(callbacks, ids, commanderOptions)) {
+ const options = commanderOptionsToCopierOptions(commanderOptions);
+ const callbacks = new NodeCompilerCallbacks(options);
+ if(!await doCopy(callbacks, ids, options)) {
return await exitProcess(1);
}
}
/* c8 ignore stop */
-async function doCopy(callbacks: NodeCompilerCallbacks, sources: string | string[], commanderOptions: any): Promise {
+async function doCopy(callbacks: NodeCompilerCallbacks, sources: string | string[], options: CopierOptions): Promise {
const source = sources;
if(!source || typeof source != 'string') {
@@ -62,8 +68,6 @@ async function doCopy(callbacks: NodeCompilerCallbacks, sources: string | string
return false;
}
- const options = commanderOptionsToCopierOptions(commanderOptions);
-
if(options.dryRun) {
// For dry run, we may need a lot of log messages, to show
// where all the files are copied to
diff --git a/developer/src/kmc/src/commands/generate.ts b/developer/src/kmc/src/commands/generate.ts
index d5abcec989..b362d01125 100644
--- a/developer/src/kmc/src/commands/generate.ts
+++ b/developer/src/kmc/src/commands/generate.ts
@@ -6,6 +6,7 @@ import { BaseOptions } from '../util/baseOptions.js';
import { exitProcess } from '../util/sysexits.js';
import { GeneratorOptions, KeymanKeyboardGenerator, LdmlKeyboardGenerator, LexicalModelGenerator } from '@keymanapp/kmc-generate';
import { CompilerCallbacks, KeymanCompiler } from '@keymanapp/developer-utils';
+import { commanderOptionsToBaseOptions } from '../util/extendedCompilerOptions.js';
/* c8 ignore start */
@@ -18,7 +19,7 @@ export function declareGenerate(program: Command) {
function declareGenerateKmnKeyboard(command: Command) {
const subCommand = command.command('keyman-keyboard ');
- BaseOptions.addLogLevel(subCommand);
+ BaseOptions.addAll(subCommand);
subCommand
.description('Generate a .kmn keyboard project')
.option('-t, --target ', 'Target platforms',
@@ -38,7 +39,7 @@ function declareGenerateKmnKeyboard(command: Command) {
function declareGenerateLdmlKeyboard(command: Command) {
const ldmlSubCommand = command.command('ldml-keyboard ');
- BaseOptions.addLogLevel(ldmlSubCommand);
+ BaseOptions.addAll(ldmlSubCommand);
ldmlSubCommand
.description('Generate an LDML .xml keyboard project')
.option('-t, --target ', 'Target platforms',
@@ -56,7 +57,7 @@ function declareGenerateLdmlKeyboard(command: Command) {
function declareGenerateLexicalModel(command: Command) {
const modelSubCommand = command.command('lexical-model ');
- BaseOptions.addLogLevel(modelSubCommand);
+ BaseOptions.addAll(modelSubCommand);
modelSubCommand
.description('Generate a wordlist lexical model project')
.option('-o, --out-path ', 'Output path (may exist)')
@@ -74,6 +75,7 @@ function declareGenerateLexicalModel(command: Command) {
function commanderOptionsToGeneratorOptions(id: string, options: any): GeneratorOptions {
const result: GeneratorOptions = {
+ ...commanderOptionsToBaseOptions(options),
// TODO-GENERATE: icon support
// icon: options.icon ?? true,
id,
@@ -84,7 +86,6 @@ function commanderOptionsToGeneratorOptions(id: string, options: any): Generator
version: options.version ?? '1.0',
author: options.author ?? id,
copyright: options.copyright ?? options.author ?? id,
- logLevel: options.logLevel ?? 'info',
description: options.description ?? '',
};
return result;
@@ -99,32 +100,35 @@ const generateLexicalModel = async (ids: string | string[], _options: any, comma
async function generate(generator: KeymanCompiler, ids: string | string[], commander: any): Promise {
const commanderOptions = commander.optsWithGlobals();
- const callbacks = new NodeCompilerCallbacks({logLevel: commanderOptions.logLevel ?? 'info'});
- if(!await doGenerate(callbacks, generator, ids, commanderOptions)) {
+
+ const id = ids;
+
+ if(!id || typeof id != 'string') {
+ // Note that commander can pass an array for the ids parameter, so we
+ // constrain here; as this is a parameter error before we have parsed
+ // other options, we use a 'default' compiler callback to generate
+ // the error message
+ (new NodeCompilerCallbacks({})).reportMessage(InfrastructureMessages.Error_GenerateRequiresId());
+ return await exitProcess(1);
+ }
+
+ const options = commanderOptionsToGeneratorOptions(id, commanderOptions);
+
+ const callbacks = new NodeCompilerCallbacks(options);
+ if(!await doGenerate(callbacks, generator, options)) {
return await exitProcess(1);
}
}
/* c8 ignore stop */
-async function doGenerate(callbacks: CompilerCallbacks, generator: KeymanCompiler, ids: string | string[], commanderOptions: any): Promise {
- const id = ids;
-
- if(!id || typeof id != 'string') {
- // Note that commander can pass an array for the ids parameter, so we
- // constrain here
- callbacks.reportMessage(InfrastructureMessages.Error_GenerateRequiresId());
- return false;
- }
-
- const options = commanderOptionsToGeneratorOptions(id, commanderOptions);
-
+async function doGenerate(callbacks: CompilerCallbacks, generator: KeymanCompiler, options: GeneratorOptions): Promise {
try {
if(!await generator.init(callbacks, options)) {
// errors will have been reported by the generator
return false;
}
- const result = await generator.run(id); // note: id is currently ignored here
+ const result = await generator.run(options.id); // note: id is currently ignored here
if(!result) {
// errors will have been reported by the generator
return false;
diff --git a/developer/src/kmc/src/commands/messageCommand.ts b/developer/src/kmc/src/commands/messageCommand.ts
index 257eacc913..d0feac380e 100644
--- a/developer/src/kmc/src/commands/messageCommand.ts
+++ b/developer/src/kmc/src/commands/messageCommand.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { Command, Option } from 'commander';
-import { escapeMarkdownChar, KeymanUrls, CompilerBaseOptions, CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerEvent } from '@keymanapp/developer-utils';
+import { escapeMarkdownChar, KeymanUrls, CompilerBaseOptions, CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerEvent, dedentCompilerMessageDetail } from '@keymanapp/developer-utils';
import { CompilerMessageSource, messageNamespaceKeys, messageSources } from '../messages/messageNamespaces.js';
import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js';
import { exitProcess } from '../util/sysexits.js';
@@ -189,7 +189,7 @@ function getMessageDetail(cls: any, id: string, escapeMarkdown: boolean): Compil
throw new Error(`Call to ${cls.name}.${f} returned null`);
}
- event.detail = (event.detail ?? '').replace(/^[ ]+/gm, ''); // TODO(lowpri): dedent may be too naive?
+ event.detail = dedentCompilerMessageDetail(event);
event.message = event.message ?? '';
event.message = event?.exceptionVar
? 'This is an internal error; the message will vary'
diff --git a/developer/src/kmc/src/kmlmp.ts b/developer/src/kmc/src/kmlmp.ts
index bb0cda5e53..a1346ac0d3 100644
--- a/developer/src/kmc/src/kmlmp.ts
+++ b/developer/src/kmc/src/kmlmp.ts
@@ -3,7 +3,7 @@
* kmlmp - Keyman Lexical Model Package Compiler
*/
-// Note: this is a deprecated package and will be removed in Keyman 18.0
+// Note: this is a deprecated package and will be removed in Keyman 19.0
import { Command } from 'commander';
import { KmpCompiler } from '@keymanapp/kmc-package';
diff --git a/developer/src/kmc/src/util/TestKeymanSentry.ts b/developer/src/kmc/src/util/TestKeymanSentry.ts
index 430ab73d68..b8572516bd 100644
--- a/developer/src/kmc/src/util/TestKeymanSentry.ts
+++ b/developer/src/kmc/src/util/TestKeymanSentry.ts
@@ -43,7 +43,7 @@ export class TestKeymanSentry {
try {
throw new Error('Test error from -sentry-client-test-exception event');
} catch(e: any) {
- await KeymanSentry.captureException(e);
+ await KeymanSentry.captureException(e, true);
}
}
}
diff --git a/developer/src/kmc/src/util/baseOptions.ts b/developer/src/kmc/src/util/baseOptions.ts
index ab07921dbd..f3223238ec 100644
--- a/developer/src/kmc/src/util/baseOptions.ts
+++ b/developer/src/kmc/src/util/baseOptions.ts
@@ -10,9 +10,10 @@ export interface CommandLineBaseOptions {
logLevel?: CompilerLogLevel;
logFormat?: CompilerLogFormat;
color?: boolean;
+};
- // This option is not in CompilerBaseOptions
- outFile?:string;
+export interface CommandLineBuildBaseOptions extends CommandLineBaseOptions {
+ outFile?: string;
}
/**
@@ -27,6 +28,22 @@ export class BaseOptions {
return program.addOption(new Option('--log-format ', 'Log format').choices(ALL_COMPILER_LOG_FORMATS).default('formatted'));
}
+ public static addColor(program: Command) {
+ return program
+ .option('--color', 'Force colorization for log messages')
+ .option('--no-color', 'No colorization for log messages; if both omitted, detects from console');
+ }
+
+ public static addAll(program: Command) {
+ return [
+ this.addLogLevel,
+ this.addLogFormat,
+ this.addColor,
+ ].reduce((p,f) => f(p), program);
+ }
+}
+
+export class BuildBaseOptions extends BaseOptions {
public static addOutFile(program: Command) {
return program.option('-o, --out-file ', 'Override the default path and filename for the output file')
}
@@ -35,6 +52,7 @@ export class BaseOptions {
return [
this.addLogLevel,
this.addLogFormat,
+ this.addColor,
this.addOutFile,
].reduce((p,f) => f(p), program);
}
diff --git a/developer/src/kmc/src/util/extendedCompilerOptions.ts b/developer/src/kmc/src/util/extendedCompilerOptions.ts
index 0d72b5b550..9a2e482f68 100644
--- a/developer/src/kmc/src/util/extendedCompilerOptions.ts
+++ b/developer/src/kmc/src/util/extendedCompilerOptions.ts
@@ -1,4 +1,4 @@
-import { CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageOverride, CompilerMessageOverrideMap, CompilerOptions } from '@keymanapp/developer-utils';
+import { CompilerBaseOptions, CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageOverride, CompilerMessageOverrideMap, CompilerOptions } from '@keymanapp/developer-utils';
import { InfrastructureMessages } from '../messages/infrastructureMessages.js';
import { CompilerMessageSource, messageNamespaceKeys, messageSources } from '../messages/messageNamespaces.js';
@@ -27,7 +27,7 @@ export interface ExtendedCompilerOptions extends CompilerOptions {
* @returns CompilerMessageOverride map with the new severity level for the
* message; or `null` if parameter is invalid.
*/
-function commandOptionsMessageToCompilerOptionsMessage(message: string, callbacks: CompilerCallbacks): CompilerMessageOverride {
+function commanderOptionsMessageToCompilerOptionsMessage(message: string, callbacks: CompilerCallbacks): CompilerMessageOverride {
const pattern = /^(KM)?([0-9a-f]+)(:(D|Disable|I|Info|H|Hint|W|Warn|E|Error))?$/i;
const result = message.match(pattern);
if(!result) {
@@ -50,14 +50,14 @@ function commandOptionsMessageToCompilerOptionsMessage(message: string, callback
return override;
}
-function commandOptionsMessagesToCompilerOptionsMessages(messages: any, callbacks: CompilerCallbacks): CompilerMessageOverrideMap {
+function commanderOptionsMessagesToCompilerOptionsMessages(messages: any, callbacks: CompilerCallbacks): CompilerMessageOverrideMap {
if(!messages || !Array.isArray(messages)) {
return {};
}
const result: CompilerMessageOverrideMap = {};
for(let message of messages) {
- const override = commandOptionsMessageToCompilerOptionsMessage(message, callbacks);
+ const override = commanderOptionsMessageToCompilerOptionsMessage(message, callbacks);
if(!override) {
return null;
}
@@ -165,14 +165,23 @@ function checkMessageOverride(override: CompilerMessageOverride, callbacks: Comp
return true;
}
+export function commanderOptionsToBaseOptions(options: any): CompilerBaseOptions {
+ return {
+ // CompilerBaseOptions
+ logLevel: options.logLevel,
+ logFormat: options.logFormat,
+ color: options.color,
+ };
+}
+
/**
* Maps command line compiler options to the compiler API options.
* @param options
* @param callbacks
* @returns
*/
-export function commandOptionsToCompilerOptions(options: any, callbacks: CompilerCallbacks): ExtendedCompilerOptions {
- const overrides = commandOptionsMessagesToCompilerOptionsMessages(options.message, callbacks);
+export function commanderOptionsToCompilerOptions(options: any, callbacks: CompilerCallbacks): ExtendedCompilerOptions {
+ const overrides = commanderOptionsMessagesToCompilerOptionsMessages(options.message, callbacks);
if(!overrides) {
return null;
}
@@ -181,10 +190,7 @@ export function commandOptionsToCompilerOptions(options: any, callbacks: Compile
// properties that we have in CompilerOptions, but nor do we want to rename
// CompilerOptions properties...
return {
- // CompilerBaseOptions
- logLevel: options.logLevel,
- logFormat: options.logFormat,
- color: options.color,
+ ...commanderOptionsToBaseOptions(options),
// CompilerOptions
shouldAddCompilerVersion: options.compilerVersion,
saveDebug: options.debug,
@@ -202,5 +208,5 @@ export function commandOptionsToCompilerOptions(options: any, callbacks: Compile
*/
export const unitTestEndpoints = {
checkMessageOverride,
- commandOptionsMessageToCompilerOptionsMessage
+ commanderOptionsMessageToCompilerOptionsMessage
};
diff --git a/developer/src/kmc/src/util/kmcSentryOptions.ts b/developer/src/kmc/src/util/kmcSentryOptions.ts
index bbb5c09a87..c6c47f732f 100644
--- a/developer/src/kmc/src/util/kmcSentryOptions.ts
+++ b/developer/src/kmc/src/util/kmcSentryOptions.ts
@@ -1,5 +1,19 @@
+import Sentry from "@sentry/node";
+import * as process from "node:process";
import { SentryNodeOptions } from "@keymanapp/developer-utils";
+Sentry.setContext('Command Line', {
+ // obfusate parameters with longer paths e.g. from 'c:/users/name/a/b' to
+ // '…/a/b' to minimize PII risk without losing all command line data.
+ // Also normalizes backslashes to slashes for simplicity.
+ argv: process.argv.map(param => {
+ const p = param.replaceAll('\\', '/').split('/');
+ return (p.length < 3)
+ ? param
+ : ("…/" + p.slice(-2).join('/'));
+ }).join(' ')
+});
+
/**
* Rewrites sourcemap paths for the esbuild distribution of kmc (as used in
* Keyman Developer itself) //kmc.mjs to /dist/kmc.mjs, so that
diff --git a/developer/src/kmc/test/extendedCompilerOptions.tests.ts b/developer/src/kmc/test/extendedCompilerOptions.tests.ts
index adda29dfdb..b2f04c81b7 100644
--- a/developer/src/kmc/test/extendedCompilerOptions.tests.ts
+++ b/developer/src/kmc/test/extendedCompilerOptions.tests.ts
@@ -42,7 +42,7 @@ describe('commandOptionsMessageToCompilerOptionsMessage', function () {
];
for(const test of valid) {
it(`should recognize valid message definition ${test.input}`, () => {
- const result = unitTestEndpoints.commandOptionsMessageToCompilerOptionsMessage(test.input, callbacks);
+ const result = unitTestEndpoints.commanderOptionsMessageToCompilerOptionsMessage(test.input, callbacks);
callbacks.printMessages();
assert.deepEqual(result, { code: CompilerError.error(test.result.code), level: test.result.level });
assert.isEmpty(callbacks.messages);
@@ -58,7 +58,7 @@ describe('commandOptionsMessageToCompilerOptionsMessage', function () {
];
for(const test of invalid) {
it(`should reject invalid message definition ${test.input}`, () => {
- assert.isNull(unitTestEndpoints.commandOptionsMessageToCompilerOptionsMessage(test.input, callbacks));
+ assert.isNull(unitTestEndpoints.commanderOptionsMessageToCompilerOptionsMessage(test.input, callbacks));
assert.isTrue(callbacks.hasMessage(test.code));
});
}
diff --git a/developer/src/kmc/test/generateKmnKeyboard.tests.ts b/developer/src/kmc/test/generateKmnKeyboard.tests.ts
index 5fd29f5310..811adc49b3 100644
--- a/developer/src/kmc/test/generateKmnKeyboard.tests.ts
+++ b/developer/src/kmc/test/generateKmnKeyboard.tests.ts
@@ -6,20 +6,21 @@ import 'mocha';
import { assert } from 'chai';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
-import { KeymanKeyboardGenerator } from '@keymanapp/kmc-generate';
+import { GeneratorOptions, KeymanKeyboardGenerator } from '@keymanapp/kmc-generate';
import { unitTestEndpoints } from '../src/commands/generate.js';
-import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js';
+import { KeymanTargets } from '@keymanapp/common-types';
describe('generateKeymanKeyboard', function() {
- const keyboardOptions = {
- icon: true,
+ const keyboardOptions: GeneratorOptions = {
+ // icon: true,
// id is written by generate functions
+ id: '',
languageTags: ['en'],
name: 'Sample Keyboard',
outPath: '.',
- targets: 'any',
+ targets: [ KeymanTargets.KeymanTarget.any ],
version: '1.0',
author: 'Keyman',
copyright: 'Keyman',
@@ -45,32 +46,11 @@ describe('generateKeymanKeyboard', function() {
outPath = null;
});
- it('should only allow a single id', async function() {
- assert.isFalse(await unitTestEndpoints.doGenerate(
- callbacks,
- new KeymanKeyboardGenerator(),
- ['sample1','sample2'],
- {...keyboardOptions}
- ));
- assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId));
- });
-
- it('should require an id parameter', async function() {
- assert.isFalse(await unitTestEndpoints.doGenerate(
- callbacks,
- new KeymanKeyboardGenerator(),
- null,
- {...keyboardOptions}
- ));
- assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId));
- });
-
it('should generate a Keyman keyboard project', async function() {
assert.isTrue(await unitTestEndpoints.doGenerate(
callbacks,
new KeymanKeyboardGenerator(),
- 'sample',
- {...keyboardOptions, outPath}
+ {...keyboardOptions, id: 'sample', outPath}
));
// Note: we won't test contents of files as this is tested in kmc-generate directly
diff --git a/developer/src/kmc/test/generateLdmlKeyboard.tests.ts b/developer/src/kmc/test/generateLdmlKeyboard.tests.ts
index ad885c3dab..f28d1fec8f 100644
--- a/developer/src/kmc/test/generateLdmlKeyboard.tests.ts
+++ b/developer/src/kmc/test/generateLdmlKeyboard.tests.ts
@@ -6,20 +6,21 @@ import 'mocha';
import { assert } from 'chai';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
-import { LdmlKeyboardGenerator } from '@keymanapp/kmc-generate';
+import { GeneratorOptions, LdmlKeyboardGenerator } from '@keymanapp/kmc-generate';
import { unitTestEndpoints } from '../src/commands/generate.js';
-import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js';
+import { KeymanTargets } from '@keymanapp/common-types';
describe('generateLdmlKeyboard', function() {
- const keyboardOptions = {
- icon: true,
+ const keyboardOptions: GeneratorOptions = {
+ // icon: true,
// id is written by generate functions
+ id: '',
languageTags: ['en'],
name: 'Sample Keyboard',
outPath: '.',
- targets: 'any',
+ targets: [KeymanTargets.KeymanTarget.any],
version: '1.0',
author: 'Keyman',
copyright: 'Keyman',
@@ -45,32 +46,11 @@ describe('generateLdmlKeyboard', function() {
outPath = null;
});
- it('should only allow a single id', async function() {
- assert.isFalse(await unitTestEndpoints.doGenerate(
- callbacks,
- new LdmlKeyboardGenerator(),
- ['sample1','sample2'],
- { ...keyboardOptions }
- ));
- assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId));
- });
-
- it('should require an id parameter', async function() {
- assert.isFalse(await unitTestEndpoints.doGenerate(
- callbacks,
- new LdmlKeyboardGenerator(),
- null,
- { ...keyboardOptions }
- ));
- assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId));
- });
-
it('should generate a LDML keyboard project', async function() {
assert.isTrue(await unitTestEndpoints.doGenerate(
callbacks,
new LdmlKeyboardGenerator(),
- 'sample',
- { ...keyboardOptions, outPath }
+ { ...keyboardOptions, id: 'sample', outPath }
));
// Note: we won't test contents of files as this is tested in kmc-generate directly
diff --git a/developer/src/kmc/test/generateLexicalModel.tests.ts b/developer/src/kmc/test/generateLexicalModel.tests.ts
index 00b01b489a..47a48098b8 100644
--- a/developer/src/kmc/test/generateLexicalModel.tests.ts
+++ b/developer/src/kmc/test/generateLexicalModel.tests.ts
@@ -5,20 +5,21 @@ import * as path from 'node:path';
import 'mocha';
import { assert } from 'chai';
-import { LexicalModelGenerator } from '@keymanapp/kmc-generate';
+import { GeneratorOptions, LexicalModelGenerator } from '@keymanapp/kmc-generate';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import { unitTestEndpoints } from '../src/commands/generate.js';
-import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js';
+import { KeymanTargets } from '@keymanapp/common-types';
describe('generateLexicalModel', function() {
- const modelOptions = {
- icon: true,
+ const modelOptions: GeneratorOptions = {
+ // icon: true,
// id is written by generate functions
+ id: '',
languageTags: ['en'],
name: 'Sample Model',
outPath: '.',
- targets: 'any',
+ targets: [KeymanTargets.KeymanTarget.any],
version: '1.0',
author: 'Keyman',
copyright: 'Keyman',
@@ -44,32 +45,11 @@ describe('generateLexicalModel', function() {
outPath = null;
});
- it('should only allow a single id', async function() {
- assert.isFalse(await unitTestEndpoints.doGenerate(
- callbacks,
- new LexicalModelGenerator(),
- ['sample1.en.sample','sample2.en.sample'],
- { ...modelOptions }
- ));
- assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId));
- });
-
- it('should require an id parameter', async function() {
- assert.isFalse(await unitTestEndpoints.doGenerate(
- callbacks,
- new LexicalModelGenerator(),
- null,
- { ...modelOptions }
- ));
- assert.isTrue(callbacks.hasMessage(InfrastructureMessages.ERROR_GenerateRequiresId));
- });
-
it('should generate a lexical model project', async function() {
assert.isTrue(await unitTestEndpoints.doGenerate(
callbacks,
new LexicalModelGenerator(),
- 'sample.en.sample',
- { ...modelOptions, outPath }
+ { ...modelOptions, id: 'sample.en.sample', outPath }
));
// Note: we won't test contents of files as this is tested in kmc-generate directly
diff --git a/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas
index ce4add7717..1ba224d50b 100644
--- a/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas
+++ b/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas
@@ -258,6 +258,7 @@ begin
f := TPackageContentFile.Create(kps);
f.FileName := BasePath + ID + '\' + SFolder_Source + '\' + SFile_WelcomeHTM;
kps.Files.Add(f);
+ kps.Options.WelcomeFile := f;
// Add readme
f := TPackageContentFile.Create(kps);
diff --git a/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas
index d8cfa9cd8f..77d93d8df9 100644
--- a/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas
+++ b/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas
@@ -217,6 +217,7 @@ begin
f := TPackageContentFile.Create(kps);
f.FileName := BasePath + ID + '\' + SFolder_Source + '\' + SFile_WelcomeHTM;
kps.Files.Add(f);
+ kps.Options.WelcomeFile := f;
// Add readme
f := TPackageContentFile.Create(kps);
diff --git a/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas
index b5b64f6b86..6edd42dfa2 100644
--- a/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas
+++ b/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas
@@ -19,13 +19,11 @@ type
procedure WriteWordlist;
procedure WriteKPS;
procedure WriteKPJ;
- procedure WriteModelInfo;
function GetModelFilename: string;
function GetWordlistFilename: string;
protected
const
- SFileTemplate_ModelInfo = '%s.model_info'; // in root
SFileTemplate_Package = '%s.model.kps';
SFilename_Model = 'model.ts'; // Only for source
SFilename_Wordlist = 'wordlist.tsv';
@@ -82,8 +80,6 @@ begin
WriteKPJ;
WriteRepositoryMetadata;
-
- WriteModelInfo;
end;
function TModelProjectTemplate.GetModelFilename: string;
@@ -107,15 +103,6 @@ begin
Format(SFolder_Source + '\' + SFileTemplate_Model, [ID]));
end;
-procedure TModelProjectTemplate.WriteModelInfo;
-begin
- // Write modelid.model_info
- Transform(
- Format(SFileTemplate_ModelInfo, ['model']),
- Format(SFileTemplate_ModelInfo, [ID])
- );
-end;
-
procedure TModelProjectTemplate.WriteWordlist;
begin
Transform(SFolder_Source + '\' + SFilename_Wordlist);
@@ -127,10 +114,13 @@ var
begin
kpj := TProject.Create(ptLexicalModel, GetProjectFilename, False);
try
+ kpj.Options.Version := pv20;
kpj.Options.BuildPath := '$PROJECTPATH\' + SFolder_Build;
+ kpj.Options.SourcePath := '$PROJECTPATH\' + SFolder_Source;
kpj.Options.WarnDeprecatedCode := True;
kpj.Options.CompilerWarningsAsErrors := True;
kpj.Options.CheckFilenameConventions := True;
+ kpj.Options.SkipMetadataFiles := False;
// Add model and package to project
kpj.Files.Add(TmodelTsProjectFile.Create(kpj, GetModelFilename, nil));
@@ -140,7 +130,6 @@ begin
kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + SFile_HistoryMD, nil));
kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + SFile_LicenseMD, nil));
kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + SFile_ReadmeMD, nil));
- kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + Format(SFileTemplate_ModelInfo, [ID]), nil));
kpj.Save;
finally
@@ -173,6 +162,7 @@ begin
f := TPackageContentFile.Create(kps);
f.FileName := BasePath + ID + '\' + SFolder_Source + '\' + SFile_WelcomeHTM;
kps.Files.Add(f);
+ kps.Options.WelcomeFile := f;
// Add readme
f := TPackageContentFile.Create(kps);
diff --git a/developer/src/packages.inc.sh b/developer/src/packages.inc.sh
index 61b77e6438..62993c5ab9 100644
--- a/developer/src/packages.inc.sh
+++ b/developer/src/packages.inc.sh
@@ -5,6 +5,7 @@
readonly PACKAGES=(
common/web/keyman-version
+ common/web/langtags
common/web/types
core/include/ldml
developer/src/common/web/utils
diff --git a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas
index 1712523026..1e22070ad7 100644
--- a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas
+++ b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas
@@ -39,7 +39,7 @@ uses
UframeTextEditor,
UfrmMDIEditor,
UfrmTike,
- UserMessages;
+ UserMessages, Keyman.Developer.UI.RichEdit41;
type
TLdmlDebugUIStatus = (
@@ -340,7 +340,7 @@ begin
if not SetKeyEventContext then
Exit(False);
- if km_core_process_event(FDebugCore.State, Message.WParam, modifier, 1, KM_CORE_EVENT_FLAG_DEFAULT) = KM_CORE_STATUS_OK then
+ if km_core_process_event(FDebugCore.State, VKToScanCodeToVK(Message.WParam, GetKeyboardLayout(0)), modifier, 1, KM_CORE_EVENT_FLAG_DEFAULT) = KM_CORE_STATUS_OK then
begin
// Process keystroke -- true = swallow keystroke
Result := True;
diff --git a/developer/src/tike/child/UfrmDebug.pas b/developer/src/tike/child/UfrmDebug.pas
index d322d406a7..36ef51d291 100644
--- a/developer/src/tike/child/UfrmDebug.pas
+++ b/developer/src/tike/child/UfrmDebug.pas
@@ -41,7 +41,7 @@ uses
UfrmDebugStatus,
UfrmMDIEditor,
UfrmTike,
- UserMessages;
+ UserMessages, Keyman.Developer.UI.RichEdit41;
type
TDebugLineEvent = procedure(Sender: TObject; ALine: Integer) of object;
@@ -465,7 +465,7 @@ begin
if not SetKeyEventContext then
Exit(False);
- if km_core_process_event(FDebugCore.State, Message.WParam, modifier, 1, KM_CORE_EVENT_FLAG_DEFAULT) = KM_CORE_STATUS_OK then
+ if km_core_process_event(FDebugCore.State, VKToScanCodeToVK(Message.WParam, GetKeyboardLayout(0)), modifier, 1, KM_CORE_EVENT_FLAG_DEFAULT) = KM_CORE_STATUS_OK then
begin
// Process keystroke -- true = swallow keystroke
Result := True;
diff --git a/developer/src/tike/child/UfrmPackageEditor.pas b/developer/src/tike/child/UfrmPackageEditor.pas
index 5aa02b50b3..4f3a096bd9 100644
--- a/developer/src/tike/child/UfrmPackageEditor.pas
+++ b/developer/src/tike/child/UfrmPackageEditor.pas
@@ -319,7 +319,7 @@ type
procedure UpdateStartMenuPrograms;
procedure FillFileList(combo: TComboBox; obj: TObject; FileType: TKMFileType = ftOther);
procedure UpdateImagePreviews;
- procedure AddFile(FileName: WideString);
+ procedure AddFile(FileName: string);
procedure SourceChanged(Sender: TObject);
procedure MovePackageToSource;
function MoveSourceToPackage: Boolean;
@@ -381,6 +381,7 @@ implementation
uses
System.Math,
+ System.StrUtils,
Vcl.Clipbrd,
Vcl.Imaging.GifImg,
@@ -708,7 +709,7 @@ begin
mtConfirmation, mbOkCancel, 0) = mrOk;
end;
-procedure TfrmPackageEditor.AddFile(FileName: WideString);
+procedure TfrmPackageEditor.AddFile(FileName: string);
var
f: TPackageContentFile;
ki: TKeyboardInfo;
@@ -771,7 +772,7 @@ begin
end;
pack.Files.Add(f);
- lbFiles.ItemIndex := lbFiles.Items.AddObject(ExtractFileName(FileName), f);
+ lbFiles.ItemIndex := lbFiles.Items.AddObject(ExtractFileName(FileName.Replace('/','\',[rfReplaceAll])), f);
lbFilesClick(lbFiles);
UpdateWelcomeFile;
UpdateReadme;
@@ -1315,7 +1316,7 @@ begin
lbFiles.Clear;
for i := 0 to pack.Files.Count - 1 do
- lbFiles.Items.AddObject(ExtractFileName(pack.Files[i].FileName), pack.Files[i]);
+ lbFiles.Items.AddObject(ExtractFileName(ReplaceStr(pack.Files[i].FileName,'/','\')), pack.Files[i]);
UpdateOutPath;
UpdateWelcomeFile;
diff --git a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas
index 24c070a5e8..168ea8c77d 100644
--- a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas
+++ b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas
@@ -1,20 +1,30 @@
+(*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Wrapper functions for the kmc command line tool
+ *)
unit Keyman.Developer.System.KmcWrapper;
interface
uses
- Keyman.Developer.System.Project.ProjectFile;
+ Keyman.Developer.System.Project.ProjectFile,
+ utilexecute;
type
TKmcWrapper = class
+ private
+ function Run(const parameters: TArray; const path, logFilename: string; Callback: TUtilExecuteCallbackEvent): Boolean;
public
- function Compile(ProjectFile: TProjectFile; const infile, outfile: string; debug: Boolean): Boolean;
+ function Compile(ProjectFile: TProjectFile; const infile, outfile: string; debug: Boolean; Callback: TUtilExecuteCallbackEvent = nil): Boolean;
+ function Copy(const source, dest, cwd: string; relocateExternal: Boolean; Callback: TUtilExecuteCallbackEvent = nil): Boolean;
end;
const
// TODO: these are still used for messages raised from the IDE, perhaps need to be factored out
CERR_ERROR = $00004000;
CERR_WARNING = $00002000;
+ CERR_INFO = $00000000;
implementation
@@ -24,8 +34,8 @@ uses
Keyman.Developer.System.Project.Project,
Keyman.Developer.System.Project.ProjectLog,
- Keyman.Developer.System.KeymanDeveloperPaths,
- utilexecute;
+ Keyman.Developer.UI.Project.ProjectFileUI,
+ Keyman.Developer.System.KeymanDeveloperPaths;
{ TKmcWrapper }
@@ -33,9 +43,71 @@ function TKmcWrapper.Compile(
ProjectFile: TProjectFile;
const infile: string;
const outfile: string;
- debug: Boolean
+ debug: Boolean;
+ Callback: TUtilExecuteCallbackEvent
): Boolean;
var
+ cmdline: TArray;
+begin
+ cmdline := [
+ 'build',
+ '--log-format', 'tsv',
+ '--log-level', 'info',
+ infile
+ ];
+
+ if outfile <> '' then
+ cmdline := cmdline + ['--out-file', outfile];
+
+ if Assigned(FGlobalProject) and (FGlobalProject.Options.CompilerWarningsAsErrors) then
+ cmdline := cmdline + ['--compiler-warnings-as-errors']
+ else
+ cmdline := cmdline + ['--no-compiler-warnings-as-errors'];
+
+ if not FGlobalProject.Options.WarnDeprecatedCode then
+ cmdline := cmdline + ['--no-warn-deprecated-code'];
+
+ if debug then
+ cmdline := cmdline + ['--debug'];
+
+ Result := Run(cmdline, ExtractFileDir(infile), infile, Callback);
+end;
+
+function TKmcWrapper.Copy(const source, dest, cwd: string;
+ relocateExternal: Boolean;
+ Callback: TUtilExecuteCallbackEvent): Boolean;
+var
+ destFilename, destPath: string;
+ cmdline: TArray;
+begin
+ if not dest.EndsWith('.kpj') then
+ raise Exception.Create('Invalid destination "'+dest+'" for TKmcWrapper.copy: must be a filename ending in .kpj');
+ destFilename := ChangeFileExt(ExtractFileName(dest), '');
+ destPath := ExtractFileDir(dest);
+ if ExtractFileName(destPath) <> destFilename then
+ raise Exception.Create('Invalid destination "'+dest+'" for TKmcWrapper.copy: filename must match target folder name');
+
+ cmdline := [
+ 'copy',
+ '--out-path', destPath,
+ '--log-format', 'tsv',
+ '--log-level', 'info',
+ source
+ ];
+
+ if relocateExternal then
+ cmdline := cmdline + ['--relocate-external'];
+
+ Result := Run(cmdline, cwd, dest, Callback);
+end;
+
+function TKmcWrapper.Run(
+ const parameters: TArray;
+ const path, logFilename: string;
+ Callback: TUtilExecuteCallbackEvent
+): Boolean;
+var
+ param: string;
s: TStringList;
logtext, cmdline: string;
ec: Integer;
@@ -53,26 +125,23 @@ begin
// TODO: log-level as opt?
// TODO: embed in server for better performance
- cmdline := Format('"%s" build --log-format tsv --log-level info "%s"', [TKeymanDeveloperPaths.KmcPath, infile]);
- if outfile <> '' then
- cmdline := cmdline + Format(' --out-file "%s"', [outfile]);
- if FGlobalProject.Options.CompilerWarningsAsErrors then
- cmdline := cmdline + ' --compiler-warnings-as-errors'
- else
- cmdline := cmdline + ' --no-compiler-warnings-as-errors';
- if FGlobalProject.Options.WarnDeprecatedCode then
- cmdline := cmdline + ' --no-warn-deprecated-code';
- if debug then
- cmdline := cmdline + ' --debug';
+ cmdline := '"'+TKeymanDeveloperPaths.KmcPath+'"';
+ for param in parameters do
+ begin
+ cmdline := cmdline + ' "'+param+'"';
+ end;
- Result := TUtilExecute.Console(cmdline, ExtractFileDir(infile), logtext, ec);
+ // Log the precise command line used
+ LogMessage(plsInfo, logFilename, cmdline, CERR_INFO, 0);
+
+ Result := TUtilExecute.Console(cmdline, path, logtext, ec, Callback);
logtext := UTF8ToString(AnsiString(logtext));
if not Result then
begin
- FGlobalProject.Log(plsError, infile,
- Format('Compiler failed to start with error %d: %s', [GetLastError, SysErrorMessage(GetLastError)]), CERR_ERROR, 0);
+ LogMessage(plsError, logFilename,
+ Format('kmc failed to start with error %d: %s', [GetLastError, SysErrorMessage(GetLastError)]), CERR_ERROR, 0);
end;
Result := Result and (ec = 0);
@@ -101,7 +170,7 @@ begin
else if msgType = 'warn' then
begin
state := plsWarning;
- if FGlobalProject.Options.CompilerWarningsAsErrors then
+ if Assigned(FGlobalProject) and FGlobalProject.Options.CompilerWarningsAsErrors then
Result := False;
end
else if msgType = 'error' then
@@ -116,10 +185,10 @@ begin
end
else // assume msgType = 'info' (fine also for 'verbose', 'debug')
state := plsInfo;
- FGlobalProject.Log(state, msgFilename, msgText, msgCode, msgLine);
+ LogMessage(state, msgFilename, msgText, msgCode, msgLine);
end
else
- FGlobalProject.Log(plsInfo, infile, line, 0, 0);
+ LogMessage(plsInfo, logFilename, line, 0, 0);
end;
finally
s.Free;
diff --git a/developer/src/tike/help/Keyman.Developer.System.HelpTopics.pas b/developer/src/tike/help/Keyman.Developer.System.HelpTopics.pas
index 3e150aa42c..3b9301768e 100644
--- a/developer/src/tike/help/Keyman.Developer.System.HelpTopics.pas
+++ b/developer/src/tike/help/Keyman.Developer.System.HelpTopics.pas
@@ -7,6 +7,9 @@ const
SHelpTopic_Context_BitmapEditorText = 'context/keyboard-editor#toc-icon-tab';
SHelpTopic_Context_CharacterMap = 'context/character-map';
SHelpTopic_Context_CharacterIdentifier = 'context/character-identifier';
+ SHelpTopic_Context_CloneGitHubProjectParameters = 'context/clone-github-project-parameters';
+ SHelpTopic_Context_CloneKeymanCloudProjectParameters = 'context/clone-keyman-cloud-project-parameters';
+ SHelpTopic_Context_CloneLocalProjectParameters = 'context/clone-local-project-parameters';
SHelpTopic_Context_Debug = 'context/debug';
SHelpTopic_Context_LdmlDebug = 'context/ldml-debug';
SHelpTopic_Context_LdmlEditor = 'context/ldml-editor';
diff --git a/developer/src/tike/main/UfrmMessages.dfm b/developer/src/tike/main/UfrmMessages.dfm
index bed2632463..aed03fb40c 100644
--- a/developer/src/tike/main/UfrmMessages.dfm
+++ b/developer/src/tike/main/UfrmMessages.dfm
@@ -26,7 +26,6 @@ inherited frmMessages: TfrmMessages
Font.Name = 'Courier New'
Font.Style = []
ParentFont = False
- PopupMenu = mnuPopup
ReadOnly = True
ScrollBars = ssBoth
TabOrder = 0
@@ -35,6 +34,8 @@ inherited frmMessages: TfrmMessages
OnClick = memoMessageClick
OnDblClick = memoMessageDblClick
OnKeyDown = memoMessageKeyDown
+ OnMouseDown = memoMessageMouseDown
+ OnMouseMove = memoMessageMouseMove
end
object dlgSave: TSaveDialog
DefaultExt = 'txt'
@@ -51,6 +52,10 @@ inherited frmMessages: TfrmMessages
Default = True
OnClick = mnuViewItemClick
end
+ object mnuOpenDocumentation: TMenuItem
+ Caption = 'Open &documentation on selected message'
+ OnClick = mnuOpenDocumentationClick
+ end
object mnuSeparator1: TMenuItem
Caption = '-'
end
diff --git a/developer/src/tike/main/UfrmMessages.pas b/developer/src/tike/main/UfrmMessages.pas
index f57e097fc4..9a76254c96 100644
--- a/developer/src/tike/main/UfrmMessages.pas
+++ b/developer/src/tike/main/UfrmMessages.pas
@@ -33,7 +33,7 @@ interface
uses
System.Classes,
- System.Contnrs,
+ System.Generics.Collections,
System.SysUtils,
Vcl.Controls,
Vcl.Dialogs,
@@ -54,12 +54,24 @@ uses
UfrmTike, Vcl.ComCtrls;
type
+ TMessageItemSegment = record
+ text: string;
+ color: TColor;
+ underline: Boolean;
+ index: Integer;
+ end;
+
TMessageItem = class
FileName: string;
Msg: string;
MsgCode, Line: Integer;
+
+ Segments: TArray;
+ LineLength: Integer;
end;
+ TMessageItemList = class(TObjectList);
+
TfrmMessages = class(TTikeDockForm)
dlgSave: TSaveDialog;
memoMessage: TRichEdit;
@@ -71,6 +83,7 @@ type
cmdmNextMessage: TMenuItem;
mnuViewItem: TMenuItem;
mnuSeparator1: TMenuItem;
+ mnuOpenDocumentation: TMenuItem;
procedure memoMessageDblClick(Sender: TObject);
procedure cmdmSaveToFileClick(Sender: TObject);
procedure cmdmClearClick(Sender: TObject);
@@ -83,8 +96,13 @@ type
procedure mnuPopupPopup(Sender: TObject);
procedure mnuViewItemClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
+ procedure memoMessageMouseMove(Sender: TObject; Shift: TShiftState; X,
+ Y: Integer);
+ procedure mnuOpenDocumentationClick(Sender: TObject);
+ procedure memoMessageMouseDown(Sender: TObject; Button: TMouseButton;
+ Shift: TShiftState; X, Y: Integer);
private
- FMessageItems: TObjectList;
+ FMessageItems: TMessageItemList;
function GetSelLine: Integer;
procedure SetSelLine(Value: Integer);
protected
@@ -107,6 +125,8 @@ var
implementation
uses
+ System.RegularExpressions,
+ System.Types,
Winapi.RichEdit,
UfrmMain,
@@ -116,7 +136,9 @@ uses
Keyman.Developer.System.Project.Project,
Keyman.Developer.System.Project.ProjectFile,
Keyman.Developer.UI.Project.ProjectFileUI,
- Keyman.Developer.UI.Project.ProjectUI;
+ Keyman.Developer.UI.Project.ProjectUI,
+ Upload_Settings,
+ utilexecute;
{$R *.DFM}
@@ -149,24 +171,29 @@ const // ABGR - text on white bg, Colors similar to Light+ VSCode color theme
INFO_ProjectBuiltSuccessfully = NAMESPACE_Infrastructure or $000B;
INFO_ProjectNotBuiltSuccessfully = NAMESPACE_Infrastructure or $000C;
+ Segment_Filename = 0;
+ Segment_Filename_Separator = 1;
+ Segment_Line = 2;
+ Segment_Line_Separator = 3;
+ Segment_State = 4;
+ Segment_Code = 5;
+ Segment_Code_Separator = 6;
+ Segment_Message = 7;
procedure TfrmMessages.Add(state: TProjectLogState; filename, msg: WideString; MsgCode, line: Integer);
-type
- TSegment = record
- text: string;
- color: TColor;
- end;
var
mi: TMessageItem;
FColor: TColor;
FTextColor: TColor;
- Segments: array of TSegment;
+ Segments: TArray;
+ i, x: Integer;
- procedure AddText(const text: string; color: TColor);
+ procedure AddText(const text: string; color: TColor; underline: Boolean = false);
var
- s: TSegment;
+ s: TMessageItemSegment;
begin
s.text := text;
+ s.underline := underline;
s.color := color;
SetLength(Segments, Length(Segments)+1);
Segments[High(Segments)] := s;
@@ -175,7 +202,7 @@ var
procedure AddLine;
var
t: string;
- s: TSegment;
+ s: TMessageItemSegment;
cr: TCharRange;
cf: TCharFormat;
gtle: TGetTextLengthEx;
@@ -184,7 +211,7 @@ var
// use direct messages to the RichEdit control to emit formatted text
FillChar(cf, sizeof(TCharFormat), 0);
cf.cbSize := sizeof(TCharFormat);
- cf.dwMask := CFM_COLOR;
+ cf.dwMask := CFM_COLOR or CFM_UNDERLINE;
gtle.flags := GTL_PRECISE or GTL_NUMCHARS;
gtle.codepage := 1200;
@@ -198,10 +225,13 @@ var
for s in Segments do
begin
+ if s.Text.Length = 0 then
+ Continue;
cr.cpMin := cr.cpMax;
cr.cpMax := cr.cpMax + s.text.Length;
SendMessage(memoMessage.Handle, EM_EXSETSEL, 0, NativeUint(@cr));
cf.crTextColor := ColorToRGB(s.color);
+ if s.underline then cf.dwEffects := CFE_UNDERLINE else cf.dwEffects := 0;
SendMessage(memoMessage.Handle, EM_SETCHARFORMAT, SCF_SELECTION, NativeUint(@cf));
end;
cr.cpMin := cr.cpMax;
@@ -251,15 +281,45 @@ begin
begin
AddText(':', Color_Text);
AddText(IntToStr(Line), Color_Line);
+ end
+ else
+ begin
+ // Keep same number of segments
+ AddText('', Color_Text);
+ AddText('', Color_Text);
end;
AddText(' - ', Color_Text);
- AddText(ProjectLogStateTitle[state], FColor);
- AddText(' KM'+IntToHex(MsgCode, 5), Color_Code);
+
+ if MsgCode = 0 then
+ begin
+ AddText(ProjectLogStateTitle[state], FColor);
+ AddText('', Color_Text);
+ end
+ else
+ begin
+ AddText(ProjectLogStateTitle[state]+' ', FColor);
+ AddText('KM'+IntToHex(MsgCode, 5), Color_Code, True);
+ end;
+
AddText(': ', Color_Text);
AddText(StringReplace(msg, #13#10, ' ', [rfReplaceAll]), FTextColor);
AddText(#13#10, clBlack);
+
+ // Calculate segment lengths for later cursor + link calculations
+
+ x := 0;
+ for i := 0 to High(Segments) - 1 do
+ begin
+ Segments[i].index := x; Inc(x, Segments[i].text.Length);
+ end;
+ mi.Segments := Segments;
+ mi.LineLength := x;
+
AddLine;
+
+ // Strip #$D#$A from segments for further calculations
+ SetLength(mi.Segments, Length(mi.Segments) - 1);
finally
SendMessage(memoMessage.Handle, WM_SETREDRAW, 1, 0);
SendMessage(memoMessage.Handle, EM_SETEVENTMASK, 0, eventMask);
@@ -293,7 +353,7 @@ begin
SelLine := line;
- mi := FMessageItems[line] as TMessageItem;
+ mi := FMessageItems[line];
FFilename := mi.FileName;
if FFileName = FGlobalProject.FileName then
@@ -333,6 +393,74 @@ begin
Key := 0;
end;
+procedure TfrmMessages.memoMessageMouseDown(Sender: TObject;
+ Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
+var
+ pt: TPointL;
+ ch: Integer;
+ px: TPoint;
+begin
+ if Button = mbRight then
+ begin
+ pt.x := X;
+ pt.y := Y;
+ ch := SendMessage(memoMessage.Handle, EM_CHARFROMPOS, 0, NativeInt(@pt));
+ memoMessage.SelStart := ch;
+ memoMessage.SelLength := 0;
+ px := memoMessage.ClientToScreen(Point(X, Y));
+ mnuPopup.Popup(px.X, px.Y);
+ end;
+end;
+
+procedure TfrmMessages.memoMessageMouseMove(Sender: TObject; Shift: TShiftState;
+ X, Y: Integer);
+var
+ pt: TPOINTL;
+ ch: Integer;
+ Row, Col, i: Integer;
+ mi: TMessageItem;
+ seg: TMessageItemSegment;
+begin
+ pt.x := X;
+ pt.y := Y;
+ ch := SendMessage(memoMessage.Handle, EM_CHARFROMPOS, 0, NativeInt(@pt));
+
+ x := 0;
+ Row := -1;
+ for I := 0 to FMessageItems.Count - 1 do
+ begin
+ if x + FMessageItems[i].LineLength >= ch then
+ begin
+ Row := i;
+ Break;
+ end;
+ Inc(x, FMessageItems[i].LineLength + 1);
+ end;
+
+ if Row < 0 then
+ begin
+ // Mouse is outside bounds of text
+ memoMessage.Cursor := crDefault;
+ Exit;
+ end;
+
+ Col := ch - x;
+ mi := FMessageItems[Row];
+
+ if Length(mi.Segments) <= Segment_Code then
+ begin
+ // Safety check
+ memoMessage.Cursor := crDefault;
+ Exit;
+ end;
+
+ seg := mi.Segments[Segment_Code];
+
+ if (Col >= seg.index) and (Col < seg.index + seg.text.Length)
+ then memoMessage.Cursor := crHandPoint
+ else memoMessage.Cursor := crDefault;
+end;
+
procedure TfrmMessages.cmdmSaveToFileClick(Sender: TObject);
begin
if dlgSave.Execute then
@@ -433,16 +561,37 @@ begin
memoMessage.SelLength := Length(memoMessage.Lines[Value]);
end;
+procedure TfrmMessages.mnuOpenDocumentationClick(Sender: TObject);
+var
+ line: Integer;
+ mi: TMessageItem;
+begin
+ line := SelLine;
+ if line < 0 then Exit;
+ mi := FMessageItems[line];
+ if Length(mi.Segments) <= Segment_Code then
+ Exit;
+
+ TUtilExecute.URL(URL_KmcMessage(mi.Segments[Segment_Code].text));
+end;
+
procedure TfrmMessages.mnuPopupPopup(Sender: TObject);
var
e: Boolean;
+ line: Integer;
begin
e := memoMessage.Lines.Count > 0;
- cmdmNextMessage.Enabled := e and (SelLine < memoMessage.Lines.Count - 1);
- cmdmPreviousMessage.Enabled := e and (SelLine > 0);
+ if e
+ then line := SelLine
+ else line := -1;
+ cmdmNextMessage.Enabled := e and (line < memoMessage.Lines.Count - 1);
+ cmdmPreviousMessage.Enabled := e and (line > 0);
+ e := e and (line >= 0) and (line < FMessageItems.Count);
+
mnuViewItem.Enabled := e;
- cmdmClear.Enabled := e;
- cmdmSaveToFile.Enabled := e;
+ mnuOpenDocumentation.Enabled := e;
+ cmdmClear.Enabled := FMessageItems.Count > 0;
+ cmdmSaveToFile.Enabled := FMessageItems.Count > 0;
end;
procedure TfrmMessages.mnuViewItemClick(Sender: TObject);
@@ -451,14 +600,30 @@ begin
end;
procedure TfrmMessages.memoMessageClick(Sender: TObject);
+var
+ p: TPoint;
+ mi: TMessageItem;
+ seg: TMessageItemSegment;
begin
- ;
+ p := memoMessage.CaretPos;
+ if (p.Y < 0) or (p.Y >= FMessageItems.Count) then
+ Exit;
+
+ mi := FMessageItems[p.Y];
+ if Length(mi.Segments) <= Segment_Code then
+ Exit;
+
+ seg := mi.Segments[Segment_Code];
+ if (p.X >= seg.index) and (p.X < seg.index + seg.text.Length) then
+ begin
+ TUtilExecute.URL(URL_KmcMessage(seg.text));
+ end;
end;
procedure TfrmMessages.FormCreate(Sender: TObject);
begin
inherited;
- FMessageItems := TObjectList.Create;
+ FMessageItems := TMessageItemList.Create;
end;
procedure TfrmMessages.FormDestroy(Sender: TObject);
diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas
index ee5c4cb126..dccd28a650 100644
--- a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas
+++ b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas
@@ -83,6 +83,8 @@ type
TProjectFileUIClass = class of TProjectFileUI;
+procedure LogMessage(AState: TProjectLogState; Filename, Msg: string; MsgCode, line: Integer);
+
implementation
uses
@@ -181,6 +183,11 @@ begin
frmMessages.Add(AState, Filename, Msg, MsgCode, line);
end;
+procedure LogMessage(AState: TProjectLogState; Filename, Msg: string; MsgCode, line: Integer);
+begin
+ frmMessages.Add(AState, Filename, Msg, MsgCode, line);
+end;
+
procedure TProjectUI.Refresh;
begin
if State = psReady then
diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.dfm
new file mode 100644
index 0000000000..006aa4b07f
--- /dev/null
+++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.dfm
@@ -0,0 +1,175 @@
+inherited frmCloneGitHubProjectParameters: TfrmCloneGitHubProjectParameters
+ BorderIcons = [biSystemMenu]
+ BorderStyle = bsDialog
+ Caption = 'Clone Project from GitHub'
+ ClientHeight = 297
+ ClientWidth = 738
+ Position = poScreenCenter
+ ExplicitWidth = 744
+ ExplicitHeight = 326
+ PixelsPerInch = 96
+ TextHeight = 13
+ object cmdOK: TButton
+ Left = 578
+ Top = 264
+ Width = 73
+ Height = 25
+ Anchors = [akRight, akBottom]
+ Caption = 'OK'
+ Default = True
+ TabOrder = 2
+ OnClick = cmdOKClick
+ ExplicitLeft = 441
+ ExplicitTop = 236
+ end
+ object cmdCancel: TButton
+ Left = 657
+ Top = 264
+ Width = 73
+ Height = 25
+ Anchors = [akRight, akBottom]
+ Cancel = True
+ Caption = 'Cancel'
+ ModalResult = 2
+ TabOrder = 3
+ ExplicitLeft = 520
+ ExplicitTop = 236
+ end
+ object gbNewProjectDetails: TGroupBox
+ Left = 9
+ Top = 103
+ Width = 720
+ Height = 154
+ Caption = 'New Project Details'
+ TabOrder = 1
+ object lblFileName: TLabel
+ Left = 16
+ Top = 24
+ Width = 76
+ Height = 13
+ Caption = '&New project ID:'
+ FocusControl = editProjectID
+ end
+ object lblProjectFilename: TLabel
+ Left = 15
+ Top = 126
+ Width = 101
+ Height = 13
+ Caption = 'New project &filename'
+ FocusControl = editProjectFilename
+ end
+ object lblPath: TLabel
+ Left = 15
+ Top = 99
+ Width = 83
+ Height = 13
+ Caption = 'Destination &path:'
+ FocusControl = editPath
+ end
+ object lblSourceProjectLexicalModel: TLabel
+ Left = 15
+ Top = 48
+ Width = 531
+ Height = 13
+ Caption =
+ 'The source project appears to be a lexical model. The new projec' +
+ 't ID must use the author.bcp47.uniq pattern.'
+ end
+ object lblSourceProjectKeyboard: TLabel
+ Left = 16
+ Top = 48
+ Width = 682
+ Height = 13
+ Caption =
+ 'The source project appears to be a keyboard project. The new pro' +
+ 'ject ID must be a valid keyboard id pattern (alphanumeric and un' +
+ 'derscore).'
+ end
+ object editProjectID: TEdit
+ Left = 148
+ Top = 21
+ Width = 205
+ Height = 21
+ TabOrder = 0
+ OnChange = editProjectIDChange
+ end
+ object editProjectFilename: TEdit
+ Left = 147
+ Top = 123
+ Width = 564
+ Height = 21
+ TabStop = False
+ ParentColor = True
+ ReadOnly = True
+ TabOrder = 4
+ OnChange = editProjectIDChange
+ end
+ object cmdBrowse: TButton
+ Left = 644
+ Top = 96
+ Width = 67
+ Height = 21
+ Caption = '&Browse...'
+ TabOrder = 3
+ OnClick = cmdBrowseClick
+ end
+ object editPath: TEdit
+ Left = 147
+ Top = 96
+ Width = 491
+ Height = 21
+ TabOrder = 2
+ OnChange = editGitHubURLChange
+ end
+ object chkRelocateExternal: TCheckBox
+ Left = 147
+ Top = 72
+ Width = 257
+ Height = 17
+ Caption = 'Relocate &external files into new project folder'
+ TabOrder = 1
+ end
+ end
+ object gbSourceProjectDetails: TGroupBox
+ Left = 8
+ Top = 8
+ Width = 721
+ Height = 89
+ Caption = 'Source Project'
+ TabOrder = 0
+ object Label1: TLabel
+ Left = 17
+ Top = 19
+ Width = 58
+ Height = 13
+ Caption = 'GitHub URL:'
+ FocusControl = editGitHubURL
+ end
+ object Label2: TLabel
+ Left = 17
+ Top = 43
+ Width = 512
+ Height = 13
+ Caption =
+ 'A URL to a GitHub repository (or subfolder within a repository) ' +
+ 'that matches the Keyman project file layout'
+ end
+ object Label3: TLabel
+ Left = 17
+ Top = 62
+ Width = 420
+ Height = 13
+ Caption =
+ 'e.g. https://github.com/keyman-keyboards/khmer_angkor/tree/main/' +
+ 'khmer_angkor.kpj'
+ end
+ object editGitHubURL: TEdit
+ Left = 149
+ Top = 16
+ Width = 563
+ Height = 21
+ TabOrder = 0
+ OnChange = editGitHubURLChange
+ end
+ end
+end
diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas
new file mode 100644
index 0000000000..ad042fc872
--- /dev/null
+++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas
@@ -0,0 +1,370 @@
+(*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Parameters dialog for cloning a project from a GitHub repo
+ *)
+unit Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters;
+
+interface
+
+uses
+ System.Classes,
+ System.SysUtils,
+ System.UITypes,
+ System.Variants,
+ Winapi.Messages,
+ Winapi.Windows,
+ Vcl.CheckLst,
+ Vcl.Controls,
+ Vcl.Dialogs,
+ Vcl.ExtCtrls,
+ Vcl.Forms,
+ Vcl.Graphics,
+ Vcl.Grids,
+ Vcl.StdCtrls,
+
+ kpsfile,
+ PackageInfo,
+ UfrmTike,
+ UKeymanTargets,
+ UfrmDownloadProgress,
+ utilfiletypes,
+ Browse4Folder;
+
+type
+ TfrmCloneGitHubProjectParameters = class(TTikeForm)
+ cmdOK: TButton;
+ cmdCancel: TButton;
+ gbNewProjectDetails: TGroupBox;
+ lblFileName: TLabel;
+ lblProjectFilename: TLabel;
+ editProjectID: TEdit;
+ editProjectFilename: TEdit;
+ lblPath: TLabel;
+ cmdBrowse: TButton;
+ editPath: TEdit;
+ chkRelocateExternal: TCheckBox;
+ gbSourceProjectDetails: TGroupBox;
+ Label1: TLabel;
+ editGitHubURL: TEdit;
+ Label2: TLabel;
+ Label3: TLabel;
+ lblSourceProjectLexicalModel: TLabel;
+ lblSourceProjectKeyboard: TLabel;
+ procedure cmdOKClick(Sender: TObject);
+ procedure editSourceProjectFilenameChange(Sender: TObject);
+ procedure FormCreate(Sender: TObject);
+ procedure editVersionChange(Sender: TObject);
+ procedure editGitHubURLChange(Sender: TObject);
+ procedure editProjectIDChange(Sender: TObject);
+ procedure cmdBrowseClick(Sender: TObject);
+ private
+ dlgBrowse: TBrowse4Folder;
+ frmDownloadProgress: TfrmDownloadProgress;
+ function GetBasePath: string;
+ function GetProjectID: string;
+ function Validate: Boolean;
+ procedure EnableControls;
+ procedure UpdateProjectFilename;
+ function GetProjectFilename: string;
+ function GetSourceProjectFilename: string;
+ function GetRelocateExternal: Boolean;
+ procedure DownloadCallback(Owner: TfrmDownloadProgress; var Result: Boolean);
+ procedure DownloadWrapperCallback(var Cancelled: Boolean);
+ function IsSourceProjectALexicalModelProject: Boolean;
+ procedure UpdateSourceProjectNote;
+ protected
+ function GetHelpTopic: string; override;
+ public
+ property BasePath: string read GetBasePath;
+ property ProjectID: string read GetProjectID;
+ property ProjectFilename: string read GetProjectFilename;
+ property SourceProjectFilename: string read GetSourceProjectFilename;
+ property RelocateExternal: Boolean read GetRelocateExternal;
+ end;
+
+function ShowCloneGitHubProjectParameters(Owner: TComponent): Boolean;
+
+implementation
+
+uses
+ System.Net.UrlClient,
+ System.RegularExpressions,
+ Vcl.ComCtrls,
+
+ KeymanDeveloperOptions,
+ Keyman.Developer.System.KmcWrapper,
+ Keyman.Developer.System.HelpTopics,
+ Keyman.Developer.System.Project.Project,
+ Keyman.Developer.System.Project.ProjectFile,
+ Keyman.System.KeyboardUtils,
+ Keyman.System.LexicalModelUtils,
+ UfrmMain;
+
+{$R *.dfm}
+
+function ShowCloneGitHubProjectParameters(Owner: TComponent): Boolean;
+var
+ f: TfrmCloneGitHubProjectParameters;
+ path: string;
+ p: TfrmDownloadProgress;
+begin
+ f := TfrmCloneGitHubProjectParameters.Create(Owner);
+ try
+ Result := f.ShowModal = mrOk;
+ if not Result then
+ Exit;
+
+ path := ExtractFileDir(f.BasePath);
+ if not ForceDirectories(path) then
+ begin
+ ShowMessage('Unable to create path "'+path+'": '+SysErrorMessage(GetLastError));
+ Exit;
+ end;
+
+ p := TfrmDownloadProgress.Create(Owner);
+ try
+ p.Callback := f.DownloadCallback;
+ p.progress.Style := pbstMarquee;
+ Result := p.ShowModal = mrOk;
+ finally
+ p.Free;
+ end;
+ finally
+ f.Free;
+ end;
+end;
+
+{ TfrmCloneGitHubProjectParameters }
+
+procedure TfrmCloneGitHubProjectParameters.DownloadWrapperCallback(var Cancelled: Boolean);
+begin
+ Application.ProcessMessages;
+ Cancelled := frmDownloadProgress.Cancel;
+end;
+
+procedure TfrmCloneGitHubProjectParameters.DownloadCallback(
+ Owner: TfrmDownloadProgress; var Result: Boolean);
+var
+ wrapper: TKmcWrapper;
+ path: string;
+begin
+ Result := False;
+ path := ExtractFileDir(Self.BasePath);
+
+ Update;
+ frmDownloadProgress := Owner;
+ wrapper := TKmcWrapper.Create;
+ try
+ if wrapper.Copy(Self.SourceProjectFilename, Self.ProjectFilename, path, Self.RelocateExternal, DownloadWrapperCallback) then
+ begin
+ frmKeymanDeveloper.OpenProject(Self.ProjectFilename);
+ Result := True;
+ end;
+ finally
+ wrapper.Free;
+ end;
+ frmDownloadProgress := nil;
+end;
+
+procedure TfrmCloneGitHubProjectParameters.FormCreate(Sender: TObject);
+begin
+ inherited;
+ editPath.Text := FKeymanDeveloperOptions.DefaultProjectPath;
+
+ dlgBrowse := TBrowse4Folder.Create(Self);
+ dlgBrowse.InitialDir := editPath.Text;
+ dlgBrowse.Options := [OnlySelectFileSysDir, ShowEditBox, UseNewDialogStyle];
+ dlgBrowse.Root := Desktop;
+ dlgBrowse.Title := 'Select folder to save project to';
+
+ UpdateSourceProjectNote;
+ EnableControls;
+end;
+
+procedure TfrmCloneGitHubProjectParameters.cmdBrowseClick(Sender: TObject);
+begin
+ dlgBrowse.InitialDir := editPath.Text;
+
+ if dlgBrowse.Execute and (dlgBrowse.FileName <> '') then
+ editPath.Text := ExcludeTrailingPathDelimiter(dlgBrowse.FileName);
+end;
+
+procedure TfrmCloneGitHubProjectParameters.cmdOKClick(Sender: TObject);
+begin
+ if Validate then
+ ModalResult := mrOk;
+end;
+
+procedure TfrmCloneGitHubProjectParameters.UpdateSourceProjectNote;
+begin
+ lblSourceProjectLexicalModel.Visible := IsSourceProjectALexicalModelProject;
+ lblSourceProjectKeyboard.Visible := (editGitHubURL.Text <> '') and not IsSourceProjectALexicalModelProject;
+end;
+
+procedure TfrmCloneGitHubProjectParameters.editProjectIDChange(Sender: TObject);
+begin
+ UpdateProjectFilename;
+ EnableControls;
+end;
+
+procedure TfrmCloneGitHubProjectParameters.editSourceProjectFilenameChange(Sender: TObject);
+begin
+ EnableControls;
+end;
+
+procedure TfrmCloneGitHubProjectParameters.editGitHubURLChange(Sender: TObject);
+begin
+ UpdateProjectFilename;
+ UpdateSourceProjectNote;
+ EnableControls;
+end;
+
+procedure TfrmCloneGitHubProjectParameters.editVersionChange(Sender: TObject);
+begin
+ EnableControls;
+end;
+
+function IsValidGitHubProjectURL(const uri: string): Boolean;
+const
+ // From github-urls.ts (minor RE tweaks to match required format for PCRE
+
+ {**
+ * Matches only a GitHub permanent raw URI with a commit hash, without any other
+ * components; note hash is called branch to match other URI formats
+ *}
+ GITHUB_STABLE_SOURCE = '^https:\/\/github\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)\/raw\/(?[a-f0-9]{40})\/(?.+)$';
+
+ {**
+ * Matches any GitHub git resource raw 'user content' URI which can be
+ * translated to a permanent URI with a commit hash
+ *}
+ GITHUB_RAW_URI = '^https:\/\/raw\.githubusercontent\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)\/(?:refs\/(?:heads|tags)\/)?(?[^\/]+)\/(?.+)$';
+
+ {**
+ * Matches any GitHub git resource raw URI which can be translated to a
+ * permanent URI with a commit hash
+ *}
+ GITHUB_URI = '^https:\/\/github\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)\/(?:raw|blob|tree)\/(?:refs\/(?:heads|tags)\/)?(?[^\/]+)\/(?.+)$';
+
+ {**
+ * Matches any GitHub git resource raw URI which can be translated to a
+ * permanent URI with a commit hash, with the http[s] protocol optional, for
+ * matching user-supplied URLs. groups are: `owner`, `repo`, `branch`, and
+ * `path`.
+ *}
+ GITHUB_URI_OPTIONAL_PROTOCOL = '^(?:http(?:s)?:\/\/)?github\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)(?:\/(?:(?:raw|blob|tree)\/(?:refs\/(?:heads|tags)\/)?(?[^\/]+)\/(?.*))?)?$';
+
+var
+ opt: TRegEx;
+ raw: TRegEx;
+begin
+ opt := TRegEx.Create(GITHUB_URI_OPTIONAL_PROTOCOL);
+ raw := TRegEx.Create(GITHUB_RAW_URI);
+
+ Result := opt.IsMatch(uri) or raw.IsMatch(uri);
+end;
+
+procedure TfrmCloneGitHubProjectParameters.EnableControls;
+var
+ e: Boolean;
+ id: string;
+begin
+ e :=
+ IsValidGitHubProjectURL(editGitHubURL.Text) and
+ (Trim(editPath.Text) <> '') and
+ (Trim(editProjectID.Text) <> '');
+
+ if e then
+ begin
+ id := Trim(editProjectID.Text);
+ if IsSourceProjectALexicalModelProject
+ then e := TLexicalModelUtils.IsValidLexicalModelID(id, True)
+ else e := TKeyboardUtils.IsValidKeyboardID(id, True);
+ end;
+
+ cmdOK.Enabled := e;
+end;
+
+function TfrmCloneGitHubProjectParameters.GetBasePath: string;
+begin
+ Result := Trim(editPath.Text);
+end;
+
+function TfrmCloneGitHubProjectParameters.GetHelpTopic: string;
+begin
+ Result := SHelpTopic_Context_CloneGitHubProjectParameters;
+end;
+
+function TfrmCloneGitHubProjectParameters.GetProjectID: string;
+begin
+ Result := Trim(LowerCase(editProjectID.Text));
+end;
+
+function TfrmCloneGitHubProjectParameters.GetProjectFilename: string;
+begin
+ Result := editProjectFilename.Text;
+end;
+
+function TfrmCloneGitHubProjectParameters.GetRelocateExternal: Boolean;
+begin
+ Result := chkRelocateExternal.Checked;
+end;
+
+function TfrmCloneGitHubProjectParameters.GetSourceProjectFilename: string;
+begin
+ Result := editGitHubURL.Text;
+end;
+
+function TfrmCloneGitHubProjectParameters.IsSourceProjectALexicalModelProject: Boolean;
+var
+ components: TArray;
+ sourceId: string;
+begin
+ sourceId := editGitHubURL.Text;
+ components := sourceId.Split(['\','/']);
+ if Length(components) = 0 then
+ Exit(False);
+ sourceId := components[Length(components) - 1];
+ sourceId := ChangeFileExt(sourceId, '').ToLower;
+ Result := TLexicalModelUtils.IsValidLexicalModelID(sourceId, False);
+end;
+
+function TfrmCloneGitHubProjectParameters.Validate: Boolean;
+var
+ ProjectFolder: string;
+begin
+ if IsSourceProjectALexicalModelProject
+ then Result := TLexicalModelUtils.IsValidLexicalModelID(Trim(editProjectID.Text), True)
+ else Result := TKeyboardUtils.IsValidKeyboardID(Trim(editProjectID.Text), True);
+
+ if Result then
+ begin
+ if not DirectoryExists(editPath.Text) then
+ begin
+ if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then
+ Exit(False);
+ end;
+
+ ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editProjectID.Text;
+ if DirectoryExists(ProjectFolder) then
+ begin
+ if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning,
+ mbOkCancel, 0) = mrCancel then
+ Exit(False);
+ end;
+ end;
+end;
+
+procedure TfrmCloneGitHubProjectParameters.UpdateProjectFilename;
+begin
+ editProjectFilename.Text :=
+ IncludeTrailingPathDelimiter(BasePath) +
+ ProjectID + PathDelim +
+ ProjectID + Ext_ProjectSource;
+ // Scroll to the end of the control to show the filename
+ editProjectFilename.Perform(EM_SETSEL, Length(editProjectFilename.Text), Length(editProjectFilename.Text));
+ editProjectFilename.Perform(EM_SCROLLCARET, 0, 0);
+end;
+
+end.
diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.dfm
new file mode 100644
index 0000000000..fb286bccc3
--- /dev/null
+++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.dfm
@@ -0,0 +1,119 @@
+inherited frmCloneKeymanCloudProjectParameters: TfrmCloneKeymanCloudProjectParameters
+ BorderIcons = [biSystemMenu, biMaximize]
+ Caption = 'Clone Project from Keyman Cloud'
+ ClientHeight = 506
+ ClientWidth = 840
+ Position = poScreenCenter
+ ExplicitWidth = 856
+ ExplicitHeight = 545
+ PixelsPerInch = 96
+ TextHeight = 13
+ object cmdOK: TButton
+ Left = 680
+ Top = 473
+ Width = 73
+ Height = 25
+ Anchors = [akRight, akBottom]
+ Caption = 'OK'
+ Default = True
+ TabOrder = 2
+ OnClick = cmdOKClick
+ end
+ object cmdCancel: TButton
+ Left = 759
+ Top = 473
+ Width = 73
+ Height = 25
+ Anchors = [akRight, akBottom]
+ Cancel = True
+ Caption = 'Cancel'
+ ModalResult = 2
+ TabOrder = 3
+ end
+ object panWebHost: TPanel
+ Left = 0
+ Top = 0
+ Width = 842
+ Height = 334
+ Anchors = [akLeft, akTop, akRight, akBottom]
+ BevelOuter = bvNone
+ TabOrder = 0
+ end
+ object gbNewProjectDetails: TGroupBox
+ Left = 8
+ Top = 340
+ Width = 823
+ Height = 129
+ Anchors = [akLeft, akRight, akBottom]
+ Caption = 'New Project Details'
+ TabOrder = 1
+ object lblFileName: TLabel
+ Left = 16
+ Top = 24
+ Width = 76
+ Height = 13
+ Caption = '&New project ID:'
+ FocusControl = editKeyboardID
+ end
+ object lblProjectFilename: TLabel
+ Left = 16
+ Top = 102
+ Width = 101
+ Height = 13
+ Caption = 'New project &filename'
+ FocusControl = editProjectFilename
+ end
+ object lblPath: TLabel
+ Left = 16
+ Top = 75
+ Width = 83
+ Height = 13
+ Caption = 'Destination &path:'
+ FocusControl = editPath
+ end
+ object editKeyboardID: TEdit
+ Left = 148
+ Top = 21
+ Width = 205
+ Height = 21
+ TabOrder = 0
+ OnChange = editKeyboardIDChange
+ end
+ object editProjectFilename: TEdit
+ Left = 148
+ Top = 99
+ Width = 669
+ Height = 21
+ TabStop = False
+ ParentColor = True
+ ReadOnly = True
+ TabOrder = 4
+ OnChange = editKeyboardIDChange
+ end
+ object cmdBrowse: TButton
+ Left = 744
+ Top = 72
+ Width = 73
+ Height = 21
+ Caption = '&Browse...'
+ TabOrder = 3
+ OnClick = cmdBrowseClick
+ end
+ object editPath: TEdit
+ Left = 148
+ Top = 72
+ Width = 590
+ Height = 21
+ TabOrder = 2
+ OnChange = editPathChange
+ end
+ object chkRelocateExternal: TCheckBox
+ Left = 148
+ Top = 48
+ Width = 257
+ Height = 17
+ Caption = 'Relocate &external files into new project folder'
+ TabOrder = 1
+ end
+ end
+end
diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas
new file mode 100644
index 0000000000..557fd24ec2
--- /dev/null
+++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas
@@ -0,0 +1,326 @@
+(*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Parameters dialog for cloning a project from Keyman Cloud
+ *)
+unit Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters;
+
+interface
+
+uses
+ System.Classes,
+ System.SysUtils,
+ System.UITypes,
+ System.Variants,
+ Winapi.Messages,
+ Winapi.Windows,
+ Vcl.CheckLst,
+ Vcl.Controls,
+ Vcl.Dialogs,
+ Vcl.ExtCtrls,
+ Vcl.Forms,
+ Vcl.Graphics,
+ Vcl.Grids,
+ Vcl.StdCtrls,
+
+ kpsfile,
+ PackageInfo,
+ UfrmTike,
+ UKeymanTargets,
+ UfrmDownloadProgress,
+ utilfiletypes,
+ Browse4Folder,
+ Keyman.UI.UframeCEFHost;
+
+type
+ TfrmCloneKeymanCloudProjectParameters = class(TTikeForm)
+ cmdOK: TButton;
+ cmdCancel: TButton;
+ panWebHost: TPanel;
+ gbNewProjectDetails: TGroupBox;
+ lblFileName: TLabel;
+ lblProjectFilename: TLabel;
+ editKeyboardID: TEdit;
+ editProjectFilename: TEdit;
+ lblPath: TLabel;
+ cmdBrowse: TButton;
+ editPath: TEdit;
+ chkRelocateExternal: TCheckBox;
+ procedure cmdOKClick(Sender: TObject);
+ procedure editSourceProjectFilenameChange(Sender: TObject);
+ procedure FormCreate(Sender: TObject);
+ procedure editVersionChange(Sender: TObject);
+ procedure editPathChange(Sender: TObject);
+ procedure editKeyboardIDChange(Sender: TObject);
+ procedure cmdBrowseClick(Sender: TObject);
+ private
+ cef: TframeCEFHost;
+ dlgBrowse: TBrowse4Folder;
+ FKeymanID: string;
+ frmDownloadProgress: TfrmDownloadProgress;
+ function GetBasePath: string;
+ function GetKeyboardID: string;
+ function Validate: Boolean;
+ procedure EnableControls;
+ procedure SetKeyboardID(const Value: string);
+ procedure UpdateProjectFilename;
+ function GetProjectFilename: string;
+ function GetSourceProjectFilename: string;
+ function GetRelocateExternal: Boolean;
+ procedure RefreshHTML;
+ procedure cefLoadEnd(Sender: TObject);
+ procedure DownloadCallback(Owner: TfrmDownloadProgress; var Result: Boolean);
+ procedure DownloadWrapperCallback(var Cancelled: Boolean);
+ protected
+ function GetHelpTopic: string; override;
+ public
+ property BasePath: string read GetBasePath;
+ property KeyboardID: string read GetKeyboardID write SetKeyboardID;
+ property ProjectFilename: string read GetProjectFilename;
+ property SourceProjectFilename: string read GetSourceProjectFilename;
+ property RelocateExternal: Boolean read GetRelocateExternal;
+ end;
+
+function ShowCloneKeymanCloudProjectParameters(Owner: TComponent): Boolean;
+
+implementation
+
+uses
+ System.Net.UrlClient,
+ Vcl.ComCtrls,
+
+ KeymanDeveloperOptions,
+ Keyman.Developer.System.KmcWrapper,
+ Keyman.Developer.System.HelpTopics,
+ Keyman.Developer.System.Project.Project,
+ Keyman.Developer.System.Project.ProjectFile,
+ Keyman.System.KeyboardUtils,
+ UfrmMain,
+ Upload_Settings;
+
+{$R *.dfm}
+
+function ShowCloneKeymanCloudProjectParameters(Owner: TComponent): Boolean;
+var
+ f: TfrmCloneKeymanCloudProjectParameters;
+ path: string;
+ p: TfrmDownloadProgress;
+begin
+ f := TfrmCloneKeymanCloudProjectParameters.Create(Owner);
+ try
+ Result := f.ShowModal = mrOk;
+ if not Result then
+ Exit;
+
+ path := ExtractFileDir(f.BasePath);
+ if not ForceDirectories(path) then
+ begin
+ ShowMessage('Unable to create path "'+path+'": '+SysErrorMessage(GetLastError));
+ Exit;
+ end;
+
+ p := TfrmDownloadProgress.Create(Owner);
+ try
+ p.Callback := f.DownloadCallback;
+ p.progress.Style := pbstMarquee;
+ Result := p.ShowModal = mrOk;
+ finally
+ p.Free;
+ end;
+ finally
+ f.Free;
+ end;
+end;
+
+{ TfrmCloneKeymanCloudProjectParameters }
+
+procedure TfrmCloneKeymanCloudProjectParameters.DownloadWrapperCallback(var Cancelled: Boolean);
+begin
+ Application.ProcessMessages;
+ Cancelled := frmDownloadProgress.Cancel;
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.DownloadCallback(
+ Owner: TfrmDownloadProgress; var Result: Boolean);
+var
+ wrapper: TKmcWrapper;
+ path: string;
+begin
+ Result := False;
+ path := ExtractFileDir(Self.BasePath);
+
+ Update;
+ frmDownloadProgress := Owner;
+ wrapper := TKmcWrapper.Create;
+ try
+ if wrapper.Copy(Self.SourceProjectFilename, Self.ProjectFilename, path, Self.RelocateExternal, DownloadWrapperCallback) then
+ begin
+ frmKeymanDeveloper.OpenProject(Self.ProjectFilename);
+ Result := True;
+ end;
+ finally
+ wrapper.Free;
+ end;
+ frmDownloadProgress := nil;
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.FormCreate(Sender: TObject);
+begin
+ inherited;
+ cef := TframeCEFHost.Create(Self);
+ cef.Parent := panWebHost;
+ cef.Visible := True;
+ cef.OnLoadEnd := cefLoadEnd;
+ RefreshHTML;
+
+ editPath.Text := FKeymanDeveloperOptions.DefaultProjectPath;
+
+ dlgBrowse := TBrowse4Folder.Create(Self);
+ dlgBrowse.InitialDir := editPath.Text;
+ dlgBrowse.Options := [OnlySelectFileSysDir, ShowEditBox, UseNewDialogStyle];
+ dlgBrowse.Root := Desktop;
+ dlgBrowse.Title := 'Select folder to save project to';
+
+ EnableControls;
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.RefreshHTML;
+begin
+ cef.Navigate(MakeKeymanURL(URLPath_KeymanDeveloper_KeyboardSearchForCloneKeymanCloud));
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.cefLoadEnd(Sender: TObject);
+var
+ u: TUri;
+begin
+ u := TUri.Create(cef.cef.DocumentURL);
+ // We want to match on keyman.com/keyboards/
+ // but we need to avoid keyman.com/keyboards/h/
+ if u.Path.StartsWith(URLSubPath_KeymanDeveloper_Clone_Keyboards) and
+ not u.Path.StartsWith(URLSubPath_KeymanDeveloper_Clone_Keyboards_Custom)
+ then FKeymanID := u.Path.Substring(URLSubPath_KeymanDeveloper_Clone_Keyboards.Length)
+ else FKeymanID := '';
+ EnableControls;
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.cmdBrowseClick(Sender: TObject);
+begin
+ dlgBrowse.InitialDir := editPath.Text;
+
+ if dlgBrowse.Execute and (dlgBrowse.FileName <> '') then
+ editPath.Text := ExcludeTrailingPathDelimiter(dlgBrowse.FileName);
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.cmdOKClick(Sender: TObject);
+begin
+ if Validate then
+ ModalResult := mrOk;
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.editKeyboardIDChange(Sender: TObject);
+begin
+ UpdateProjectFilename;
+ EnableControls;
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.editSourceProjectFilenameChange(Sender: TObject);
+begin
+ EnableControls;
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.editPathChange(Sender: TObject);
+begin
+ UpdateProjectFilename;
+ EnableControls;
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.editVersionChange(Sender: TObject);
+begin
+ EnableControls;
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.EnableControls;
+var
+ e: Boolean;
+begin
+ e :=
+ (FKeymanID <> '') and
+ (Trim(editPath.Text) <> '') and
+ (Trim(editKeyboardID.Text) <> '') and
+ TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True);
+
+ cmdOK.Enabled := e;
+end;
+
+function TfrmCloneKeymanCloudProjectParameters.GetBasePath: string;
+begin
+ Result := Trim(editPath.Text);
+end;
+
+function TfrmCloneKeymanCloudProjectParameters.GetHelpTopic: string;
+begin
+ Result := SHelpTopic_Context_CloneKeymanCloudProjectParameters;
+end;
+
+function TfrmCloneKeymanCloudProjectParameters.GetKeyboardID: string;
+begin
+ Result := Trim(LowerCase(editKeyboardID.Text));
+end;
+
+function TfrmCloneKeymanCloudProjectParameters.GetProjectFilename: string;
+begin
+ Result := editProjectFilename.Text;
+end;
+
+function TfrmCloneKeymanCloudProjectParameters.GetRelocateExternal: Boolean;
+begin
+ Result := chkRelocateExternal.Checked;
+end;
+
+function TfrmCloneKeymanCloudProjectParameters.GetSourceProjectFilename: string;
+begin
+ Result := 'cloud:'+FKeymanID;
+end;
+
+function TfrmCloneKeymanCloudProjectParameters.Validate: Boolean;
+var
+ ProjectFolder: string;
+begin
+ Result := TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True);
+
+ if Result then
+ begin
+ if not DirectoryExists(editPath.Text) then
+ begin
+ if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then
+ Exit(False);
+ end;
+
+ ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editKeyboardID.Text;
+ if DirectoryExists(ProjectFolder) then
+ begin
+ if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning,
+ mbOkCancel, 0) = mrCancel then
+ Exit(False);
+ end;
+ end;
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.SetKeyboardID(const Value: string);
+begin
+ editKeyboardID.Text := Value;
+ EnableControls;
+end;
+
+procedure TfrmCloneKeymanCloudProjectParameters.UpdateProjectFilename;
+begin
+ editProjectFilename.Text :=
+ IncludeTrailingPathDelimiter(BasePath) +
+ KeyboardID + PathDelim +
+ KeyboardID + Ext_ProjectSource;
+ // Scroll to the end of the control to show the filename
+ editProjectFilename.Perform(EM_SETSEL, Length(editProjectFilename.Text), Length(editProjectFilename.Text));
+ editProjectFilename.Perform(EM_SCROLLCARET, 0, 0);
+end;
+
+end.
diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.dfm
new file mode 100644
index 0000000000..c60a3bcab6
--- /dev/null
+++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.dfm
@@ -0,0 +1,174 @@
+inherited frmCloneLocalProjectParameters: TfrmCloneLocalProjectParameters
+ BorderIcons = [biSystemMenu]
+ BorderStyle = bsDialog
+ Caption = 'Clone Project'
+ ClientHeight = 257
+ ClientWidth = 721
+ Position = poScreenCenter
+ ExplicitWidth = 727
+ ExplicitHeight = 286
+ PixelsPerInch = 96
+ TextHeight = 13
+ object cmdOK: TButton
+ Left = 561
+ Top = 222
+ Width = 73
+ Height = 25
+ Caption = 'OK'
+ Default = True
+ TabOrder = 2
+ OnClick = cmdOKClick
+ end
+ object cmdCancel: TButton
+ Left = 640
+ Top = 222
+ Width = 73
+ Height = 25
+ Cancel = True
+ Caption = 'Cancel'
+ ModalResult = 2
+ TabOrder = 3
+ end
+ object gbNewProjectDetails: TGroupBox
+ Left = 8
+ Top = 63
+ Width = 705
+ Height = 153
+ Caption = 'New Project Details'
+ TabOrder = 1
+ object lblFileName: TLabel
+ Left = 12
+ Top = 22
+ Width = 76
+ Height = 13
+ Caption = '&New project ID:'
+ FocusControl = editProjectID
+ end
+ object lblProjectFilename: TLabel
+ Left = 12
+ Top = 124
+ Width = 101
+ Height = 13
+ Caption = 'New project &filename'
+ FocusControl = editProjectFilename
+ end
+ object lblPath: TLabel
+ Left = 12
+ Top = 97
+ Width = 83
+ Height = 13
+ Caption = 'Destination &path:'
+ FocusControl = editPath
+ end
+ object lblSourceProjectLexicalModel: TLabel
+ Left = 12
+ Top = 46
+ Width = 531
+ Height = 13
+ Caption =
+ 'The source project appears to be a lexical model. The new projec' +
+ 't ID must use the author.bcp47.uniq pattern.'
+ end
+ object lblSourceProjectKeyboard: TLabel
+ Left = 12
+ Top = 46
+ Width = 682
+ Height = 13
+ Caption =
+ 'The source project appears to be a keyboard project. The new pro' +
+ 'ject ID must be a valid keyboard id pattern (alphanumeric and un' +
+ 'derscore).'
+ end
+ object editProjectID: TEdit
+ Left = 144
+ Top = 19
+ Width = 205
+ Height = 21
+ TabOrder = 0
+ OnChange = editProjectIDChange
+ end
+ object editProjectFilename: TEdit
+ Left = 144
+ Top = 121
+ Width = 553
+ Height = 21
+ TabStop = False
+ ParentColor = True
+ ReadOnly = True
+ TabOrder = 4
+ OnChange = editProjectIDChange
+ end
+ object cmdBrowse: TButton
+ Left = 624
+ Top = 94
+ Width = 73
+ Height = 21
+ Caption = '&Browse...'
+ TabOrder = 3
+ OnClick = cmdBrowseClick
+ end
+ object editPath: TEdit
+ Left = 144
+ Top = 94
+ Width = 474
+ Height = 21
+ TabOrder = 2
+ OnChange = editPathChange
+ end
+ object chkRelocateExternal: TCheckBox
+ Left = 144
+ Top = 70
+ Width = 257
+ Height = 17
+ Caption = 'Relocate &external files into new project folder'
+ TabOrder = 1
+ end
+ end
+ object gbSourceProjectDetails: TGroupBox
+ Left = 8
+ Top = 8
+ Width = 705
+ Height = 49
+ Caption = 'Source Project'
+ TabOrder = 0
+ object lblKeyboardName: TLabel
+ Left = 12
+ Top = 19
+ Width = 117
+ Height = 13
+ Caption = '&Source project filename:'
+ FocusControl = editSourceProjectFilename
+ end
+ object editSourceProjectFilename: TEdit
+ Left = 144
+ Top = 16
+ Width = 474
+ Height = 21
+ TabOrder = 0
+ OnChange = editSourceProjectFilenameChange
+ end
+ object cmdBrowseSourceProject: TButton
+ Left = 624
+ Top = 16
+ Width = 73
+ Height = 21
+ Caption = 'B&rowse...'
+ TabOrder = 1
+ OnClick = cmdBrowseSourceProjectClick
+ end
+ end
+ object dlgBrowseSourceProject: TFileOpenDialog
+ DefaultExtension = '.kpj'
+ FavoriteLinks = <>
+ FileTypes = <
+ item
+ DisplayName = 'Keyman Project Files (*.kpj)'
+ FileMask = '*.kpj'
+ end>
+ OkButtonLabel = 'Select'
+ Options = [fdoPathMustExist, fdoFileMustExist, fdoNoTestFileCreate]
+ Title = 'Select source project'
+ Left = 584
+ Top = 8
+ end
+end
diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas
new file mode 100644
index 0000000000..cfc24801b3
--- /dev/null
+++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas
@@ -0,0 +1,287 @@
+(*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Parameters dialog for cloning a project from local filesystem
+ *)
+unit Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters;
+
+interface
+
+uses
+ System.Classes,
+ System.SysUtils,
+ System.UITypes,
+ System.Variants,
+ Winapi.Messages,
+ Winapi.Windows,
+ Vcl.CheckLst,
+ Vcl.Controls,
+ Vcl.Dialogs,
+ Vcl.Forms,
+ Vcl.Graphics,
+ Vcl.Grids,
+ Vcl.StdCtrls,
+
+ kpsfile,
+ PackageInfo,
+ UfrmTike,
+ UKeymanTargets,
+ utilfiletypes,
+ Browse4Folder;
+
+type
+ TfrmCloneLocalProjectParameters = class(TTikeForm)
+ cmdOK: TButton;
+ cmdCancel: TButton;
+ dlgBrowseSourceProject: TFileOpenDialog;
+ gbNewProjectDetails: TGroupBox;
+ lblFileName: TLabel;
+ lblProjectFilename: TLabel;
+ lblPath: TLabel;
+ editProjectID: TEdit;
+ editProjectFilename: TEdit;
+ cmdBrowse: TButton;
+ editPath: TEdit;
+ chkRelocateExternal: TCheckBox;
+ gbSourceProjectDetails: TGroupBox;
+ lblKeyboardName: TLabel;
+ editSourceProjectFilename: TEdit;
+ cmdBrowseSourceProject: TButton;
+ lblSourceProjectLexicalModel: TLabel;
+ lblSourceProjectKeyboard: TLabel;
+ procedure cmdOKClick(Sender: TObject);
+ procedure editSourceProjectFilenameChange(Sender: TObject);
+ procedure FormCreate(Sender: TObject);
+ procedure editVersionChange(Sender: TObject);
+ procedure editPathChange(Sender: TObject);
+ procedure editProjectIDChange(Sender: TObject);
+ procedure cmdBrowseClick(Sender: TObject);
+ procedure cmdBrowseSourceProjectClick(Sender: TObject);
+ private
+ dlgBrowse: TBrowse4Folder;
+ function GetBasePath: string;
+ function GetProjectID: string;
+ function Validate: Boolean;
+ procedure EnableControls;
+ procedure UpdateProjectFilename;
+ function GetProjectFilename: string;
+ function GetSourceProjectFilename: string;
+ function GetRelocateExternal: Boolean;
+ function IsSourceProjectALexicalModelProject: Boolean;
+ procedure UpdateSourceProjectNote;
+ protected
+ function GetHelpTopic: string; override;
+ public
+ property BasePath: string read GetBasePath;
+ property ProjectID: string read GetProjectID;
+ property ProjectFilename: string read GetProjectFilename;
+ property SourceProjectFilename: string read GetSourceProjectFilename;
+ property RelocateExternal: Boolean read GetRelocateExternal;
+ end;
+
+function ShowCloneLocalProjectParameters(Owner: TComponent): Boolean;
+
+implementation
+
+uses
+ KeymanDeveloperOptions,
+ Keyman.Developer.System.KmcWrapper,
+ Keyman.Developer.System.HelpTopics,
+ Keyman.Developer.System.Project.Project,
+ Keyman.Developer.System.Project.ProjectFile,
+ Keyman.System.KeyboardUtils,
+ Keyman.System.LexicalModelUtils,
+ UfrmMain;
+
+{$R *.dfm}
+
+function ShowCloneLocalProjectParameters(Owner: TComponent): Boolean;
+var
+ f: TfrmCloneLocalProjectParameters;
+ wrapper: TKmcWrapper;
+begin
+ f := TfrmCloneLocalProjectParameters.Create(Owner);
+ try
+ Result := f.ShowModal = mrOk;
+ if not Result then
+ Exit;
+
+ wrapper := TKmcWrapper.Create;
+ if wrapper.Copy(f.SourceProjectFilename, f.ProjectFilename, ExtractFileDir(f.SourceProjectFilename), f.RelocateExternal) then
+ begin
+ frmKeymanDeveloper.OpenProject(f.ProjectFilename);
+ Result := True;
+ end;
+ finally
+ f.Free;
+ end;
+end;
+
+{ TfrmCloneLocalProjectParameters }
+
+procedure TfrmCloneLocalProjectParameters.FormCreate(Sender: TObject);
+begin
+ inherited;
+ editPath.Text := FKeymanDeveloperOptions.DefaultProjectPath;
+
+ dlgBrowse := TBrowse4Folder.Create(Self);
+ dlgBrowse.InitialDir := editPath.Text;
+ dlgBrowse.Options := [OnlySelectFileSysDir, ShowEditBox, UseNewDialogStyle];
+ dlgBrowse.Root := Desktop;
+ dlgBrowse.Title := 'Select folder to save project to';
+
+ UpdateSourceProjectNote;
+ EnableControls;
+end;
+
+procedure TfrmCloneLocalProjectParameters.cmdBrowseClick(Sender: TObject);
+begin
+ dlgBrowse.InitialDir := editPath.Text;
+
+ if dlgBrowse.Execute and (dlgBrowse.FileName <> '') then
+ editPath.Text := ExcludeTrailingPathDelimiter(dlgBrowse.FileName);
+end;
+
+procedure TfrmCloneLocalProjectParameters.cmdBrowseSourceProjectClick(
+ Sender: TObject);
+begin
+ if dlgBrowseSourceProject.Execute then
+ begin
+ editSourceProjectFilename.Text := dlgBrowseSourceProject.Filename;
+ UpdateSourceProjectNote;
+ EnableControls;
+ end;
+end;
+
+procedure TfrmCloneLocalProjectParameters.UpdateSourceProjectNote;
+begin
+ lblSourceProjectLexicalModel.Visible := IsSourceProjectALexicalModelProject;
+ lblSourceProjectKeyboard.Visible := (editSourceProjectFilename.Text <> '') and not IsSourceProjectALexicalModelProject;
+end;
+
+procedure TfrmCloneLocalProjectParameters.cmdOKClick(Sender: TObject);
+begin
+ if Validate then
+ ModalResult := mrOk;
+end;
+
+procedure TfrmCloneLocalProjectParameters.editProjectIDChange(Sender: TObject);
+begin
+ UpdateProjectFilename;
+ EnableControls;
+end;
+
+procedure TfrmCloneLocalProjectParameters.editSourceProjectFilenameChange(Sender: TObject);
+begin
+ EnableControls;
+ UpdateSourceProjectNote;
+end;
+
+procedure TfrmCloneLocalProjectParameters.editPathChange(Sender: TObject);
+begin
+ UpdateProjectFilename;
+ EnableControls;
+end;
+
+procedure TfrmCloneLocalProjectParameters.editVersionChange(Sender: TObject);
+begin
+ EnableControls;
+end;
+
+function TfrmCloneLocalProjectParameters.IsSourceProjectALexicalModelProject: Boolean;
+var
+ sourceId: string;
+begin
+ sourceId := ChangeFileExt(ExtractFileName(editSourceProjectFilename.Text), '').ToLower;
+ Result := TLexicalModelUtils.IsValidLexicalModelID(sourceId, False);
+end;
+
+procedure TfrmCloneLocalProjectParameters.EnableControls;
+var
+ e: Boolean;
+ id: string;
+begin
+ e := (Trim(editSourceProjectFilename.Text) <> '') and
+ FileExists(Trim(editSourceProjectFilename.Text)) and
+ (Trim(editPath.Text) <> '') and
+ (Trim(editProjectID.Text) <> '');
+
+ if e then
+ begin
+ id := Trim(editProjectID.Text);
+ if IsSourceProjectALexicalModelProject
+ then e := TLexicalModelUtils.IsValidLexicalModelID(id, True)
+ else e := TKeyboardUtils.IsValidKeyboardID(id, True);
+ end;
+
+ cmdOK.Enabled := e;
+end;
+
+function TfrmCloneLocalProjectParameters.GetBasePath: string;
+begin
+ Result := Trim(editPath.Text);
+end;
+
+function TfrmCloneLocalProjectParameters.GetHelpTopic: string;
+begin
+ Result := SHelpTopic_Context_CloneLocalProjectParameters;
+end;
+
+function TfrmCloneLocalProjectParameters.GetProjectID: string;
+begin
+ Result := Trim(LowerCase(editProjectID.Text));
+end;
+
+function TfrmCloneLocalProjectParameters.GetProjectFilename: string;
+begin
+ Result := editProjectFilename.Text;
+end;
+
+function TfrmCloneLocalProjectParameters.GetRelocateExternal: Boolean;
+begin
+ Result := chkRelocateExternal.Checked;
+end;
+
+function TfrmCloneLocalProjectParameters.GetSourceProjectFilename: string;
+begin
+ Result := editSourceProjectFilename.Text;
+end;
+
+function TfrmCloneLocalProjectParameters.Validate: Boolean;
+var
+ ProjectFolder: string;
+begin
+ if IsSourceProjectALexicalModelProject
+ then Result := TLexicalModelUtils.IsValidLexicalModelID(Trim(editProjectID.Text), True)
+ else Result := TKeyboardUtils.IsValidKeyboardID(Trim(editProjectID.Text), True);
+
+ if Result then
+ begin
+ if not DirectoryExists(editPath.Text) then
+ begin
+ if MessageDlg('The target folder '+editPath.Text+' does not exist. Create it now?', mtConfirmation, mbOkCancel, 0) = mrCancel then
+ Exit(False);
+ end;
+
+ ProjectFolder := IncludeTrailingPathDelimiter(editPath.Text) + editProjectID.Text;
+ if DirectoryExists(ProjectFolder) then
+ begin
+ if MessageDlg('The project folder '+ProjectFolder+' already exists. Are you sure you want to overwrite it?', mtWarning,
+ mbOkCancel, 0) = mrCancel then
+ Exit(False);
+ end;
+ end;
+end;
+
+procedure TfrmCloneLocalProjectParameters.UpdateProjectFilename;
+begin
+ editProjectFilename.Text :=
+ IncludeTrailingPathDelimiter(BasePath) +
+ ProjectID + PathDelim +
+ ProjectID + Ext_ProjectSource;
+ // Scroll to the end of the control to show the filename
+ editProjectFilename.Perform(EM_SETSEL, Length(editProjectFilename.Text), Length(editProjectFilename.Text));
+ editProjectFilename.Perform(EM_SCROLLCARET, 0, 0);
+end;
+
+end.
diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.dfm
index 3942364fd8..6ef44a8f7a 100644
--- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.dfm
+++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.dfm
@@ -94,7 +94,7 @@ inherited frmNewLDMLKeyboardProjectParameters: TfrmNewLDMLKeyboardProjectParamet
Top = 260
Width = 102
Height = 13
- Caption = 'Supported &languages'
+ Caption = '&Supported languages'
FocusControl = memoLanguages
end
object Label4: TLabel
diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm
index 5208255bbc..1761e2f1c2 100644
--- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm
+++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm
@@ -42,18 +42,26 @@ inherited frmNewProject: TfrmNewProject
FullDrag = True
HideSelection = False
Items.ItemData = {
- 05FA0000000400000001000000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000
- 000F4B00650079006D0061006E0020004B006500790062006F00610072006400
+ 05DE0100000700000001000000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000
+ 000F4B00650079006D0061006E0020006B006500790062006F00610072006400
01000000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000000D4C0044004D004C
- 0020004B006500790062006F0061007200640001000000FFFFFFFFFFFFFFFF00
+ 0020006B006500790062006F0061007200640001000000FFFFFFFFFFFFFFFF00
000000FFFFFFFF000000001749006D0070006F00720074002000570069006E00
- 64006F007700730020004B006500790062006F0061007200640002000000FFFF
+ 64006F007700730020006B006500790062006F0061007200640002000000FFFF
FFFFFFFFFFFF00000000FFFFFFFF000000001657006F00720064006C00690073
- 00740020004C00650078006900630061006C0020004D006F00640065006C00}
+ 00740020006C00650078006900630061006C0020006D006F00640065006C0001
+ 000000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000001343006C006F006E00
+ 650020006C006F00630061006C002000700072006F006A006500630074000100
+ 0000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000001F43006C006F006E0065
+ 002000700072006F006A006500630074002000660072006F006D0020004B0065
+ 0079006D0061006E00200043006C006F007500640001000000FFFFFFFFFFFFFF
+ FF00000000FFFFFFFF000000001943006C006F006E0065002000700072006F00
+ 6A006500630074002000660072006F006D002000470069007400480075006200}
LargeImages = ilLarge
ReadOnly = True
SmallImages = ilSmall
TabOrder = 0
+ ViewStyle = vsList
OnDblClick = lvItemsDblClick
OnSelectItem = lvItemsSelectItem
end
diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas
index 535a584ad6..36f65bd103 100644
--- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas
+++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas
@@ -81,6 +81,9 @@ uses
Keyman.Developer.System.Project.Project,
Keyman.Developer.System.Project.ProjectFile,
Keyman.Developer.UI.Project.ProjectUI,
+ Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters,
+ Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters,
+ Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters,
Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters,
Keyman.Developer.UI.Project.UfrmNewProjectParameters,
Keyman.Developer.UI.Project.UfrmNewModelProjectParameters,
@@ -115,6 +118,12 @@ begin
Result := ShowNewModelProjectParameters(Owner);
kptImportWindowsKeyboard:
Result := ShowImportWindowsKeyboard(Owner);
+ kptCloneLocalProject:
+ Result := ShowCloneLocalProjectParameters(Owner);
+ kptCloneKeymanCloudProject:
+ Result := ShowCloneKeymanCloudProjectParameters(Owner);
+ kptCloneGitHubProject:
+ Result := ShowCloneGitHubProjectParameters(Owner);
end;
end;
@@ -143,14 +152,20 @@ begin
// Overly wordy but clarifies that we may have other types later
if not Assigned(lvItems.Selected) then
Result := kptUnknown
- else if lvItems.Selected.Caption = 'Keyman Keyboard' then
+ else if lvItems.Selected.Caption = 'Keyman keyboard' then
Result := kptBasic
- else if lvItems.Selected.Caption = 'LDML Keyboard' then
+ else if lvItems.Selected.Caption = 'LDML keyboard' then
Result := kptLDMLKeyboard
- else if lvItems.Selected.Caption = 'Import Windows Keyboard' then
+ else if lvItems.Selected.Caption = 'Import Windows keyboard' then
Result := kptImportWindowsKeyboard
- else if lvItems.Selected.Caption = 'Wordlist Lexical Model' then
+ else if lvItems.Selected.Caption = 'Wordlist lexical model' then
Result := kptWordlistLexicalModel
+ else if lvItems.Selected.Caption = 'Clone local project' then
+ Result := kptCloneLocalProject
+ else if lvItems.Selected.Caption = 'Clone project from Keyman Cloud' then
+ Result := kptCloneKeymanCloudProject
+ else if lvItems.Selected.Caption = 'Clone project from GitHub' then
+ Result := kptCloneGitHubProject
else
Result := kptUnknown;
end;
@@ -192,6 +207,16 @@ begin
'Creates a new lexical model project that matches the folder structure used in '+
'the Keyman lexical-models repository and includes all basic files '+
'needed for a wordlist lexical model.';
+ kptCloneLocalProject: lblDescription.Caption :=
+ 'Copies a Keyman keyboard or lexical model project on your local filesystem '+
+ 'into a new folder, giving the project and included files a new name.';
+ kptCloneKeymanCloudProject: lblDescription.Caption :=
+ 'Copies a Keyman keyboard or lexical model project from keyman.com '+
+ 'into a new folder, giving the project and included files a new name.';
+ kptCloneGitHubProject: lblDescription.Caption :=
+ 'Copies a Keyman keyboard or lexical model project from a GitHub '+
+ 'repository into a new folder, giving the project and included files a '+
+ 'new name.';
end;
end;
diff --git a/developer/src/tike/tike.dpr b/developer/src/tike/tike.dpr
index e686949fc0..6724e254f4 100644
--- a/developer/src/tike/tike.dpr
+++ b/developer/src/tike/tike.dpr
@@ -228,7 +228,7 @@ uses
Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter in '..\kmconvert\Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas',
Keyman.Developer.System.Project.kmnProjectFileAction in 'project\Keyman.Developer.System.Project.kmnProjectFileAction.pas',
Keyman.Developer.System.Project.kpsProjectFileAction in 'project\Keyman.Developer.System.Project.kpsProjectFileAction.pas',
- Keyman.Developer.UI.Project.UfrmNewProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas' {frmNewProjectParameters},
+ Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmCloneGitHubProjectParameters.pas' {frmCloneGitHubProjectParameters},
Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmNewLDMLKeyboardProjectParameters.pas' {frmNewLDMLKeyboardProjectParameters},
Keyman.Developer.UI.Project.UfrmNewProject in 'project\Keyman.Developer.UI.Project.UfrmNewProject.pas' {frmNewProject},
Keyman.Developer.System.KeyboardProjectTemplate in '..\kmconvert\Keyman.Developer.System.KeyboardProjectTemplate.pas',
@@ -300,7 +300,10 @@ uses
Keyman.Developer.System.Main in 'main\Keyman.Developer.System.Main.pas',
Keyman.Developer.System.LaunchProjects in 'main\Keyman.Developer.System.LaunchProjects.pas',
Keyman.System.Debug.DebugUtils in 'debug\Keyman.System.Debug.DebugUtils.pas',
- Keyman.Developer.UI.RichEdit41 in '..\common\delphi\components\Keyman.Developer.UI.RichEdit41.pas';
+ Keyman.Developer.UI.RichEdit41 in '..\common\delphi\components\Keyman.Developer.UI.RichEdit41.pas',
+ Keyman.Developer.UI.Project.UfrmNewProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas' {frmNewProjectParameters},
+ Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas' {frmCloneLocalProjectParameters},
+ Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmCloneKeymanCloudProjectParameters.pas' {frmCloneKeymanCloudProjectParameters};
{$R *.RES}
{$R ICONS.RES}
diff --git a/developer/src/tike/tike.dproj b/developer/src/tike/tike.dproj
index 2a626a30ac..9ee2d83579 100644
--- a/developer/src/tike/tike.dproj
+++ b/developer/src/tike/tike.dproj
@@ -471,8 +471,8 @@
-
-
+
+
dfm
@@ -585,6 +585,18 @@
+
+
+ dfm
+
+
+
+ dfm
+
+
+
+ dfm
+
Cfg_2
diff --git a/docs/build/linux-ubuntu.md b/docs/build/linux-ubuntu.md
index 0f531acb59..98e15185bf 100644
--- a/docs/build/linux-ubuntu.md
+++ b/docs/build/linux-ubuntu.md
@@ -23,7 +23,7 @@ The following projects **cannot** be built on Linux:
### System Requirements
-- Minimum Ubuntu version: Ubuntu 20.04
+- Minimum Ubuntu version: Ubuntu 22.04
Other Linux distributions will also work if appropriate dependencies are installed.
@@ -113,7 +113,7 @@ All dependencies are already installed if you followed the instructions under
Keyman Core can be built with the `core/build.sh` script.
-- [Building Keyman Core](../../core/doc/BUILDING.md)
+- [Building Keyman Core](../../core/docs/BUILDING.md)
## Keyman for Linux
diff --git a/docs/linux/README.md b/docs/linux/README.md
index 80c1a78434..64ef6c967f 100644
--- a/docs/linux/README.md
+++ b/docs/linux/README.md
@@ -147,7 +147,7 @@ Run `ibus restart` after installing any of them.
Keyman tries to activate the keyboard automatically. If you want to activate
it for a different language, you can do so by following the steps below.
-#### GNOME3 (focal and bionic default, also newer Ubuntu versions)
+#### GNOME3 (Ubuntu default)
- Click the connection/sound/shutdown section in the top right. Then the tools
icon for Settings.
diff --git a/docs/minimum-versions.md b/docs/minimum-versions.md
index 174f4b518e..4f67d5a626 100644
--- a/docs/minimum-versions.md
+++ b/docs/minimum-versions.md
@@ -4,7 +4,7 @@ title: Keyman Minimum Versions
Changes in [minimum-versions.inc.sh](minimum-versions.inc.sh) should manually be propagated to the linked help files below:
-## Keyman 18.0
+## Keyman 19.0
Target Operating System and Platform Versions
@@ -53,7 +53,7 @@ https://help.keyman.com/developer/engine/android/latest-version/
| KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME | 53.0 |
| KEYMAN_MIN_TARGET_VERSION_IOS | 12.2 |
| KEYMAN_MIN_TARGET_VERSION_MAC | 10.13 |
-| KEYMAN_MIN_TARGET_VERSION_UBUNTU | 20.04 |
+| KEYMAN_MIN_TARGET_VERSION_UBUNTU | 22.04 |
| KEYMAN_MIN_TARGET_VERSION_WEB_CHROME | 95.0 |
| KEYMAN_MIN_TARGET_VERSION_WEB_FIREFOX | 79.0 |
| KEYMAN_MIN_TARGET_VERSION_WEB_OPERA | 47.0 |
diff --git a/docs/minimum-versions.md.in b/docs/minimum-versions.md.in
index ffe508e12f..41a664d454 100644
--- a/docs/minimum-versions.md.in
+++ b/docs/minimum-versions.md.in
@@ -4,7 +4,7 @@ title: Keyman Minimum Versions
Changes in [minimum-versions.inc.sh](minimum-versions.inc.sh) should manually be propagated to the linked help files below:
-## Keyman 18.0
+## Keyman 19.0
Target Operating System and Platform Versions
diff --git a/docs/npm-packages.md b/docs/npm-packages.md
index 494bec0044..8306130d36 100644
--- a/docs/npm-packages.md
+++ b/docs/npm-packages.md
@@ -19,6 +19,7 @@ Packages are **never** published from a developer's machine.
* `@keymanapp/kmc-model` -- located at `developer/src/kmc-model`
* `@keymanapp/kmc-model-info` -- located at `developer/src/kmc-model-info`
* `@keymanapp/kmc-package` -- located at `developer/src/kmc-package`
+* `@keymanapp/langtags` -- located at `common/web/langtags`
### Deprecated npm packages
diff --git a/ios/Cartfile.resolved b/ios/Cartfile.resolved
index 4e1a058a51..ca05da1ae9 100644
--- a/ios/Cartfile.resolved
+++ b/ios/Cartfile.resolved
@@ -1,4 +1,4 @@
github "ashleymills/Reachability.swift" "v5.2.4"
github "devicekit/DeviceKit" "5.5.0"
-github "getsentry/sentry-cocoa" "8.38.0"
+github "getsentry/sentry-cocoa" "8.44.0"
github "weichsel/ZIPFoundation" "0.9.19"
diff --git a/ios/docs/engine/guides/in-app/index.md b/ios/docs/engine/guides/in-app/index.md
index 14ba8ba4c5..3dfebbf82d 100644
--- a/ios/docs/engine/guides/in-app/index.md
+++ b/ios/docs/engine/guides/in-app/index.md
@@ -1,5 +1,5 @@
---
-title: Guide: build an in-app keyboard for iPhone and iPad with Keyman Engine
+title: Guide: build an in-app keyboard
---
Keyman Engine for iPhone and iPad allows you to use any Keyman touch
diff --git a/ios/docs/engine/guides/system-keyboard/index.md b/ios/docs/engine/guides/system-keyboard/index.md
index 4a533f08d8..ea652360b2 100644
--- a/ios/docs/engine/guides/system-keyboard/index.md
+++ b/ios/docs/engine/guides/system-keyboard/index.md
@@ -1,5 +1,5 @@
---
-title: Guide: build a system keyboard app on iPhone and iPad with Keyman Engine
+title: Guide: build a system keyboard app
---
In [part 1](../in-app/) of this series, we looked at the steps involved
diff --git a/ios/docs/engine/index.md b/ios/docs/engine/index.md
index b87af4e260..bdba88b741 100644
--- a/ios/docs/engine/index.md
+++ b/ios/docs/engine/index.md
@@ -4,7 +4,7 @@ title: Keyman for iPhone and iPad Developer Support
## Overview
-The Keyman Engine for iPhone and iPad 18.0 SDK is designed to provide
+The Keyman Engine for iPhone and iPad 19.0 SDK is designed to provide
advanced international keyboard support to iOS apps.
As a developer, you simply need to use (or subclass) TextView or
diff --git a/ios/docs/help/about/index.md b/ios/docs/help/about/index.md
index d37b9ed978..71ead99252 100644
--- a/ios/docs/help/about/index.md
+++ b/ios/docs/help/about/index.md
@@ -1,5 +1,5 @@
---
-title: About Keyman for iPhone and iPad
+title: About Keyman
---
* [Welcome to Keyman](welcome)
diff --git a/ios/docs/help/about/whatsnew.md b/ios/docs/help/about/whatsnew.md
index aa4cf6b333..b123b1f23c 100644
--- a/ios/docs/help/about/whatsnew.md
+++ b/ios/docs/help/about/whatsnew.md
@@ -1,8 +1,5 @@
---
-title: What's New in Keyman 18.0 for iPhone and iPad
+title: What's New in Keyman 19.0 for iPhone and iPad
---
-Here are some of the new features we have added to Keyman for iPhone and iPad 18.0:
-Additional changes:
-
-* Performance improvements
+Here are some of the new features we have added to Keyman for iPhone and iPad 19.0:
diff --git a/ios/docs/help/basic/index.md b/ios/docs/help/basic/index.md
index e93f77f3aa..b67688e03c 100644
--- a/ios/docs/help/basic/index.md
+++ b/ios/docs/help/basic/index.md
@@ -1,5 +1,5 @@
---
-title: Keyman for iPhone and iPad Basic Help
+title: Basic Help
---
* [Using the Banner on the Keyboard](using-the-banner)
diff --git a/ios/docs/help/context/gestures.md b/ios/docs/help/context/gestures.md
index ab15c3203d..251de0f1ce 100644
--- a/ios/docs/help/context/gestures.md
+++ b/ios/docs/help/context/gestures.md
@@ -1,5 +1,5 @@
---
-title: Gesture Hints and Tips - Keyman for iPhone and iPad
+title: Gesture Hints and Tips
---
## Using Gestures on the Keyboard Keys
diff --git a/ios/docs/help/index.md b/ios/docs/help/index.md
index 994bbbda9e..cd0f81300e 100644
--- a/ios/docs/help/index.md
+++ b/ios/docs/help/index.md
@@ -1,5 +1,5 @@
---
-title: Keyman for iPhone and iPad 18.0 Help
+title: Keyman for iPhone and iPad 19.0 Help
---
## [About Keyman](about/)
diff --git a/ios/docs/help/start/index.md b/ios/docs/help/start/index.md
index d7bb7ad72e..f48fc3f185 100644
--- a/ios/docs/help/start/index.md
+++ b/ios/docs/help/start/index.md
@@ -1,5 +1,5 @@
---
-title: Getting Started with Keyman for iPhone and iPad
+title: Getting Started
---
* [How To - Adding a Keyboard](searching-for-keyboards)
diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift
index c101ebd832..a0c8c6d5e4 100644
--- a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift
+++ b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift
@@ -137,7 +137,7 @@ class KeymanWebViewController: UIViewController {
webView!.backgroundColor = UIColor.clear
webView!.navigationDelegate = self
webView!.scrollView.isScrollEnabled = false
-
+
if #available(iOSApplicationExtension 16.4, *) {
if(Version.current.tier != .stable) {
webView!.isInspectable = true
@@ -164,6 +164,9 @@ class KeymanWebViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
+ if #available(iOSApplicationExtension 14.0, *) {
+ self.userContentController.removeAllScriptMessageHandlers()
+ }
self.userContentController.add(self, name: keymanWebViewName)
}
@@ -600,6 +603,10 @@ extension KeymanWebViewController: KeymanWebDelegate {
os_log("%{public}s", log: KeymanEngineLogger.engine, type: .info, message)
SentryManager.breadcrumb(message, sentryLevel: .debug)
+ // Overwrites the baseline "embeddingApp" text available before Web engine init with the
+ // active bundle identifier.
+ webView!.evaluateJavaScript("setHostAppName(\"\(Bundle.main.infoDictionary!["CFBundleIdentifier"]!)\")")
+
self.setSentryState()
resizeKeyboard()
@@ -696,7 +703,7 @@ extension KeymanWebViewController: KeymanWebDelegate {
// MARK: - Manage views
extension KeymanWebViewController {
// MARK: - Sizing
-
+
@objc func menuKeyHeld(_ keymanWeb: KeymanWebViewController) {
self.delegate?.menuKeyHeld(self)
}
@@ -713,10 +720,10 @@ extension KeymanWebViewController {
let width: CGFloat
width = UIScreen.main.bounds.width
var height: CGFloat
-
+
// get orientation differently if system or in-app keyboard
let portrait = Util.isSystemKeyboard ? InputViewController.isPortrait : UIDevice.current.orientation.isPortrait
-
+
/**
* If keyboard height is saved in UserDefaults, then use it
*/
@@ -728,17 +735,17 @@ extension KeymanWebViewController {
*/
height = self.determineDefaultKeyboardHeight(isPortrait: portrait)
self.writeKeyboardHeightIfDoesNotExist(isPortrait: portrait, height: height)
-
+
/**
* If we need to write out the keyboard height for one orientation, then we
- * expect that the other must be written also.
+ * expect that the other must be written also.
* Write it out now, but only if a value for keyboard height does not already exist.
*/
height = self.determineDefaultKeyboardHeight(isPortrait: !portrait)
self.writeKeyboardHeightIfDoesNotExist(isPortrait: !portrait, height: height)
}
-
- /**
+
+ /**
* no need to check for Util.isSystemKeyboard because this is shared storage
* the UserDefaults are readable and writeable by both system and in-app
*/
@@ -768,7 +775,7 @@ extension KeymanWebViewController {
return height;
}
-
+
/**
* Write out the keyboard height to the UserDefaults but only if it does not exist there yet.
* If it exists, then we assume it was configured by the user and do not want to
@@ -789,7 +796,7 @@ extension KeymanWebViewController {
}
}
}
-
+
var keyboardSize: CGSize {
get {
if kbSize.equalTo(CGSize.zero) {
diff --git a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js
index bd323b970b..b390b438de 100644
--- a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js
+++ b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js
@@ -131,7 +131,7 @@ function getOskWidth() {
function getOskHeight() {
var height = oskHeight;
- if(keyman.osk.banner._activeType != 'blank') {
+ if(keyman.osk && keyman.osk.banner && keyman.osk.banner._activeType != 'blank') {
height = height - keyman.osk.banner.height;
}
return height;
@@ -149,6 +149,11 @@ function setKeymanLanguage(stub) {
});
}
+function setHostAppName(name) {
+ // Sentry will capture this in a breadcrumb for Web errors.
+ keyman.config.embeddingApp = name;
+}
+
var fragmentToggle = 0;
/**
* Inserts the selected string s
diff --git a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj
index 6211f26573..a8b4f8624b 100644
--- a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj
+++ b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj
@@ -1004,7 +1004,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = "/usr/bin/env bash";
- shellScript = "\"$KEYMAN_ROOT/resources/build/xcode-wrap.sh\" \"$KEYMAN_ROOT/resources/build/set-bundle-versions-and-settings-tagged.sh\"\n";
+ shellScript = "\"$KEYMAN_ROOT/resources/build/xcode-wrap.sh\" \"$KEYMAN_ROOT/resources/build/set-bundle-versions-and-settings-tagged.sh\"\necho \"script - set version complete\"\n";
showEnvVarsInLog = 0;
};
CED3CFDA240E49DF001540A1 /* ShellScript */ = {
diff --git a/linux/debian/changelog b/linux/debian/changelog
index 99ce27f238..a15c962527 100644
--- a/linux/debian/changelog
+++ b/linux/debian/changelog
@@ -1,3 +1,9 @@
+keyman (17.0.335-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Eberhard Beilharz Thu, 06 Feb 2025 10:33:00 +0100
+
keyman (17.0.334-1) unstable; urgency=medium
* New upstream release.
diff --git a/linux/debian/control b/linux/debian/control
index fbe7066f5c..e12da18aa3 100644
--- a/linux/debian/control
+++ b/linux/debian/control
@@ -10,7 +10,7 @@ Build-Depends:
debhelper-compat (= 12),
dh-python,
gawk,
- gir1.2-webkit2-4.1 | gir1.2-webkit2-4.0,
+ gir1.2-webkit2-4.1,
ibus,
libevdev-dev,
libgtk-3-dev,
@@ -88,7 +88,7 @@ Architecture: all
Depends:
dbus-x11,
dconf-cli,
- gir1.2-webkit2-4.1 | gir1.2-webkit2-4.0,
+ gir1.2-webkit2-4.1,
keyman-engine,
python3-bs4,
python3-fonttools,
diff --git a/linux/debian/rules b/linux/debian/rules
index 8da37f40e4..5138b58330 100755
--- a/linux/debian/rules
+++ b/linux/debian/rules
@@ -66,10 +66,7 @@ override_dh_auto_install:
rm $(CURDIR)/debian/keyman/usr/share/locale/*.po*
# Don't call `build.sh install` - dh_auto_install does some extra smarts
dh_auto_install --sourcedir=linux/keyman-config --buildsystem=pybuild $@
- # Unfortunately bash-completion 2.10 (focal) doesn't yet provide dh-sequence-bash-completion,
- # which we could add as build-dependency, so we'll have to explicitly call
- # dh_bash_completion here instead of using `dh $@ --with-python3 --with bash-completion`
- dh_bash-completion -O--buildsystem=pybuild
+ dh $@ --with-python3 --with bash-completion
dh_python3 -O--buildsystem=pybuild
override_dh_missing:
diff --git a/linux/docs/help/about/welcome.md b/linux/docs/help/about/welcome.md
index eacc8c449c..dfd410b46e 100644
--- a/linux/docs/help/about/welcome.md
+++ b/linux/docs/help/about/welcome.md
@@ -6,6 +6,6 @@ Thank you for installing Keyman for Linux. Whether you're a new or returning use
## What is Keyman for Linux?
-Keyman for Linux makes it possible to type in over 1,000 languages in any Linux application.
+Keyman for Linux makes it possible to type in over 2,000 languages in any Linux application.
[Click here](../start/) to get started.
diff --git a/linux/docs/help/about/whatsnew.md b/linux/docs/help/about/whatsnew.md
index 4a4d5a30e1..5521261b96 100644
--- a/linux/docs/help/about/whatsnew.md
+++ b/linux/docs/help/about/whatsnew.md
@@ -1,5 +1,5 @@
---
-title: What's New in Keyman 18.0 for Linux
+title: What's New in Keyman 19.0 for Linux
---
-Here are some of the new features we have added to Keyman for Linux 18.0:
+Here are some of the new features we have added to Keyman for Linux 19.0:
diff --git a/linux/docs/help/common/index.md b/linux/docs/help/common/index.md
index b2d606b38d..a7f9e786bf 100644
--- a/linux/docs/help/common/index.md
+++ b/linux/docs/help/common/index.md
@@ -59,8 +59,8 @@ no on-screen keyboard for Wayland that works with Keyman.
**A.** Keyman runs on Debian, Ubuntu, Wasta Linux and can be compiled to run
from source in most distributions.
-**Note:** Ubuntu versions before Ubuntu 20.04 LTS are no longer supported with
-Keyman 17. If you are still running an older version and require Keyman you'll
+**Note:** Ubuntu versions before Ubuntu 22.04 LTS are no longer supported with
+Keyman 18. If you are still running an older version and require Keyman you'll
have to install an older Keyman version.
## Q. Will my existing Windows Keyman keyboard work with Keyman for Linux?
diff --git a/linux/docs/help/index.md b/linux/docs/help/index.md
index 58d245063a..329c66f69b 100644
--- a/linux/docs/help/index.md
+++ b/linux/docs/help/index.md
@@ -1,5 +1,5 @@
---
-title: Keyman for Linux 18.0 Help
+title: Keyman for Linux 19.0 Help
---
Need help using Keyman for Linux? In time, this product documentation will grow and explain frequently asked questions.
diff --git a/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp b/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp
index 50dbccba8d..b411e808f7 100644
--- a/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp
+++ b/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp
@@ -15,7 +15,6 @@ extern gboolean testing;
class KeymanSystemServiceClient {
private:
- sd_bus_error *error = NULL;
sd_bus_message *msg = NULL;
sd_bus *bus = NULL;
@@ -25,6 +24,7 @@ public:
void SetCapsLockIndicator(guint32 capsLock);
gint32 GetCapsLockIndicator();
+ void CallOrderedOutputSentinel();
};
KeymanSystemServiceClient::KeymanSystemServiceClient() {
@@ -40,18 +40,13 @@ KeymanSystemServiceClient::KeymanSystemServiceClient() {
}
KeymanSystemServiceClient::~KeymanSystemServiceClient() {
- if (error) { sd_bus_error_free(error); }
if (msg) { sd_bus_message_unref(msg); }
if (bus) { sd_bus_unref(bus); }
}
void KeymanSystemServiceClient::SetCapsLockIndicator(guint32 capsLock) {
- // If Set/GetCapsLockIndicator is called more than once and previously
- // failed, we will leak `error`.
- assert(error == NULL);
-
if (!bus) {
- // we already reported the error, so just return
+ // we already reported the error in the c'tor, so just return
return;
}
@@ -60,30 +55,30 @@ void KeymanSystemServiceClient::SetCapsLockIndicator(guint32 capsLock) {
return;
}
+ sd_bus_error *error = NULL;
int result = sd_bus_call_method(bus, KEYMAN_BUS_NAME, KEYMAN_OBJECT_PATH,
KEYMAN_INTERFACE_NAME, "SetCapsLockIndicator", error, &msg, "b", capsLock);
if (result < 0) {
g_error("%s: Failed to call method SetCapsLockIndicator: %s. %s. %s.",
__FUNCTION__, strerror(-result), error ? error->name : "-", error ? error->message : "-");
+ sd_bus_error_free(error);
return;
}
}
gint32 KeymanSystemServiceClient::GetCapsLockIndicator() {
- // If Set/GetCapsLockIndicator is called more than once and previously
- // failed, we will leak `error`.
- assert(error == NULL);
-
if (!bus) {
- // we already reported the error, so just return
+ // we already reported the error in the c'tor, so just return
return -1;
}
+ sd_bus_error *error = NULL;
int result = sd_bus_call_method(bus, KEYMAN_BUS_NAME, KEYMAN_OBJECT_PATH,
KEYMAN_INTERFACE_NAME, "GetCapsLockIndicator", error, &msg, "");
if (result < 0) {
g_error("%s: Failed to call method GetCapsLockIndicator: %s. %s. %s.",
__FUNCTION__, strerror(-result), error ? error->name : "-", error ? error->message : "-");
+ sd_bus_error_free(error);
return -1;
}
@@ -98,9 +93,26 @@ gint32 KeymanSystemServiceClient::GetCapsLockIndicator() {
return capsLock;
}
-void set_capslock_indicator(
- guint32 capsLock
-) {
+void
+KeymanSystemServiceClient::CallOrderedOutputSentinel() {
+ if (!bus) {
+ // we already reported the error in the c'tor, so just return
+ return;
+ }
+
+ sd_bus_error *error = NULL;
+ int result = sd_bus_call_method(bus, KEYMAN_BUS_NAME, KEYMAN_OBJECT_PATH,
+ KEYMAN_INTERFACE_NAME, "CallOrderedOutputSentinel", error, &msg, "");
+ if (result < 0) {
+ g_error("%s: Failed to call method CallOrderedOutputSentinel: %s. %s. %s.",
+ __FUNCTION__, strerror(-result), error ? error->name : "-", error ? error->message : "-");
+ sd_bus_error_free(error);
+ return;
+ }
+}
+
+void
+set_capslock_indicator(guint32 capsLock) {
KeymanSystemServiceClient client;
client.SetCapsLockIndicator(capsLock);
}
@@ -109,3 +121,10 @@ gint32 get_capslock_indicator() {
KeymanSystemServiceClient client;
return client.GetCapsLockIndicator();
}
+
+void
+call_ordered_output_sentinel() {
+ g_message("%s: Calling order output sentinel on keyman-system-service", __FUNCTION__);
+ KeymanSystemServiceClient client;
+ client.CallOrderedOutputSentinel();
+}
diff --git a/linux/ibus-keyman/src/KeymanSystemServiceClient.h b/linux/ibus-keyman/src/KeymanSystemServiceClient.h
index 746974df51..bf5766434f 100644
--- a/linux/ibus-keyman/src/KeymanSystemServiceClient.h
+++ b/linux/ibus-keyman/src/KeymanSystemServiceClient.h
@@ -9,6 +9,7 @@ extern "C" {
void set_capslock_indicator(guint32 capsLockState);
gint32 get_capslock_indicator();
+void call_ordered_output_sentinel();
#ifdef __cplusplus
}
diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c
index 296467e863..29d1be00d2 100644
--- a/linux/ibus-keyman/src/engine.c
+++ b/linux/ibus-keyman/src/engine.c
@@ -39,29 +39,18 @@
#include "engine.h"
#include "keycodes.h"
-// Fallback for older ibus versions that don't define IBUS_PREFILTER_MASK
-#ifndef IBUS_HAS_PREFILTER
-#ifdef KEYMAN_PKG_BUILD
-// When building packages on Ubuntu and Debian servers we probably don't have
-// a patched ibus available and additionally treat warnings as errors, but
-// still want to build packages.
-#pragma message "Compiling against ibus version that does not include prefilter mask patch\n(https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled."
-#else
-#warning Compiling against ibus version that does not include prefilter mask patch (https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled.
-#endif
-
-#define IBUS_PREFILTER_MASK (1 << 23)
-#endif
-
#define MAXCONTEXT_ITEMS 128
+
+// Values from /usr/include/linux/input-event-codes.h
#define KEYMAN_BACKSPACE 14
#define KEYMAN_BACKSPACE_KEYSYM IBUS_KEY_BackSpace
-#define KEYMAN_LCTRL 29 // 0x1D
-#define KEYMAN_LALT 56 // 0x38
-#define KEYMAN_RCTRL 97 // 0x61
-#define KEYMAN_RALT 100 // 0x64
-#define KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL 202
-#define KEYMAN_NOCHAR_KEYSYM (0xfdd0 | 0x1000000) // Unicode NOCHAR
+#define KEYMAN_LCTRL 29 // 0x1D
+#define KEYMAN_LSHIFT 42 // 0x2A
+#define KEYMAN_RSHIFT 54 // 0x36
+#define KEYMAN_LALT 56 // 0x38
+#define KEYMAN_RCTRL 97 // 0x61
+#define KEYMAN_RALT 100 // 0x64
+#define KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL 194 // 0xC2
typedef struct _IBusKeymanEngine IBusKeymanEngine;
typedef struct _IBusKeymanEngineClass IBusKeymanEngineClass;
@@ -69,11 +58,12 @@ typedef struct _IBusKeymanEngineClass IBusKeymanEngineClass;
#define MAX_QUEUE_SIZE 100
typedef struct _commit_queue_item {
- // char_buffer and emitting_keystroke as well as more than one queue
- // item are only used if ibus supports prefilter but the client
- // doesn't support surrounding text (non-compliant app)
+ // char_buffer, emitting_keystroke and code_points_to_delete as well as
+ // more than one queue item are only used if the client doesn't
+ // support surrounding text (non-compliant app)
gchar *char_buffer;
gboolean emitting_keystroke;
+ guint code_points_to_delete;
guint keyval;
guint keycode;
@@ -253,17 +243,6 @@ debug_utf8_with_codepoints(const gchar *utf8) {
#endif
}
-static gboolean
-client_supports_prefilter(IBusEngine *engine)
-{
- g_assert(engine != NULL);
-#ifdef IBUS_HAS_PREFILTER
- return (engine->client_capabilities & IBUS_CAP_PREFILTER) != 0;
-#else
- return FALSE;
-#endif
-}
-
static gboolean
client_supports_surrounding_text(IBusEngine *engine) {
g_assert(engine != NULL);
@@ -660,14 +639,14 @@ process_output_action(IBusEngine *engine, const km_core_usv* output_utf32) {
IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine;
gchar *output_utf8 = g_ucs4_to_utf8(output_utf32, -1, NULL, NULL, NULL);
g_autofree gchar *debug = NULL;
- if (client_supports_prefilter(engine) && !client_supports_surrounding_text(engine)) {
- // non-compliant app with patched ibus
+ if (!client_supports_surrounding_text(engine)) {
+ // non-compliant app
g_message("%s: Adding to commit queue: %s", __FUNCTION__, debug = debug_utf8_with_codepoints(output_utf8));
g_assert(keyman->commit_item->char_buffer == NULL);
keyman->commit_item->char_buffer = output_utf8;
// don't free output_utf8 - assigned to char_buffer!
} else {
- // compliant app or unpatched ibus
+ // compliant app
g_message("%s: Outputing %s", __FUNCTION__, debug = debug_utf8_with_codepoints(output_utf8));
commit_string(keyman, output_utf8);
g_free(output_utf8);
@@ -695,11 +674,9 @@ process_backspace_action(IBusEngine *engine, unsigned int code_points_to_delete)
g_message("%s: compliant app: deleting surrounding text %d codepoints", __FUNCTION__, code_points_to_delete);
ibus_engine_delete_surrounding_text(engine, -code_points_to_delete, code_points_to_delete);
} else {
- g_message("%s: non-compliant app: forwarding %d backspaces", __FUNCTION__, code_points_to_delete);
- while (code_points_to_delete > 0) {
- ibus_engine_forward_key_event(engine, KEYMAN_BACKSPACE_KEYSYM, KEYMAN_BACKSPACE, 0);
- code_points_to_delete--;
- }
+ g_message("%s: non-compliant app: queueing %d backspaces", __FUNCTION__, code_points_to_delete);
+ IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine;
+ keyman->commit_item->code_points_to_delete = code_points_to_delete;
}
}
@@ -723,8 +700,8 @@ process_emit_keystroke_action(IBusEngine *engine, km_core_bool emit_keystroke) {
return;
}
IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine;
- if (!client_supports_prefilter(engine) || client_supports_surrounding_text(engine)) {
- // compliant app or unpatched ibus version
+ if (client_supports_surrounding_text(engine)) {
+ // compliant app
ibus_engine_forward_key_event(engine, keyman->commit_item->keyval,
keyman->commit_item->keycode, keyman->commit_item->state);
return;
@@ -745,21 +722,42 @@ process_capslock_action(km_core_caps_state caps_state) {
static void
commit_current_queue_item(IBusKeymanEngine *keyman) {
- // only called for non-compliant apps with patched ibus
+ // only called for non-compliant apps
g_assert(keyman != NULL);
- g_assert(client_supports_prefilter((IBusEngine *)keyman));
- g_assert(!client_supports_surrounding_text((IBusEngine *)keyman));
+ IBusEngine* engine = (IBusEngine *)keyman;
+ g_assert(!client_supports_surrounding_text(engine));
- if (keyman->commit_item <= keyman->commit_queue)
+ if (keyman->commit_item <= keyman->commit_queue){
+ g_message("%s: queue is empty", __FUNCTION__);
return;
+ }
commit_queue_item *current_item = &keyman->commit_queue[0];
+ if (current_item->code_points_to_delete > 0) {
+ g_message("%s: Forwarding %d backspaces from commit queue", __FUNCTION__, current_item->code_points_to_delete);
+ while (current_item->code_points_to_delete > 0) {
+ ibus_engine_forward_key_event(engine, KEYMAN_BACKSPACE_KEYSYM, KEYMAN_BACKSPACE, 0);
+ current_item->code_points_to_delete--;
+ }
+ // don't remove the item from the queue yet - we need to process it
+ // again for the output and keystrokes. Instead emit the sentinel key
+ // again.
+ g_message("%s: Forcing ordered output", __FUNCTION__);
+ call_ordered_output_sentinel();
+ return;
+ }
if (current_item->char_buffer != NULL) {
+ g_autofree gchar *debug = NULL;
+ g_message("%s: Committing from commit queue: %s", __FUNCTION__,
+ debug = debug_utf8_with_codepoints(current_item->char_buffer));
commit_string(keyman, current_item->char_buffer);
g_free(current_item->char_buffer);
+ current_item->char_buffer = NULL;
}
if (current_item->emitting_keystroke) {
- ibus_engine_forward_key_event((IBusEngine*)keyman, current_item->keyval, current_item->keycode, current_item->state);
+ g_message("%s: Forwarding key from commit queue: keyval=0x%02x, keycode=0x%02x, state=0x%02x",
+ __FUNCTION__, current_item->keyval, current_item->keycode, current_item->state);
+ ibus_engine_forward_key_event(engine, current_item->keyval, current_item->keycode, current_item->state);
}
keyman->commit_item--;
memmove(keyman->commit_queue, &keyman->commit_queue[1], sizeof(commit_queue_item) * MAX_QUEUE_SIZE - 1);
@@ -770,30 +768,41 @@ static void
finish_process_actions(IBusEngine *engine) {
g_assert(engine != NULL);
IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine;
- if (!client_supports_prefilter(engine) || client_supports_surrounding_text(engine)) {
- // compliant app or unpatched ibus
+ if (client_supports_surrounding_text(engine)) {
+ // compliant app
return;
}
- // non-compliant app with patched ibus
- guint state = keyman->commit_item->state;
- keyman->commit_item++;
- if (keyman->commit_item > &keyman->commit_queue[MAX_QUEUE_SIZE-1]) {
- g_error("Overflow of keyman commit_queue!");
- // TODO: log to Sentry
- keyman->commit_item--;
- }
+ // non-compliant app
+ guint keycode = keyman->commit_item->keycode;
- // Forward a fake key event to get the correct order of events so that any backspace key we
- // generated will be processed before the character we're adding. We need to send a
- // valid keyval/keycode combination so that it doesn't get swallowed by GTK but which
- // isn't very likely used in real keyboards. F24 seems to work for that.
- ibus_engine_forward_key_event(engine,
- KEYMAN_NOCHAR_KEYSYM,
- KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL,
- (state & IBUS_RELEASE_MASK)
- ? IBUS_PREFILTER_MASK | IBUS_RELEASE_MASK
- : IBUS_PREFILTER_MASK);
+ switch (keycode) {
+ case KEYMAN_LSHIFT:
+ case KEYMAN_RSHIFT:
+ case KEYMAN_LCTRL:
+ case KEYMAN_RCTRL:
+ case KEYMAN_LALT:
+ case KEYMAN_RALT:
+ // we don't forward modifier keys that the user holds while pressing another
+ // key.
+ g_message("%s: Ignoring modifier key", __FUNCTION__);
+ break;
+ default:
+ keyman->commit_item++;
+ if (keyman->commit_item > &keyman->commit_queue[MAX_QUEUE_SIZE - 1]) {
+ g_error("Overflow of keyman commit_queue!");
+ // TODO: log to Sentry
+ keyman->commit_item--;
+ }
+
+ // Forward a fake key event to get the correct order of events so that any backspace key we
+ // generated will be processed before the character we're adding. We need to send a
+ // valid keycode so that it doesn't get swallowed by GTK but which isn't very likely used
+ // in real keyboards. F24 seems to work for that.
+ g_message("%s: Forcing ordered output", __FUNCTION__);
+ call_ordered_output_sentinel();
+ break;
+ }
}
static void
@@ -826,13 +835,25 @@ ibus_keyman_engine_process_key_event(
g_message("-----------------------------------------------------------------------------------------------------------------");
g_message(
- "DAR: %s - keyval=0x%02x keycode=0x%02x, state=0x%02x, isKeyDown=%d, supports_prefilter=%d, compliant=%d", __FUNCTION__, keyval, keycode,
- state, isKeyDown, client_supports_prefilter(engine), client_supports_surrounding_text(engine));
+ "DAR: %s - keyval=0x%02x keycode=0x%02x, state=0x%02x, isKeyDown=%d, compliant=%d", __FUNCTION__, keyval, keycode,
+ state, isKeyDown, client_supports_surrounding_text(engine));
// This keycode is a fake keycode that we send when it's time to commit the text, ensuring the
// correct output order of backspace and text.
- if (client_supports_prefilter(engine) && !client_supports_surrounding_text(engine) &&
- keycode == KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL && (state & IBUS_PREFILTER_MASK)) {
+ if (!client_supports_surrounding_text(engine) && keycode == KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL) {
+ if (!isKeyDown) {
+ g_message("%s: got F24 Sentinel, ignore keyup", __FUNCTION__);
+ return TRUE;
+ }
+ g_message("%s: got F24 Sentinel, queue content:", __FUNCTION__);
+ for (int i = 0; i < MAX_QUEUE_SIZE && &keyman->commit_queue[i] != keyman->commit_item; i++) {
+ commit_queue_item *item = &keyman->commit_queue[i];
+ g_message(
+ " queue item %d: keyval=0x%02x keycode=0x%02x, state=0x%02x, char_buffer=%s, "
+ "emitting_keystroke=%d, code_points_to_delete=%d",
+ i, item->keyval, item->keycode, item->state, item->char_buffer,
+ item->emitting_keystroke, item->code_points_to_delete);
+ }
commit_current_queue_item(keyman);
return TRUE;
}
@@ -869,7 +890,7 @@ ibus_keyman_engine_process_key_event(
// #10476: Core currently doesn't handle IBUS_MOD{2-4}_MASK modifiers.
// On Ubuntu 23.10/24.04 we get IBUS_MOD4_MASK set on keycode 0x39 (space) when
// the user tries to switch keyboards. Since this is not a regular keypress
- // and Core doesn't handle it, we need to just return and let the Gnome deal
+ // and Core doesn't handle it, we need to just return and let GTK deal
// with it. We could consider to add it to Core and let Core ignore it.
// As for IBUS_MOD3_MASK it's unclear when/how that gets set, so we
// just not deal with that for now until we notice problems.
@@ -883,7 +904,7 @@ ibus_keyman_engine_process_key_event(
// it. At the moment however we let Core process the keypress and since
// it doesn't have rules for the numeric keypad keys we eventually
// forward the key to ibus (in process_emit_keystroke_action) and let
- // Gnome deal with it.
+ // GTK deal with it.
// keyman modifiers are different from X11/ibus
uint16_t km_mod_state = 0;
@@ -929,15 +950,14 @@ ibus_keyman_engine_process_key_event(
process_actions(engine, core_actions);
- // If we have a new ibus version that supports prefilter and a non-compliant
- // client, i.e. a client that doesn't support surrounding text (e.g.
- // Chromium as of v104) we forwarded the key event with IBUS_PREFILTER_MASK
- // set and now stop further processing by returning TRUE.
- // With an old ibus version without prefilter support as well as with
- // a compliant client (i.e. it does support surrounding text), we return
+ // If we have a non-compliant client, i.e. a client that doesn't support
+ // surrounding text (e.g. Chromium as of v104) we sent the key event
+ // to the system service and now stop further processing by returning TRUE.
+ // With a compliant client (i.e. it does support surrounding text), we return
// TRUE because we completely processed the event and no further
// processing should happen.
g_message("%s: after processing all actions: %s", __FUNCTION__, debug_context2 = get_context_debug(engine));
+
return TRUE;
}
diff --git a/linux/ibus-keyman/src/keymanutil.c b/linux/ibus-keyman/src/keymanutil.c
index 9f48f0ffce..4bb0ac4078 100644
--- a/linux/ibus-keyman/src/keymanutil.c
+++ b/linux/ibus-keyman/src/keymanutil.c
@@ -649,9 +649,8 @@ _ptr_array_new_from_array(
}
// `g_ptr_array_new_from_null_terminated_array` is only available in GLib 2.76, but we're still
-// stuck to 2.64 (Ubuntu 20.04 Focal) and 2.72 (Ubuntu 22.04 Jammy). Therefore we
-// copy the implementation here (slightly simplified). Once we're past 2.76 we can use the GLib method
-// directly.
+// stuck to 2.72 (Ubuntu 22.04 Jammy). Therefore we copy the implementation here (slightly
+// simplified). Once we're past 2.76 we can use the GLib method directly.
GPtrArray *
_g_ptr_array_new_from_null_terminated_array(
gpointer *data,
diff --git a/linux/ibus-keyman/src/test/keymanutil.tests.c b/linux/ibus-keyman/src/test/keymanutil.tests.c
index 13636857d8..56965a41ad 100644
--- a/linux/ibus-keyman/src/test/keymanutil.tests.c
+++ b/linux/ibus-keyman/src/test/keymanutil.tests.c
@@ -123,22 +123,6 @@ _free_tst_kb_data(add_keyboard_data* kb_data) {
G_DEFINE_AUTOPTR_CLEANUP_FUNC(add_keyboard_data, _free_tst_kb_data)
-// Newer glib versions have g_assert_cmpstrv which would allow to do
-// g_assert_cmpstrv(result, keyboards);
-// but unfortunately Ubuntu 20.04 Focal doesn't have that, so we roll
-// our own
-void
-_kmn_assert_cmpstrv(gchar** result, gchar** expected) {
- g_assert_nonnull(result);
- g_assert_nonnull(expected);
- int i = 0;
- for (; result[i] && expected[i]; i++) {
- g_assert_cmpstr(result[i], ==, expected[i]);
- }
- g_assert_null(result[i]);
- g_assert_null(expected[i]);
-}
-
//----------------------------------------------------------------------------------------------
void
test_keyman_put_keyboard_options_todconf__invalid() {
@@ -526,7 +510,7 @@ test_keyman_set_custom_keyboards__new_key() {
// Verify
g_auto(GStrv) result = _get_tst_kbds_key();
g_assert_nonnull(result);
- _kmn_assert_cmpstrv(result, keyboards);
+ g_assert_cmpstrv(result, keyboards);
// Cleanup
_delete_tst_kbds_key();
@@ -546,7 +530,7 @@ test_keyman_set_custom_keyboards__overwrite_key() {
// Verify
g_auto(GStrv) result = _get_tst_kbds_key();
g_assert_nonnull(result);
- _kmn_assert_cmpstrv(result, keyboards);
+ g_assert_cmpstrv(result, keyboards);
// Cleanup
_delete_tst_kbds_key();
@@ -565,7 +549,7 @@ test_keyman_set_custom_keyboards__delete_key_NULL() {
g_auto(GStrv) result = _get_tst_kbds_key();
gchar* expected[] = {NULL};
g_assert_nonnull(result);
- _kmn_assert_cmpstrv(result, expected);
+ g_assert_cmpstrv(result, expected);
// Cleanup
_delete_tst_kbds_key();
@@ -585,7 +569,7 @@ test_keyman_set_custom_keyboards__delete_key_empty_array() {
// Verify
g_auto(GStrv) result = _get_tst_kbds_key();
g_assert_nonnull(result);
- _kmn_assert_cmpstrv(result, keyboards);
+ g_assert_cmpstrv(result, keyboards);
// Cleanup
_delete_tst_kbds_key();
@@ -604,7 +588,7 @@ test_keyman_set_custom_keyboards__invalid_values() {
g_auto(GStrv) result = _get_tst_kbds_key();
g_assert_nonnull(result);
gchar* expected[] = {"fr:/tmp/test/test.kmx", NULL};
- _kmn_assert_cmpstrv(result, expected);
+ g_assert_cmpstrv(result, expected);
// Cleanup
_delete_tst_kbds_key();
@@ -622,7 +606,7 @@ test_keyman_get_custom_keyboards__value() {
// Verify
g_assert_nonnull(result);
- _kmn_assert_cmpstrv(result, keyboards);
+ g_assert_cmpstrv(result, keyboards);
// Cleanup
_delete_tst_kbds_key();
@@ -640,7 +624,7 @@ test_keyman_get_custom_keyboards__invalid_values() {
// Verify
gchar* expected[] = {"fr:/tmp/test/test.kmx", NULL};
g_assert_nonnull(result);
- _kmn_assert_cmpstrv(result, expected);
+ g_assert_cmpstrv(result, expected);
// Cleanup
_delete_tst_kbds_key();
diff --git a/linux/ibus-keyman/tests/ibusimcontext.c b/linux/ibus-keyman/tests/ibusimcontext.c
index c6d7af1e98..d3181727ce 100644
--- a/linux/ibus-keyman/tests/ibusimcontext.c
+++ b/linux/ibus-keyman/tests/ibusimcontext.c
@@ -23,26 +23,13 @@
*/
// This file is based on https://github.com/ibus/ibus/blob/master/client/gtk2/ibusimcontext.c
-// commit 506ac9993d5166196b7c4e9bfa9fb0f9d3792ffa plus our two prefilter commits.
+// commit 506ac9993d5166196b7c4e9bfa9fb0f9d3792ffa.
// It simulates the GTK2 client, leaving out code for GTK3 and GTK4 and
// simplyfying the code a bit by replacing async calls with direct synchronous
// method calls.
#include
-#ifndef IBUS_HAS_PREFILTER
-#ifdef KEYMAN_PKG_BUILD
-// When building packages on Ubuntu and Debian servers we probably don't have
-// a patched ibus available and additionally treat warnings as errors, but
-// still want to build packages.
-#pragma message "Compiling against ibus version that does not include prefilter mask patch\n(https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled."
-#else
-#warning Compiling against ibus version that does not include prefilter mask patch (https://github.com/ibus/ibus/pull/2440). Output ordering guarantees will be disabled.
-#endif
-
-#define IBUS_PREFILTER_MASK (1 << 23)
-#endif
-
#include "ibusimcontext.h"
#include
#include
@@ -249,7 +236,6 @@ _process_key_event_done(GObject *object, GAsyncResult *res, gpointer user_data)
if (retval == FALSE) {
((GdkEventKey *)event)->state |= IBUS_IGNORED_MASK;
- ((GdkEventKey *)event)->state &= ~IBUS_PREFILTER_MASK;
gdk_event_put(event);
}
gdk_event_free(event);
@@ -401,10 +387,6 @@ ibus_im_context_init(GObject *obj) {
ibusimcontext->caps |= IBUS_CAP_SURROUNDING_TEXT;
}
-#ifdef IBUS_HAS_PREFILTER
- ibusimcontext->caps |= IBUS_CAP_PREFILTER;
-#endif
-
ibusimcontext->events_queue = g_queue_new();
if (ibus_bus_is_connected(_bus)) {
@@ -463,7 +445,7 @@ ibus_im_context_filter_keypress(GtkIMContext *context, GdkEventKey *event) {
/* Do not call gtk_im_context_filter_keypress() because
* gtk_im_context_simple_filter_keypress() binds Ctrl-Shift-u
*/
- if (event->state & IBUS_IGNORED_MASK && !(event->state & IBUS_PREFILTER_MASK))
+ if (event->state & IBUS_IGNORED_MASK)
return ibus_im_context_commit_event(ibusimcontext, event);
/* XXX it is a workaround for some applications do not set client
@@ -956,10 +938,6 @@ _ibus_context_forward_key_event_cb(
/* _create_gdk_event() will add 8 to keycode. */
if (keycode != 0)
keycode -= 8;
- } else if (state & IBUS_PREFILTER_MASK) {
- // _create_gdk_event() will add 8 to keycode
- if (keycode != 0)
- keycode -= 8;
}
GdkEventKey *event = _create_gdk_event(ibusimcontext, keyval, keycode, state);
@@ -983,8 +961,6 @@ _ibus_context_forward_key_event_cb(
}
} while (index >= 0);
g_string_erase(ibusimcontext->text, index, len);
- } else if (state & IBUS_PREFILTER_MASK) {
- gtk_im_context_filter_keypress((GtkIMContext *)ibusimcontext, event);
} else {
gdk_event_put((GdkEvent *)event);
}
diff --git a/linux/ibus-keyman/tests/meson.build b/linux/ibus-keyman/tests/meson.build
index 5ae10ae211..6e3c4355a4 100644
--- a/linux/ibus-keyman/tests/meson.build
+++ b/linux/ibus-keyman/tests/meson.build
@@ -66,103 +66,104 @@ find_tests = find_program('find-tests.sh', dirs: [meson.current_source_dir() / '
mutter = find_program('mutter', required: false, version: '>=40')
can_build_wayland = mutter.found()
-test(
- 'setup-x11',
- setup_tests,
- args: ['--x11', env_file, cleanup_file, pid_file],
- env: test_env,
- priority: -10,
- is_parallel: false,
- protocol: 'exitcode'
-)
+# TODO: re-enable and fix test setup
+# test(
+# 'setup-x11',
+# setup_tests,
+# args: ['--x11', env_file, cleanup_file, pid_file],
+# env: test_env,
+# priority: -10,
+# is_parallel: false,
+# protocol: 'exitcode'
+# )
-test(
- 'teardown-x11',
- teardown_tests,
- args: [cleanup_file],
- priority: -19,
- is_parallel: false,
- protocol: 'exitcode'
-)
+# test(
+# 'teardown-x11',
+# teardown_tests,
+# args: [cleanup_file],
+# priority: -19,
+# is_parallel: false,
+# protocol: 'exitcode'
+# )
-if can_build_wayland
- test(
- 'setup-wayland',
- setup_tests,
- args: ['--wayland', env_file, cleanup_file, pid_file],
- env: test_env,
- priority: -20,
- is_parallel: false,
- protocol: 'exitcode'
- )
+# if can_build_wayland
+# test(
+# 'setup-wayland',
+# setup_tests,
+# args: ['--wayland', env_file, cleanup_file, pid_file],
+# env: test_env,
+# priority: -20,
+# is_parallel: false,
+# protocol: 'exitcode'
+# )
- test(
- 'teardown-wayland',
- teardown_tests,
- args: [cleanup_file],
- priority: -29,
- is_parallel: false,
- protocol: 'exitcode'
- )
-endif
+# test(
+# 'teardown-wayland',
+# teardown_tests,
+# args: [cleanup_file],
+# priority: -29,
+# is_parallel: false,
+# protocol: 'exitcode'
+# )
+# endif
-kmxtest_files = run_command(
- find_tests,
- [ common_dir / 'test/keyboards/baseline' ],
- check: true,
-).stdout().split('\n')
+# kmxtest_files = run_command(
+# find_tests,
+# [ common_dir / 'test/keyboards/baseline' ],
+# check: true,
+# ).stdout().split('\n')
-foreach kmx: kmxtest_files
- filename = kmx.split('\t')
- if filename[0] == ''
- continue
- endif
- testname = filename[1].split('.kmx')[0]
- test_args = [ '--tap', '-k', '--env', env_file, '--cleanup', cleanup_file, '--check', pid_file, '--', filename]
- test(
- 'X11-' + testname + '__surrounding-text',
- run_test,
- args: [ '--testname', testname, '--x11', '--surrounding-text', test_args],
- env: test_env,
- depends: [test_exe],
- priority: -11,
- is_parallel: false,
- timeout: 120,
- protocol: 'tap',
- )
- test(
- 'X11-' + testname + '__no-surrounding-text',
- run_test,
- args: [ '--testname', testname, '--x11', '--no-surrounding-text', test_args],
- env: test_env,
- depends: [test_exe],
- priority: -12,
- is_parallel: false,
- timeout: 120,
- protocol: 'tap',
- )
- if can_build_wayland
- test(
- 'Wayland-' + testname + '__surrounding-text',
- run_test,
- args: [ '--testname', testname, '--wayland', '--surrounding-text', test_args],
- env: test_env,
- depends: [test_exe],
- priority: -21,
- is_parallel: false,
- timeout: 120,
- protocol: 'tap',
- )
- test(
- 'Wayland-' + testname + '__no-surrounding-text',
- run_test,
- args: [ '--testname', testname, '--wayland', '--no-surrounding-text', test_args],
- env: test_env,
- depends: [test_exe],
- priority: -22,
- is_parallel: false,
- timeout: 120,
- protocol: 'tap',
- )
- endif
-endforeach
+# foreach kmx: kmxtest_files
+# filename = kmx.split('\t')
+# if filename[0] == ''
+# continue
+# endif
+# testname = filename[1].split('.kmx')[0]
+# test_args = [ '--tap', '-k', '--env', env_file, '--cleanup', cleanup_file, '--check', pid_file, '--', filename]
+# test(
+# 'X11-' + testname + '__surrounding-text',
+# run_test,
+# args: [ '--testname', testname, '--x11', '--surrounding-text', test_args],
+# env: test_env,
+# depends: [test_exe],
+# priority: -11,
+# is_parallel: false,
+# timeout: 120,
+# protocol: 'tap',
+# )
+# test(
+# 'X11-' + testname + '__no-surrounding-text',
+# run_test,
+# args: [ '--testname', testname, '--x11', '--no-surrounding-text', test_args],
+# env: test_env,
+# depends: [test_exe],
+# priority: -12,
+# is_parallel: false,
+# timeout: 120,
+# protocol: 'tap',
+# )
+# if can_build_wayland
+# test(
+# 'Wayland-' + testname + '__surrounding-text',
+# run_test,
+# args: [ '--testname', testname, '--wayland', '--surrounding-text', test_args],
+# env: test_env,
+# depends: [test_exe],
+# priority: -21,
+# is_parallel: false,
+# timeout: 120,
+# protocol: 'tap',
+# )
+# test(
+# 'Wayland-' + testname + '__no-surrounding-text',
+# run_test,
+# args: [ '--testname', testname, '--wayland', '--no-surrounding-text', test_args],
+# env: test_env,
+# depends: [test_exe],
+# priority: -22,
+# is_parallel: false,
+# timeout: 120,
+# protocol: 'tap',
+# )
+# endif
+# endforeach
diff --git a/linux/ibus-keyman/tests/scripts/run-tests.sh b/linux/ibus-keyman/tests/scripts/run-tests.sh
index 012dee996f..4b28327259 100755
--- a/linux/ibus-keyman/tests/scripts/run-tests.sh
+++ b/linux/ibus-keyman/tests/scripts/run-tests.sh
@@ -65,7 +65,11 @@ function run_tests() {
setup "$DISPLAY_SERVER" "$ENV_FILE" "$CLEANUP_FILE" "$PID_FILE" --standalone
- echo "# NOTE: When the tests fail check /tmp/ibus-engine-keyman.log and /tmp/ibus-daemon.log!"
+ if [[ "${DOCKER_RUNNING:-false}" == "true" ]]; then
+ echo "# NOTE: When the tests fail check ibus-engine-keyman.log, ibus-daemon.log and km-test-server.log in build/docker-linux/tmp/!"
+ else
+ echo "# NOTE: When the tests fail check /tmp/ibus-engine-keyman.log, /tmp/ibus-daemon.log and /tmp/km-test-server.log!"
+ fi
echo ""
if [ $# -gt 0 ]; then
diff --git a/linux/ibus-keyman/tests/scripts/test-helper.inc.sh b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh
index 940c5ad310..3287acd05c 100755
--- a/linux/ibus-keyman/tests/scripts/test-helper.inc.sh
+++ b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh
@@ -140,7 +140,7 @@ function _setup_test_dbus_server() {
CLEANUP_FILE=$2
# Start test dbus server. This will create `/tmp/km-test-server.env`.
- "${TOP_BINDIR}/tests/km-dbus-test-server" &> /dev/null &
+ "${TOP_BINDIR}/tests/km-dbus-test-server" &> /tmp/km-test-server.log &
sleep 1
cat /tmp/km-test-server.env >> "$ENV_FILE"
@@ -251,7 +251,7 @@ function _setup_ibus() {
#shellcheck disable=SC2086
ibus-daemon ${ARG_VERBOSE-} --daemonize --panel=disable --address=unix:abstract="${TEMP_DATA_DIR}/test-ibus" ${IBUS_CONFIG-} &> /tmp/ibus-daemon.log
PID=$(pgrep -f "${TEMP_DATA_DIR}/test-ibus")
- if [[ "${STANDALONE}" == "--standalone" ]] && [[ ! "${DOCKER_RUNNING:-false}" ]]; then
+ if [[ "${STANDALONE}" == "--standalone" ]] && [[ "${DOCKER_RUNNING:-false}" != "true" ]]; then
# manual test run
echo "if kill -9 ${PID}; then ibus restart || ibus start; fi # ibus-daemon" >> "${CLEANUP_FILE}"
else
@@ -321,6 +321,15 @@ function cleanup() {
rm "$CLEANUP_FILE"
echo "# Finished shutdown of processes."
fi
+
+ if [[ "${DOCKER_RUNNING:-false}" == "true" ]]; then
+ # Note: build/tmp is build/docker-linux/tmp on the host!
+ BUILD_TMP="$(dirname "$0")/../../../build/tmp"
+ mkdir -p "${BUILD_TMP}"
+ [[ -f /tmp/ibus-engine-keyman.log ]] && mv /tmp/ibus-engine-keyman.log "${BUILD_TMP}/ibus-engine-keyman.log"
+ [[ -f /tmp/ibus-daemon.log ]] && mv /tmp/ibus-daemon.log "${BUILD_TMP}/ibus-daemon.log"
+ [[ -f /tmp/km-test-server.log ]] && mv /tmp/km-test-server.log "${BUILD_TMP}/km-test-server.log"
+ fi
}
function exit_on_package_build() {
diff --git a/linux/keyman-config/keyman_config/downloadkeyboard.py b/linux/keyman-config/keyman_config/downloadkeyboard.py
index 42ff74308d..ca3b091f92 100755
--- a/linux/keyman-config/keyman_config/downloadkeyboard.py
+++ b/linux/keyman-config/keyman_config/downloadkeyboard.py
@@ -7,12 +7,7 @@ import urllib.parse
import gi
gi.require_version('Gtk', '3.0')
-try:
- gi.require_version('WebKit2', '4.1')
-except ValueError:
- # TODO: Remove once we drop support for Ubuntu 20.04 Focal
- gi.require_version('WebKit2', '4.0')
-
+gi.require_version('WebKit2', '4.1')
from gi.repository import Gtk, WebKit2
from keyman_config import KeymanComUrl, _, __releaseversion__, __tier__
diff --git a/linux/keyman-config/keyman_config/install_window.py b/linux/keyman-config/keyman_config/install_window.py
index dda242c7e1..1342dd44f6 100755
--- a/linux/keyman-config/keyman_config/install_window.py
+++ b/linux/keyman-config/keyman_config/install_window.py
@@ -15,12 +15,7 @@ import packaging.version
import gi
gi.require_version('Gtk', '3.0')
-try:
- gi.require_version('WebKit2', '4.1')
-except ValueError:
- # TODO: Remove once we drop support for Ubuntu 20.04 Focal
- gi.require_version('WebKit2', '4.0')
-
+gi.require_version('WebKit2', '4.1')
from gi.repository import Gtk, WebKit2
from keyman_config import _, secure_lookup
diff --git a/linux/keyman-config/keyman_config/keyboard_options_view.py b/linux/keyman-config/keyman_config/keyboard_options_view.py
index 795386d7bf..38499a558a 100644
--- a/linux/keyman-config/keyman_config/keyboard_options_view.py
+++ b/linux/keyman-config/keyman_config/keyboard_options_view.py
@@ -8,12 +8,7 @@ from urllib.parse import parse_qsl, urlencode
import gi
gi.require_version('Gtk', '3.0')
-try:
- gi.require_version('WebKit2', '4.1')
-except ValueError:
- # TODO: Remove once we drop support for Ubuntu 20.04 Focal
- gi.require_version('WebKit2', '4.0')
-
+gi.require_version('WebKit2', '4.1')
from gi.repository import Gtk, WebKit2
from keyman_config import _
diff --git a/linux/keyman-config/keyman_config/welcome.py b/linux/keyman-config/keyman_config/welcome.py
index af001ea464..a14d747c6a 100644
--- a/linux/keyman-config/keyman_config/welcome.py
+++ b/linux/keyman-config/keyman_config/welcome.py
@@ -7,11 +7,7 @@ import webbrowser
import gi
gi.require_version('Gtk', '3.0')
-try:
- gi.require_version('WebKit2', '4.1')
-except ValueError:
- # TODO: Remove once we drop support for Ubuntu 20.04 Focal
- gi.require_version('WebKit2', '4.0')
+gi.require_version('WebKit2', '4.1')
from gi.repository import Gtk, WebKit2
from keyman_config import _
diff --git a/linux/keyman-system-service/src/KeyboardDevice.cpp b/linux/keyman-system-service/src/KeyboardDevice.cpp
index a744a96b82..783902acaf 100644
--- a/linux/keyman-system-service/src/KeyboardDevice.cpp
+++ b/linux/keyman-system-service/src/KeyboardDevice.cpp
@@ -10,19 +10,26 @@ using namespace std;
KeyboardDevice::KeyboardDevice()
{
- dev = nullptr;
- fd = -1;
+ dev = nullptr;
+ fd = -1;
hasCapsLockLed = -1;
debug = false;
}
KeyboardDevice::~KeyboardDevice()
+{
+ Close();
+}
+
+void KeyboardDevice::Close()
{
if (dev) {
libevdev_free(dev);
+ dev = nullptr;
}
if (fd != -1) {
close(fd);
+ fd = -1;
}
}
@@ -34,14 +41,14 @@ bool KeyboardDevice::Initialize(const char* name)
fd = open(path.c_str(), O_RDWR);
if (fd < 0) {
std::cerr << "Failed to open device " << path << ": " << strerror(errno) << std::endl;
+ Close();
return false;
}
int rc = libevdev_new_from_fd(fd, &dev);
if (rc < 0) {
std::cerr << "Failed to init libevdev for " << path << ": " << strerror(-rc) << std::endl;
- close(fd);
- fd = -1;
+ Close();
return false;
}
diff --git a/linux/keyman-system-service/src/KeyboardDevice.h b/linux/keyman-system-service/src/KeyboardDevice.h
index af9ce119f3..87310db15e 100644
--- a/linux/keyman-system-service/src/KeyboardDevice.h
+++ b/linux/keyman-system-service/src/KeyboardDevice.h
@@ -3,6 +3,7 @@
#include
+// Any keyboard device available on the system
class KeyboardDevice
{
public:
@@ -20,6 +21,8 @@ class KeyboardDevice
int fd;
int hasCapsLockLed;
bool debug;
+
+ void Close();
};
#endif // __KEYBOARDDEVICE_H__
diff --git a/linux/keyman-system-service/src/KeymanSystemService.cpp b/linux/keyman-system-service/src/KeymanSystemService.cpp
index 03d7add533..5e5b4f6e3e 100644
--- a/linux/keyman-system-service/src/KeymanSystemService.cpp
+++ b/linux/keyman-system-service/src/KeymanSystemService.cpp
@@ -63,10 +63,24 @@ on_get_caps_lock_indicator(
return sd_bus_reply_method_return(msg, "b", state);
}
+static int32_t
+on_call_ordered_output_sentinel(
+ sd_bus_message *msg,
+ void *user_data,
+ sd_bus_error *ret_error
+) {
+ *ret_error = SD_BUS_ERROR_NULL;
+
+ KeymanSystemService *service = static_cast(user_data);
+ service->CallOrderedOutputSentinel();
+ return sd_bus_reply_method_return(msg, "");
+}
+
static const sd_bus_vtable system_service_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_METHOD("SetCapsLockIndicator", "b", "", on_set_caps_lock_indicator, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD("GetCapsLockIndicator", "", "b", on_get_caps_lock_indicator, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("CallOrderedOutputSentinel", "", "", on_call_ordered_output_sentinel, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_VTABLE_END
};
@@ -75,6 +89,7 @@ KeymanSystemService::KeymanSystemService()
int ret;
GetKbdDevices();
+ CreateOrderedOutputDevice();
#ifdef KEYMAN_TESTING
ret = sd_bus_open_user(&bus);
@@ -120,6 +135,12 @@ KeymanSystemService::~KeymanSystemService()
delete device;
}
delete kbd_devices;
+ kbd_devices = nullptr;
+ }
+
+ if (kbd_ordered_output) {
+ delete kbd_ordered_output;
+ kbd_ordered_output = nullptr;
}
}
@@ -171,6 +192,16 @@ void KeymanSystemService::GetKbdDevices() {
}
}
+void KeymanSystemService::CreateOrderedOutputDevice() {
+ if (!kbd_ordered_output) {
+ kbd_ordered_output = new OrderedOutputDevice();
+ if (!kbd_ordered_output->Initialize()) {
+ delete kbd_ordered_output;
+ kbd_ordered_output = nullptr;
+ }
+ }
+}
+
// Set the CapsLock indicator on all keyboard devices.
void
KeymanSystemService::SetCapsLockIndicatorOnDevices(uint32_t state) {
@@ -198,3 +229,13 @@ KeymanSystemService::GetCapsLockIndicatorOnDevices() {
}
return state;
}
+
+// Emit a ordered output sentinel key event
+void
+KeymanSystemService::CallOrderedOutputSentinel() {
+ if (!kbd_ordered_output) {
+ syslog(LOG_USER | LOG_ERR, "%s: No keyboard initialized", __FUNCTION__);
+ return;
+ }
+ kbd_ordered_output->PressSentinelKey();
+}
diff --git a/linux/keyman-system-service/src/KeymanSystemService.h b/linux/keyman-system-service/src/KeymanSystemService.h
index 86637f27a9..19c7efe6cc 100644
--- a/linux/keyman-system-service/src/KeymanSystemService.h
+++ b/linux/keyman-system-service/src/KeymanSystemService.h
@@ -8,18 +8,21 @@
#else
#include
#endif
+#include "OrderedOutputDevice.h"
#include "KeyboardDevice.h"
using namespace std;
class KeymanSystemService {
private:
- std::list* kbd_devices = NULL;
- sd_bus_slot *slot = NULL;
- sd_bus *bus = NULL;
- bool failed = false;
+ std::list* kbd_devices = nullptr;
+ OrderedOutputDevice *kbd_ordered_output = nullptr;
+ sd_bus_slot *slot = nullptr;
+ sd_bus *bus = nullptr;
+ bool failed = false;
void GetKbdDevices();
+ void CreateOrderedOutputDevice();
public:
KeymanSystemService();
@@ -33,6 +36,7 @@ public:
int Loop();
void SetCapsLockIndicatorOnDevices(uint32_t state);
uint32_t GetCapsLockIndicatorOnDevices();
+ void CallOrderedOutputSentinel();
};
#endif // __KEYMANSYSTEMSERVICE_H__
diff --git a/linux/keyman-system-service/src/OrderedOutputDevice.cpp b/linux/keyman-system-service/src/OrderedOutputDevice.cpp
new file mode 100644
index 0000000000..cf90ffa2f1
--- /dev/null
+++ b/linux/keyman-system-service/src/OrderedOutputDevice.cpp
@@ -0,0 +1,79 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "OrderedOutputDevice.h"
+
+using namespace std;
+
+#define KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL 194 // 0xC2
+
+OrderedOutputDevice::OrderedOutputDevice() {
+ uinput_dev = nullptr;
+}
+
+OrderedOutputDevice::~OrderedOutputDevice() {
+ Close();
+}
+
+void
+OrderedOutputDevice::Close() {
+ if (uinput_dev) {
+ libevdev_uinput_destroy(uinput_dev);
+ uinput_dev = nullptr;
+ }
+}
+
+bool
+OrderedOutputDevice::Initialize() {
+ struct libevdev* dev;
+
+ syslog(LOG_USER | LOG_ALERT, "%s: creating fake device", __FUNCTION__);
+
+ dev = libevdev_new();
+ libevdev_set_name(dev, "Ordered Output Keyman Keyboard Device");
+
+ libevdev_enable_event_type(dev, EV_KEY);
+
+ // F24 is the only key we support.
+ libevdev_enable_event_code(dev, EV_KEY, KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL, NULL);
+
+ int rc = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED, &uinput_dev);
+ if (rc < 0) {
+ syslog(LOG_USER | LOG_ERR, "%s: Failed to create Ordered Output keyman keyboard device: %s",
+ __FUNCTION__, strerror(-rc));
+ libevdev_free(dev);
+ Close();
+ return false;
+ }
+
+ return true;
+}
+
+bool
+OrderedOutputDevice::PressSentinelKey() {
+ int error = libevdev_uinput_write_event(uinput_dev, EV_KEY, KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL, 1);
+ if (error < 0) {
+ syslog(LOG_USER | LOG_ERR,
+ "%s: Error writing send key event for sentinel key (down): %s",
+ __FUNCTION__, strerror(-error));
+ return false;
+ }
+ error = libevdev_uinput_write_event(uinput_dev, EV_KEY, KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL, 0);
+ if (error < 0) {
+ syslog(LOG_USER | LOG_ERR, "%s: Error writing send key event for sentinel key (up): %s", __FUNCTION__, strerror(-error));
+ return false;
+ }
+ // process the key event immediately
+ error = libevdev_uinput_write_event(uinput_dev, EV_SYN, SYN_REPORT, 0);
+ if (error < 0) {
+ syslog(LOG_USER | LOG_ERR,
+ "%s: Error writing syn event for sentinel key: %s", __FUNCTION__,
+ strerror(-error));
+ return false;
+ }
+ return true;
+}
diff --git a/linux/keyman-system-service/src/OrderedOutputDevice.h b/linux/keyman-system-service/src/OrderedOutputDevice.h
new file mode 100644
index 0000000000..0fb2d15a3f
--- /dev/null
+++ b/linux/keyman-system-service/src/OrderedOutputDevice.h
@@ -0,0 +1,24 @@
+#ifndef __ORDEREDOUTPUTDEVICE_H__
+#define __ORDEROUTPUTDEVICE_H__
+
+#include
+
+// The fake keyboard we use to force the serializing of the output (#10799/#7079)
+// Generate a fake key event to get the correct order of events so that any backspace key we
+// generate will be processed before the character we're adding. We need to send a
+// valid keycode so that it doesn't get swallowed by GTK but which isn't very likely used
+// in real keyboards. F24 seems to work for that.
+class OrderedOutputDevice {
+public:
+ OrderedOutputDevice();
+ virtual ~OrderedOutputDevice();
+
+ bool Initialize();
+ bool PressSentinelKey();
+
+private:
+ void Close();
+ struct libevdev_uinput *uinput_dev;
+};
+
+#endif // __ORDEROUTPUTDEVICE_H__
diff --git a/linux/keyman-system-service/src/com.keyman.SystemService1.System.xml b/linux/keyman-system-service/src/com.keyman.SystemService1.System.xml
index b75a1f2390..b93bcffa3a 100644
--- a/linux/keyman-system-service/src/com.keyman.SystemService1.System.xml
+++ b/linux/keyman-system-service/src/com.keyman.SystemService1.System.xml
@@ -25,5 +25,14 @@
+
+
+
+
+
diff --git a/linux/keyman-system-service/src/meson.build b/linux/keyman-system-service/src/meson.build
index 3c3dd27fa6..a99c977efb 100644
--- a/linux/keyman-system-service/src/meson.build
+++ b/linux/keyman-system-service/src/meson.build
@@ -1,6 +1,7 @@
service_files = files(
'KeyboardDevice.cpp',
'KeymanSystemService.cpp',
+ 'OrderedOutputDevice.cpp',
'main.cpp',
)
diff --git a/linux/keyman-system-service/tests/KeyboardDeviceMock.cpp b/linux/keyman-system-service/tests/KeyboardDeviceMock.cpp
index dbaa6aafab..c909e13025 100644
--- a/linux/keyman-system-service/tests/KeyboardDeviceMock.cpp
+++ b/linux/keyman-system-service/tests/KeyboardDeviceMock.cpp
@@ -24,6 +24,9 @@ KeyboardDeviceMock::KeyboardDeviceMock() {
KeyboardDeviceMock::~KeyboardDeviceMock() {
}
+void KeyboardDeviceMock::Close() {
+}
+
bool
KeyboardDeviceMock::Initialize(const char* name) {
return true;
diff --git a/linux/keyman-system-service/tests/meson.build b/linux/keyman-system-service/tests/meson.build
index 749e098591..917f0b15e6 100644
--- a/linux/keyman-system-service/tests/meson.build
+++ b/linux/keyman-system-service/tests/meson.build
@@ -1,6 +1,7 @@
test_service_files = files(
'../src/main.cpp',
'../src/KeymanSystemService.cpp',
+ '../src/OrderedOutputDevice.cpp',
'KeyboardDeviceMock.cpp',
)
diff --git a/linux/scripts/launchpad.sh b/linux/scripts/launchpad.sh
index 98ab9238f0..4b89980994 100755
--- a/linux/scripts/launchpad.sh
+++ b/linux/scripts/launchpad.sh
@@ -33,7 +33,7 @@ else
fi
echo "ppa: ${ppa}"
-distributions="${DIST:-focal jammy noble oracular plucky}"
+distributions="${DIST:-jammy noble oracular plucky}"
packageversion="${PACKAGEVERSION:-1~sil1}"
BASEDIR=$(pwd)
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib
index e6cc0173aa..a257c34419 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib
@@ -1,9 +1,9 @@
-
+
-
-
+
+
@@ -203,12 +203,6 @@
-
-
-
-
-
-
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h
index 684db6fb38..c1264cb210 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h
@@ -14,5 +14,6 @@
- (void)menuAction:(id)sender;
- (void)handleBackspace:(NSEvent *)event;
-
+- (void)changeClients:(NSString *)clientAppId;
+- (void)deactivateClient;
@end
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m
index dd53b49622..c628311b9f 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m
@@ -25,22 +25,13 @@ KMInputMethodEventHandler* _eventHandler;
- (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)inputClient
{
- os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getRunningApplicationId]);
+ os_log_debug([KMLogs lifecycleLog], "+++KMInputController initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getRunningApplicationId]);
self = [super initWithServer:server delegate:delegate client:inputClient];
if (self) {
- self.appDelegate.inputController = self;
+ os_log_debug([KMLogs lifecycleLog], " +++initWithServer, self: %p", self);
}
- /**
- * Register to receive the Deactivated and ChangedClient notification generated from KMInputMethodLifecycle so
- * that the eventHandler can be changed. There is no need to receive the Activated notification because
- * the InputController does it all it needs to when it receives the ChangedClient.
- */
-
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil];
-
return self;
}
@@ -67,35 +58,81 @@ KMInputMethodEventHandler* _eventHandler;
}
/**
- * The Keyman input method is deactivating because the user chose a different input method: notification from KMInputMethodLifecycle
+ * Called by the app delegate when KMInputMethodLifecycle determines the user has switched clients
*/
-- (void)inputMethodDeactivated:(NSNotification *)notification {
- os_log_debug([KMLogs lifecycleLog], "***KMInputController inputMethodDeactivated, deactivating eventHandler");
- if (_eventHandler != nil) {
- [_eventHandler deactivate];
- }
+- (void)changeClients:(NSString *)clientAppId {
+ os_log_debug([KMLogs lifecycleLog], "***KMInputController changeClients, deactivate old eventHandler and activate new one");
+
+ [self deactivateEventHandler];
+ [self activateEventHandler:clientAppId];
}
/**
- * The user has switched to a different text input client: notification from KMInputMethodLifecycle
+ * Called by the app delegate when KMInputMethodLifecycle determines the user has deactivated Keyman
*/
-- (void)inputMethodChangedClient:(NSNotification *)notification {
- os_log_debug([KMLogs lifecycleLog], "***KMInputController inputMethodChangedClient, deactivating old eventHandler and activating new one");
- if (_eventHandler != nil) {
- [_eventHandler deactivate];
- }
- _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getRunningApplicationId] client:self.client];
-
+- (void)deactivateClient {
+ os_log_debug([KMLogs lifecycleLog], "***KMInputController deactivateClient, deactivate old eventHandler");
+
+ [self deactivateEventHandler];
}
+- (void)deactivateEventHandler {
+ if (_eventHandler != nil) {
+ os_log_debug([KMLogs lifecycleLog], " * KMInputController deactivate old eventHandler");
+ [_eventHandler deactivate];
+ }
+}
+
+- (void)activateEventHandler:(NSString *)clientAppId {
+ os_log_debug([KMLogs lifecycleLog], " * KMInputController activate new eventHandler");
+ _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId client:self.client];
+}
+
+/**
+ * Called by the OS to activate the input method, but some calls are not useful and are followed milliseconds later
+ * by a deactivateServer. These short-lived activations may be generated by clicking and releasing menu items without changing applications.
+ * Rather than treat every message received as a true activation, we send a message to KMInputMethodLifeCycle to evaluate.
+ */
- (void)activateServer:(id)sender {
+ os_log_info([KMLogs lifecycleLog], " +++KMInputController, activateServer, self=%p", self);
[sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"];
+
+ /*
+ When this KMInputController becomes the active server for the input method,
+ then immediately update the AppDelegate. The duration that this controller is the
+ active server may be extremely short, but, if so, we will receive another
+ call to activateServer moments later and can update the AppDelegate again.
+ */
+ [self attachToAppDelegate];
+
+ /*
+ Call the shared lifecycle object so it can evaluate the current state
+ and determine whether this is
+ 1) a real activation of the input method or
+ 2) a change in clients or
+ 3) a false alarm
+ */
[KMInputMethodLifecycle.shared activateClient:sender];
}
+- (void)attachToAppDelegate {
+ self.appDelegate.inputController = self;
+}
+
+/**
+ * Called by the OS to deactivate the input method
+ */
- (void)deactivateServer:(id)sender {
+ os_log_info([KMLogs lifecycleLog], " +++KMInputController, deactivateServer, self=%p", self);
+
+ /*
+ Call the shared lifecycle object so it can evaluate the current state
+ and determine whether this is
+ 1) a real deactivation of the input method or
+ 2) a change in clients or
+ 3) a false alarm
+ */
[KMInputMethodLifecycle.shared deactivateClient:sender];
- [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSMenu *)menu {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
index 0c4199959e..5e88d2b59b 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
@@ -123,15 +123,39 @@ id _lastServerWithOSKShowing = nil;
// register to receive notifications generated from KMInputMethodLifecycle
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodActivated:) name:kInputMethodActivatedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil];
// start Input Method lifecycle
[KMInputMethodLifecycle.shared startLifecycle];
}
/**
- * When the input method is deactivated, hide the OSK and disable the low-level event tap
+ * When the input method is activated -- notification from KMInputMethodLifecycle
+ */
+- (void)inputMethodActivated:(NSNotification *)notification {
+
+ // enable event tap
+ if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) {
+ os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap...");
+ CGEventTapEnable(self.lowLevelEventTap, YES);
+ }
+
+ // show OSK if necessary
+ os_log_debug([KMLogs lifecycleLog], "--- inputMethodActivated, kvk is non-nil: %{public}@ showOskOnActivate: %{public}@", (_kvk!=nil)?@"true":@"false", [KMInputMethodLifecycle.shared shouldShowOskOnActivate]?@"true":@"false");
+
+ if ([KMInputMethodLifecycle.shared shouldShowOskOnActivate]) {
+ os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK");
+ [KMSentryHelper addDebugBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"];
+ [self showOSK];
+ }
+}
+
+/**
+ * When the input method is deactivated -- notification from KMInputMethodLifecycle
*/
- (void)inputMethodDeactivated:(NSNotification *)notification {
+
+ // if the OSK is visible, hide it
if ([self.oskWindow.window isVisible]) {
os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK");
[KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"hiding OSK on input method deactivation"];
@@ -141,28 +165,22 @@ id _lastServerWithOSKShowing = nil;
}
[KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]];
+ // disable the event tap
if (self.lowLevelEventTap) {
os_log_debug([KMLogs lifecycleLog], "***inputMethodDeactivated, disabling event tap");
CGEventTapEnable(self.lowLevelEventTap, NO);
}
+
+ // deactive the text input client
+ [self.inputController deactivateClient];
}
/**
- * When the input method is activated, enable the low-level event tap and show the OSK
+ * When the user switches to a different text input client -- notification from KMInputMethodLifecycle
*/
-- (void)inputMethodActivated:(NSNotification *)notification {
- if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) {
- os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap...");
- CGEventTapEnable(self.lowLevelEventTap, YES);
- }
-
- os_log_debug([KMLogs lifecycleLog], "--- inputMethodActivated, kvk is non-nil: %{public}@ showOskOnActivate: %{public}@", (_kvk!=nil)?@"true":@"false", [KMInputMethodLifecycle.shared shouldShowOskOnActivate]?@"true":@"false");
-
- if ([KMInputMethodLifecycle.shared shouldShowOskOnActivate]) {
- os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK");
- [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"];
- [self showOSK];
- }
+- (void)inputMethodChangedClient:(NSNotification *)notification {
+ os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodChangedClient");
+ [self.inputController changeClients:[KMInputMethodLifecycle getRunningApplicationId]];
}
- (KeymanVersionInfo)versionInfo {
diff --git a/mac/build.sh b/mac/build.sh
index 8f17ba868d..00e577a45d 100755
--- a/mac/build.sh
+++ b/mac/build.sh
@@ -109,7 +109,7 @@ execBuildCommand() {
set +e
local ret_code=0
eval "$cmnd" || ret_code=$?
- set -ex
+ set -e
printXCodeBuildScriptLogs
diff --git a/mac/docs/help/about/whatsnew.md b/mac/docs/help/about/whatsnew.md
index 73ccb4d49b..8cc29bdadb 100644
--- a/mac/docs/help/about/whatsnew.md
+++ b/mac/docs/help/about/whatsnew.md
@@ -1,5 +1,5 @@
---
-title: What's New in Keyman 18.0 for macOS
+title: What's New in Keyman 19.0 for macOS
---
-Here are some of the new features we have added to Keyman 18.0 for macOS:
+Here are some of the new features we have added to Keyman 19.0 for macOS:
diff --git a/mac/docs/help/index.md b/mac/docs/help/index.md
index aa507a531e..12397a903d 100644
--- a/mac/docs/help/index.md
+++ b/mac/docs/help/index.md
@@ -1,5 +1,5 @@
---
-title: Keyman 18.0 for macOS Help
+title: Keyman 19.0 for macOS Help
---
Need help using Keyman for macOS? You'll find everything you need here, including product documentation,
diff --git a/oem/firstvoices/ios/Cartfile b/oem/firstvoices/ios/Cartfile
index 0dae92f36b..9ffc07d330 100644
--- a/oem/firstvoices/ios/Cartfile
+++ b/oem/firstvoices/ios/Cartfile
@@ -1,4 +1,4 @@
github "weichsel/ZIPFoundation" ~> 0.9
github "devicekit/DeviceKit" ~> 5.0
github "ashleymills/Reachability.swift"
-github "getsentry/sentry-cocoa" ~> 8.7.0
+github "getsentry/sentry-cocoa" ~> 8.38.0
diff --git a/oem/firstvoices/ios/Cartfile.resolved b/oem/firstvoices/ios/Cartfile.resolved
index 95e608b932..ca05da1ae9 100644
--- a/oem/firstvoices/ios/Cartfile.resolved
+++ b/oem/firstvoices/ios/Cartfile.resolved
@@ -1,4 +1,4 @@
-github "ashleymills/Reachability.swift" "v5.1.0"
-github "devicekit/DeviceKit" "5.1.0"
-github "getsentry/sentry-cocoa" "8.15.2"
-github "weichsel/ZIPFoundation" "0.9.17"
+github "ashleymills/Reachability.swift" "v5.2.4"
+github "devicekit/DeviceKit" "5.5.0"
+github "getsentry/sentry-cocoa" "8.44.0"
+github "weichsel/ZIPFoundation" "0.9.19"
diff --git a/oem/firstvoices/windows/src/xml/strings.xml b/oem/firstvoices/windows/src/xml/strings.xml
index 692a582dd7..51b19c060d 100644
--- a/oem/firstvoices/windows/src/xml/strings.xml
+++ b/oem/firstvoices/windows/src/xml/strings.xml
@@ -390,13 +390,8 @@
-
- Automatically download updates in the background, for installation later
-
-
-
-
- Automatically check keyman.com weekly for updates
+
+ Automatically check for updates and download
diff --git a/package-lock.json b/package-lock.json
index 22eca5d230..ea094f457f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,8 +25,10 @@
"developer/src/kmc",
"developer/src/server",
"common/test/resources",
- "common/tools/*",
- "common/web/*",
+ "common/web/keyman-version",
+ "common/web/langtags",
+ "common/web/types",
+ "common/tools/sourcemap-path-remapper",
"common/tools/hextobin",
"web",
"web/src/engine/osk/gesture-processor",
@@ -38,6 +40,7 @@
"@keymanapp/developer-utils": "file:developer/src/common/web/utils",
"@keymanapp/hextobin": "file:common/tools/hextobin",
"@keymanapp/keyman-version": "file:common/web/keyman-version",
+ "@keymanapp/langtags": "file:common/web/langtags",
"@keymanapp/ldml-keyboard-constants": "file:core/include/ldml"
},
"devDependencies": {
@@ -113,6 +116,8 @@
},
"common/tools/sourcemap-path-remapper/node_modules/convert-source-map": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"license": "MIT"
},
"common/web/es6-shim": {
@@ -131,6 +136,13 @@
"typescript": "^5.4.5"
}
},
+ "common/web/langtags": {
+ "name": "@keymanapp/langtags",
+ "license": "MIT",
+ "devDependencies": {
+ "typescript": "^5.4.5"
+ }
+ },
"common/web/types": {
"name": "@keymanapp/common-types",
"license": "MIT",
@@ -773,7 +785,8 @@
"dependencies": {
"@keymanapp/common-types": "*",
"@keymanapp/developer-utils": "*",
- "@keymanapp/keyman-version": "*"
+ "@keymanapp/keyman-version": "*",
+ "@keymanapp/langtags": "*"
},
"devDependencies": {
"@keymanapp/developer-test-helpers": "*",
@@ -888,7 +901,8 @@
"dependencies": {
"@keymanapp/common-types": "*",
"@keymanapp/developer-utils": "*",
- "@keymanapp/kmc-package": "*"
+ "@keymanapp/kmc-package": "*",
+ "@keymanapp/langtags": "*"
},
"devDependencies": {
"@types/mocha": "^5.2.7",
@@ -3011,6 +3025,10 @@
"resolved": "developer/src/kmc-package",
"link": true
},
+ "node_modules/@keymanapp/langtags": {
+ "resolved": "common/web/langtags",
+ "link": true
+ },
"node_modules/@keymanapp/ldml-keyboard-constants": {
"resolved": "core/include/ldml",
"link": true
diff --git a/package.json b/package.json
index 135b0fdd3e..f0bc72f6b2 100644
--- a/package.json
+++ b/package.json
@@ -50,8 +50,10 @@
"developer/src/kmc",
"developer/src/server",
"common/test/resources",
- "common/tools/*",
- "common/web/*",
+ "common/web/keyman-version",
+ "common/web/langtags",
+ "common/web/types",
+ "common/tools/sourcemap-path-remapper",
"common/tools/hextobin",
"web",
"web/src/engine/osk/gesture-processor",
@@ -59,6 +61,7 @@
],
"dependencies": {
"@keymanapp/common-types": "file:common/web/types",
+ "@keymanapp/langtags": "file:common/web/langtags",
"@keymanapp/developer-test-helpers": "file:developer/src/common/web/test-helpers",
"@keymanapp/developer-utils": "file:developer/src/common/web/utils",
"@keymanapp/hextobin": "file:common/tools/hextobin",
diff --git a/resources/build/increment-version.sh b/resources/build/increment-version.sh
index 3be11dba1f..9e9ac699b8 100755
--- a/resources/build/increment-version.sh
+++ b/resources/build/increment-version.sh
@@ -192,7 +192,7 @@ if [ "$action" == "commit" ]; then
git commit -m "$message (history cherry-pick to master)"
# TODO: once we are sure this is stable, add `-l auto` to get the "auto:"
# label
- hub pull-request -f --no-edit -b master
+ hub pull-request -fp --no-edit
fi
# Return to our best working branch
diff --git a/resources/build/minimum-versions.inc.sh b/resources/build/minimum-versions.inc.sh
index 41a7c3690f..2e1a2914b1 100644
--- a/resources/build/minimum-versions.inc.sh
+++ b/resources/build/minimum-versions.inc.sh
@@ -1,4 +1,4 @@
-# Required minimum versions as of Keyman 18.0
+# Required minimum versions as of Keyman 19.0
#
# This is a list of minimum, maximum, and specific versions of any external
# components or dependencies found in Keyman.
@@ -14,7 +14,7 @@ KEYMAN_MIN_TARGET_VERSION_ANDROID=5.0 # Lollipop
KEYMAN_MIN_TARGET_VERSION_IOS=12.2 # iOS 12.2
KEYMAN_MIN_TARGET_VERSION_WINDOWS=10 # Windows 10
KEYMAN_MIN_TARGET_VERSION_MAC=10.13 # MacOS 10.13 (High Sierra)
-KEYMAN_MIN_TARGET_VERSION_UBUNTU=20.04 # Ubuntu 20.04 Focal
+KEYMAN_MIN_TARGET_VERSION_UBUNTU=22.04 # Ubuntu 22.04 Jammy
KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME=53.0 # min version of Chrome for Keyman for Android
# Target web browsers for KeymanWeb -- we do not have polyfills for
diff --git a/resources/standards-data/ldml-keyboards/45/cldr_info.json b/resources/standards-data/ldml-keyboards/45/cldr_info.json
deleted file mode 100644
index c7dd469c87..0000000000
--- a/resources/standards-data/ldml-keyboards/45/cldr_info.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "sha": "dd5776f650183c9172abe2a57313ed41619d41fb",
- "description": "release-45",
- "date": "Fri, 19 Apr 2024 22:23:26 +0000"
-}
diff --git a/resources/standards-data/ldml-keyboards/45/readme.md b/resources/standards-data/ldml-keyboards/45/readme.md
deleted file mode 100644
index b2265826b7..0000000000
--- a/resources/standards-data/ldml-keyboards/45/readme.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# ldml-keyboards/techpreview
-
-See [../readme.md](../readme.md)
diff --git a/resources/standards-data/ldml-keyboards/45/3.0/bn.xml b/resources/standards-data/ldml-keyboards/46/3.0/bn.xml
similarity index 98%
rename from resources/standards-data/ldml-keyboards/45/3.0/bn.xml
rename to resources/standards-data/ldml-keyboards/46/3.0/bn.xml
index b579e39f38..800f71d7a9 100644
--- a/resources/standards-data/ldml-keyboards/45/3.0/bn.xml
+++ b/resources/standards-data/ldml-keyboards/46/3.0/bn.xml
@@ -1,4 +1,8 @@
+
diff --git a/resources/standards-data/ldml-keyboards/45/3.0/ja-Hira-t-k0-flicks.xml b/resources/standards-data/ldml-keyboards/46/3.0/ja-Hira-t-k0-flicks.xml
similarity index 97%
rename from resources/standards-data/ldml-keyboards/45/3.0/ja-Hira-t-k0-flicks.xml
rename to resources/standards-data/ldml-keyboards/46/3.0/ja-Hira-t-k0-flicks.xml
index c10342320a..6448542a26 100644
--- a/resources/standards-data/ldml-keyboards/45/3.0/ja-Hira-t-k0-flicks.xml
+++ b/resources/standards-data/ldml-keyboards/46/3.0/ja-Hira-t-k0-flicks.xml
@@ -1,4 +1,8 @@
+
diff --git a/resources/standards-data/ldml-keyboards/45/3.0/ja-Latn.xml b/resources/standards-data/ldml-keyboards/46/3.0/ja-Latn.xml
similarity index 88%
rename from resources/standards-data/ldml-keyboards/45/3.0/ja-Latn.xml
rename to resources/standards-data/ldml-keyboards/46/3.0/ja-Latn.xml
index a2ffda522a..2758d077e8 100644
--- a/resources/standards-data/ldml-keyboards/45/3.0/ja-Latn.xml
+++ b/resources/standards-data/ldml-keyboards/46/3.0/ja-Latn.xml
@@ -1,4 +1,8 @@
+
diff --git a/resources/standards-data/ldml-keyboards/45/3.0/mt-t-k0-47key.xml b/resources/standards-data/ldml-keyboards/46/3.0/mt-t-k0-47key.xml
similarity index 95%
rename from resources/standards-data/ldml-keyboards/45/3.0/mt-t-k0-47key.xml
rename to resources/standards-data/ldml-keyboards/46/3.0/mt-t-k0-47key.xml
index 9c7ed0fccc..6a875b37f2 100644
--- a/resources/standards-data/ldml-keyboards/45/3.0/mt-t-k0-47key.xml
+++ b/resources/standards-data/ldml-keyboards/46/3.0/mt-t-k0-47key.xml
@@ -1,4 +1,8 @@
+
diff --git a/resources/standards-data/ldml-keyboards/45/3.0/mt.xml b/resources/standards-data/ldml-keyboards/46/3.0/mt.xml
similarity index 95%
rename from resources/standards-data/ldml-keyboards/45/3.0/mt.xml
rename to resources/standards-data/ldml-keyboards/46/3.0/mt.xml
index d552888f7e..b76b08cd9a 100644
--- a/resources/standards-data/ldml-keyboards/45/3.0/mt.xml
+++ b/resources/standards-data/ldml-keyboards/46/3.0/mt.xml
@@ -4,6 +4,10 @@ Copyright © 1991-2024 Unicode, Inc.
SPDX-License-Identifier: Unicode-3.0
CLDR data files are interpreted according to the LDML specification (http://unicode.org/reports/tr35/)
-->
+
diff --git a/resources/standards-data/ldml-keyboards/45/3.0/pcm.xml b/resources/standards-data/ldml-keyboards/46/3.0/pcm.xml
similarity index 92%
rename from resources/standards-data/ldml-keyboards/45/3.0/pcm.xml
rename to resources/standards-data/ldml-keyboards/46/3.0/pcm.xml
index 8447330284..cf8002f94d 100644
--- a/resources/standards-data/ldml-keyboards/45/3.0/pcm.xml
+++ b/resources/standards-data/ldml-keyboards/46/3.0/pcm.xml
@@ -1,4 +1,8 @@
+
diff --git a/resources/standards-data/ldml-keyboards/45/3.0/pt-t-k0-abnt2.xml b/resources/standards-data/ldml-keyboards/46/3.0/pt-t-k0-abnt2.xml
similarity index 94%
rename from resources/standards-data/ldml-keyboards/45/3.0/pt-t-k0-abnt2.xml
rename to resources/standards-data/ldml-keyboards/46/3.0/pt-t-k0-abnt2.xml
index d612141166..003d1a6af2 100644
--- a/resources/standards-data/ldml-keyboards/45/3.0/pt-t-k0-abnt2.xml
+++ b/resources/standards-data/ldml-keyboards/46/3.0/pt-t-k0-abnt2.xml
@@ -1,4 +1,8 @@
+
diff --git a/resources/standards-data/ldml-keyboards/46/cldr_info.json b/resources/standards-data/ldml-keyboards/46/cldr_info.json
new file mode 100644
index 0000000000..8895bf132c
--- /dev/null
+++ b/resources/standards-data/ldml-keyboards/46/cldr_info.json
@@ -0,0 +1,5 @@
+{
+ "sha": "e1d37acce5dae468c414172be53b666d58d45be8",
+ "description": "release-46",
+ "date": "Thu, 30 Jan 2025 21:17:01 +0000"
+}
diff --git a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.dtd b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.dtd
similarity index 98%
rename from resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.dtd
rename to resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.dtd
index 618cf1cafd..58f294a37c 100644
--- a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.dtd
+++ b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.dtd
@@ -10,7 +10,7 @@ CLDR data files are interpreted according to the LDML specification (http://unic
-
+
@@ -32,7 +32,7 @@ CLDR data files are interpreted according to the LDML specification (http://unic
-
+
diff --git a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.xsd b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.xsd
similarity index 98%
rename from resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.xsd
rename to resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.xsd
index 6cad096c41..84dc19e492 100644
--- a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboard3.xsd
+++ b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboard3.xsd
@@ -35,6 +35,7 @@ Note: DTD @-annotations are not currently converted to .xsd. For full CLDR file
+
@@ -74,10 +75,10 @@ Note: DTD @-annotations are not currently converted to .xsd. For full CLDR file
-
+
-
+
diff --git a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboardTest3.dtd b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboardTest3.dtd
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboardTest3.dtd
rename to resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboardTest3.dtd
diff --git a/resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboardTest3.xsd b/resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboardTest3.xsd
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/dtd/ldmlKeyboardTest3.xsd
rename to resources/standards-data/ldml-keyboards/46/dtd/ldmlKeyboardTest3.xsd
diff --git a/resources/standards-data/ldml-keyboards/45/import/README.md b/resources/standards-data/ldml-keyboards/46/import/README.md
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/import/README.md
rename to resources/standards-data/ldml-keyboards/46/import/README.md
diff --git a/resources/standards-data/ldml-keyboards/45/import/keys-Latn-implied.xml b/resources/standards-data/ldml-keyboards/46/import/keys-Latn-implied.xml
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/import/keys-Latn-implied.xml
rename to resources/standards-data/ldml-keyboards/46/import/keys-Latn-implied.xml
diff --git a/resources/standards-data/ldml-keyboards/45/import/keys-Zyyy-currency.xml b/resources/standards-data/ldml-keyboards/46/import/keys-Zyyy-currency.xml
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/import/keys-Zyyy-currency.xml
rename to resources/standards-data/ldml-keyboards/46/import/keys-Zyyy-currency.xml
diff --git a/resources/standards-data/ldml-keyboards/45/import/keys-Zyyy-punctuation.xml b/resources/standards-data/ldml-keyboards/46/import/keys-Zyyy-punctuation.xml
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/import/keys-Zyyy-punctuation.xml
rename to resources/standards-data/ldml-keyboards/46/import/keys-Zyyy-punctuation.xml
diff --git a/resources/standards-data/ldml-keyboards/45/import/scanCodes-implied.xml b/resources/standards-data/ldml-keyboards/46/import/scanCodes-implied.xml
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/import/scanCodes-implied.xml
rename to resources/standards-data/ldml-keyboards/46/import/scanCodes-implied.xml
diff --git a/resources/standards-data/ldml-keyboards/45/ldml-keyboard3.schema.json b/resources/standards-data/ldml-keyboards/46/ldml-keyboard3.schema.json
similarity index 99%
rename from resources/standards-data/ldml-keyboards/45/ldml-keyboard3.schema.json
rename to resources/standards-data/ldml-keyboards/46/ldml-keyboard3.schema.json
index fe6496c17e..e6660661fd 100644
--- a/resources/standards-data/ldml-keyboards/45/ldml-keyboard3.schema.json
+++ b/resources/standards-data/ldml-keyboards/46/ldml-keyboard3.schema.json
@@ -178,9 +178,6 @@
"additionalProperties": false,
"properties": {
"base": {
- "enum": [
- "cldr"
- ],
"type": "string"
},
"path": {
@@ -601,7 +598,7 @@
"properties": {
"cldrVersion": {
"enum": [
- "45"
+ "46"
],
"type": "string"
},
@@ -618,7 +615,8 @@
"properties": {
"conformsTo": {
"enum": [
- "45"
+ "45",
+ "46"
],
"type": "string"
},
@@ -691,6 +689,6 @@
"required": [
"keyboard3"
],
- "title": "45/dtd/ldmlKeyboard3.xsd",
+ "title": "46/dtd/ldmlKeyboard3.xsd",
"type": "object"
}
diff --git a/resources/standards-data/ldml-keyboards/45/ldml-keyboardtest3.schema.json b/resources/standards-data/ldml-keyboards/46/ldml-keyboardtest3.schema.json
similarity index 99%
rename from resources/standards-data/ldml-keyboards/45/ldml-keyboardtest3.schema.json
rename to resources/standards-data/ldml-keyboards/46/ldml-keyboardtest3.schema.json
index b386de8882..f3c624a2ce 100644
--- a/resources/standards-data/ldml-keyboards/45/ldml-keyboardtest3.schema.json
+++ b/resources/standards-data/ldml-keyboards/46/ldml-keyboardtest3.schema.json
@@ -220,6 +220,6 @@
"required": [
"keyboardTest3"
],
- "title": "45/dtd/ldmlKeyboardTest3.xsd",
+ "title": "46/dtd/ldmlKeyboardTest3.xsd",
"type": "object"
}
diff --git a/resources/standards-data/ldml-keyboards/45/test/README.md b/resources/standards-data/ldml-keyboards/46/test/README.md
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/test/README.md
rename to resources/standards-data/ldml-keyboards/46/test/README.md
diff --git a/resources/standards-data/ldml-keyboards/45/test/bn-test.xml b/resources/standards-data/ldml-keyboards/46/test/bn-test.xml
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/test/bn-test.xml
rename to resources/standards-data/ldml-keyboards/46/test/bn-test.xml
diff --git a/resources/standards-data/ldml-keyboards/45/test/fr-t-k0-test-test.xml b/resources/standards-data/ldml-keyboards/46/test/fr-t-k0-test-test.xml
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/test/fr-t-k0-test-test.xml
rename to resources/standards-data/ldml-keyboards/46/test/fr-t-k0-test-test.xml
diff --git a/resources/standards-data/ldml-keyboards/45/test/ja-Latn-test.xml b/resources/standards-data/ldml-keyboards/46/test/ja-Latn-test.xml
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/test/ja-Latn-test.xml
rename to resources/standards-data/ldml-keyboards/46/test/ja-Latn-test.xml
diff --git a/resources/standards-data/ldml-keyboards/45/test/pcm-test.xml b/resources/standards-data/ldml-keyboards/46/test/pcm-test.xml
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/test/pcm-test.xml
rename to resources/standards-data/ldml-keyboards/46/test/pcm-test.xml
diff --git a/resources/standards-data/ldml-keyboards/45/test/pt-t-k0-abnt2-test.xml b/resources/standards-data/ldml-keyboards/46/test/pt-t-k0-abnt2-test.xml
similarity index 100%
rename from resources/standards-data/ldml-keyboards/45/test/pt-t-k0-abnt2-test.xml
rename to resources/standards-data/ldml-keyboards/46/test/pt-t-k0-abnt2-test.xml
diff --git a/resources/standards-data/ldml-keyboards/fixup-schema.js b/resources/standards-data/ldml-keyboards/fixup-schema.js
index 4760d24db5..67fdf25753 100644
--- a/resources/standards-data/ldml-keyboards/fixup-schema.js
+++ b/resources/standards-data/ldml-keyboards/fixup-schema.js
@@ -85,6 +85,8 @@ if (data.title.endsWith('ldmlKeyboard3.xsd')) {
delete item.items.type;
item.items['$ref'] = `#/definitions/${newName}`;
}
+ // Workaround CLDR-18138 by not making 'cldr' the default for the base
+ delete data.definitions?.import?.properties?.base?.enum;
}
if (data.title.endsWith('ldmlKeyboardTest3.xsd')) {
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 9369051820..6880709c17 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -26,6 +26,7 @@
"paths": {
"@keymanapp/common-types": ["./common/web/types/src/main"],
+ "@keymanapp/langtags": ["./common/web/langtags/src/main"],
"@keymanapp/keyman": ["./web" ],
"@keymanapp/models-templates": ["./web/src/engine/predictive-text/templates/"],
"@keymanapp/models-wordbreakers": ["./web/src/engine/predictive-text/wordbreakers"],
diff --git a/tsconfig.json b/tsconfig.json
index 855c39f765..ac2a37037b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,8 @@
{ "path": "./common/tools/hextobin/" },
{ "path": "./common/web/keyman-version" },
- { "path": "./common/web/types/" },
+ { "path": "./common/web/langtags" },
+ { "path": "./common/web/types" },
{ "path": "./developer/src/common/web/test-helpers/tsconfig.json" },
{ "path": "./developer/src/common/web/utils/tsconfig.json" },
@@ -44,9 +45,9 @@
{ "path": "./web/src/engine/predictive-text/worker-thread" },
{ "path": "./web/src/engine/osk/gesture-processor/tsconfig.json" },
{ "path": "./web/src/engine/osk/gesture-processor/src/tools/unit-test-resources/tsconfig.json" },
- { "path": "./web/src/engine/sentry-manager" },
+ { "path": "./web/src/engine/sentry-manager/src" },
- { "path": "./web/src/tools/recorder-recorder" },
+ { "path": "./web/src/tools/testing/recorder" },
// { "path": "./web/tools/recorder/tsconfig.json" },
// { "path": "./web/tools/sourcemap-root/tsconfig.json" },
]
diff --git a/web/docs/engine/guide/examples/__auto-control.html b/web/docs/engine/guide/examples/__auto-control.html
index e7fc2768c6..050e03a9f0 100644
--- a/web/docs/engine/guide/examples/__auto-control.html
+++ b/web/docs/engine/guide/examples/__auto-control.html
@@ -26,6 +26,6 @@
-Back to Document
+Back to Document