diff --git a/developer/docs/help/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty.md b/developer/docs/help/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty.md
index f33ea20c34..9d5c20e526 100644
--- a/developer/docs/help/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty.md
+++ b/developer/docs/help/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty.md
@@ -131,7 +131,7 @@ Character Keys"). Typically, you would use only the "common" virtual key
- `U_####[_####]` is used as a shortcut for a key that will output those
Unicode values, if no rule matches it. This is similar to the overloaded
- behaviour for `K_` ids. Thus `####` must be valid Unicode characters.
+ behaviour for `K_` ids. Thus `####` must be valid Unicode characters.
The square bracket characters `[` and `]` indicate an optional portion of the sequence
and are not to be included in the shortcut. E.g.
`U_0259` would generate a schwa if no rule matches. It is still valid to
@@ -226,338 +226,9 @@ A number of special text labels are recognized as identifying special purpose
keys, such as Shift, Backspace, Enter, etc., for which icons are more
appropriately used than a text label. A special font including these icons is
included with Keyman and automatically embedded and used in any web page using
-Keyman. The list of icons in the font will probably be extended in future, but
-for now the following special labels are recognized:
-
-
-
-
- | Text String |
- Key Cap |
- Key Purpose |
-
-
-
-
- | `*Shift*` |
- |
- Select Shift layer (inactive). Use on the Shift key to indicate that it switches to the shift layer. |
-
-
- | `*Shifted*` |
- |
- Select Shift layer (active). Use on the Shift key on the shift layer to switch back to the default layer. |
-
-
- | `*ShiftLock*` |
- |
- Switch to Caps layer (inactive). Not commonly used; generally double-tap on Shift key is used to access the
- caps layer. |
-
-
- | `*ShiftedLock*` |
- |
- Switch to Caps layer (active). Use on the Shift key on the caps layer to switch back to the default layer.
- |
-
-
- | `*Enter*` |
- or |
- Return or Enter key (shape determined by writing system direction) |
-
-
- | `*LTREnter*` |
- |
- Return or Enter key (left-to-right script shape) |
-
-
- | `*RTLEnter*` |
- |
- Return or Enter key (right-to-left script shape) |
-
-
- | `*BkSp*` |
- or |
- Backspace key (shape determined by writing system direction) |
-
-
- | `*LTRBkSp*` |
- |
- Backspace key (left-to-right script shape) |
-
-
- | `*RTLBkSp*` |
- |
- Backspace key (right-to-left script shape) |
-
-
- | `*Menu*` |
- |
- Globe key; display the language menu. Use on the `K_LOPT` key. |
-
-
- | `*Hide*` |
- |
- Hide the on screen keyboard. Use on the `K_ROPT` key. |
-
-
- | `*ABC*` |
- |
- Select alphabetic layer (Uppercase) |
-
-
- | `*abc*` |
- |
- Select alphabetic layer (Lowercase) |
-
-
- | `*123*` |
- |
- Select the numeric layer |
-
-
- | `*Symbol*` |
- |
- Select the symbol layer |
-
-
- | `*Currency*` |
- |
- Select the currency symbol layer |
-
-
- | `*ZWNJ*` |
- (iOS) or (Android) |
- Zero Width Non Joiner (shape determined by current platform) |
-
-
- | `*ZWNJiOS*` |
- |
- Zero Width Non Joiner (iOS style shape) |
-
-
- | `*ZWNJAndroid*` |
- |
- Zero Width Non Joiner (Android style shape) |
-
-
- | `*ZWNJGeneric*` |
- |
- Zero Width Non Joiner (not platform-specific) |
-
-
- | `*Sp*` |
- |
- Regular space |
-
-
- | `*NBSp*` |
- |
- No-Break Space |
-
-
- | `*NarNBSp*` |
- |
- Narrow No-Break Space |
-
-
- | `*EnQ*` |
- |
- En Quad |
-
-
- | `*EmQ*` |
- |
- Em Quad |
-
-
- | `*EnSp*` |
- |
- En Space |
-
-
- | `*EmSp*` |
- |
- Em Space |
-
-
- | `*PunctSp*` |
- |
- Punctuation Space |
-
-
- | `*ThSp*` |
- |
- Thin Space |
-
-
- | `*HSp*` |
- |
- Hair Space |
-
-
- | `*ZWSp*` |
- |
- Zero Width Space |
-
-
- | `*ZWJ*` |
- |
- Zero Width Joiner |
-
-
- | `*WJ*` |
- |
- Word Joiner |
-
-
- | `*CGJ*` |
- |
- Combining Grapheme Joiner |
-
-
- | `*LTRM*` |
- |
- Left-to-right Mark |
-
-
- | `*RTLM*` |
- |
- Right-to-left Mark |
-
-
- | `*SH*` |
- |
- Soft Hyphen |
-
-
- | `*HTab*` |
- |
- Horizontal Tabulation |
-
-
-
-
-
-The following additional symbols are also available, but intended for working
-with legacy desktop layouts, and not recommended for general use:
-
-
-
-
- | Text String |
- Key Cap |
- Key Purpose |
-
-
-
-
- | `*Tab*` |
- |
- Move to next input element in tab order |
-
-
- | `*TabLeft*` |
- |
- Move to previous input element in tab order |
-
-
- | `*Caps*` |
- |
- Select caps layer (legacy) |
-
-
- | `*AltGr*` |
- |
- Select AltGr (Right-Alt) layer (desktop layout compatibility) |
-
-
- | `*Alt*` |
- |
- Select Alt layer (desktop layout compatibility) |
-
-
- | `*Ctrl*` |
- |
- Select Ctrl layer (desktop layout compatibility) |
-
-
- | `*LAlt*` |
- |
- Select Left-Alt layer (desktop layout compatibility) |
-
-
- | `*RAlt*` |
- |
- Select Right-Alt layer (desktop layout compatibility) |
-
-
- | `*LCtrl*` |
- |
- Select Left-Ctrl layer (desktop layout compatibility) |
-
-
- | `*RCtrl*` |
- |
- Select Right-Ctrl layer (desktop layout compatibility) |
-
-
- | `*LAltCtrl*` |
- |
- Select Left-Alt-Ctrl layer (desktop layout compatibility) |
-
-
- | `*RAltCtrl*` |
- |
- Select Right-Alt-Ctrl layer (desktop layout compatibility) |
-
-
- | `*LAltCtrlShift*` |
- |
- Select Left-Alt-Ctrl-Shift layer (desktop layout compatibility) |
-
-
- | `*RAltCtrlShift*` |
- |
- Select Right-Alt-Ctrl-Shift layer (desktop layout compatibility) |
-
-
- | `*AltShift*` |
- |
- Select Alt-Shift layer (desktop layout compatibility) |
-
-
- | `*CtrlShift*` |
- |
- Select Ctrl-Shift layer (desktop layout compatibility) |
-
-
- | `*AltCtrlShift*` |
- |
- Select Alt-Ctrl-Shift layer (desktop layout compatibility) |
-
-
- | `*LAltShift*` |
- |
- Select Left-Alt-Shift layer (desktop layout compatibility) |
-
-
- | `*RAltShift*` |
- |
- Select Right-Alt-Shift layer (desktop layout compatibility) |
-
-
- | `*LCtrlShift*` |
- |
- Select Left-Ctrl-Shift layer (desktop layout compatibility) |
-
-
- | `*RCtrlShift*` |
- |
- Select Right-Ctrl-Shift layer (desktop layout compatibility) |
-
-
-
+Keyman. The list of icons in the font may be extended in future. See
+the list of special characters in the
+[.keyman-touch-layout reference](../../reference/file-types/keyman-touch-layout).
### Key type
diff --git a/developer/docs/help/reference/file-types/keyman-touch-layout.md b/developer/docs/help/reference/file-types/keyman-touch-layout.md
index 9f9bbd6fc3..e689c0aa41 100644
--- a/developer/docs/help/reference/file-types/keyman-touch-layout.md
+++ b/developer/docs/help/reference/file-types/keyman-touch-layout.md
@@ -97,7 +97,7 @@ Character Keys"). Typically, you would use only the "common" virtual key
- `U_####[_####]` is used as a shortcut for a key that will output those
Unicode values, if no rule matches it. This is similar to the overloaded
- behaviour for `K_` ids. Thus `####` must be valid Unicode characters.
+ behaviour for `K_` ids. Thus `####` must be valid Unicode characters.
The square bracket characters `[` and `]` indicate an optional portion of the sequence
and are not to be included in the shortcut. E.g.
`U_0259` would generate a schwa if no rule matches. It is still valid to
@@ -192,9 +192,10 @@ A number of special text labels are recognized as identifying special purpose
keys, such as Shift, Backspace, Enter, etc., for which icons are more
appropriately used than a text label. A special font including these icons is
included with Keyman and automatically embedded and used in any web page using
-Keyman. The list of icons in the font will probably be extended in future, but
-for now the following special labels are recognized:
+Keyman. The list of icons in the font may be extended in future.
+The special labels are listed below:
+
@@ -293,8 +294,9 @@ for now the following special labels are recognized:
| `*ZWNJ*` |
- (iOS) or (Android) |
- Zero Width Non Joiner (shape determined by current platform) |
+ |
+ Zero Width Non Joiner (shape determined by current platform,
+ will be on Android) |
| `*ZWNJiOS*` |
@@ -524,6 +526,7 @@ with legacy desktop layouts, and not recommended for general use:
+
### Key type
diff --git a/developer/src/kmc-kmn/.gitignore b/developer/src/kmc-kmn/.gitignore
index 6a608d487b..76fb645213 100644
--- a/developer/src/kmc-kmn/.gitignore
+++ b/developer/src/kmc-kmn/.gitignore
@@ -1,3 +1,6 @@
# WASM interfaces from kmcmplib are copied here so we can avoid having
# to make tsc aware of debug vs release paths
src/import/
+
+# We copy some files from other modules for comparison purposes here
+test/kmw/_imported_*
\ No newline at end of file
diff --git a/developer/src/kmc-kmn/build.sh b/developer/src/kmc-kmn/build.sh
index 7d94f880df..d1cc681663 100755
--- a/developer/src/kmc-kmn/build.sh
+++ b/developer/src/kmc-kmn/build.sh
@@ -65,6 +65,17 @@ function do_build() {
function do_test() {
copy_deps
+
+ # We want to compare the key cap values from both KMW and Developer and ensure
+ # that all three are in sync. We'll copy the relevant source files and patch
+ # them in directly. Builder's constants.js is not an ES6 module, so we hackily
+ # patch that here.
+ echo 'export const builder = {specialCharacters:{}}' > ./test/kmw/_imported_constants.js
+ # shellcheck disable=SC2016
+ echo 'function $(v) {v()}' >> ./test/kmw/_imported_constants.js
+ cat "${KEYMAN_ROOT}/developer/src/tike/xml/layoutbuilder/constants.js" >> ./test/kmw/_imported_constants.js
+ cp "${KEYMAN_ROOT}/web/src/engine/osk/src/specialCharacters.ts" ./test/kmw/_imported_specialCharacters.ts
+
typescript_run_eslint_mocha_tests 80
}
diff --git a/developer/src/kmc-kmn/src/kmw-compiler/constants.ts b/developer/src/kmc-kmn/src/kmw-compiler/constants.ts
index 1c41df145b..a678dc8726 100644
--- a/developer/src/kmc-kmn/src/kmw-compiler/constants.ts
+++ b/developer/src/kmc-kmn/src/kmw-compiler/constants.ts
@@ -7,7 +7,16 @@ export enum TRequiredKey {
export const
CRequiredKeys: TRequiredKey[] = [TRequiredKey.K_LOPT, TRequiredKey.K_BKSP, TRequiredKey.K_ENTER]; // I4447
-// See also builder.js: specialCharacters; web/source/osk/oskKey.ts: specialCharacters
+// Defines the various 'special' modifier/control/non-printing keys on keyboards.
+//
+// `CSpecialText*` must be kept in sync with:
+// * /web/src/engine/osk/src/specialCharacters.ts
+// * /developer/src/tike/xml/layoutBuilder/constants.js
+// * /core/include/ldml/keyman_core_ldml.ts
+//
+// More information, and unit test, in /developer/src/kmc-kmn/tests/kmw/constants.tests.ts
+//
+// Note that this mapping here is version-mapped for verification purposes.
export const
CSpecialText10: string = '*Shift*\0*Enter*\0*Tab*\0*BkSp*\0*Menu*\0*Hide*\0*Alt*\0*Ctrl*\0*Caps*\0' +
'*ABC*\0*abc*\0*123*\0*Symbol*\0*Currency*\0*Shifted*\0*AltGr*\0*TabLeft*\0',
diff --git a/developer/src/kmc-kmn/test/kmw/constants.tests.ts b/developer/src/kmc-kmn/test/kmw/constants.tests.ts
new file mode 100644
index 0000000000..d9acdb7d35
--- /dev/null
+++ b/developer/src/kmc-kmn/test/kmw/constants.tests.ts
@@ -0,0 +1,152 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Created by mcdurdin on 2025-10-16
+ *
+ * Key cap special values (such as "*Shift*") are defined in multiple modules;
+ * this data is not currently in a common module, as it would create unhelpful
+ * dependencies or require significant refactoring. So, instead, for now we just
+ * verify that the values line up. Note that the _imported_ files are copied in
+ * by build.sh before running the tests in order to avoid pathing issues.
+ *
+ * This is testing that the values in these files are equivalent:
+ * developer/src/tike/xml/layoutbuilder/constants.js
+ * web/src/engine/osk/src/specialCharacters.ts
+ * developer/src/kmc-kmn/kmw-compiler/constants.ts
+ * core/include/ldml/keyman_core_ldml.ts
+ * developer/docs/help/reference/file-types/keyman-touch-layout.md
+ */
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import 'mocha';
+import {assert} from 'chai';
+
+import keymanWebSpecialCharacters from "./_imported_specialCharacters.js";
+import { CSpecialText17, CSpecialText14, CSpecialText10, CSpecialText17ZWNJ } from "../../src/kmw-compiler/constants.js";
+import { builder } from "./_imported_constants.js";
+import { constants as coreConstants } from "@keymanapp/ldml-keyboard-constants";
+
+/** Verify key cap constants across 4 modules: KMW treated as primary */
+describe('Key cap special text values from KeymanWeb', function() {
+
+ it('should match key cap special text in Developer Touch Layout Builder', function() {
+ // The key cap special text objects in these two files should be exactly equal:
+ // developer/src/tike/xml/layoutbuilder/constants.js
+ // web/src/engine/osk/src/specialCharacters.ts
+ assert.deepEqual(builder.specialCharacters, keymanWebSpecialCharacters);
+ });
+
+ it('should match key cap special text in Developer kmc-kmn KMW compiler', function() {
+ // These two files should have the same strings for key caps:
+ // developer/src/kmc-kmn/kmw-compiler/constants.ts
+ // web/src/engine/osk/src/specialCharacters.ts
+
+ // No values to compare here - just key names
+
+ // TODO: the following key cap strings are not verified in the compiler,
+ // why? It also appears that the compiler does not verify when an
+ // unrecognized key cap string is used
+ const specialCharactersPatch = Object.keys(keymanWebSpecialCharacters).filter(e =>
+ e !== "*LAlt*" &&
+ e !== "*RAlt*" &&
+ e !== "*LCtrl*" &&
+ e !== "*RCtrl*" &&
+ e !== "*LAltCtrl*" &&
+ e !== "*RAltCtrl*" &&
+ e !== "*LAltCtrlShift*" &&
+ e !== "*RAltCtrlShift*" &&
+ e !== "*AltShift*" &&
+ e !== "*CtrlShift*" &&
+ e !== "*AltCtrlShift*" &&
+ e !== "*LAltShift*" &&
+ e !== "*RAltShift*" &&
+ e !== "*LCtrlShift*" &&
+ e !== "*RCtrlShift*"
+ ).sort();
+
+ const compilerSpecialCharacters = [
+ ...CSpecialText10.split('\0'),
+ ...CSpecialText14.split('\0'),
+ ...CSpecialText17.split('\0'),
+ CSpecialText17ZWNJ,
+ ].filter(e => e !== "") // remove blanks coming from the splitting
+ .sort();
+
+ assert.deepEqual(compilerSpecialCharacters, specialCharactersPatch);
+ });
+
+ // TODO-EMBED-OSK-IN-KMX: enable once we have complete the Core LDML headers
+ it.skip('should match key cap special text in Core constants', function() {
+ // These two files should have the same constant values for key caps:
+ // core/include/ldml/keyman_core_ldml.ts
+ // web/src/engine/osk/src/specialCharacters.ts
+
+ // The Core constants and KeymanWeb constants vary a little:
+ //
+ // 1. We need to special case *ABC* and *abc* because they have identical
+ // values but we are case-insensitive in our LDML definitions
+ const specialCharactersPatch: any = {...keymanWebSpecialCharacters};
+ specialCharactersPatch['*abc_lower*'] = specialCharactersPatch['*abc*'];
+ delete specialCharactersPatch['*abc*'];
+
+ specialCharactersPatch['*ABC_upper*'] = specialCharactersPatch['*ABC*'];
+ delete specialCharactersPatch['*ABC*'];
+
+ // 2. Map all the "*Name*" key names to "dis2_key_cap_name"...
+ const dis2_key_cap_expected: any = {};
+ for(const key of Object.keys(specialCharactersPatch)) {
+ const newKey = key.replace(/^\*(.+)\*$/, 'dis2_key_cap_$1').toLowerCase();
+ dis2_key_cap_expected[newKey] = specialCharactersPatch[key];
+ }
+
+ // 3. We only want to compare the dis2_key_cap_ values from the
+ // coreConstants object
+ const coreConstantsFiltered: any = {};
+ for(const key of Object.keys(coreConstants)) {
+ if(key.match(/^dis2_key_cap_/)) {
+ coreConstantsFiltered[key] = (coreConstants)[key];
+ }
+ }
+
+ assert.deepEqual(coreConstantsFiltered, dis2_key_cap_expected);
+ });
+
+ it('should be documented correctly', function() {
+ // These two files should have the same constant values for key caps:
+ // developer/docs/help/reference/file-types/keyman-touch-layout.md
+ // web/src/engine/osk/src/specialCharacters.ts
+ const helpFile =
+ path.join(path.dirname(fileURLToPath(import.meta.url)), '../../../../../docs/help/reference/file-types/keyman-touch-layout.md');
+ const lines = fs.readFileSync(helpFile, 'utf-8').replaceAll(/\r\n/g, '\n').split('\n');
+
+ // Find the relevant section in the file between start:special_key_caps and
+ // end:special_key_caps
+ const line0 = lines.findIndex(line => line.includes('start:special_key_caps'));
+ assert.notEqual(line0, -1);
+ const line1 = lines.findIndex(line => line.includes('end:special_key_caps'));
+ assert.notEqual(line1, -1);
+
+ const content = lines.slice(line0+1, line1);
+
+ const markdownConstants: any = {};
+
+ // Iterate over cells in the relevant section of the file and build an
+ // array of values, assuming the format of the markdown doesn't change
+ let nextCap: string = null;
+ for(const line of content) {
+ const mCap = line.match(/ | `(\*.+\*)`<\/td>/i);
+ if(mCap) {
+ nextCap = mCap[1];
+ continue;
+ }
+ const mVal = line.match(/ | (.+);<\/td>/);
+ if(mVal) {
+ markdownConstants[nextCap] = parseInt(mVal[1], 16);
+ }
+ }
+
+ assert.deepEqual(markdownConstants, keymanWebSpecialCharacters);
+ });
+});
diff --git a/developer/src/kmc-kmn/test/tsconfig.json b/developer/src/kmc-kmn/test/tsconfig.json
index dee653dfac..ffdcf60ae7 100644
--- a/developer/src/kmc-kmn/test/tsconfig.json
+++ b/developer/src/kmc-kmn/test/tsconfig.json
@@ -10,6 +10,8 @@
},
"include": [
"**/*.tests.ts",
+ "./kmw/_imported_constants.js",
+ "./kmw/_imported_specialCharacters.ts",
"./helpers/index.ts",
"./kmw/util.ts"
],
diff --git a/developer/src/tike/xml/layoutbuilder/constants.js b/developer/src/tike/xml/layoutbuilder/constants.js
index 117020e5d5..7680725d40 100644
--- a/developer/src/tike/xml/layoutbuilder/constants.js
+++ b/developer/src/tike/xml/layoutbuilder/constants.js
@@ -552,7 +552,14 @@ $(function() {
this.lookupKeyNames.sort();
// Defines the PUA code mapping for the various 'special' modifier/control/non-printing keys on keyboards.
- // This is lifted directly from specialCharacters.ts and must be kept in sync. See also CompileKeymanWeb.pas: CSpecialText10
+ // This is lifted directly from web/.../specialCharacters.ts and must be kept in sync.
+ //
+ // `specialCharacters` must be kept in sync with the same values in:
+ // * /web/src/engine/osk/src/specialCharacters.ts
+ // * /developer/src/kmc-kmn/src/kmw-compiler/constants.ts
+ // * /core/include/ldml/keyman_core_ldml.ts
+ //
+ // More information, and unit test, in /developer/src/kmc-kmn/tests/kmw/constants.tests.ts
this.specialCharacters = {
'*Shift*': 8,
'*Enter*': 5,
diff --git a/web/src/engine/osk/src/specialCharacters.ts b/web/src/engine/osk/src/specialCharacters.ts
index c13e63715e..4efac09233 100644
--- a/web/src/engine/osk/src/specialCharacters.ts
+++ b/web/src/engine/osk/src/specialCharacters.ts
@@ -1,5 +1,12 @@
// Defines the PUA code mapping for the various 'special' modifier/control/non-printing keys on keyboards.
-// `specialCharacters` must be kept in sync with the same variable in constants.js. See also CompileKeymanWeb.pas: CSpecialText10
+//
+// `specialCharacters` must be kept in sync with the same values in:
+// * /developer/src/tike/xml/layoutBuilder/constants.js
+// * /developer/src/kmc-kmn/src/kmw-compiler/constants.ts
+// * /core/include/ldml/keyman_core_ldml.ts
+//
+// More information, and unit test, in /developer/src/kmc-kmn/tests/kmw/constants.tests.ts
+
const specialCharacters = {
'*Shift*': 8,
'*Enter*': 5,
|