Merge branch 'epic/mac-config' into feat/mac/kmp-inf
Some checks failed
Keyman Build Summary / Summarize build status checks (push) Has been cancelled

This commit is contained in:
Shawn Schantz 2026-09-14 10:14:21 -04:00 • committed by GitHub
commit b7eb4dc9d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
279 changed files with 11878 additions and 11508 deletions

View file

@ -1,5 +1,76 @@
# Keyman Version History
## 19.0.285 alpha 2026-09-11
* fix(web): handle 'unload' message while attempting model 'load' (#16548)
* fix(android): load KMP files from app-external sources (#16392)
## 19.0.284 alpha 2026-09-10
* maint(developer): remove unused ajv dependency from kmc-convert (#16539)
* fix(web): remove scroll-to-top on page load (#16531)
## 19.0.283 alpha 2026-09-09
* change(web): allow autocorrection on input of basic whitespaces (#16541)
## 19.0.282 alpha 2026-09-08
* refactor(web): clarify function scope and update some function comments (#16532)
## 19.0.281 alpha 2026-09-07
* chore(deps): bump fast-uri from 3.1.5 to 3.1.7 (#16511)
* chore(deps): bump nanoid from 3.3.11 to 3.3.18 (#16512)
* chore(deps): bump qs and express (#16513)
* chore(web): adjust types in guide-examples tests (#16523)
## 19.0.280 alpha 2026-09-05
* fix(windows): open chm external links in user's browser (#16515)
## 19.0.279 alpha 2026-09-03
* chore(web): tweak error message (#16495)
* change(web): stage construction of CorrectionPredictionTuple instances (#16429)
* change(web): narrow the range of suggestions examined to determine autocompletion (#16436)
* fix(web): remove deprecated `onunload` handler (#16504)
* docs(developer): add comment to test fixtures to clarify expectations (#16505)
* fix(developer): hide keyboard uninstall confirmation (#16506)
* fix(developer): remember presentation and layer when switching platforms in touch layout editor (#16509)
* fix(developer): ensure JSON regenerated after undo in touch layout editor (#16516)
* fix(developer): improve validation of minDeviceWidth to match spec (#16501)
* chore(developer): improve kmc-ldml test messages (#16502)
* chore(web): add const for cookie name (#16519)
* refactor(web): clarify scope of PageContextAttachment functions (#16521)
## 19.0.278 alpha 2026-09-02
* feat(developer): support output folder and `--continue-on-error` (#16453)
* fix(developer): validate the number of hardware layers only in Layr compiler (#16455)
* fix(developer): validate id and modifiers attributes in Layr compiler (#16456)
* fix(developer): disable Copy Link in Model Editor if no link to copy (#16475)
* fix(developer): open context help when F1 pressed in Project window (#16481)
* chore(web): update file headers (#16497)
## 19.0.277 alpha 2026-09-01
* fix(developer): handle comment without trailing whitespace (#16479)
* fix(developer): improve scroll wheel behavior in Character Map (#16474)
* fix(developer): call 'Show Console' twice to make the console actually show on first use (#16483)
* fix(developer): use path not dependent on pwd for starting Server (#16476)
* chore(developer): remove duplicated versioning information from Server (#16478)
* fix(developer): handle leading delimiters correctly in `u16tok()` (#16446)
* chore(android): upload webview source map during android and ios build (#16449)
* refactor(windows): rename `GetCapsAndNumlockState()` to `RefreshModifierState()` (#16444)
* chore(developer): strip out defunct printing support (#16480)
* fix(developer): make tool windows non-topmost when TIKE is deactivated (#16482)
* fix(windows): simplify keyboard refresh and make it more on-demand (#16435)
* refactor(windows): cleanup unused wm_keyman_control values (#16439)
* maint(windows): remove `koSwitchLanguageForAllApplications`, `koAutoSwitchOSKPages`, osVista, osWin7, osWin8, cleanup (#16443)
* change(developer): deprecate `fix`, `clearcontext`, bump max .kmx version to 19.0 (#16445)
* fix(developer): add missing message parameters and cleanup kmc-kmn messages further (#16493)
## 19.0.276 alpha 2026-08-28
* fix(windows): add manifest to insthelper and set trustinfo to asInvoker (#16430)

View file

@ -1 +1 @@
19.0.277
19.0.286

View file

@ -85,42 +85,33 @@
<!--
Capture content by MIME type, which is how Gmail broadcasts
attachment open requests. pathPattern and file extensions
are ignored, so the MIME type *MUST* be explicit, otherwise
we will match absolutely every file opened.
-->
Capture content by MIME type, which is how Gmail broadcasts
attachment open requests. pathPattern and file extensions
are ignored, so the MIME type *MUST* be explicit, otherwise
we will match absolutely every file opened.
See https://developer.android.com/guide/components/intents-filters#DataTest, item 3.
-->
<intent-filter android:priority="50">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<!-- needed for properly formatted email messages -->
<data
android:mimeType="application/vnd.keyman"
android:scheme="content" />
<data android:scheme="content" />
<data android:host="*" />
<!-- needed for mangled email messages -->
<data
android:mimeType="application/keyman"
android:scheme="content" />
<!-- needed for mangled email messages -->
<data
android:mimeType="application/octet-stream"
android:scheme="content" />
<data
android:mimeType="application/x-keyman-package"
android:scheme="content" />
<data android:mimeType="application/vnd.keyman.kmp+zip"/>
<data android:mimeType="application/octet-stream"/>
</intent-filter>
<!--
Capture file open requests (pathPattern is honoured) where no
MIME type is provided in the Intent. An Intent with a null
MIME type will never be matched by a filter with a set MIME
type, so we need a second intent-filter if we wish to also
match files with this extension and a non-null MIME type
(even if it is non-null but zero length).
-->
Capture open requests (pathPattern is honoured) where MIME
type is provided in the Intent and a URI is provided.
See https://developer.android.com/guide/components/intents-filters#DataTest, item 4.
-->
<intent-filter android:priority="50">
<action android:name="android.intent.action.VIEW" />
@ -128,19 +119,28 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:host="*" />
<data android:pathPattern="/.*\\.kmp" />
<data android:pathPattern=".*\\.kmp" />
<data android:mimeType="application/vnd.keyman.kmp+zip" />
<!--
Backup MIME intents
-->
<data android:mimeType="application/octet-stream" />
</intent-filter>
<!--
Capture file open requests (pathPattern is honoured) where a
(possibly blank) MIME type is provided in the Intent. This
filter may only be necessary for supporting ES File Explorer,
which has the probably buggy behaviour of using an Intent
with a MIME type that is set but zero-length. It's
impossible to match such a type except by using a global
wildcard.
-->
Capture file open requests (pathPattern is honoured) where no
MIME type is provided in the Intent. An Intent with a null
MIME type will never be matched by a filter with a set MIME
type, so we need a second intent-filter if we wish to also
match files with this extension and a non-null MIME type
(even if it is non-null but zero length).
See https://developer.android.com/guide/components/intents-filters#DataTest, item 2.
-->
<intent-filter android:priority="50">
<action android:name="android.intent.action.VIEW" />
@ -148,25 +148,10 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern="/.*\\.kmp" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- http:// and https:// protocols -->
<data
android:host="*"
android:pathPattern="/.*\\.kmp"
android:scheme="http" />
<data
android:host="*"
android:pathPattern="/.*\\.kmp"
android:scheme="https" />
<data android:pathPattern=".*\\.kmp" />
</intent-filter>
<intent-filter>
@ -192,35 +177,14 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="keyman-staging.com"
android:scheme="http"
android:pathPrefix="/go/package/download" />
<data android:scheme="http" />
<data android:scheme="https" />
<data
android:host="keyman.com"
android:scheme="https"
android:pathPrefix="/go/package/download" />
</intent-filter>
<intent-filter android:priority="50">
<!-- KMAPro should also be able to handle /keyboards/install links and convert to /go/package/download -->
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="keyman-staging.com"
android:scheme="http"
android:pathPrefix="/keyboards/install" />
<data
android:host="keyman.com"
android:scheme="https"
android:pathPrefix="/keyboards/install" />
<data android:host="keyman-staging.com" />
<data android:host="keyman.com" />
<data android:pathPrefix="/go/package/download" />
<data android:pathPrefix="/keyboards/install" />
</intent-filter>
</activity>

View file

@ -33,13 +33,8 @@ public class CheckPermissions {
// API 30-32
permissionsOK = Environment.isExternalStorageManager() ||
checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE);
} else {
// API 33+
// We had to remove these MEDIA permissions from AndroidManifest.xml so these will end up failing
// https://support.google.com/googleplay/android-developer/answer/14115180?hl=en
permissionsOK = permissionsOK && checkPermission(activity, Manifest.permission.READ_MEDIA_IMAGES);
permissionsOK = permissionsOK && checkPermission(activity, Manifest.permission.READ_MEDIA_VIDEO);
}
// API 33+ does not need any special permissions.
return permissionsOK;
}

View file

@ -9,6 +9,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../resources/build/builder-full.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "${KEYMAN_ROOT}/resources/build/ci/sentry-control.inc.sh"
# ################################ Main script ################################
@ -19,6 +20,7 @@ builder_describe "Builds Keyman Engine for Android." \
"configure" \
"build" \
"test Runs lint and unit tests." \
"publish-symbols Publishes symbols to Sentry." \
":engine Builds Engine"
builder_parse "$@"
@ -79,6 +81,13 @@ do_test() {
./gradlew $GRADLE_DAEMON $TEST_FLAGS
}
builder_run_action clean:engine rm -rf build app/build
builder_run_action build:engine do_build
builder_run_action test:engine do_test
do_publish_symbols() {
if builder_is_ci_build && builder_is_ci_build_level_release; then
sentry_upload_web "${KEYMAN_ROOT}/web/build/app/webview/release/"
fi
}
builder_run_action clean:engine rm -rf build app/build
builder_run_action build:engine do_build
builder_run_action test:engine do_test
builder_run_action publish-symbols do_publish_symbols

View file

@ -326,10 +326,20 @@ int u16ncmp(const KMX_WCHAR* p, const KMX_WCHAR* q, size_t count) {
* @return Pointer to the first token in p
*/
KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR ch, KMX_WCHAR** ctx) {
if(ch == 0 || !ctx) {
return NULL;
}
if (!p) {
p = *ctx;
if (!p)
if (!p) {
return NULL;
}
}
// skip initial delimiter
while (*p == ch) {
p++;
}
KMX_WCHAR* q = p;
@ -356,10 +366,20 @@ KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR ch, KMX_WCHAR** ctx) {
* @return Pointer to the first token in p
*/
KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR* delimiters, KMX_WCHAR** ctx) {
if(!ctx || !delimiters || !(*delimiters)) {
return NULL;
}
if (!p) {
p = *ctx;
if (!p)
if (!p) {
return NULL;
}
}
// skip initial delimiters
while (*p && u16chr(delimiters, *p)) {
p++;
}
KMX_WCHAR* q = p;

View file

@ -51,9 +51,10 @@ namespace kmx {
#define VERSION_160 0x00001000
#define VERSION_170 0x00001100
#define VERSION_190 0x00001300
#define VERSION_MIN VERSION_50
#define VERSION_MAX VERSION_170
#define VERSION_MAX VERSION_190
//
// Backspace types
@ -201,7 +202,7 @@ namespace kmx {
//#define CODE_EXTENDEDEND 0x0B deprecated
#define CODE_SWITCH 0x0C
#define CODE_KEY 0x0D
#define CODE_CLEARCONTEXT 0x0E
#define CODE_CLEARCONTEXT 0x0E // deprecated in 19.0
#define CODE_CALL 0x0F
// UC_SENTINEL_EXTENDEDEND 0x10
#define CODE_CONTEXTEX 0x11
@ -234,7 +235,7 @@ namespace kmx {
#define U_CODE_DEADKEY u"\u0008"
#define U_CODE_EXTENDED u"\u000A"
#define U_CODE_SWITCH u"\u000C"
#define U_CODE_CLEARCONTEXT u"\u000E"
#define U_CODE_CLEARCONTEXT u"\u000E" // deprecated in 19.0
#define U_CODE_CALL u"\u000F"
#define U_CODE_EXTENDEDEND u"\u0010"
#define U_CODE_CONTEXTEX u"\u0011"
@ -256,7 +257,7 @@ namespace kmx {
#define C_CODE_DEADKEY(deadkey) U_UC_SENTINEL U_CODE_DEADKEY deadkey
#define C_CODE_EXTENDED(varargs) U_UC_SENTINEL U_CODE_EXTENDED varargs
#define C_CODE_SWITCH(val) U_UC_SENTINEL U_CODE_SWITCH val
#define C_CODE_CLEARCONTEXT() U_UC_SENTINEL U_CODE_CLEARCONTEXT
#define C_CODE_CLEARCONTEXT() U_UC_SENTINEL U_CODE_CLEARCONTEXT // deprecated in 19.0
#define C_CODE_CALL(val) U_UC_SENTINEL U_CODE_CALL val
#define C_CODE_CONTEXTEX(val) U_UC_SENTINEL U_CODE_CONTEXTEX val
#define C_CODE_NOTANY(val) U_UC_SENTINEL U_CODE_NOTANY val

View file

@ -24,7 +24,8 @@ export enum KMX_Version {
VERSION_140 = 0x00000E00,
VERSION_150 = 0x00000F00,
VERSION_160 = 0x00001000,
VERSION_170 = 0x00001100
VERSION_170 = 0x00001100,
VERSION_190 = 0x00001300,
};
@ -165,9 +166,10 @@ export class KMXFile {
public static readonly VERSION_150 = KMX_Version.VERSION_150;
public static readonly VERSION_160 = KMX_Version.VERSION_160;
public static readonly VERSION_170 = KMX_Version.VERSION_170;
public static readonly VERSION_190 = KMX_Version.VERSION_190;
public static readonly VERSION_MIN = this.VERSION_50;
public static readonly VERSION_MAX = this.VERSION_170;
public static readonly VERSION_MAX = this.VERSION_190;
//
// Backspace types
@ -308,7 +310,7 @@ export class KMXFile {
//public static readonly CODE_EXTENDEDEND = 0x0B; deprecated
public static readonly CODE_SWITCH = 0x0C;
public static readonly CODE_KEY = 0x0D;
public static readonly CODE_CLEARCONTEXT = 0x0E;
public static readonly CODE_CLEARCONTEXT = 0x0E; // deprecated in 19.0
public static readonly CODE_CALL = 0x0F;
// UC_SENTINEL_EXTENDEDEND 0x10
public static readonly CODE_CONTEXTEX = 0x11;

View file

@ -75,8 +75,9 @@
#define VERSION_150 0x00000F00
#define VERSION_160 0x00001000
#define VERSION_170 0x00001100
#define VERSION_190 0x00001300
#define VERSION_MIN VERSION_50
#define VERSION_MAX VERSION_170
#define VERSION_MAX VERSION_190
/*
Special flag for WM_CHAR/WM_KEY???/WM_SYSKEY???: says that key has been
@ -257,7 +258,7 @@
//#define CODE_EXTENDEDEND 0x0B deprecated
#define CODE_SWITCH 0x0C
#define CODE_KEY 0x0D
#define CODE_CLEARCONTEXT 0x0E
#define CODE_CLEARCONTEXT 0x0E // deprecated in 19.0
#define CODE_CALL 0x0F
// UC_SENTINEL_EXTENDEDEND 0x10
#define CODE_CONTEXTEX 0x11

View file

@ -896,22 +896,36 @@ end;
procedure TfrmCharacterMapNew.gridMouseWheelDown(Sender: TObject;
Shift: TShiftState; MousePos: TPoint; var Handled: Boolean);
begin
Handled := True;
if ssCtrl in Shift then
begin
Handled := True;
// Zoom
if tbSize.Position < tbSize.Max then
tbSize.Position := tbSize.Position + tbSize.PageSize;
end
else
begin
// Scroll
if grid.TopRow < grid.RowCount - grid.VisibleRowCount then
grid.TopRow := grid.TopRow + 1;
end;
end;
procedure TfrmCharacterMapNew.gridMouseWheelUp(Sender: TObject;
Shift: TShiftState; MousePos: TPoint; var Handled: Boolean);
begin
Handled := True;
if ssCtrl in Shift then
begin
Handled := True;
// Zoom
if tbSize.Position > tbSize.Min then
tbSize.Position := tbSize.Position - tbSize.PageSize;
end
else
begin
// Scroll
if grid.TopRow > 0 then
grid.TopRow := grid.TopRow - 1;
end;
end;

View file

@ -1,18 +1,18 @@
(*
Name: GetOsVersion
Copyright: Copyright (C) SIL International.
Documentation:
Description:
Documentation:
Description:
Create Date: 4 Dec 2006
Modified Date: 28 May 2014
Authors: mcdurdin
Related Files:
Dependencies:
Related Files:
Dependencies:
Bugs:
Todo:
Notes:
Bugs:
Todo:
Notes:
History: 04 Dec 2006 - mcdurdin - Add osVista
04 May 2012 - mcdurdin - I3306 - V9.0 - Remove TntControls + Win9x support
24 Oct 2012 - mcdurdin - I3487 - V9.0 - Add detection of Win8
@ -27,7 +27,7 @@ uses
Winapi.Windows;
type
TOS = (osLegacy, osVista, osWin7, osWin8, osWin10, osOther); // I3669 // I4222
TOS = (osLegacy, osWin10, osOther); // I3669 // I4222
function GetOs: TOS;
@ -46,12 +46,7 @@ begin
begin
case osv.dwMajorVersion of
5: Result := osLegacy;
6: case osv.dwMinorVersion of
0: Result := osVista;
1: Result := osWin7;
2: Result := osWin8; // I3487
else Result := osWin8; // I3669
end;
6: Result := osLegacy;
10: Result := osWin10;
else
Result := osWin10;

View file

@ -35,12 +35,6 @@ var
t: TBCP47Tag;
LangTag: TLangTag;
begin
// We do not try and canonicalize language tags on Windows 7, because it does
// not follow the same patterns as for later versions of Windows. For example,
// zh-CN is not canonicalized to zh-Hans-CN
if GetOs = osWin7 then
Exit(Tag);
if Tag = '' then
Exit('');

View file

@ -127,9 +127,6 @@ const
SRegValue_ProxyLogin = 'proxy login';
SRegValue_AutoOpenOSK = 'auto open osk'; // CU, default true
SRegValue_AutoSwitchOSKPages = 'auto switch osk pages'; // CU, default true
SRegValue_SwitchLanguageWithKeyboard = 'switch language with keyboard'; // CU, default true
SRegValue_SwitchLanguageForAllApplications = 'switch language for all applications'; // CU, default true
SRegValue_DeepTSFIntegration = 'deep tsf integration'; // LM, 0=disable, 1=enable, 2=default
SRegKey_AppIntegration = SRegKey_KeymanEngine_LM + '\App Integration'; //KM

View file

@ -56,7 +56,7 @@ const
//#define CODE_EXTENDEDEND = $0B deprecated
CODE_SWITCH = $0C;
CODE_KEY = $0D;
CODE_CLEARCONTEXT = $0E;
CODE_CLEARCONTEXT = $0E; // deprecated in 19.0
CODE_CALL = $0F;
// UC_SENTINEL_EXTENDEDEND 0x10
CODE_CONTEXTEX = $11;
@ -99,9 +99,10 @@ const
VERSION_150 = $00000F00;
VERSION_160 = $00001000;
VERSION_170 = $00001100;
VERSION_190 = $00001300;
VERSION_MIN = VERSION_50;
VERSION_MAX = VERSION_170;
VERSION_MAX = VERSION_190;
VERSION_MASK_MINOR = $00FF;
VERSION_MASK_MAJOR = $FF00;

View file

@ -163,7 +163,7 @@ begin
CODE_CONTEXTEX: Inc(Result, 2);
CODE_NOTANY: Inc(Result, 2);
CODE_CLEARCONTEXT: Inc(Result, 2); // I3442
CODE_CLEARCONTEXT: Inc(Result, 2); // I3442 // deprecated in 19.0
CODE_IFOPT: Inc(Result, 4); // I3442
CODE_IFSYSTEMSTORE: Inc(Result, 4); // I3442
CODE_SETOPT: Inc(Result, 3); // I3442

View file

@ -35,7 +35,7 @@ const int km::core::kmx::CODE__SIZE[] = {
-1, // CODE_EXTENDEDEND 0x0B (unused)
1, // CODE_SWITCH 0x0C
-1, // CODE_KEY 0x0D (never used)
0, // CODE_CLEARCONTEXT 0x0E
0, // CODE_CLEARCONTEXT 0x0E (deprecated in 19.0)
1, // CODE_CALL 0x0F
-1, // UC_SENTINEL_EXTENDEDEND 0x10 (not valid with UC_SENTINEL)
1, // CODE_CONTEXTEX 0x11

View file

@ -495,6 +495,7 @@ int KMX_ProcessEvent::PostString(PKMX_WCHAR str, LPKEYBOARD lpkb, PKMX_WCHAR end
FoundUse = TRUE;
break;
case CODE_CLEARCONTEXT:
// statement deprecated in 19.0
// no longer supported, no-op
break;
case CODE_INDEX:

View file

@ -71,7 +71,7 @@ working on ‘layr’, using ‘disp’ as a model from https://github.com/keyma
- update basic.xml and basic.txt
- Tweak `eveloper/src/kmc-ldml/test/fixtures/basic.xml` as needed
- You can use `developer/src/kmc-ldml/build.sh build-fixtures` which will generate these. The two .kmx files are supposed to match: if not, fix `basic.txt` or fix other bugs.
- You can use `developer/src/kmc-ldml/build.sh fixtures-build` which will generate these. The two .kmx files are supposed to match: if not, fix `basic.txt` or fix other bugs.
- `developer/src/kmc-ldml/build/test/fixtures/basic-txt.kmx` - KMX generated from basic.txt.
- `developer/src/kmc-ldml/build/test/fixtures/basic-xml.kmx` - KMX generated from basic.xml.
- `developer/src/kmc-ldml/build/test/fixtures/basic-xml.kvk` - KVK generated from basic.xml.

View file

@ -14,7 +14,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="a" /> <!-- number row -->
</layer>
</layers>

View file

@ -15,7 +15,7 @@ Keys that don't show up in a row don't generate an output
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="1" />
<row keys="q" />
</layer>

View file

@ -17,7 +17,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="hmaqtugha that" /> <!-- number row -->
</layer>
</layers>

View file

@ -16,7 +16,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="hmaqtugha that screamcat" /> <!-- number row -->
</layer>
</layers>

View file

@ -18,14 +18,14 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-ke
</keys>
<layers formId="us">
<layer modifiers="none" id="base">
<layer modifiers="none">
<row keys="hat 1 2 3 4 5 6 7 8 9 0 hyphen equal" />
<row keys="q w e r t y u i o p" />
<row keys="a s d f g h j k l" />
<row keys="z x c v b n m" />
<row keys="space" />
</layer>
<layer modifiers="shift" id="shift">
<layer modifiers="shift">
<row keys="untransformed-hat 1 2 3 4 5 6 7 8 9 0 hyphen equal" />
<row keys="Q W E R T Y U I O P" />
<row keys="A S D F G H J K L" />

View file

@ -18,16 +18,16 @@
</keys>
<layers formId="us">
<layer id="base" modifiers="none">
<layer modifiers="none">
<row keys="hmaqtugha that" /> <!-- number row -->
</layer>
<layer id="shift" modifiers="shift">
<layer modifiers="shift">
<row keys="seven eee" /> <!-- number row -->
</layer>
<layer id="control" modifiers="ctrl">
<layer modifiers="ctrl">
<row keys="that gap" /> <!-- number row -->
</layer>
<layer id="catchall" modifiers="other">
<layer modifiers="other">
<row keys="eee gap" /> <!-- number row -->
</layer>
</layers>

View file

@ -12,7 +12,7 @@
<keys/> <!-- implied only -->
<layers formId="us">
<layer id="base" modifiers="none">
<layer modifiers="none">
<row keys="a" /> <!-- K_BKQUOTE -->
</layer>
<layer modifiers="shift">

View file

@ -11,14 +11,14 @@
</keys>
<layers formId="us">
<layer modifiers="none" id="base">
<layer modifiers="none">
<row keys="hat 1 2 3 4 5 6 7 8 9 0 hyphen equal" />
<row keys="q w e r t y u i o p" />
<row keys="a s d f g h j k l" />
<row keys="z x c v b n m" />
<row keys="space" />
</layer>
<layer modifiers="shift" id="shift">
<layer modifiers="shift">
<row keys="untransformed-hat 1 2 3 4 5 6 7 8 9 0 hyphen equal" />
<row keys="Q W E R T Y U I O P" />
<row keys="A S D F G H J K L" />

View file

@ -18,14 +18,14 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-ke
</keys>
<layers formId="us">
<layer modifiers="none" id="base">
<layer modifiers="none">
<row keys="grave 1 2 3 4 5 6 7 8 9 0" />
<row keys="q w e r t y u i o p" />
<row keys="a s d f g h j k l" />
<row keys="z x c v b n m" />
<row keys="space" />
</layer>
<layer modifiers="shift" id="shift">
<layer modifiers="shift">
<row keys="grave 1 2 3 4 5 6 7 8 9 0" />
<row keys="Q W E R T Y U I O P" />
<row keys="A S D F G H J K L" />

View file

@ -21,14 +21,14 @@ https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-keyboar
</keys>
<layers formId="us">
<layer modifiers="none" id="base">
<layer modifiers="none">
<row keys="u-0320 1 2 3 4 5 6 7 8 9 0" />
<row keys="q w e r t y u i o p" />
<row keys="a s d f g h j k l" />
<row keys="z x c v b n m" />
<row keys="space" />
</layer>
<layer modifiers="shift" id="shift">
<layer modifiers="shift">
<row keys="u-0300 u-00e8 nfd nfc not-nfd stampy lgtm u-0344" />
</layer>
</layers>

View file

@ -21,14 +21,14 @@ like k_008 but in NFC (and other normalizations)
</keys>
<layers formId="us">
<layer modifiers="none" id="base">
<layer modifiers="none">
<row keys="u-0320 1 2 3 4 5 6 7 8 9 0" />
<row keys="q w e r t y u i o p" />
<row keys="a s d f g h j k l" />
<row keys="z x c v b n m" />
<row keys="space" />
</layer>
<layer modifiers="shift" id="shift">
<layer modifiers="shift">
<row keys="u-0300 u-00e8 nfd nfc not-nfd stampy lgtm u-0344" />
</layer>
</layers>

View file

@ -14,13 +14,13 @@ will match the default layer
<keys/>
<layers formId="us">
<layer id="base" modifiers="none">
<layer modifiers="none">
<row keys="b" /> <!-- number row -->
</layer>
<layer id="shift" modifiers="shift">
<layer modifiers="shift">
<row keys="s" /> <!-- number row -->
</layer>
<layer id="other" modifiers="other">
<layer modifiers="other">
<row keys="d" /> <!-- number row -->
</layer>
</layers>

View file

@ -14,7 +14,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="a gap"/>
</layer>
</layers>

View file

@ -17,7 +17,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="a" /> <!-- number row -->
</layer>
</layers>

View file

@ -33,7 +33,7 @@ Comment: enter=not mappable, causes ctx reset.
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="a gap" /> <!-- number row -->
<row keys="q gap" /> <!-- q w ... -->
<row keys="gap" />

View file

@ -21,7 +21,7 @@
</keys>
<layers formId="us">
<layer modifiers="none" id="base">
<layer modifiers="none">
<row keys="roast" />
<row keys="gap wa gap gap t2 gap gap gap o gap" />
<row keys="gap sakot gap gap gap gap gap kha gap" />

View file

@ -20,7 +20,7 @@
</keys>
<layers formId="us">
<layer modifiers="none" id="base">
<layer modifiers="none">
<row keys="gap overbar underbar circumflex" />
<row keys="gap gap e gap gap y u i o gap" />
<row keys="a" />

View file

@ -21,7 +21,7 @@
</keys>
<layers formId="us">
<layer modifiers="none" id="base">
<layer modifiers="none">
<row keys="grave acute caret hacek squiggle" />
<row keys="q w e" /> <!-- etc -->
<row keys="a s d" /> <!-- etc -->

View file

@ -18,7 +18,7 @@
</keys>
<layers formId="us">
<layer modifiers="none" id="base">
<layer modifiers="none">
<row keys="B 1 2 3" />
<row keys="q w e" /> <!-- etc -->
<row keys="a s d" /> <!-- etc -->

View file

@ -8,7 +8,7 @@
</keys>
<layers formId="us">
<layer modifiers="none" id="base">
<layer modifiers="none">
<row keys="caret umlaut" />
<row keys="q w e r t y" />
<row keys="a" />

View file

@ -185,9 +185,24 @@ The following parameters are available:
`-o <filename>`, `--out-file <filename>`
: Overrides the default path and filename for the output file(s). Note that
some compilers emit multiple files, in which case, the output filenames
will vary by file extension.
: Overrides the default path and filename for the output file(s). Note that some
compilers emit multiple files, in which case, the output filenames will vary
by file extension.
If the output filename ends in a forward slash (`/`) or backslash (`\\`), or
if multiple input files are specified, or if the output filename exists and is
already a folder, then the filename will be treated as a folder, and all
output files will be written within that.
An error will be raised if an output file is specified and already exists, and
is a regular file, but is expected to be a folder.
`--continue-on-error`
: When building multiple input files, continue building subsequent files even if
a file fails to build. Note that `kmc` will still abort if an internal error
is encountered.
## `kmc build file` additional options

View file

@ -45,17 +45,6 @@ type
function CanClearSelection: Boolean;
end;
IKMDPrintActions = interface
['{C6F05AD6-42A3-404F-944C-904393A51837}']
//function PageSetup: Boolean;
function PrintFile: Boolean;
end;
IKMDPrintPreviewActions = interface(IKMDPrintActions)
['{B5879BC4-7A0C-4E00-AAE3-E2D48489152A}']
function PrintPreview: Boolean;
end;
IKMDViewExpandEditorActions = interface
['{680A172E-A221-4CDA-B68C-1E2AF8487510}']
procedure ExpandContractEditor;

View file

@ -285,6 +285,8 @@ namespace KmnCompilerMessages {
ERROR_NameMustNotContainParentheses = SevError | 0x0B9,
ERROR_NameMustNotContainSquareBrackets = SevError | 0x0BA,
WARN_DeprecatedStatement = SevWarn | 0x0BB,
FATAL_BufferOverflow = SevFatal | 0x0C0
// FATAL_Break = SevFatal | 0x0C1, unused
};

View file

@ -59,7 +59,7 @@ export enum CompilerErrorMask {
Severity = 0x00F00000, // includes reserved bits, 16 possible severity levels
Error = 0x000FFFFF, // error | namespace
Namespace = 0x000FF000, // 256 possible namespaces
BaseError = 0x00000FFF, // error code, 2,048 possible error codes per namespace
BaseError = 0x00000FFF, // error code, 4,096 possible error codes per namespace
Reserved = 0xFF000000, // do not use these error values at this time
};

View file

@ -192,7 +192,7 @@ export interface KL_Key {
* Ether the action attribute or the output attribute must be specified for a key, but not both.
* kmc-convert does not use the concept of 'anonymous actions' at present.
*/
action?: string; //TODO-KMC-CONVERT: Support <action> sub-element 'anonymous actions' in the future
action?: string;
/**
* The output attribute specifies the character that is produced when a key is pressed.
* Ether the action attribute or the output attribute must be specified for a key, but not both.

View file

@ -21,7 +21,7 @@
<key id="that" output="&#xFFFF;" /> <!-- illegal NCR (single char)-->
</keys>
<layers formId="us" minDeviceWidth="123">
<layers formId="touch" minDeviceWidth="123">
<layer id="&#xFFFE;">
<row keys="hmaqtugha that" />
</layer>

View file

@ -41,7 +41,7 @@
]
},
"layers": {
"formId": "us",
"formId": "touch",
"minDeviceWidth": "123",
"layer": {
"id": "￾",

View file

@ -38,9 +38,6 @@
"@keymanapp/resources-gosh": "*",
"@types/node": "^20.4.1",
"@types/semver": "^7.3.12",
"ajv": "^8.12.0",
"ajv-cli": "^5.0.0",
"ajv-formats": "^2.1.1",
"c8": "^7.12.0",
"chalk": "^2.4.2",
"typescript": "^5.4.5"

File diff suppressed because it is too large Load diff

View file

@ -248,7 +248,7 @@ export function incxstr(p: string, x: number): number {
case KMX.KMXFile.CODE_CONTEXTEX: x += 2; break;
case KMX.KMXFile.CODE_NOTANY: x += 2; break;
case KMX.KMXFile.CODE_CLEARCONTEXT: x += 2; break;
case KMX.KMXFile.CODE_CLEARCONTEXT: x += 2; break; // deprecated in 19.0
case KMX.KMXFile.CODE_IFOPT: x += 4; break;
case KMX.KMXFile.CODE_IFSYSTEMSTORE: x += 4; break;
case KMX.KMXFile.CODE_SETOPT: x += 3; break;

View file

@ -1,10 +1,15 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import fs from 'node:fs';
import 'mocha';
import { assert } from 'chai';
import { KmnCompiler } from '../src/main.js';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import { KmxFileReader } from '@keymanapp/common-types';
import { compileTestKeyboard } from './helpers/index.js';
import { KmnCompiler } from '../src/main.js';
const __dirname = dirname(fileURLToPath(import.meta.url)).replace(/\\/g, '/');
const keyboardsDir = __dirname + '/../../../../../common/test/keyboards/';
@ -13,6 +18,16 @@ const baselineDir = keyboardsDir + 'baseline/';
describe('Compiler class', function() {
const callbacks = new TestCompilerCallbacks(this);
this.beforeEach(function() {
callbacks.clear();
});
this.afterEach(function() {
if(this.currentTest?.isFailed()) {
callbacks.printMessages();
}
});
it('should throw on failure', async function() {
const compiler = new KmnCompiler();
const callbacks : any = null; // ERROR
@ -129,4 +144,20 @@ describe('Compiler class', function() {
assert.deepEqual(kvkData, kvkFixtureData);
});
it('should trim all whitespace for `&targets` store', async function() {
const result = await compileTestKeyboard(callbacks, ['keyboards', 'targets-with-whitespace.kmn']);
assert.isNotNull(result);
// Verify implictly that `&targets` store was interpreted correctly as 'any'
// because the compiler generated both JS and KMX targets (#13721)
assert.isNotNull(result.artifacts.js);
assert.isNotNull(result.artifacts.kmx);
// Then verify directly that the `&targets` store was trimmed by looking at
// the final value in the kmx data
const reader = new KmxFileReader();
const keyboard = reader.read(result.artifacts.kmx.data);
assert.equal(keyboard.targets, 'any');
});
});

View file

@ -0,0 +1,11 @@
store(&NAME) 'targets_with_whitespace'
store(&VERSION) '10.0'
c this used to generate KM0207B: At least one compile target must be specified
store(&TARGETS) ' any'
begin unicode > use(main)
group(main) using keys
+ 'x' > 'y'

View file

@ -0,0 +1,9 @@
store(&NAME) 'warn_deprecated_statement-clearcontext-17'
store(&VERSION) '17.0'
begin unicode > use(main)
group(main) using keys
c NOTE: using `clearcontext` should not generate a warning in v17 target version compile (unlike v19 target version)
+ 'x' > clearcontext

View file

@ -0,0 +1,9 @@
store(&NAME) 'warn_deprecated_statement-clearcontext-19'
store(&VERSION) '19.0'
begin unicode > use(main)
group(main) using keys
c NOTE: using `clearcontext` SHOULD generate a warning in v19 target version compile (unlike v17 target version)
+ 'x' > clearcontext

View file

@ -0,0 +1,9 @@
store(&NAME) 'warn_deprecated_statement-fix-17'
store(&VERSION) '17.0'
begin unicode > use(main)
group(main) using keys
c NOTE: using `fix` should not generate a warning in v17 target version compile (unlike v19 target version)
+ 'x' > fix

View file

@ -0,0 +1,9 @@
store(&NAME) 'warn_deprecated_statement-fix-19'
store(&VERSION) '19.0'
begin unicode > use(main)
group(main) using keys
c NOTE: using `fix` SHOULD generate a warning in v19 target version compile (unlike v17 target version)
+ 'x' > fix

View file

@ -1,8 +1,13 @@
/**
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Helpers and utilities for the Mocha tests.
*/
import * as path from 'path';
import { fileURLToPath } from 'url';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { assert } from 'chai';
import { CompilerCallbacks } from '@keymanapp/developer-utils';
import { KmnCompiler } from '../../src/compiler/compiler.js';
/**
* Builds a path to the fixture with the given path components.
@ -15,3 +20,13 @@ import { fileURLToPath } from 'url';
export function makePathToFixture(...components: string[]): string {
return fileURLToPath(new URL(path.join('..', '..', '..', 'test', 'fixtures', ...components), import.meta.url));
}
export async function compileTestKeyboard(callbacks: CompilerCallbacks, fixture: string[]) {
const compiler = new KmnCompiler();
assert(await compiler.init(callbacks, {saveDebug: true, shouldAddCompilerVersion: false}));
assert(compiler.verifyInitialized());
const kmnPath = makePathToFixture(...fixture);
return await compiler.run(kmnPath, null);
}

View file

@ -3,9 +3,24 @@ import { assert } from 'chai';
import { CompilerErrorMask, CompilerErrorNamespace } from '@keymanapp/developer-utils';
import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
import { KmnCompiler } from '../src/main.js';
import { KmnCompilerMessageRanges, KmnCompilerMessages } from '../src/compiler/kmn-compiler-messages.js';
import { KmcKmnCompilerEvent, KmnCompilerMessageRanges, KmnCompilerMessages } from '../src/compiler/kmn-compiler-messages.js';
import { makePathToFixture } from './helpers/index.js';
// Source - https://stackoverflow.com/a/9924463
// Posted by Jack Allan, modified by community. See post 'Timeline' for change history
// Retrieved 2026-09-01, License - CC BY-SA 3.0
function getParamNames(func: any) {
const STRIP_COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg;
const ARGUMENT_NAMES = /([^\s,]+)/g;
const fnStr = func.toString().replace(STRIP_COMMENTS, '');
let result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
if(result === null)
result = [];
return result;
}
describe('KmnCompilerMessages', function () {
const callbacks = new TestCompilerCallbacks(this);
@ -13,6 +28,53 @@ describe('KmnCompilerMessages', function () {
return verifyCompilerMessagesObject(KmnCompilerMessages, CompilerErrorNamespace.KmnCompiler);
});
it('should have correct parameter types and numeric ranges for kmcmplib vs kmw vs kmc-kmn messages', function() {
// See comment in kmn-compiler-messages.ts for logic.
// This function follows the same pattern as `verifyCompilerMessagesObject` but with specific tests for
// this more constrained object
const toTitleCase = (s: string) => s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
const keys = Object.keys(KmnCompilerMessages);
const m = KmnCompilerMessages as Record<string,any>;
for(const key of keys) {
if(typeof m[key] == 'number') {
const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key);
const f = toTitleCase(o[1]) + '_' + o[2];
// note: validation done in verifyCompilerMessagesObject, don't repeat it here
const v: KmcKmnCompilerEvent = m[f]('','','','','','','','','','','','' /* ignore arguments*/);
const code = m[key] & CompilerErrorMask.BaseError;
const hex = code.toString(16).padStart(3, '0');
if(code >= KmnCompilerMessageRanges.RANGE_CompilerMessage_Min &&code <= KmnCompilerMessageRanges.RANGE_CompilerMessage_Max) {
// kmc-kmn messages, should not be using mw() function or mc() function
//
assert.equal(v.kmcKmnSource, 'kmc-kmn', `expected ${v.kmcKmnSource} to equal 'kmc-kmn' for ${hex}, '${key}'.`);
} else if(code >= KmnCompilerMessageRanges.RANGE_KMN_COMPILER_MIN && code <= KmnCompilerMessageRanges.RANGE_KMN_COMPILER_MAX) {
// kmcmplib or kmw compiler messages, should not be using m() function or mx() function
assert.oneOf(v.kmcKmnSource, ['kmcmplib', 'kmw-compiler'], `expected ${v.kmcKmnSource} to equal 'kmcmplib' or 'kmw-compiler for ${hex}, '${key}'.`);
if(v.kmcKmnSource == 'kmcmplib') {
// The only allowed param is an object o with property p. We
// cannot test parameter shape directly, so we will compare
// outputs of calling if o.p is set, which should not match what
// we get when o.p is not set.
const params = getParamNames(m[f]);
if(params.length) {
const v2: KmcKmnCompilerEvent = m[f]({p:['1','2','3','4','5','6','7','8','9']});
assert.notEqual(v.message, v2.message, `Message '${key}' (${hex}) must use KmcmpLibMessageParameters pattern`);
}
}
} else if(code >= KmnCompilerMessageRanges.RANGE_LEXICAL_MODEL_MIN && code <= KmnCompilerMessageRanges.RANGE_LEXICAL_MODEL_MAX) {
assert.fail(`Message identifier ${hex} for '${key}' is reserved and should not be used (LEXICAL_MODEL range)`);
} else {
assert.fail(`Message identifier ${hex} for '${key}' is out of range`);
}
}
}
});
it('should have a 1:1 correspondence with kmn_compiler_errors.h', function() {
const headerFilename = 'kmn_compiler_errors.h';
@ -285,4 +347,13 @@ describe('KmnCompilerMessages', function () {
await testForMessage(this, ['invalid-keyboards', 'error_name_must_not_contain_square_brackets-deadkey.kmn'], [KmnCompilerMessages.ERROR_NameMustNotContainSquareBrackets, KmnCompilerMessages.ERROR_InvalidDeadkey]);
});
// WARN_DeprecatedStatement
it('should generate WARN_DeprecatedStatement if the file has `clearcontext` or `fix` statements and is 19.0', async function() {
await testForMessage(this, ['keyboards', 'warn_deprecated_statement-clearcontext-19.kmn'], KmnCompilerMessages.WARN_DeprecatedStatement);
await testForMessage(this, ['keyboards', 'warn_deprecated_statement-fix-19.kmn'], KmnCompilerMessages.WARN_DeprecatedStatement);
await testForMessage(this, ['keyboards', 'warn_deprecated_statement-clearcontext-17.kmn']);
await testForMessage(this, ['keyboards', 'warn_deprecated_statement-fix-17.kmn']);
});
});

View file

@ -23,7 +23,7 @@ builder_describe "Keyman kmc Keyboard Compiler module" \
"api analyze API and prepare API documentation" \
"clean" \
"test" \
"build-fixtures builds test fixtures for manual examination"
"fixtures-build builds test fixtures for manual examination"
builder_describe_outputs \
configure /developer/src/kmc-ldml/src/util/abnf/46/transform-from-required.js \
@ -84,6 +84,6 @@ function do_build_fixtures() {
builder_run_action clean do_clean
builder_run_action configure do_configure
builder_run_action build do_build
builder_run_action build-fixtures do_build_fixtures
builder_run_action fixtures-build do_build_fixtures
builder_run_action api typescript_run_api_extractor developer/src/kmc-ldml main.d.ts
builder_run_action test typescript_run_eslint_mocha_tests 90
builder_run_action test typescript_run_eslint_mocha_tests

View file

@ -9,7 +9,7 @@ import Keys = KMXPlus.Keys;
import KeysKeys = KMXPlus.KeysKeys;
import ListItem = KMXPlus.ListItem;
import KeysFlicks = KMXPlus.KeysFlicks;
import { allUsedKeyIdsInFlick, allUsedKeyIdsInKey, allUsedKeyIdsInLayers, calculateUniqueKeys, hashFlicks, hashKeys, translateLayerAttrToModifier, validModifier } from '../util/util.js';
import { allUsedKeyIdsInFlick, allUsedKeyIdsInKey, allUsedKeyIdsInLayers, calculateUniqueKeys, hashFlicks, hashKeys, translateLayerAttrToModifier } from '../util/util.js';
import { SubstitutionUse, Substitutions } from './substitution-tracker.js';
/** reserved name for the special gap key. space is not allowed in key ids. */
@ -225,13 +225,9 @@ export class KeysCompiler extends SectionCompiler {
// Finally, kmap
// Use LayerMap + keys to generate compiled keys for hardware
const hardwareLayers = this.hardwareLayers();
/* c8 ignore next 3 */
if (hardwareLayers.length > 1) {
// validation should have already caught this
throw Error(
`Internal error: Expected 0 or 1 hardware layer, not ${hardwareLayers.length}`
);
} else if (hardwareLayers.length === 1) {
if (hardwareLayers.length >= 1) {
// Only 1 hardware layer is supported; however, `LayrCompiler` will report
// on this error, so we can just process the first one here
const theLayers = hardwareLayers[0];
const { formId } = theLayers;
for (const layer of theLayers.layer) {
@ -461,12 +457,6 @@ export class KeysCompiler extends SectionCompiler {
let valid = true;
const { modifiers } = layer;
if (!validModifier(modifiers)) {
this.callbacks.reportMessage(
LdmlCompilerMessages.Error_InvalidModifier({ modifiers }, layer)
);
valid = false;
}
if (layer.row.length > keymap.length) {
this.callbacks.reportMessage(
@ -484,7 +474,7 @@ export class KeysCompiler extends SectionCompiler {
LdmlCompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({
row: y + 1,
hardware: layers.formId,
modifiers: modifiers || 'none',
modifiers,
}, row)
);
valid = false;
@ -501,7 +491,7 @@ export class KeysCompiler extends SectionCompiler {
keyId: key,
col: x + 1,
row: y + 1,
layer: layer.id,
layer: layer.id ?? layer.modifiers, // just to give a useful reference point in the error message
form: "hardware",
}, row)
);

View file

@ -20,18 +20,26 @@ export class LayrCompiler extends SectionCompiler {
let valid = true;
let totalLayerCount = 0;
let hardwareLayers = 0;
let touchLayers = 0;
let touchLayerCount = 0;
let hasNullDeviceWidth = false;
const deviceWidths = new Set<number>();
this.keyboard3.layers?.forEach((layers) => {
const { formId } = layers;
if (formId === 'touch') {
touchLayers++;
touchLayerCount++;
totalLayerCount += layers.layer?.length;
const { minDeviceWidth } = layers;
if (!minDeviceWidth ||
if (minDeviceWidth === undefined || minDeviceWidth === null) {
if(hasNullDeviceWidth) {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_MultipleTouchFormsWithoutMinDeviceWidth(layers));
}
hasNullDeviceWidth = true;
} else if (
(typeof minDeviceWidth === 'string' && (<string>minDeviceWidth).trim() === '') ||
Number.isNaN(Number(minDeviceWidth)) ||
minDeviceWidth < constants.layr_min_minDeviceWidth ||
minDeviceWidth > constants.layr_max_minDeviceWidth ||
Number.isNaN(Number(minDeviceWidth))) {
minDeviceWidth > constants.layr_max_minDeviceWidth
) {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidLayerWidth({minDeviceWidth}, layers));
} else if (deviceWidths.has(minDeviceWidth)) {
@ -40,6 +48,17 @@ export class LayrCompiler extends SectionCompiler {
} else {
deviceWidths.add(minDeviceWidth);
}
// For touch layers, id attr must exist, and modifiers attribute should not
layers.layer.forEach(layer => {
if(typeof layer.id === 'undefined') {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_TouchLayerRequiresId({minDeviceWidth}, layer));
valid = false;
}
if(typeof layer.modifiers !== 'undefined') {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_TouchLayerHasModifiers({minDeviceWidth, id: layer.id}, layer));
}
totalLayerCount++;
});
} else {
// hardware
hardwareLayers++;
@ -47,15 +66,22 @@ export class LayrCompiler extends SectionCompiler {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_ExcessHardware({formId}, layers));
}
layers.layer.forEach(layer => {
const { modifiers } = layer;
if(typeof modifiers === 'undefined' || modifiers == '') {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_HardwareLayerRequiresModifiers({ formId }, layer));
valid = false;
}
else if (!validModifier(modifiers)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidModifier({ modifiers }, layer));
valid = false;
}
if(typeof layer.id !== 'undefined') {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_HardwareLayerHasId({formId, id: layer.id}, layer));
}
totalLayerCount++;
});
}
layers.layer.forEach((layer) => {
const { modifiers } = layer;
totalLayerCount++;
if (!validModifier(modifiers)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidModifier({ modifiers }, layer));
valid = false;
}
});
});
if (totalLayerCount === 0) { // TODO-LDML: does not validate touch layers yet
// no layers seen anywhere

View file

@ -40,7 +40,7 @@ export class LdmlCompilerMessages {
static ERROR_KeyNotFoundInKeyBag = SevError | 0x0005;
static Error_KeyNotFoundInKeyBag = (o: { keyId: string, col: number, row: number, layer: string, form: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_KeyNotFoundInKeyBag, compileContext,
`Key '${def(o.keyId)}' in position #${def(o.col)} on row #${def(o.row)} of layer ${def(o.layer)}, form '${def(o.form)}' not found in key bag`,
`Key '${def(o.keyId)}' in position #${def(o.col)} on row #${def(o.row)} of layer '${def(o.layer)}', form '${def(o.form)}' not found in key bag`,
);
static HINT_OneOrMoreRepeatedLocales = SevHint | 0x0006;
@ -277,7 +277,7 @@ export class LdmlCompilerMessages {
);
static ERROR_InvalidLayerWidth = SevError | 0x002D;
static Error_InvalidLayerWidth = (o: { minDeviceWidth: number }, compileContext?: ObjectWithCompileContext) => mx(
static Error_InvalidLayerWidth = (o: { minDeviceWidth: number | string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_InvalidLayerWidth, compileContext,
`Invalid Layers minDeviceWidth=${def(o.minDeviceWidth)}`,
`Width must be between 1-999 (millimeters), inclusive.` // sync with layr_max_minDeviceWidth / layr_max_maxDeviceWidth (from spec)
@ -292,6 +292,50 @@ export class LdmlCompilerMessages {
`**Hint**: Use "${def(o.recommended)}"`,
);
static ERROR_TouchLayerRequiresId = SevError | 0x0031;
static Error_TouchLayerRequiresId = (o: { minDeviceWidth: number }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_TouchLayerRequiresId, compileContext,
`Layer for touch form with minDeviceWidth=${def(o.minDeviceWidth)} requires an "id" attribute`, `
Touch layers must have an \`id\` attribute, but should not have a \`modifiers\`
attribute, and conversely, hardware layers must have a \`modifiers\` attribute
and should not have an \`id\` attribute.
`);
static HINT_TouchLayerHasModifiers = SevHint | 0x0032;
static Hint_TouchLayerHasModifiers = (o: { minDeviceWidth: number, id: string }, compileContext?: ObjectWithCompileContext) => mx(
this.HINT_TouchLayerHasModifiers, compileContext,
`Touch layer with id "${def(o.id)}" for touch form with minDeviceWidth=${def(o.minDeviceWidth)} should not have a "modifiers" attribute`, `
Touch layers must have an \`id\` attribute, but should not have a \`modifiers\`
attribute, and conversely, hardware layers must have a \`modifiers\` attribute
and should not have an \`id\` attribute.
`);
static HINT_HardwareLayerHasId = SevHint | 0x0033;
static Hint_HardwareLayerHasId = (o: { formId: string, id: string }, compileContext?: ObjectWithCompileContext) => mx(
this.HINT_HardwareLayerHasId, compileContext,
`Layer for hardware form "${def(o.formId)}" should not have an "id" attribute (currently "${def(o.id)}")`, `
Touch layers must have an \`id\` attribute, but should not have a \`modifiers\`
attribute, and conversely, hardware layers must have a \`modifiers\` attribute
and should not have an \`id\` attribute.
`);
static ERROR_HardwareLayerRequiresModifiers = SevError | 0x0034;
static Error_HardwareLayerRequiresModifiers = (o: { formId: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_HardwareLayerRequiresModifiers, compileContext,
`Layers for hardware form "${def(o.formId)}" require a "modifiers" attribute`, `
Touch layers must have an \`id\` attribute, but should not have a \`modifiers\`
attribute, and conversely, hardware layers must have a \`modifiers\` attribute
and should not have an \`id\` attribute.
`);
static HINT_MultipleTouchFormsWithoutMinDeviceWidth = SevHint | 0x0035;
static Hint_MultipleTouchFormsWithoutMinDeviceWidth = (compileContext?: ObjectWithCompileContext) => mx(
this.HINT_MultipleTouchFormsWithoutMinDeviceWidth, compileContext,
`When multiple touch forms are present, 'minDeviceWidth' is required to differentiate them`, `
Touch forms are differentiated by their minimum device width, so when there is
more than one, at most one form may omit the \`minDeviceWidth\` attribute.
`);
//
// Transform syntax errors begin at ...F00 (SevErrorTransform)

View file

@ -8,6 +8,7 @@
import { ModifierKeyConstants, KMXPlus, VisualKeyboard } from "@keymanapp/common-types";
import { CompilerCallbacks } from "@keymanapp/developer-utils";
import { LdmlCompilerMessages } from "./ldml-compiler-messages.js";
import { modifiersToString } from "../util/util.js";
// This is a partial polyfill for findLast, so not polluting Array.prototype
// https://medium.com/@stheodorejohn/findlast-method-polyfill-in-javascript-bridging-browser-gaps-c3baf6aabae1
@ -93,7 +94,7 @@ export class LdmlKeyboardVisualKeyboardCompiler {
layer: KMXPlus.LayrEntry,
hardware: string,
) {
const layerId = layer.id.value;
const layerId = layer.id.value ?? modifiersToString(layer.mod); // used only for reference in error messages
hardware = 'us'; // TODO-LDML: US Only. We need to clean this up for other hardware forms

View file

@ -183,7 +183,7 @@ export function kmxToXml(kmx: KMXPlus.KMXPlusFile): string {
return {
layers: layr.lists.map(({ hardware, minDeviceWidth, layers }) => ({
...stringToAttr('formId', hardware),
...numberToAttr('minDeviceWidth', minDeviceWidth),
...numberToAttr('minDeviceWidth', minDeviceWidth === 0 ? undefined : minDeviceWidth),
layer: layers.map(({ id, mod, rows }) => ({
...stringToAttr('id', id),
...asAttr('modifiers', modToString(mod)),

View file

@ -164,6 +164,15 @@ export function translateLayerAttrToModifier(layer: LDMLKeyboard.LKLayer) : numb
return modifiers.split(',').map(m => translateModifierSubsetToLayer(m)).sort();
}
export function modifiersToString(modifiers: number) : string {
if (!modifiers) return 'none';
const result: string[] = [];
for(const mod of constants.keys_mod_map) {
if(mod[1] != constants.keys_mod_none && modifiers & mod[1]) result.push(mod[0]);
}
return result.join(' ');
}
function translateModifierSubsetToLayer(modifiers: string) : number {
// TODO-LDML: Default #11072
if (modifiers) {
@ -186,7 +195,7 @@ function translateModifierSubsetToLayer(modifiers: string) : number {
* @returns true if valid
*/
export function validModifier(modifier?: string) : boolean {
if (!modifier) return true; // valid to have no modifier, == none
if (!modifier) return false; // hardware layer must have a modifier
// TODO-LDML: enforce illegal combinations per spec.
for (const sub of modifier.trim().split(',')) {
for (const str of sub.trim().split(' ')) {

View file

@ -11,7 +11,7 @@
<key id="hmaqtugha" output="ħ" longPressKeyIds="a e"/>
<key id="that" output="ថា"/>
</keys>
<layers formId="us" minDeviceWidth="123">
<layers formId="us">
<layer modifiers="none">
<row keys="hmaqtugha that"/>
</layer>

View file

@ -12,7 +12,7 @@
#
# cd developer/src/kmc
# ./build.sh configure build # if needed
# ./build.sh build-fixtures
# ./build.sh fixtures-build
#
# This will compile both the .xml and the .txt to build/test/fixtures and also emit the
# checksum for basic-xml.kmx so you can patch that into this file.
@ -408,7 +408,7 @@ block(layr) # struct COMP_KMXPLUS_LAYR {
index(strNull,strUs,2) # KMXPLUS_STR hardware = 'us'
00 00 00 00 # KMX_DWORD layer;
01 00 00 00 # count
7B 00 00 00 # KMX_DWORD minDeviceWidth; // 123
00 00 00 00 # KMX_DWORD minDeviceWidth; // 0
# layers 0
00 00 00 00 # KMXPLUS_STR id;
00 00 00 00 # KMX_DWORD mod

View file

@ -23,8 +23,7 @@
<key id="that" output="ថា" />
</keys>
<layers formId="us" minDeviceWidth="123">
<!-- TODO-LDML: unit test for <layers formId="us" with <layer id="base" should be illegal -->
<layers formId="us">
<layer modifiers="none">
<row keys="hmaqtugha that" />
</layer>

View file

@ -30,11 +30,11 @@
</layer>
</layers>
<layers formId="iso">
<layer id="base">
<layer modifiers="none">
<!-- beware: this is mapping ` and 1! -->
<row keys="Q W" />
</layer>
<layer id="shift">
<layer modifiers="shift">
<!-- beware: this is mapping ` and 1! -->
<row keys="q w" />
</layer>

View file

@ -8,7 +8,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="grave" />
</layer>
</layers>

View file

@ -11,11 +11,11 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<!-- beware: this is mapping ` and 1! -->
<row keys="Q W" />
</layer>
<layer id="shift">
<layer modifiers="shift">
<!-- beware: this is mapping ` and 1! -->
<row keys="q w" />
</layer>

View file

@ -11,11 +11,11 @@
</keys>
<layers formId="us">
<layer id="base" modifiers="none">
<layer modifiers="none">
<!-- beware: this is mapping ` and 1! -->
<row keys="qqq www" />
</layer>
<layer id="shift" modifiers="shift">
<layer modifiers="shift">
<!-- beware: this is mapping ` and 1! -->
<row keys="QQQ WWW" />
</layer>

View file

@ -8,7 +8,7 @@
</keys>
<layers formId="iso">
<layer id="base" modifiers="none">
<layer modifiers="none">
<row keys="grave 1 2 3 4 5 6 7 8 9 0 hyphen equal" />
<row keys="q w e r t y u i o p open-square close-square" />
<row keys="a s d f g h j k l semi-colon apos" />

View file

@ -8,7 +8,7 @@
</keys>
<layers formId="us">
<layer id="base" modifiers="none">
<layer modifiers="none">
<row keys="grave 1 2 3 4 5 6 7 8 9 0 hyphen equal" />
<row keys="q w e r t y u i o p open-square close-square backslash" />
<row keys="a s d f g h j k l semi-colon apos" />

View file

@ -8,7 +8,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="snail interrobang" />
</layer>
</layers>

View file

@ -22,7 +22,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<!-- beware: this is mapping ` and 1! -->
<row keys="grave one two three four five six seven eight nine ten eleven minus equal" />
</layer>

View file

@ -9,7 +9,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<!-- beware: this is mapping ` and 1! -->
<row keys="Q W" />
<row keys="Q W" />

View file

@ -8,7 +8,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<!-- beware: this is mapping `! -->
<row keys="Q" />
</layer>

View file

@ -12,7 +12,7 @@
</flick>
</flicks>
<layers formId="iso">
<layer id="base">
<layer modifiers="none">
<!-- beware: this is mapping ` and 1! -->
<row keys="Q W" />
</layer>

View file

@ -8,7 +8,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="foo" />
</layer>
</layers>

View file

@ -8,7 +8,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="5" />
</layer>
</layers>

View file

@ -9,7 +9,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="7" />
</layer>
</layers>

View file

@ -7,7 +7,7 @@
</keys>
<layers formId="iso">
<layer id="base" modifiers="none">
<layer modifiers="none">
<row keys="a" />
</layer>
<layer modifiers="altR, ctrl shift">

View file

@ -17,7 +17,7 @@
</flicks>
<layers formId="iso">
<layer id="base">
<layer modifiers="none">
<row keys="ww" />
</layer>
</layers>

View file

@ -38,11 +38,11 @@
</layer>
</layers>
<layers formId="iso">
<layer id="base">
<layer modifiers="none">
<!-- beware: this is mapping ` and 1! -->
<row keys="Q W" />
</layer>
<layer id="shift" modifiers="shift">
<layer modifiers="shift">
<!-- beware: this is mapping ` and 1! -->
<row keys="q w amarker" />
</layer>

View file

@ -36,11 +36,11 @@
</layer>
</layers>
<layers formId="iso">
<layer id="base">
<layer modifiers="none">
<!-- beware: this is mapping ` and 1! -->
<row keys="Q W" />
</layer>
<layer id="shift" modifiers="shift">
<layer modifiers="shift">
<!-- beware: this is mapping ` and 1! -->
<row keys="q w amarker" />
</layer>

View file

@ -9,7 +9,7 @@
</keys>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="grave mistake" />
</layer>
</layers>

View file

@ -20,7 +20,7 @@
</displays>
<layers formId="iso">
<layer id="base" modifiers="none">
<layer modifiers="none">
<row keys="mark-dotbelow mark-dotabove mark-dotabovebelow mark-dotbelowabove" />
<row keys="base rebase"/>
<row keys="space" />

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info name="layr-error-custom-form"/>
<keys>
<key id="one" output="1" />
<key id="two" output="2" />
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer modifiers="none">
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth=""> <!-- a blank value is not permitted (instead, omit the attribute entirely) -->
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
</keyboard3>

View file

@ -8,7 +8,7 @@
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer id="base">
<layer modifiers="none">
<row keys="one two three" />
</layer>
</layers>
@ -19,7 +19,7 @@
<row keys="three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="0"> <!-- dup -->
<layers formId="touch" minDeviceWidth="0"> <!-- width 0 not permitted -->
<layer id="base">
<row keys="one" />
<row keys="two" />

View file

@ -8,7 +8,7 @@
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer id="base">
<layer modifiers="none">
<row keys="one two three" />
</layer>
</layers>
@ -19,7 +19,7 @@
<row keys="three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="1024"> <!-- dup -->
<layers formId="touch" minDeviceWidth="1024"> <!-- max width 999mm -->
<layer id="base">
<row keys="one" />
<row keys="two" />

View file

@ -8,11 +8,11 @@
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer id="base">
<layer modifiers="none">
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="1500">
<layers formId="touch" minDeviceWidth="1500"> <!-- max width 999mm -->
<layer id="base">
<row keys="one" />
<row keys="two" />

View file

@ -8,11 +8,11 @@
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer id="base">
<layer modifiers="none">
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="x">
<layers formId="touch" minDeviceWidth="x"> <!-- not a number -->
<layer id="base">
<row keys="one" />
<row keys="two" />

View file

@ -16,7 +16,7 @@
</form>
</forms>
<layers formId="us">
<layer id="base">
<layer modifiers="none">
<row keys="one two three" />
</layer>
</layers>

View file

@ -16,7 +16,7 @@
</form>
</forms>
<layers formId="zzz">
<layer id="base">
<layer modifiers="none">
<row keys="one two three" />
</layer>
</layers>

View file

@ -8,7 +8,7 @@
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer id="base">
<layer modifiers="none">
<row keys="one two three" />
</layer>
</layers>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info name="error-hardware-layer-requires-modifiers"/>
<keys>
<key id="one" output="1" />
<key id="two" output="2" />
<key id="three" output="3" />
</keys>
<layers formId="us">
<layer> <!-- modifiers is missing -->
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="120">
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
</keyboard3>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info name="error-touch-layer-requires-id"/>
<keys>
<key id="one" output="1" />
<key id="two" output="2" />
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer modifiers="none">
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="120">
<layer> <!-- id is missing -->
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
</keyboard3>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info name="hint-hardware-layer-has-id"/>
<keys>
<key id="one" output="1" />
<key id="two" output="2" />
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer modifiers="none" id="base"> <!-- hardware form should not have id -->
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="120">
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
</keyboard3>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info name="layr-hint-multiple-touch-forms-without-min-device-width"/>
<keys>
<key id="one" output="1" />
<key id="two" output="2" />
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer modifiers="none">
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch">
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
<layers formId="touch"> <!-- two forms without minDeviceWidth -->
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
</keyboard3>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info name="hint-touch-layer-has-modifiers"/>
<keys>
<key id="one" output="1" />
<key id="two" output="2" />
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer modifiers="none">
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="120">
<layer id="base" modifiers="shift"> <!-- touch layer has modifiers -->
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
</keyboard3>

View file

@ -6,7 +6,7 @@
<keys />
<layers formId="us">
<layer id="base" modifiers="altR-shift"> <!-- invalid, should be "altR shift" -->
<layer modifiers="altR-shift"> <!-- invalid, should be "altR shift" -->
<row keys="a b c" />
</layer>
</layers>

Some files were not shown because too many files have changed in this diff Show more