change(developer): add unit test to verify special key cap definitions

Special key caps such as '*ZWNJ*' are defined in a number of places in
the source. In theory, we could DRY these definitions out, but that
would add a number of unhelpful dependencies or take considerable time
to implement. So, for now I opted to write a unit test to compare the
definitions as found in the following files, treating web's definition
as primary:

 *   web/src/engine/osk/src/specialCharacters.ts
 *   developer/src/tike/xml/layoutbuilder/constants.js
 *   developer/src/kmc-kmn/kmw-compiler/constants.ts
 *   developer/docs/help/reference/file-types/keyman-touch-layout.md
 *   core/include/ldml/keyman_core_ldml.ts

Note that the keyman_core_ldml.ts file changes are not included in this
commit, and hence the unit test for it is currently skipped. This will
be enabled in a subsequent commit along with other KMX+ changes to
support epic/embed-osk-in-kmx.

Test-bot: skip
This commit is contained in:
Marc Durdin 2025-10-16 12:37:44 +02:00
parent 9406e16294
commit db5aa518c0
9 changed files with 206 additions and 341 deletions

View file

@ -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:
<table class="display">
<thead>
<tr>
<th>Text String</th>
<th>Key Cap</th>
<th>Key Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td markdown="1">`*Shift*`</td>
<td class="special-osk">&#xE008;</td>
<td>Select Shift layer (inactive). Use on the Shift key to indicate that it switches to the shift layer.</td>
</tr>
<tr>
<td markdown="1">`*Shifted*`</td>
<td class="special-osk">&#xE009;</td>
<td>Select Shift layer (active). Use on the Shift key on the shift layer to switch back to the default layer.</td>
</tr>
<tr>
<td markdown="1">`*ShiftLock*`</td>
<td class="special-osk">&#xE073;</td>
<td>Switch to Caps layer (inactive). Not commonly used; generally double-tap on Shift key is used to access the
caps layer.</td>
</tr>
<tr>
<td markdown="1">`*ShiftedLock*`</td>
<td class="special-osk">&#xE074;</td>
<td>Switch to Caps layer (active). Use on the Shift key on the caps layer to switch back to the default layer.
</td>
</tr>
<tr>
<td markdown="1">`*Enter*`</td>
<td class="special-osk">&#xE005; or &#xE071;</td>
<td>Return or Enter key (shape determined by writing system direction)</td>
</tr>
<tr>
<td markdown="1">`*LTREnter*`</td>
<td class="special-osk">&#xE005;</td>
<td>Return or Enter key (left-to-right script shape)</td>
</tr>
<tr>
<td markdown="1">`*RTLEnter*`</td>
<td class="special-osk">&#xE071;</td>
<td>Return or Enter key (right-to-left script shape)</td>
</tr>
<tr>
<td markdown="1">`*BkSp*`</td>
<td class="special-osk">&#xE004; or &#xE072;</td>
<td>Backspace key (shape determined by writing system direction)</td>
</tr>
<tr>
<td markdown="1">`*LTRBkSp*`</td>
<td class="special-osk">&#xE004;</td>
<td>Backspace key (left-to-right script shape)</td>
</tr>
<tr>
<td markdown="1">`*RTLBkSp*`</td>
<td class="special-osk">&#xE072;</td>
<td>Backspace key (right-to-left script shape)</td>
</tr>
<tr>
<td markdown="1">`*Menu*`</td>
<td class="special-osk">&#xE00B;</td>
<td markdown="1">Globe key; display the language menu. Use on the `K_LOPT` key.</td>
</tr>
<tr>
<td markdown="1">`*Hide*`</td>
<td class="special-osk">&#xE00A;</td>
<td markdown="1">Hide the on screen keyboard. Use on the `K_ROPT` key.</td>
</tr>
<tr>
<td markdown="1">`*ABC*`</td>
<td class="special-osk">&#xE010;</td>
<td>Select alphabetic layer (Uppercase)</td>
</tr>
<tr>
<td markdown="1">`*abc*`</td>
<td class="special-osk">&#xE011;</td>
<td>Select alphabetic layer (Lowercase)</td>
</tr>
<tr>
<td markdown="1">`*123*`</td>
<td class="special-osk">&#xE013;</td>
<td>Select the numeric layer</td>
</tr>
<tr>
<td markdown="1">`*Symbol*`</td>
<td class="special-osk">&#xE015;</td>
<td>Select the symbol layer</td>
</tr>
<tr>
<td markdown="1">`*Currency*`</td>
<td class="special-osk">&#xE014;</td>
<td>Select the currency symbol layer</td>
</tr>
<tr>
<td markdown="1">`*ZWNJ*`</td>
<td class="special-osk">&#xE075; (iOS) or &#xE076; (Android)</td>
<td>Zero Width Non Joiner (shape determined by current platform)</td>
</tr>
<tr>
<td markdown="1">`*ZWNJiOS*`</td>
<td class="special-osk">&#xE075;</td>
<td>Zero Width Non Joiner (iOS style shape)</td>
</tr>
<tr>
<td markdown="1">`*ZWNJAndroid*`</td>
<td class="special-osk">&#xE076;</td>
<td>Zero Width Non Joiner (Android style shape)</td>
</tr>
<tr>
<td markdown="1">`*ZWNJGeneric*`</td>
<td class="special-osk">&#xE079;</td>
<td>Zero Width Non Joiner (not platform-specific)</td>
</tr>
<tr>
<td markdown="1">`*Sp*`</td>
<td class="special-osk">&#xE080;</td>
<td>Regular space</td>
</tr>
<tr>
<td markdown="1">`*NBSp*`</td>
<td class="special-osk">&#xE082;</td>
<td>No-Break Space</td>
</tr>
<tr>
<td markdown="1">`*NarNBSp*`</td>
<td class="special-osk">&#xE083;</td>
<td>Narrow No-Break Space</td>
</tr>
<tr>
<td markdown="1">`*EnQ*`</td>
<td class="special-osk">&#xE084;</td>
<td>En Quad</td>
</tr>
<tr>
<td markdown="1">`*EmQ*`</td>
<td class="special-osk">&#xE085;</td>
<td>Em Quad</td>
</tr>
<tr>
<td markdown="1">`*EnSp*`</td>
<td class="special-osk">&#xE086;</td>
<td>En Space</td>
</tr>
<tr>
<td markdown="1">`*EmSp*`</td>
<td class="special-osk">&#xE087;</td>
<td>Em Space</td>
</tr>
<tr>
<td markdown="1">`*PunctSp*`</td>
<td class="special-osk">&#xE08C;</td>
<td>Punctuation Space</td>
</tr>
<tr>
<td markdown="1">`*ThSp*`</td>
<td class="special-osk">&#xE08D;</td>
<td>Thin Space</td>
</tr>
<tr>
<td markdown="1">`*HSp*`</td>
<td class="special-osk">&#xE08E;</td>
<td>Hair Space</td>
</tr>
<tr>
<td markdown="1">`*ZWSp*`</td>
<td class="special-osk">&#xE081;</td>
<td>Zero Width Space</td>
</tr>
<tr>
<td markdown="1">`*ZWJ*`</td>
<td class="special-osk">&#xE077;</td>
<td>Zero Width Joiner</td>
</tr>
<tr>
<td markdown="1">`*WJ*`</td>
<td class="special-osk">&#xE078;</td>
<td>Word Joiner</td>
</tr>
<tr>
<td markdown="1">`*CGJ*`</td>
<td class="special-osk">&#xE07A;</td>
<td>Combining Grapheme Joiner</td>
</tr>
<tr>
<td markdown="1">`*LTRM*`</td>
<td class="special-osk">&#xE090;</td>
<td>Left-to-right Mark</td>
</tr>
<tr>
<td markdown="1">`*RTLM*`</td>
<td class="special-osk">&#xE091;</td>
<td>Right-to-left Mark</td>
</tr>
<tr>
<td markdown="1">`*SH*`</td>
<td class="special-osk">&#xE0A1;</td>
<td>Soft Hyphen</td>
</tr>
<tr>
<td markdown="1">`*HTab*`</td>
<td class="special-osk">&#xE0A2;</td>
<td>Horizontal Tabulation</td>
</tr>
</tbody>
</table>
The following additional symbols are also available, but intended for working
with legacy desktop layouts, and not recommended for general use:
<table class="display">
<thead>
<tr>
<th>Text String</th>
<th>Key Cap</th>
<th>Key Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td markdown="1">`*Tab*`</td>
<td class="special-osk">&#xE006;</td>
<td>Move to next input element in tab order</td>
</tr>
<tr>
<td markdown="1">`*TabLeft*`</td>
<td class="special-osk">&#xE007;</td>
<td>Move to previous input element in tab order</td>
</tr>
<tr>
<td markdown="1">`*Caps*`</td>
<td class="special-osk">&#xE003;</td>
<td>Select caps layer (legacy)</td>
</tr>
<tr>
<td markdown="1">`*AltGr*`</td>
<td class="special-osk">&#xE002;</td>
<td>Select AltGr (Right-Alt) layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*Alt*`</td>
<td class="special-osk">&#xE019;</td>
<td>Select Alt layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*Ctrl*`</td>
<td class="special-osk">&#xE001;</td>
<td>Select Ctrl layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*LAlt*`</td>
<td class="special-osk">&#xE056;</td>
<td>Select Left-Alt layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*RAlt*`</td>
<td class="special-osk">&#xE057;</td>
<td>Select Right-Alt layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*LCtrl*`</td>
<td class="special-osk">&#xE058;</td>
<td>Select Left-Ctrl layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*RCtrl*`</td>
<td class="special-osk">&#xE059;</td>
<td>Select Right-Ctrl layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*LAltCtrl*`</td>
<td class="special-osk">&#xE060;</td>
<td>Select Left-Alt-Ctrl layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*RAltCtrl*`</td>
<td class="special-osk">&#xE061;</td>
<td>Select Right-Alt-Ctrl layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*LAltCtrlShift*`</td>
<td class="special-osk">&#xE062;</td>
<td>Select Left-Alt-Ctrl-Shift layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*RAltCtrlShift*`</td>
<td class="special-osk">&#xE063;</td>
<td>Select Right-Alt-Ctrl-Shift layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*AltShift*`</td>
<td class="special-osk">&#xE064;</td>
<td>Select Alt-Shift layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*CtrlShift*`</td>
<td class="special-osk">&#xE065;</td>
<td>Select Ctrl-Shift layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*AltCtrlShift*`</td>
<td class="special-osk">&#xE066;</td>
<td>Select Alt-Ctrl-Shift layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*LAltShift*`</td>
<td class="special-osk">&#xE067;</td>
<td>Select Left-Alt-Shift layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*RAltShift*`</td>
<td class="special-osk">&#xE068;</td>
<td>Select Right-Alt-Shift layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*LCtrlShift*`</td>
<td class="special-osk">&#xE069;</td>
<td>Select Left-Ctrl-Shift layer (desktop layout compatibility)</td>
</tr>
<tr>
<td markdown="1">`*RCtrlShift*`</td>
<td class="special-osk">&#xE070;</td>
<td>Select Right-Ctrl-Shift layer (desktop layout compatibility)</td>
</tr>
</tbody>
</table>
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

View file

@ -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:
<!-- start:special_key_caps -->
<table class="display">
<thead>
<tr>
@ -293,8 +294,9 @@ for now the following special labels are recognized:
</tr>
<tr>
<td markdown="1">`*ZWNJ*`</td>
<td class="special-osk">&#xE075; (iOS) or &#xE076; (Android)</td>
<td>Zero Width Non Joiner (shape determined by current platform)</td>
<td class="special-osk">&#xE075;</td>
<td>Zero Width Non Joiner (shape determined by current platform,
will be <span class="special-osk">&#xE076;</span> on Android)</td>
</tr>
<tr>
<td markdown="1">`*ZWNJiOS*`</td>
@ -524,6 +526,7 @@ with legacy desktop layouts, and not recommended for general use:
</tr>
</tbody>
</table>
<!-- end:special_key_caps -->
### Key type

View file

@ -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_*

View file

@ -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
}

View file

@ -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',

View file

@ -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] = (<any>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 <td> 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 markdown="1">`(\*.+\*)`<\/td>/i);
if(mCap) {
nextCap = mCap[1];
continue;
}
const mVal = line.match(/<td class="special-osk">&#xE(.+);<\/td>/);
if(mVal) {
markdownConstants[nextCap] = parseInt(mVal[1], 16);
}
}
assert.deepEqual(markdownConstants, keymanWebSpecialCharacters);
});
});

View file

@ -10,6 +10,8 @@
},
"include": [
"**/*.tests.ts",
"./kmw/_imported_constants.js",
"./kmw/_imported_specialCharacters.ts",
"./helpers/index.ts",
"./kmw/util.ts"
],

View file

@ -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,

View file

@ -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,