Merge branch 'feat/developer/kmc-convert' into feat/developer/kmc-convert-useArtifacts

This commit is contained in:
SabineSIL 2025-08-19 18:10:44 +02:00 • committed by GitHub
commit 4dbdf7ec83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
250 changed files with 18378 additions and 2106 deletions

View file

@ -17,12 +17,12 @@ jobs:
if: ${{ github.repository == 'keymanapp/keyman' && github.actor == 'keyman-server' && startsWith(github.event.pull_request.title, 'auto:') }}
steps:
- name: auto approve PR from keyman-server
uses: hmarr/auto-approve-action@v3
uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: auto merge PR from keyman-server
uses: "pascalgn/automerge-action@7854d3bd607dccdaf0b2c134b699a812c8960213"
env:
GITHUB_TOKEN: "${{ secrets.AUTOINC_GITHUB_TOKEN }}"
MERGE_LABELS: ""
MERGE_LABELS: "automerge"
MERGE_FORKS: false

View file

@ -127,7 +127,7 @@ jobs:
strategy:
fail-fast: true
matrix:
dist: [jammy, noble, oracular]
dist: [jammy, noble, plucky]
steps:
- name: Checkout
@ -153,7 +153,7 @@ jobs:
strategy:
fail-fast: true
matrix:
dist: [plucky]
dist: [questing]
steps:
- name: Checkout

208
.github/workflows/pr-build-status.yml vendored Normal file
View file

@ -0,0 +1,208 @@
# GENERATED FILE - DO NOT EDIT!
#
# Keyman is copyright (C) SIL Global. MIT License.
#
# Do not modify the script in .github/workflows/pr-build-status.yml directly;
# instead work on the sources in resources/build/pr-build-status and use the
# build.sh script to rebuild the .github/workflows/pr-build-status.yml file from
# them.
#
# build.sh will append the relevant portions of
# resources/build/pr-build-status/pr-build-status.mjs to the content in
# resources/build/pr-build-status/pr-build-status.prefix.yml to form the .github
# workflow file.
#
name: Keyman Build Summary
on:
status:
push:
branches-ignore:
- master
- beta
- stable-*
workflow_dispatch:
inputs:
commit:
description: 'Commit sha'
required: true
type: string
jobs:
run_pr_build_status:
name: Summarize build status checks
runs-on: ubuntu-latest
steps:
- name: Check PR build status
id: run_pr_build_status_script
uses: actions/github-script@v7
with:
script: |
// This code is copied out of resources/build/pr-build-status/pr-build-status.mjs
// where it is tested. It is copied inline here in order to avoid requiring the
// repository to be checked out, which dramatically reduces the run time of the
// check.
//
// Note: we don't currently look at check runs, only statuses
//
// Verify the following statuses:
// 'user_testing'
// 'API Verification' (github-actions[bot])
//
// At least 1 of the following statuses must be found:
// 'Test*' (keyman-server), e.g. 'Test Build (Keyman)'
// 'Ubuntu Packaging' (github-actions[bot])
//
// Ignore the following statuses:
// check/web/file-size
//
function reduceStatuses(statuses) {
const filtered_statuses = statuses.reduce((o, status) => {
if(status.creator?.login == 'keyman-server' && status.context.startsWith('Test')) {
if(!o[status.context]) o[status.context] = {type: 'build', state: status.state};
} else if(status.creator?.login == 'keymanapp-test-bot[bot]' && status.context == 'user_testing') {
if(!o[status.context]) o[status.context] = {type: 'user-test', state: status.state};;
} else if(status.context == 'API Verification') {
if(!o[status.context]) o[status.context] = {type: 'check', state: status.state};
} else if(status.context == 'Ubuntu Packaging') {
if(!o[status.context]) o[status.context] = {type: 'build', state: status.state};
} else if(status.context == 'check/web/file-size') {
// Ignore check/web/file-size -- we won't block automerge for this at this point
} else {
// We fail with an 'unknown status' response if we get a new status check
// so we can be sure we are not skipping known status checks
o[status.context] = {type: 'unknown', state: status.state};
}
return o;
}, {});
return filtered_statuses;
}
//
// Given the collection of status checks we care about, return
// an aggregate status -- error, failed, pending, or success,
// and a summary description
//
function calculateFinalStatus(filtered_statuses) {
const counts = {};
let hasBuilds = false;
for(const context of Object.keys(filtered_statuses)) {
const { state, type } = filtered_statuses[context];
if(type == 'unknown') {
// We special-case for unknown status checks, and never permit them
return [
'error', `An unknown context ${context} was found, cannot calculate build status.`
];
}
if(type == 'build') {
hasBuilds = true;
}
counts[state] = counts[state] ? counts[state] + 1 : 1;
}
// If we do not have any statuses yet, we wait
if(Object.keys(filtered_statuses).length == 0 || !hasBuilds) {
return ['pending', 'Builds have not yet been triggered ⌛'];
}
const state =
counts.error ? 'error' :
counts.failed ? 'failed' :
counts.pending ? 'pending' :
'success';
let description = '';
function appendDescription(count, state) {
if(!count) return;
if(description != '') description += '; ';
description += `${count} check${count == 1 ? '' : 's'} ${state}`;
}
appendDescription(counts.error, 'in an error state ❌');
appendDescription(counts.failed, 'failed ❌');
appendDescription(counts.pending, 'pending ⌛');
appendDescription(counts.success, 'completed successfully ✅');
return [ state, description ];
}
async function getCommitStatuses(github, owner, repo, sha) {
const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{sha}/statuses', {
owner,
repo,
sha,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
return statuses;
}
async function getCommitCheckRuns(github, owner, repo, sha) {
const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{sha}/check-runs', {
owner,
repo,
sha,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
return statuses;
}
function calculateCheckResult(statuses) {
if(!Array.isArray(statuses)) {
return ['error', 'Failed to retrieve status checks from GitHub ❌'];
}
const filtered_statuses = reduceStatuses(statuses);
const result = calculateFinalStatus(filtered_statuses);
return result;
}
async function test(github, owner, repo, sha) {
// Get statuses from sha
const statuses = await getCommitStatuses(github, owner, repo, sha);
return calculateCheckResult(statuses);
}
async function createCheck(github, owner, repo, sha) {
const check = await github.rest.checks.create({
owner,
repo,
head_sha: sha,
name: 'Build Outcome',
status: 'in_progress',
});
return check.data.id;
}
async function updateCheck(github, owner, repo, checkRunId, status, description) {
const checkStatus = status == 'pending' ? 'in_progress' : 'completed';
const conclusion = checkStatus == 'in_progress' ? undefined : (status == 'success' ? 'success' : 'failure');
await github.rest.checks.update({
owner,
repo,
check_run_id: checkRunId,
status: checkStatus,
conclusion,
output: {
title: description,
summary: ''
}
});
}
const { owner, repo } = context.repo;
const sha =
context.payload?.check_suite?.sha || /* check run completed */
context.payload?.inputs?.commit || /* manual run */
context.payload?.after || /* push */
context.payload?.commit?.sha || /* status */
context.sha; /* probably 'master'! */
const checkRunId = await createCheck(github, owner, repo, sha);
const res = await test(github, owner, repo, sha);
await updateCheck(github, owner, repo, checkRunId, res[0], res[1]);

View file

@ -1,5 +1,194 @@
# Keyman Version History
## 19.0.94 alpha 2025-08-01
* maint(common): use `cpp_std=c++17` in meson project default_options (#14433)
## 19.0.93 alpha 2025-07-29
* test(developer): add unit tests for `isValidUtf8` (#14381)
* maint(linux): fix stopping of test server (#14403)
## 19.0.92 alpha 2025-07-28
* build(deps): bump multer from 2.0.1 to 2.0.2 (#14373)
* refactor(linux): remove shellcheck warnings (#14409)
* refactor(linux): move dbus defines to common include (#14407)
* docs(linux): update readme for test service (#14408)
* fix(windows): add check a fix for registry datatypes (#14358)
## 19.0.91 alpha 2025-07-26
* maint(windows): fix calls of renamed functions (#14413)
## 19.0.90 alpha 2025-07-25
* maint(common): fix standalone use of `resources/builder.inc.sh` (#14399)
* maint(resources): use `tc_` prefix for all `tc-*.inc.sh` functions (#14267)
## 19.0.89 alpha 2025-07-24
* docs(common): Document minimum version of Chrome 95.0 for Android (#14385)
* refactor(android): Update minimum target Chrome version to 95.0 and refactor to use constants (#14391)
## 19.0.88 alpha 2025-07-23
* maint(windows): consider `GO_FAST` env variable in `add_zip_files` (#14338)
* maint(common): add unit tests for `locate_emscripten.inc.sh` (#14346)
* refactor(android): create functions for actions (#14351)
* maint(linux): fix `locate_emscripten` if `$EMSCRIPTEN_BASE` dir missing (#14347)
* maint(android): consolidate creating zip file (#14353)
## 19.0.87 alpha 2025-07-22
* refactor(common): use test framework for zip tests (#14380)
* maint(common): fix use of `jq.inc.sh` in git hooks (#14382)
* test(common): improve zip tests (#14386)
* maint(common): fix `add_zip_files` if flags come before zip filename (#14387)
## 19.0.86 alpha 2025-07-21
* maint(common): fail build if adding relative file to zip (#14352)
* maint(common): set C++17 in standard.meson.build (#14355)
* fix(linux): add `python3-pkg-resources` as explicit dependency (#14356)
* docs(developer): add ansi and iso context and image, change img tags, and polish for readability (#14243)
* maint(mac): consolidate `write-download_info` (#14325)
* maint(common): add simple unit testing framework for bash (#14345)
* maint(common): consolidate functions to determine OS (#14324)
* fix(android): use correct path to zip.inc.sh for CI (#14377)
* test(common): add unit test for reporting test failures (#14379)
## 19.0.85 alpha 2025-07-18
* refactor(common): remove warnings (#14368)
* refactor(developer): replace deprecated std::codecvt_utf8_utf16 (#14369)
* refactor(linux): move defines to common header files (#14372)
## 19.0.84 alpha 2025-07-15
* maint(common): add support for inline file exclusions to `add_zip_files` and unit tests (#14333)
* maint(developer): use `add_zip_files` function (#14334)
## 19.0.83 alpha 2025-07-14
* maint(resources): add `build.sh` to run all `resource/build/test` tests (#14335)
* maint(developer): consolidate creating KMC_ZIP file (#14309)
## 19.0.82 alpha 2025-07-10
* maint(android): fix Android release build (#14329)
* maint(mac): fix end of blocks (#14330)
* maint(mac): don't run `build.sh publish` twice (#14331)
## 19.0.81 alpha 2025-07-09
* docs(developer): explanation of the Description field added in Details section. (#14304)
* fix(ios): restore keyboard scale map for iPad Air 2 (#14305)
* maint(linux): remove Oracular, add Questing (#14319)
* maint(android): remove `--ci` parameter from Android release builds (#14320)
* maint(android): use absolute paths (#14308)
* maint(windows): fix platform name for Windows (#14321)
* maint(developer): include missing shellHelperFunctions.sh file (#14322)
* maint(windows): replace hardcoded `symbols` paths with parameters (#14323)
## 19.0.80 alpha 2025-07-08
* maint(common): remove include of deleted `tc-download-info.inc.sh` (#14311)
* maint(resources): fix triggering of builds (#14314)
## 19.0.79 alpha 2025-07-07
* maint(android): use `--release` for release builds (#14307)
* refactor(common): some reformattings in `shellHelperFunctions.sh` (#14269)
* maint(common): consolidate `write_download_info` functions (#14270)
## 19.0.78 alpha 2025-07-04
* docs(developer): Fix links in gestures guide (#14297)
## 19.0.77 alpha 2025-07-04
* maint(mac): rename `_do_publish` function to `do_publish` to fix release build (#14273)
* maint(ios): remove extra `cd` command from ios release builds (#14272)
* maint(android): remove `--ci` parameter from android builds (#14264)
* refactor(resources): replace shebang for TC include files (#14265)
* fix(core): fix failing `common-test-core.sh` script (#14271)
* maint(common): match OSTYPE correctly on macOS (#14277)
* fix(common): fix triggering builds if no builds to start (#14274)
* maint(common): trigger common builds for resources/teamcity/common changes (#14279)
## 19.0.76 alpha 2025-07-03
* maint(common): move TC configuration for common tests (#14205)
* refactor(common): improve downloads include script (#14253)
* maint(web): move rsync code to shared functions (#14254)
* maint(developer): replace Powershell scripts (#14255)
* maint(windows): move TC configuration for Keyman Windows to scripts (#14186)
* feat(windows): human text message for 413 error (#14055)
* refactor(web): move helper functions to beginning of function (#14252)
* maint(android): add TC build scripts for Keyman for Android (#14222)
* maint(linux): use `tc_rsync_upload` and `write_download_info` functions (#14258)
* maint(ios): add TC build scripts for iOS (#14229)
* maint(mac): add TC build scripts for macOS (#14239)
## 19.0.75 alpha 2025-07-02
* feat(resources): support ignoring unknown options in builder (#14216)
* maint(common): consolidate 7Z_HOME, SEVENZ_HOME, SEVEN_Z_HOME env vars (#14162)
## 19.0.74 alpha 2025-07-01
* maint(web): adjust rsync path (#14249)
## 19.0.73 alpha 2025-07-01
* fix(web): deploy `web/build/publish` folder (#14237)
* fix(web): change env variable name (#14244)
## 19.0.72 alpha 2025-07-01
* maint(web): cleanup rsync call from bash for Windows agents and refactor (#14236)
## 19.0.71 alpha 2025-06-30
* refactor(linux): rename linux agent specific functions (#14231)
## 19.0.70 alpha 2025-06-27
* docs(developer): Add details on Template choices (#14228)
* chore(common,developer): expand 'x' to 'compileContext' (#14218)
## 19.0.69 alpha 2025-06-25
* maint(resources): support additional workflow events for pr-build-status (#14219)
* chore: add tips and caution of gestures for Keyman Developer + update license + removal of span tag (#14204)
## 19.0.68 alpha 2025-06-24
* maint(resources): Build-bot now available to control test builds (#14125)
* maint(developer): support buildLevel (#14196)
* fix(resources): allow branch names in run-required-test-builds shortcut call (#14217)
* maint(resources): add pr-build-status GitHub Action to summarize build status (#14199)
## 19.0.67 alpha 2025-06-22
* chore(developer): ldml: additional line numbers (#14104)
## 19.0.66 alpha 2025-06-16
* fix(windows): add values wucUpdateAvailable and wucNotChecked to TRemoteUpdateCheckResult enum (#14123)
## 19.0.65 alpha 2025-06-13
* maint(developer): fix developer release build (#14185)
* maint(developer,web): use double quotes inside `--rsync-path` again (#14194)
## 19.0.64 alpha 2025-06-13
* fix(windows): fix download failing when cache.json missing (#14095)
* maint(resources): only automatically merge keyman-server PRs with automerge label (#14171)
* maint(developer,web): use braces for variables in powershell scripts (#14184)
## 19.0.63 alpha 2025-06-12
* maint(web): add some more arguments for web release build script (#14167)

View file

@ -1 +1 @@
19.0.64
19.0.95

View file

@ -864,6 +864,9 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
private void displayUpdateChrome() {
// TextView's default string is to update Chrome
TextView textView = (TextView)findViewById(R.id.kmWebViewChromeTextView);
textView.setText(String.format(getString(R.string.text_require_chrome_version),
WebViewUtils.KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME));
Button button = (Button)findViewById(R.id.webViewChromeButton);
button.setText(getString(R.string.button_update_chrome));

View file

@ -15,7 +15,7 @@
android:background="@android:color/white"
android:gravity="start"
android:layout_gravity="center_vertical"
android:text="@string/text_require_chrome_57"
android:text="@string/text_require_chrome_version"
android:layout_marginStart="@dimen/button_playstore_margin"
android:textSize="@dimen/update_chrome_label_textsize" />

View file

@ -30,10 +30,13 @@
<!-- Context: Title -->
<string name="title_version" comment="Title of Keyman for Android version">Version: %1$s</string>
<!-- Context: Text label when old Chrome version installed -->
<!-- Context: Text label when old Chrome version installed. Label replaced in Keyman 19.0 -->
<string name="text_require_chrome_57" comment="Banner text when old version of Chrome is installed">
Keyman requires Chrome version 57 or newer.</string>
<string name="text_require_chrome_version" comment="Banner text when old version of Chrome is installed">
Keyman requires Chrome version %1$s or newer.</string>
<!-- Context: Button label -->
<string name="button_update_chrome" comment="Prompt to upgrade Chrome in the Play Store">Update Chrome</string>

View file

@ -495,6 +495,11 @@ public final class KMManager {
didCopyAssets = true;
}
calculateDefaultKeyboardHeights(context);
SharedPreferences prefs = context.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
KeyboardHeight_Context_Portrait_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightPortrait, KMManager.KeyboardHeight_Context_Portrait_Default);
KeyboardHeight_Context_Landscape_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightLandscape, KMManager.KeyboardHeight_Context_Landscape_Default);
if (keyboardType == KeyboardType.KEYBOARD_TYPE_UNDEFINED) {
String msg = "Cannot initialize: Invalid keyboard type";
KMLog.LogError(TAG, msg);
@ -508,12 +513,6 @@ public final class KMManager {
migrateCloudKeyboards(appContext);
CloudDownloadMgr.getInstance().initialize(appContext);
calculateDefaultKeyboardHeights(context);
SharedPreferences prefs = context.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
KeyboardHeight_Context_Portrait_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightPortrait, KMManager.KeyboardHeight_Context_Portrait_Default);
KeyboardHeight_Context_Landscape_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightLandscape, KMManager.KeyboardHeight_Context_Landscape_Default);
}
public static void executeResourceUpdate(Context aContext)

View file

@ -32,6 +32,13 @@ public final class WebViewUtils {
FULL; // WebView installed and enabled
}
// Min version of Chrome for Keyman for Android for EngineWebViewVersionStatus.DEGRADED
public static final String KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME = "37.0";
// Min version of Chrome for Keyman for Android (EngineWebViewVersionStatus.FULL)
// TODO: Keep this version in sync with resources/build/minimum-versions.inc.sh
public static final String KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME = "95.0";
private static final String CHROME_INSTALL_PATTERN_FORMATSTR = "^.*Chrome/([\\d.]+).*$";
private static final Pattern installPattern = Pattern.compile(CHROME_INSTALL_PATTERN_FORMATSTR);
@ -52,9 +59,11 @@ public final class WebViewUtils {
chromeVersion = getChromeVersion(context, webView);
}
if (FileUtils.compareVersions("37.0", chromeVersion) == FileUtils.VERSION_GREATER) {
if (FileUtils.compareVersions(KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME, chromeVersion)
== FileUtils.VERSION_GREATER) {
return EngineWebViewVersionStatus.DISABLED;
} else if (FileUtils.compareVersions("57.0", chromeVersion) == FileUtils.VERSION_GREATER) {
} else if (FileUtils.compareVersions(KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME, chromeVersion)
== FileUtils.VERSION_GREATER) {
return EngineWebViewVersionStatus.DEGRADED;
}

View file

@ -83,13 +83,13 @@
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Error en el teclado %1$s:%2$s para %3$s idioma.</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed"> Keyman requires WebView to be installed.\"</string>
<string name="body_install_webview" comment="Notification when WebView not installed"> Keyman requiere que WebView esté instalado.\"</string>
<!-- Context: Alert Dialog body: System WebView installed but not enabled -->
<string name="body_enable_webview" comment="Notification when WebView installed but not enabled"> Keyman requires WebView to be enabled.\"</string>
<string name="body_enable_webview" comment="Notification when WebView installed but not enabled"> Keyman requiere que WebView esté habilitado.\"</string>
<!-- Context Button label: Install WebView -->
<string name="label_install_webview" comment="Button label to install webview">Install WebView</string>
<string name="label_install_webview" comment="Button label to install webview">Instalar WebView</string>
<!-- Context Button label: Enable WebView -->
<string name="label_enable_webview" comment="Button label to enable webview">Enable WebView</string>
<string name="label_enable_webview" comment="Button label to enable webview">Habilitar WebView</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Comprobando si hay un diccionario asociado para descargar</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> No se puede conectar al servidor Keyman para comprobar si hay un diccionario asociado para descargar</string>
@ -124,7 +124,7 @@
<!-- Context: General Updates -->
<string name="update_check_unavailable" comment="Error message when a Keyman server can't be reached">¡Falló al acceder al servidor!</string>
<!-- Context: Update-check failure notification -->
<string name="update_check_download_manager_disabled" comment="Notification that a system service needed for updates is disabled">DownloadManager disabled - cannot check for updates</string>
<string name="update_check_download_manager_disabled" comment="Notification that a system service needed for updates is disabled">Descargador deshabilitado: no se pueden comprobar actualizaciones</string>
<!-- Context: General Updates -->
<string name="update_check_current" comment="Notification that all resources are up to date">"¡Todos los recursos están actualizados!"</string>
<!-- Context: General Updates -->

View file

@ -32,28 +32,30 @@ public class WebViewUtilsTest {
@Test
public void test_Chrome36_EngineWebViewVersionStatusDisabled() {
String chromeVersion = "36.0";
double chromeVersionFloat = Float.parseFloat(WebViewUtils.KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME) - 1.0;
String chromeVersion = String.valueOf(chromeVersionFloat);
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
WebViewUtils.EngineWebViewVersionStatus.DISABLED);
}
@Test
public void test_Chrome37_EngineWebViewVersionStatusDegraded() {
String chromeVersion = "37.0";
String chromeVersion = WebViewUtils.KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME;
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
WebViewUtils.EngineWebViewVersionStatus.DEGRADED);
}
@Test
public void test_Chrome56_EngineWebViewVersionStatusDegraded() {
String chromeVersion = "56.0";
public void test_Chrome94_EngineWebViewVersionStatusDegraded() {
double chromeVersionFloat = Float.parseFloat(WebViewUtils.KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME) - 1.0;
String chromeVersion = String.valueOf(chromeVersionFloat);
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
WebViewUtils.EngineWebViewVersionStatus.DEGRADED);
}
@Test
public void test_Chrome57_EngineWebViewVersionStatusFull() {
String chromeVersion = "57.0";
public void test_Chrome95_EngineWebViewVersionStatusFull() {
String chromeVersion = WebViewUtils.KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME;
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
WebViewUtils.EngineWebViewVersionStatus.FULL);
}

View file

@ -6,7 +6,7 @@ ext.rootPath = '../../../'
apply from: "$rootPath/version.gradle"
android {
compileSdk 34
compileSdk 35
namespace="com.keyman.android.tests.keyboardHarness"
// Don't compress kmp files so they can be copied via AssetManager
@ -21,7 +21,7 @@ android {
defaultConfig {
applicationId "com.keyman.android.tests.keyboardHarness"
minSdkVersion 21
targetSdkVersion 34
targetSdkVersion 35
// KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle
versionCode KEYMAN_VERSION_CODE as Integer

View file

@ -15,8 +15,8 @@ buildscript {
allprojects {
repositories {
maven { url uri("${projectDir}/libs") }
google()
jcenter()
mavenCentral()
}
}

View file

@ -1,11 +1,18 @@
apply plugin: 'com.android.application'
plugins {
id 'com.android.application'
}
ext.rootPath = '../../../'
apply from: "$rootPath/version.gradle"
android {
compileSdk 34
namespace="com.keyman.android.tests.keycode"
namespace "com.keyman.android.tests.keycode"
buildFeatures {
// needed for custom BuildConfigField values in defaultConfig
buildConfig = true
}
defaultConfig {
applicationId "com.keyman.android.tests.keycode"
@ -27,11 +34,10 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'androidx.test.ext:junit:1.1.5'
testImplementation 'org.robolectric:robolectric:4.10.3'
testImplementation 'androidx.test.ext:junit:1.2.1'
testImplementation 'org.robolectric:robolectric:4.14.1'
testImplementation 'androidx.test:core:1.6.1'
}

View file

@ -1,25 +1,25 @@
package com.keyman.android.tests.keycode;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import android.content.Context;
import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
@RunWith(RobolectricTestRunner.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
Context appContext = ApplicationProvider.getApplicationContext();
assertEquals("com.keyman.android.tests.keycode", appContext.getPackageName());
}

View file

@ -1,4 +1,4 @@
package com.example.darcy.keycode;
package com.keyman.android.tests.keycode;
import org.junit.Test;

View file

@ -4,10 +4,10 @@ buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath 'com.android.tools.build:gradle:8.9.2'
// NOTE: Do not place your application dependencies here; they belong
@ -18,10 +18,6 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View file

@ -14,4 +14,6 @@ org.gradle.jvmargs=-Xmx1024m
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AGP 8.0 generates R classes for resources defined in the current module only.
android.nonTransitiveRClass=false

View file

@ -13,9 +13,10 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh"
. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh"
. "$KEYMAN_ROOT/resources/build/zip.inc.sh"
# shellcheck disable=SC2154
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
. "${KEYMAN_ROOT}/resources/build/build-utils-ci.inc.sh"
. "${KEYMAN_ROOT}/resources/build/zip.inc.sh"
################################ Main script ################################
@ -24,8 +25,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
#
oemtargets=()
if [ ! -z "${RELEASE_OEM+x}" ]; then
if [ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]; then
if [[ ! -z "${RELEASE_OEM+x}" ]]; then
if [[ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]]; then
oemtargets+=(":fv=../oem/firstvoices/android OEM FirstVoices for Android app")
fi
fi
@ -38,8 +39,7 @@ builder_describe \
build \
test \
"publish Publishes symbols to Sentry and the APKs to the Play Store." \
"archive Copy release artifacts to upload/ and rsync to downloads.keyman" \
"--ci+ Deprecated build option. Remove in 20.0" \
"archive Copy release artifacts to upload/ and create zip" \
--upload-sentry+ \
":engine=KMEA Keyman Engine for Android" \
":app=KMAPro Keyman for Android" \
@ -51,72 +51,75 @@ builder_describe \
builder_parse "$@"
# Override JAVA_HOME to OpenJDK 11
set_java_home
# This script also responsible for cleaning up /android/upload
builder_run_child_actions clean
if builder_start_action clean; then
function do_clean() {
builder_heading "Cleanup /android/upload"
rm -rf "$KEYMAN_ROOT/android/upload"
builder_finish_action success clean
fi
builder_run_child_actions configure build test
function do_test_help() {
check-markdown "$KEYMAN_ROOT/android/docs/help"
check-markdown "$KEYMAN_ROOT/android/docs/engine"
rm -rf "${KEYMAN_ROOT}/android/upload"
}
builder_run_action test:help do_test_help
function do_test_help() {
check-markdown "${KEYMAN_ROOT}/android/docs/help"
check-markdown "${KEYMAN_ROOT}/android/docs/engine"
}
builder_run_child_actions publish
function archive_artifacts() {
local UPLOAD_PATH KEYMAN_ENGINE_ANDROID_ZIP KEYMAN_APK FIRSTVOICES_APK
# Copy release artifacts to upload/ and rsync to downloads.keyman.com
if builder_start_action archive; then
UPLOAD_PATH="$KEYMAN_ROOT/android/upload/${KEYMAN_VERSION}"
UPLOAD_PATH="${KEYMAN_ROOT}/android/upload/${KEYMAN_VERSION}"
KEYMAN_ENGINE_ANDROID_ZIP="keyman-engine-android-${KEYMAN_VERSION}.zip"
KEYMAN_APK="keyman-${KEYMAN_VERSION}.apk"
FIRSTVOICES_APK="firstvoices-${KEYMAN_VERSION}.apk"
ZIP_FILE="${UPLOAD_PATH}/${KEYMAN_ENGINE_ANDROID_ZIP}"
ZIP_FLAGS=("-q" "-r") # quiet, recursive
rm -rf "${UPLOAD_PATH}"
mkdir -p "${UPLOAD_PATH}"
# Create Keyman Engine for Android archive
builder_echo "Copying Keyman Engine for Android into ${UPLOAD_PATH}..."
cd "${UPLOAD_PATH}"
cp "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs/keyman-engine.aar" ./
add_zip_files "${ZIP_FILE}" "${ZIP_FLAGS[@]}" "keyman-engine.aar"
rm -f "keyman-engine.aar"
builder_echo "Adding Keyman Engine for Android to zip..."
(
# shellcheck disable=SC2164
cd "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs"
add_zip_files -q -r "${UPLOAD_PATH}/${KEYMAN_ENGINE_ANDROID_ZIP}" keyman-engine.aar
)
builder_echo "Copying Keyman Engine for Android Sample projects into ${UPLOAD_PATH}..."
cp -rf "${KEYMAN_ROOT}/android/Samples" ./
add_zip_files "${ZIP_FILE}" "-x@../../zip-excludes" "${ZIP_FLAGS[@]}" "Samples"
rm -rf "Samples"
builder_echo "Adding Keyman Engine for Android Sample projects to zip..."
(
# shellcheck disable=SC2164
cd "${KEYMAN_ROOT}/android"
add_zip_files -q -r -xr!build.sh "${UPLOAD_PATH}/${KEYMAN_ENGINE_ANDROID_ZIP}" Samples
)
# Copy release APK
cp "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/release/${KEYMAN_APK}" ./
cp "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/release/${KEYMAN_APK}" "${UPLOAD_PATH}"
# FirstVoices app
if [ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]; then
cp "${KEYMAN_ROOT}/oem/firstvoices/android/app/build/outputs/apk/release/${FIRSTVOICES_APK}" ./
if [[ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]]; then
cp "${KEYMAN_ROOT}/oem/firstvoices/android/app/build/outputs/apk/release/${FIRSTVOICES_APK}" "${UPLOAD_PATH}"
fi
#
# Write download info files
#
cd "${UPLOAD_PATH}"
write_download_info "Keyman Engine for Android" "${KEYMAN_ENGINE_ANDROID_ZIP}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android"
write_download_info "Keyman for Android" "${KEYMAN_APK}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android"
write_download_info "${UPLOAD_PATH}" "${KEYMAN_ENGINE_ANDROID_ZIP}" "Keyman Engine for Android" zip android
write_download_info "${UPLOAD_PATH}" "${KEYMAN_APK}" "Keyman for Android" apk android
if [ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]; then
write_download_info "FirstVoices Keyboards" "${FIRSTVOICES_APK}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android"
if [[ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]]; then
write_download_info "${UPLOAD_PATH}" "${FIRSTVOICES_APK}" "FirstVoices Keyboards" apk android
fi
}
builder_finish_action success archive
fi
# Override JAVA_HOME to OpenJDK 11
set_java_home
# This script also responsible for cleaning up /android/upload
builder_run_child_actions clean
builder_run_action clean do_clean
builder_run_child_actions configure build test
builder_run_action test:help do_test_help
builder_run_child_actions publish
builder_run_action archive archive_artifacts

View file

@ -9,7 +9,7 @@ Keyman for Android will run on Android phones and tablets that have a minimum ve
[Android 5.0 (Lollipop)](https://developer.android.com/about/versions/lollipop).
### Minimum Chrome Version
Keyman for Android requires a minimum version 53.0 of [Google
Keyman for Android requires a minimum version 95.0 of [Google
Chrome](https://play.google.com/store/apps/details?id=com.android.chrome).
If you have an older device, you may need to upgrade Chrome before you can use
Keyman, e.g. through the Play Store.

View file

@ -18,7 +18,7 @@ builder_describe "Keyman common Linux modules" \
builder_parse "$@"
if [[ $BUILDER_OS != linux ]]; then
if ! builder_is_linux; then
builder_echo grey "Platform is not linux; skipping common/linux"
exit 0
fi

View file

@ -18,7 +18,7 @@ builder_describe "Keyman common mac modules" \
builder_parse "$@"
if [[ $BUILDER_OS != mac ]]; then
if ! builder_is_macos; then
builder_echo grey "Platform is not macOS; skipping common/mac"
exit 0
fi

View file

@ -12,7 +12,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
## END STANDARD BUILD SCRIPT INCLUDE
if [[ $BUILDER_OS == win ]]; then
if builder_is_windows; then
# BrowserStackLocal may not exist, so always pass
taskkill //f //im BrowserStackLocal.exe || true
fi

View file

@ -81,11 +81,11 @@ export class ElementString extends Array<ElemElement> {
// error. So we can just exit here.
return null; // UnicodeSet error
}
const uset = sections.usetparser.parseUnicodeSet(item.segment, needRanges, options?.x);
const uset = sections.usetparser.parseUnicodeSet(item.segment, needRanges, options?.compileContext);
if (!uset) {
return null; // UnicodeSet error already added to callback
}
elem.uset = sections.uset.allocUset(uset, sections, options?.x);
elem.uset = sections.uset.allocUset(uset, sections, options?.compileContext);
elem.value = sections.strs.allocString('', {...options, singleOk: true}); // no string
} else if (item.type === ElementType.codepoint || item.type === ElementType.escaped || item.type === ElementType.string) {
// some kind of a string

View file

@ -141,7 +141,7 @@ export class StrsItem {
/** add any context from the options to this strsitem */
setContext(opts?: StrsOptions) {
// At present, there's only a single piece of context available
this._context = this._context || opts?.x;
this._context = this._context || opts?.compileContext;
}
get context() : any {
@ -174,7 +174,7 @@ export interface StrsOptions {
/** string can be stored as a single CharStrsItem, not in strs table. */
singleOk?: boolean;
/** optional context */
x?: any;
compileContext?: any;
};
export class Strs extends Section {
@ -374,13 +374,13 @@ export class Vars extends Section {
export class VarsItem extends Section {
id: StrsItem;
value: StrsItem;
x?: any;
compileContext?: any;
constructor(id: string, value: string, sections: DependencySections, x?: any) {
constructor(id: string, value: string, sections: DependencySections, compileContext?: any) {
super();
this.id = sections.strs.allocString(id);
this.value = sections.strs.allocString(value, {unescape: true});
this.x = x;
this.value = sections.strs.allocString(value, { unescape: true });
this.compileContext = compileContext;
}
valid() : boolean {
@ -389,11 +389,11 @@ export class VarsItem extends Section {
};
export class UnicodeSetItem extends VarsItem {
constructor(id: string, value: string, sections: DependencySections, usetparser: UnicodeSetParser, x?: any) {
super(id, value, sections, x);
constructor(id: string, value: string, sections: DependencySections, usetparser: UnicodeSetParser, compileContext?: any) {
super(id, value, sections, compileContext);
const needRanges = sections.usetparser.sizeUnicodeSet(value);
if (needRanges >= 0) {
this.unicodeSet = sections.usetparser.parseUnicodeSet(value, needRanges);
this.unicodeSet = sections.usetparser.parseUnicodeSet(value, needRanges, compileContext);
} // otherwise: error (was recorded via callback)
}
unicodeSet?: UnicodeSet;
@ -403,9 +403,9 @@ export class UnicodeSetItem extends VarsItem {
};
export class SetVarItem extends VarsItem {
constructor(id: string, value: string[], sections: DependencySections, x?: any) {
super(id, value.join(' '), sections, x);
this.items = sections.elem.allocElementString(sections, {x}, value);
constructor(id: string, value: string[], sections: DependencySections, compileContext?: any) {
super(id, value.join(' '), sections, compileContext);
this.items = sections.elem.allocElementString(sections, { compileContext }, value);
}
items: ElementString; // element string array
valid() : boolean {
@ -414,8 +414,8 @@ export class SetVarItem extends VarsItem {
};
export class StringVarItem extends VarsItem {
constructor(id: string, value: string, sections: DependencySections, x?: any) {
super(id, value, sections, x);
constructor(id: string, value: string, sections: DependencySections, compileContext?: any) {
super(id, value, sections, compileContext);
}
// no added fields
};
@ -453,7 +453,7 @@ export class Tran extends Section {
};
export class UsetItem {
constructor(public uset: UnicodeSet, public str: StrsItem, public x?: any) {
constructor(public uset: UnicodeSet, public str: StrsItem, public compileContext?: any) {
}
compareTo(other: UsetItem) : number {
return this.str.compareTo(other.str);
@ -462,11 +462,11 @@ export class UsetItem {
export class Uset extends Section {
usets: UsetItem[] = [];
allocUset(set: UnicodeSet, sections: DependencySections, x?: any) : UsetItem {
allocUset(set: UnicodeSet, sections: DependencySections, compileContext?: any) : UsetItem {
// match the same pattern
let result = this.usets.find(s => set.pattern == s.uset.pattern);
if (result === undefined) {
result = new UsetItem(set, sections.strs.allocString(set.pattern), x);
result = new UsetItem(set, sections.strs.allocString(set.pattern), compileContext);
this.usets.push(result);
}
return result;

View file

@ -8,13 +8,13 @@ export interface UnicodeSetParser {
* @param pattern string to parse such as `[a-z]`
* @param rangeCount number of ranges to allow for
*/
parseUnicodeSet(pattern: string, rangeCount: number, x?: any) : UnicodeSet | null;
parseUnicodeSet(pattern: string, rangeCount: number, compileContext?: any) : UnicodeSet | null;
/**
* Calculate the number of ranges in a UnicodeSet
* @param pattern string to parse such as `[a-z]`
* @returns number of ranges, or -1 (with callback-reported err) on err
*/
sizeUnicodeSet(pattern: string, x?: any) : number;
sizeUnicodeSet(pattern: string, compileContext?: any) : number;
}
/**

View file

@ -19,6 +19,8 @@ export * as KmpJsonFile from './package/kmp-json-file.js';
export { Uni_IsSurrogate1, Uni_IsSurrogate2 } from './util/util.js';
export * as util from './util/util.js';
export { ObjectWithCompileContext } from './util/types.js';
export * as KeymanFileTypes from './util/file-types.js';
export * as Schemas from './schemas.js';

View file

@ -0,0 +1,6 @@
/**
* Any object with metadata, for example for line number errs.
* Defined as 'any' here to reduce noise on the client side.
* @see {@link KeymanXMLReader.getMetaData()}
*/
export type ObjectWithCompileContext = any;

View file

@ -3,7 +3,7 @@ export { MATCH_HEX_ESCAPE, CONTAINS_QUAD_ESCAPE, MATCH_QUAD_ESCAPE };
/**
* xml2js will not place single-entry objects into arrays. Easiest way to fix
* this is to box them ourselves as needed. Ensures that o.x is an array.
* this is to box them ourselves as needed. Ensures that o[x] is always an array.
*
* @param o Object with property to box
* @param x Name of element to box

View file

@ -18,7 +18,7 @@ builder_describe "Keyman common Windows modules" \
builder_parse "$@"
if [[ $BUILDER_OS != win ]]; then
if ! builder_is_windows; then
builder_echo grey "Platform is not Windows; skipping common/windows"
exit 0
fi

View file

@ -91,7 +91,7 @@ do_test() {
if [[ $target =~ ^(x86|x64)$ ]]; then
cmd //C build.bat $target $BUILDER_CONFIGURATION test $testparams
else
if [[ $target == wasm ]] && [[ $BUILDER_OS == mac ]]; then
if [[ $target == wasm ]] && builder_is_macos; then
# 11794 -- parallel tests failing on some mac build agents; temporary
# mitigation until we diagnose root cause
meson test -j 1 -C "$MESON_PATH" $testparams
@ -136,7 +136,7 @@ do_uninstall() {
# ----------------------------------------------------------------------------
build_meson_cross_file_for_wasm() {
if [ $BUILDER_OS == win ]; then
if builder_is_windows; then
local R=$(cygpath -w $(echo $EMSCRIPTEN_BASE) | sed 's_\\_\\\\_g')
else
local R=$(echo $EMSCRIPTEN_BASE | sed 's_/_\\/_g')

View file

@ -65,6 +65,7 @@ kmc_cmd = [node, '--enable-source-maps', kmc_root]
# Build all keyboards in output folder
foreach kbd : tests_needing_copy
# This will output a (wrong) deprecation warning with meson < 1.5 - see #13250
configure_file(input: kbd + '.xml', output: kbd + '.xml', copy: true)
endforeach
@ -77,6 +78,7 @@ foreach kbd : tests_with_testdata
endforeach
foreach kbd : tests_from_cldr
# This will output a (wrong) deprecation warning with meson < 1.5 - see #13250
configure_file(input: ldml_data / kbd + '.xml', output: kbd + '.xml', copy: true)
configure_file(
command: kmc_cmd + ['build', 'ldml-test-data', '@INPUT@', '--out-file', '@OUTPUT@'],

View file

@ -13,13 +13,10 @@ multiple source files, which are all managed within this one editor.
The following image shows the first tab of the editor, the Details tab,
for a brand new keyboard, with those three initial tabs.
![Keyboard Editor - New file, Details tab](../images/ui/frmKeymanWizard_New.png)
<img src="../images/ui/frmKeymanWizard_New.png" style="width:100%" alt="Keyboard Editor - New file, Details tab"/>
> ### Note
Users of earlier versions of Keyman Developer may initially have trouble
finding tabs such as the Icon tab or the On-Screen tab, as they are not
initially visible. The Features grid on the Details tab allows you to
add these extra features into the keyboard.
> [!Note]
> Users of earlier versions of Keyman Developer may initially have trouble finding tabs such as the Icon tab or the On-Screen tab, as they are not initially visible. The Features grid on the Details tab allows you to add these extra features into the keyboard.
## Keyboard component files
@ -47,7 +44,7 @@ because that reflects both the Details and the Layout tab.
## Details tab
![Keyboard Editor - Details tab](../images/ui/frmKeymanWizard_Details.png)
<img src="../images/ui/frmKeymanWizard_Details.png" style="width:100%" alt="Keyboard Editor - Details tab"/>
The Details tab grows as you add more options to a keyboard. The fields
here are:
@ -102,7 +99,7 @@ Features
## Layout tab
![Keyboard Editor - Layout tab, Design view](../images/ui/frmKeymanWizard_Layout_Design.png)
<img src="../images/ui/frmKeymanWizard_Layout_Design.png" style="width:100%" alt="Keyboard Editor - Layout tab, Design view"/>
The Layout tab gives you a simple interface to quickly create a keyboard
using a visual representation of a desktop/laptop computer keyboard. You
@ -124,13 +121,11 @@ There are two ways to assign characters to the keys:
onto the appropriate key. This will set the key to output that
character.
> ### Hint
To add the character to a key with existing characters, hold
<kbd>Ctrl</kbd> while dropping it onto the key.
> [!TIP]
> To add the character to a key with existing characters, hold <kbd>Ctrl</kbd> while dropping it onto the key.
<!-- Saperator -->
> ### Note
Any key that does not have a character assigned to it will output what the selected Windows layout specifies.
> [!Note]
> Any key that does not have a character assigned to it will output what the selected Windows layout specifies.
Distinguish between left and right Ctrl/Alt
: A Keyman keyboard can treat left and right Ctrl and Alt identically,
@ -151,9 +146,7 @@ Display 102nd Key (as on European keyboards)
You can press and release <kbd>Ctrl</kbd> to select
another key on the keyboard using your keyboard.
<br/>
![Keyboard Editor - Layout tab, Code view](../images/ui/frmKeymanWizard_Layout_Code.png)
<img src="../images/ui/frmKeymanWizard_Layout_Code.png" style="width:100%" alt="Keyboard Editor - Layout tab, Code view"/>
The Code view shows the source code of the keyboard file. This is where
all the information in the Details and Layout tab is stored, and
@ -165,7 +158,7 @@ editor shown within this view.
## On-Screen tab
![Keyboard Editor - On-Screen tab](../images/ui/OnScreenKeyboard.png)
<img src="../images/ui/OnScreenKeyboard.png" style="width:100%" alt="Keyboard Editor - On-Screen tab"/>
This tab allows you to edit the visual representation of your keyboard
layout. The content on this tab is stored in the .kvks file associated
@ -213,17 +206,28 @@ Distinguish between left and right Ctrl/Alt
: If checked, treats the left and right Ctrl/Alt as separate layers
Display 102nd Key (as on European keyboards)
: European keyboards have one extra key that is not on US keyboards.
: European keyboards (ISO layout) have one extra key that is not on US keyboards (ANSI layout).
This key is positioned to the right of the left shift key. Some
other keyboards have additional keys; these are not shown on the
layout designer. If you have a European layout selected as your
layout designer.
If you have a European layout selected as your
Windows layout, the 102nd key will always be visible in the
designer; ensure you select this checkbox if you want the 102nd key
to always be visible to end users of the layout, irrespective of
their selected base layout. When a European layout is selected as
their selected base layout.
When a European layout is selected as
your Windows layout, the shape of the Enter key will also change to
take two rows, and the backslash key will move down one row, but not
otherwise change in behaviour.
otherwise change in behaviour.
<img src="../images/ui/OnScreenKeyboard_ISO.png" style="width:100%" alt="Keyboard Editor - On-Screen tab with ISO layout"/>
*See the difference between the two layouts in Keyman Developer.*
> [!NOTE]
> The "Display 102nd Key (as on European keyboards)" checkbox can be left unchecked.
> To ensure compatibility, the extra key shouldn't be assigned since it won't be accessible on an ANSI physical keyboard.
Auto-fill underlying layout
: When the Fill from layout button is clicked, if this option is
@ -232,7 +236,7 @@ Auto-fill underlying layout
## Touch Layout tab
![Keyboard Editor - Touch Layout tab, Design view](../images/ui/TouchLayout_Design.png)
<img src="../images/ui/TouchLayout_Design.png" style="width:100%" alt="Keyboard Editor - Touch Layout tab, Design view"/>
The Touch Layout tab is used to create the visible representation of the
keyboard layout for touch devices. It works similarly to the On Screen
@ -251,7 +255,7 @@ including iPhone and iPad, in different orientations, to allow you to
visualize the keyboard layout before you load it onto a device. The
following image shows all aspects of the touch design view.
![Keyboard Editor - Touch Layout tab, Design view closeup](../images/ui/TouchLayout_Design_2.png)
<img src="../images/ui/TouchLayout_Design_2.png" style="width:100%" alt="Keyboard Editor - Touch Layout tab, Design view closeup"/>
### Left sidebar controls
@ -266,6 +270,20 @@ Template...
be lost. Therefore, selecting an appropriate template early in the
development process is suggested.
- template-basic: has only a phone layout (4 rows of keys), with three layers: default (K_A through K_Z
plus some punctuation), shift (Shift K_A through Shift K_Z, plus some punctuation), numeric (K_0 through
K_9 unshifted and shifted, plus other non-alphanumerice keys).
- template-latin: has both phone and tablet layouts (which are identical except for some key spacing,
each with 4 rows of keys), with four layers: default, shift, numeric, symbol. The first three layers
are similar to those of template-basic, with the addition of longpress keys for accented characters.
The symbol layer has many symbols not usually seen on a physical keyboard.
- template-traditional: has both phone (4 rows of keys) and tablet (5 rows of keys) layouts. The phone
layout is similar to the template-basic layout. The tablet layout has eight layers: default, shift, ctrl,
shift-ctrl, alt, shift-alt, ctrl-alt, shift-ctrl-alt. The default layer has the keys from the default layer
of the desktop layout, the shift layer has the keys from the shift layer of the desktop layout, and so on.
Import from On Screen
: If you have an existing On-Screen Keyboard for your keyboard layout,
importing the design from the On-Screen Keyboard can reduce the
@ -454,7 +472,7 @@ dragged between the main keyboard area and the long press area. The bar
below the long press area contains the corresponding controls for the
long press keys.
![Keyboard Editor - Touch Layout tab, Code view](../images/ui/frmKeymanWizard_TouchLayout_Code.png)
<img src="../images/ui/frmKeymanWizard_TouchLayout_Code.png" style="width:100%" alt="Keyboard Editor - Touch Layout tab, Code view"/>
The Code view allows you to edit the JSON source of the touch layout
file. This makes certain operations simpler, such as batched rearranging
@ -467,7 +485,7 @@ The format of the layout file is described in [Creating a touch keyboard layout
Underneath the keyboard area is the Long-press, Flicks, and Multitaps controls.
![Long-press, Flicks, and Multitaps layout](../images/ui/LongPress_Flicks_Multitaps.png)
<img src="../images/ui/LongPress_Flicks_Multitaps.png" style="width:100%" alt="Long-press, Flicks, and Multitaps layout"/>
Red circle with an X
: This button to the top right of the key will delete the key from the
@ -494,7 +512,7 @@ the selected key for different Gesture Type. The only difference between this co
## Icon tab
![Keyboard Editor - Icon tab](../images/ui/frmKeymanWizard_Icon.png)
<img src="../images/ui/frmKeymanWizard_Icon.png" style="width:100%" alt="Keyboard Editor - Icon tab"/>
The icon tab allows you to edit the icon associated with the keyboard. An external icon editor will allow you to create more sophisticated icons with alpha transparency and multiple resolutions, but this will cover the standard 16x16 256 colour icon, which is all that many keyboards require. The controls at the top left are:
@ -560,7 +578,7 @@ This tab allows you to edit the [named constants](/developer/language/guide/cons
## Build tab
![Keyboard Editor - Build tab](../images/ui/BuildTab.png)
<img src="../images/ui/BuildTab.png" style="width:100%" alt="Keyboard Editor - Build tab"/>
A set of common controls is across the top of this tab:

View file

@ -129,6 +129,12 @@ are optional.
A web site is encouraged and should be filled in the web site field, including
the initial "http://" or "https://".
The Description is the keyboard summary that users will see on the keyboard’s
overview page at the keyman.com web site.
[GitHub style Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax)
can be used in the description text. However, HTML tags are not supported.
A package can optionally include a 140x250 JPEG or PNG image file to be shown
during installation. This image file must be added to the Files list in step 1,
then selected from the list here.
@ -191,4 +197,4 @@ keyboard to the [Keyman Cloud Keyboards Repository](/developer/keyboards/).
## Package Installers
As of Keyman Developer 17, bundled executable package installers for Keyman for
Windows can be created using [kmc](kmc), but cannot be created within the IDE.
Windows can be created using [kmc](kmc), but cannot be created within the IDE.

View file

@ -0,0 +1,26 @@
---
title: Using Gestures in Keyman Developer
---
Keyman Developer offers **Longpress** keys, **Flicks**, and **Multitaps** for touch keyboard layout. To allow gestures onto your keyboard, go to the Touch layout tab and define the keys as follows:
<img src="../../../images/touch-keyboard-tutorial/touch-layout-gestures.jpg" alt="Keyman Developer touch layout gestures" width="70%"/>
## Tips
***Flicks** and **long presses** can both be used in a single keyboard. In this scenario, it is recommended to use flicks in only one direction—south (down). Long presses work well alongside this pattern. See, for example, [sil_euro_latin](https://help.keyman.com/keyboard/sil_euro_latin/3.0.3/sil_euro_latin#toc-mobile-keyboard-layout).
* Another helpful tip seen in the sil_euro_latin keyboard is using south-flicks on the base and shift layers corresponding to the numeric/symbol layer, this aids in both memorization and discovery.
* **Flicks** in multiple directions are possible, but currently Keyman does not support hints for multiple directions, so they are not very discoverable.
* **Long presses** are more discoverable, but **flicks** are faster. We suggest using south flicks along with long presses, and including the flick output in the long press for maximum discoverability and optimum ergonomics.
## Caution
* **Flicks** near screen edges tend to be problematic--once you drag off the screen, then Keyman can no longer tell that you are flicking (or you may end up activating system controls on the bottom edge):
* On the bottom row, south flicks are not recommended.
* On the top row, we have the banner space reserved in order to be able to support flicks and long presses up into that region of the screen.
* Left/right screen edges, horizontal flicks are not the best practice.
## See also
* The [Winchus keyboard](https://help.keyman.com/keyboard/winchus/1.4/winchus#guia-rapida-celular)
* The [GFF Amharic keyboard](https://help.keyman.com/keyboard/gff_amharic/3.2.1/gff_amharic#toc-using-this-keyboard)

View file

@ -38,17 +38,23 @@ ago) used the <kbd>/</kbd> key as the deadkey.
<kbd>/</kbd><kbd>d</kbd> gave ɗ, and so on.
It would be possible to use that desktop keyboard logic on touch
screens.  But your mobile device users will find these rules annoying.
screens. But your mobile device users will find these rules annoying.
Instead of two keys they would probably have to press four, since the
standard alphabetical keyboard on most touch devices does not have the
slash key. So they would have to press the key to switch to the numeric
keyboard, then press slash, then press the key to go back to the
alphabetical keyboard, then the letter. What touch screen users would
keyboard, then press slash, then press the key to switch back to the
alphabetical keyboard, and then the letter. What touch screen users would
appreciate is something that uses the great feature of touch screens,
the long press or press and hold on a letter, to see analogous letters.
So we’ll set up a touch screen keyboard that lets you press and hold ‘d’
to see the two hooked d characters, press and hold ‘b’ to see the hooked
b characters, and so on.
So we’ll set up a touch screen keyboard that lets you
* press and hold ‘d’ to see the two hooked d characters
* press and hold ‘b’ to see the hooked b characters, and so on.
[Next: Making, testing and distributing the touch
keyboard](making-touch-keyboard)
## See also
* Tips for using [gestures](gestures) in Keyman Developer

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -2,6 +2,6 @@
title: Additional Credits
---
## <span id="id536439"></span>Code2000, Code2001 and Code2002 fonts
## Code2000, Code2001 and Code2002 fonts
© 2006 James Kass.

View file

@ -4,7 +4,7 @@ title: Keyman® Developer License
MIT License
Copyright (c) 1994-2018 SIL International
Copyright (c) 1994-2025 SIL Global
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the

View file

@ -102,7 +102,7 @@ Example: keys.ts \- compiler \- call site for error
```js
static ERROR_InvalidScanCode = SevError | 0x0009;
static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, x: ObjectWithMetadata) => mx(
static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, compileContext:ObjectWithCompileContext) => mx(
this.ERROR_InvalidScanCode, context,
Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}',
`…additional markdown detail…`
@ -126,7 +126,7 @@ The `mx()` function does the following (note that `m()` is called first).
```js
let evt = m(code, message, detail); // raw message
evt = LdmlCompilerMessages.offset(evt, x); // with offset
evt = LdmlCompilerMessages.offset(evt, compileContext); // with offset
```
Note: We could have: `ml(e:CompilerEvent, lineNumber?: number)` so that compilers with a line number could pass that in directly.

View file

@ -67,7 +67,7 @@ builder_parse "$@"
#-------------------------------------------------------------------------------------------------------------------
if [[ $BUILDER_OS == win ]]; then
if builder_is_windows; then
source "$KEYMAN_ROOT/resources/build/win/environment.inc.sh"
fi

View file

@ -1,5 +1,5 @@
import { CompilerCallbacks } from "./compiler-callbacks.js";
import { ObjectWithMetadata } from "./symbol-utils.js";
import { ObjectWithCompileContext } from '@keymanapp/common-types';
import { KeymanXMLReader, XML_FILENAME_SYMBOL } from "./xml-utils.js";
/**
@ -215,9 +215,9 @@ export class CompilerError {
* @param x any object parsed from XML or with the XML_META_DATA_SYMBOL symbol copied over
* @returns modified event object
*/
public static setFromMetadata(event: CompilerEvent, x?: ObjectWithMetadata): CompilerEvent {
if(x) {
const metadata = KeymanXMLReader.getMetaData(x) || {};
public static setFromMetadata(event: CompilerEvent, compileContext?: ObjectWithCompileContext): CompilerEvent {
if (compileContext) {
const metadata = KeymanXMLReader.getMetaData(compileContext) || {};
const offset = metadata?.startIndex;
if (offset) {
event.offset = offset;
@ -462,8 +462,8 @@ export function dedentCompilerMessageDetail(event: CompilerEvent) {
* // it contains redundant info.
* // 2. No code execution within the arrow function other than the 'mx' call, string interpolation,
* // with `${def(o.property)}` as the max complexity of interpolation.
* static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, x: ObjectWithMetadata) => mx(
* this.ERROR_InvalidScanCode, x,
* static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, compileContext: ObjectWithCompileContext) => mx(
* this.ERROR_InvalidScanCode, compileContext,
* `Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}'`,
* // Note: If detail is omitted, leave the trailing comma on the prior line to leave room for it
* `…additional markdown detail…`
@ -478,7 +478,7 @@ export function dedentCompilerMessageDetail(event: CompilerEvent) {
* @see CompilerMessageSpec
* @returns the event
*/
export function CompilerMessageObjectSpec(code: number, context: ObjectWithMetadata, message: string, detail?: string): CompilerEvent {
export function CompilerMessageObjectSpec(code: number, context: ObjectWithCompileContext, message: string, detail?: string): CompilerEvent {
let evt = CompilerMessageSpec(code, message, detail); // constructs raw message
evt = CompilerError.setFromMetadata(evt, context); // updates with offset from context
return evt;

View file

@ -1,8 +1,9 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m } from './compiler-interfaces.js';
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m, CompilerMessageObjectSpec as mx } from './compiler-interfaces.js';
import { constants } from '@keymanapp/ldml-keyboard-constants';
import { ObjectWithCompileContext } from '@keymanapp/common-types';
const DeveloperUtilsErrMask = CompilerErrorNamespace.DeveloperUtils;
// const SevInfo = CompilerErrorSeverity.Info | DeveloperUtilsErrMask;
@ -14,8 +15,10 @@ const SevError = CompilerErrorSeverity.Error | DeveloperUtilsErrMask;
export class DeveloperUtilsMessages {
// structured Ajv validation error
static ERROR_SchemaValidationError = SevError | 0x0001;
static Error_SchemaValidationError = (o:{instancePath:string, keyword:string, message: string, params: string}) => m(this.ERROR_SchemaValidationError,
`Error validating LDML XML file: ${def(o.instancePath)}: ${def(o.keyword)}: ${def(o.message)} ${def(o.params)}`);
static Error_SchemaValidationError = (o:{instancePath:string, keyword:string, message: string, params: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_SchemaValidationError, compileContext,
`Error validating LDML XML file: ${def(o.instancePath)}: ${def(o.keyword)}: ${def(o.message)} ${def(o.params)}`,
);
static ERROR_ImportInvalidBase = SevError | 0x0002;
static Error_ImportInvalidBase = (o: { base: string, path: string, subtag: string }) =>

View file

@ -70,8 +70,8 @@ export { UrlSubpathCompilerCallback } from './utils/UrlSubpathCompilerCallback.j
export { DeveloperUtilsMessages } from './developer-utils-messages.js';
export * as SourceFilenamePatterns from './source-filename-patterns.js';
export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader, KeymanXMLMetadata, XML_FILENAME_SYMBOL } from './xml-utils.js';
export { SymbolUtils, ObjectWithMetadata } from './symbol-utils.js';
export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader, KeymanXMLMetadata, XML_FILENAME_SYMBOL, withOffset } from './xml-utils.js';
export { SymbolUtils } from './symbol-utils.js';
export * as LineUtils from './line-utils.js';
export * as GitHubUrls from './github-urls.js';
export * as CloudUrls from './cloud-urls.js';

View file

@ -6,13 +6,7 @@
* Utilities for manipulating Symbol properties
*/
/**
* Any object with metadata, for example for line number errs.
* Defined as 'any' here to reduce noise on the client side.
* @see {@link KeymanXMLReader.getMetaData()}
*/
export type ObjectWithMetadata = any;
import { ObjectWithCompileContext } from '@keymanapp/common-types';
export class SymbolUtils {
/**
@ -21,8 +15,8 @@ export class SymbolUtils {
* @param from source for symbols
* @returns the onto object
*/
public static copySymbols<T>(onto: T, from: ObjectWithMetadata): T {
const o = onto as ObjectWithMetadata;
public static copySymbols<T>(onto: T, from: ObjectWithCompileContext): T {
const o = onto as ObjectWithCompileContext;
for (const sym of Object.getOwnPropertySymbols(from)) {
o[sym] = from[sym];
}

View file

@ -12,7 +12,7 @@ import { constants } from '@keymanapp/ldml-keyboard-constants';
import { LDMLKeyboardTestDataXMLSourceFile, LKTTest, LKTTests } from './ldml-keyboard-testdata-xml.js';
import boxXmlArray = util.boxXmlArray;
import { LineFinderEventResolver } from '../../line-utils.js';
import { XML_FILENAME_SYMBOL, KeymanXMLReader } from '../../xml-utils.js';
import { XML_FILENAME_SYMBOL, KeymanXMLReader, findInstanceObject } from '../../xml-utils.js';
interface NameAndProps {
'$'?: any; // content
@ -310,12 +310,13 @@ export class LDMLKeyboardXMLSourceFileReader implements EventResolver {
public validate(source: LDMLKeyboardXMLSourceFile | LDMLKeyboardTestDataXMLSourceFile): boolean {
if(!SchemaValidators.default.ldmlKeyboard3(source)) {
for (const err of (<any>SchemaValidators.default.ldmlKeyboard3).errors) {
const context = findInstanceObject(source, err?.instancePath?.split('/'));
this.callbacks.reportMessage(DeveloperUtilsMessages.Error_SchemaValidationError({
instancePath: err.instancePath,
keyword: err.keyword,
message: err.message || 'Unknown AJV Error', // docs say 'message' is optional if 'messages:false' in options
params: Object.entries(err.params || {}).sort().map(([k,v])=>`${k}="${v}"`).join(' '),
}));
}, context));
}
return false;
}

View file

@ -412,7 +412,6 @@ export class KeymanXMLWriter {
}
}
/**
* traverse an AJV instancePath and map to an object if possible
* @param source object tree root (contains the root object)
@ -435,6 +434,7 @@ export function findInstanceObject(source: any, path: string[]) : any {
return null;
}
}
/**
* Return an object simulating an XML object with an offset number
* For use in calling message functions
@ -448,4 +448,4 @@ export function withOffset(c: number, compileContext?: any) : KeymanXMLMetadata
startIndex: c
});
return o;
}
}

View file

@ -8,7 +8,7 @@ import { testReaderCases } from '../helpers/reader-callback-test.js';
import CLDRScanToVkey = Constants.CLDRScanToVkey;
import CLDRScanToKeyMap = Constants.CLDRScanToKeyMap;
import USVirtualKeyCodes = Constants.USVirtualKeyCodes;
import { KeymanXMLReader, XML_FILENAME_SYMBOL } from '../../src/xml-utils.js';
import { KeymanXMLReader, withOffset, XML_FILENAME_SYMBOL } from '../../src/xml-utils.js';
function pluckKeysFromKeybag(keys: LKKey[], ids: string[]) {
return keys.filter(({id}) => ids.indexOf(id) !== -1);
@ -25,7 +25,7 @@ describe('ldml keyboard xml reader tests', function () {
keyword: 'required',
message: `must have required property 'info'`,
params: 'missingProperty="info"',
})],
}, withOffset(39))],
},
{
subpath: 'invalid-conforms-to.xml',
@ -34,7 +34,7 @@ describe('ldml keyboard xml reader tests', function () {
keyword: 'enum',
message: `must be equal to one of the allowed values`,
params: 'allowedValues="45,46"', // this has to be kept in sync with the DTD
})],
}, withOffset(39))],
},
{
subpath: 'import-minimal.xml',

View file

@ -12,7 +12,7 @@ import { env } from 'node:process';
import { readFileSync, writeFileSync } from 'node:fs';
import { KeymanXMLType, KeymanXMLReader, KeymanXMLWriter } from '../src/xml-utils.js';
import { KeymanXMLType, KeymanXMLReader, KeymanXMLWriter, findInstanceObject } from '../src/xml-utils.js';
import { LineFinder } from '../src/line-utils.js';
import { makePathToFixture } from './helpers/index.js';
import { SymbolUtils } from '../src/symbol-utils.js';
@ -198,5 +198,36 @@ describe(`XML Reader line number test`, () => {
LineFinder.offsetToLineColumn(
getMetaData(actual.keyboard3.transforms).startIndex, lines), { line: 8, column: 2 });
});
describe('findInstanceObject test', () => {
const path0 = '/keyboard3/layers/0';
const TARGET = Symbol("Looking for this!");
it(`Should be able to parse ${path0}`, () => {
const o = {
keyboard3: {
layers: [
TARGET,
]
}
};
assert.equal(findInstanceObject(o, path0.split('/')), TARGET);
});
// path to property
const path1 = '/keyboard3/conformsTo';
it(`Should be able to parse ${path1}`, () => {
const keyboard3 = { conformsTo: "1234"};
const o = {
keyboard3,
};
assert.equal(findInstanceObject(o, path1.split('/')), keyboard3);
});
const path2 = '/keyboard3/bad/path';
it(`Should be able to handle ${path2}`, () => {
const keyboard3 = { conformsTo: "1234"};
const o = {
keyboard3,
};
assert.equal(findInstanceObject(o, path2.split('/')), undefined);
});
});
});

View file

@ -20,9 +20,9 @@ builder_describe_outputs \
builder_parse "$@"
. "$KEYMAN_ROOT/resources/build/win/environment.inc.sh"
. "$KEYMAN_ROOT/resources/build/win/wix.inc.sh"
. "$KEYMAN_ROOT/resources/build/win/zip.inc.sh"
. "${KEYMAN_ROOT}/resources/build/win/environment.inc.sh"
. "${KEYMAN_ROOT}/resources/build/win/wix.inc.sh"
. "${KEYMAN_ROOT}/resources/build/zip.inc.sh"
# In dev environments, we'll hack the tier to alpha; CI sets this for us in real builds.
if [[ -z ${KEYMAN_TIER+x} ]]; then
@ -51,7 +51,7 @@ function do_clean() {
#-------------------------------------------------------------------------------------------------------------------
function do_publish() {
verify-program-signatures
builder_if_release_build_level verify-program-signatures
verify-node-installer-version
"$KEYMAN_ROOT/common/windows/cef-checkout.sh"
@ -80,7 +80,7 @@ function do_publish() {
copy-kmdev
verify-installer-signatures
builder_if_release_build_level verify-installer-signatures
}
function do_test() {
@ -233,7 +233,7 @@ function make-installer() {
echo "Version=$KEYMAN_VERSION" >> setup.inf
echo "MSIFileName=keymandeveloper.msi" >> setup.inf
echo "Title=Keyman Developer ${KEYMAN_VERSION_WITH_TAG}" >>setup.inf
wzzip setup.zip keymandeveloper.msi setup.inf
add_zip_files setup.zip keymandeveloper.msi setup.inf
cat "$DEVELOPER_PROGRAM/setup.exe" setup.zip > "keymandeveloper-$KEYMAN_VERSION.exe"
wrap-signcode //d "Keyman Developer" "keymandeveloper-$KEYMAN_VERSION.exe"
@ -242,27 +242,24 @@ function make-installer() {
#
}
# TODO: rename this to keyman-developer-cli-$Version.zip
KMC_ZIP="$DEVELOPER_ROOT/release/$KEYMAN_VERSION/kmcomp-$KEYMAN_VERSION.zip"
function make-kmc-install-zip() {
builder_heading make-kmc-install-zip
copy-schemas
cd "$DEVELOPER_ROOT/bin"
(
# shellcheck disable=2164
cd "${DEVELOPER_ROOT}/bin"
wzzip -bd -bb0 "$KMC_ZIP" \
kmconvert.exe \
sentry.dll sentry.x64.dll \
kmdecomp.exe \
keyboard_info.schema.json \
kmp.schema.json \
keyman-touch-layout.spec.json keyman-touch-layout.clean.spec.json \
xml/layoutbuilder/*.keyman-touch-layout \
projects/* \
server/*
# TODO: rename this to keyman-developer-cli-$Version.zip
local KMCOMP_ZIP="${DEVELOPER_ROOT}/release/${KEYMAN_VERSION}/kmcomp-${KEYMAN_VERSION}.zip"
cd "$THIS_SCRIPT_PATH"
add_zip_files "${KMCOMP_ZIP}" -q -r \
kmconvert.exe \
keyboard_info.schema.json \
xml/layoutbuilder/*.keyman-touch-layout \
projects/ \
server/
)
}
# TODO: are these required?

View file

@ -32,7 +32,7 @@ function do_build() {
build_version.res
vs_msbuild kmanalyze.vcxproj //t:Build "//p:Platform=Win32"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
cp "$WIN32_TARGET_PATH/kmanalyze.pdb" "$DEVELOPER_DEBUGPATH"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmanalyze.pdb" "$DEVELOPER_DEBUGPATH"
}
function do_publish() {

View file

@ -28,7 +28,7 @@ export class KmnFileWriter {
let data: string = "\n";
// add top part of kmn file: STORES
data += this.writeData_Stores(data_ukelele);
data += this.write_KmnFileHeader(data_ukelele);
// add bottom part of kmn file: RULES
data += this.writeData_Rules(data_ukelele);
@ -46,7 +46,7 @@ export class KmnFileWriter {
let data: string = "\n";
// add top part of kmn file: STORES
data += this.writeData_Stores(data_ukelele);
data += this.write_KmnFileHeader(data_ukelele);
// add bottom part of kmn file: RULES
data += this.writeData_Rules(data_ukelele);
@ -64,7 +64,7 @@ export class KmnFileWriter {
* @param data_ukelele an object containing all data read from a .keylayout file
* @return string - all stores to be printed
*/
public writeData_Stores(data_ukelele: convert_object): string {
public write_KmnFileHeader(data_ukelele: convert_object): string {
let data: string = "";
@ -97,6 +97,9 @@ export class KmnFileWriter {
let data: string = "";
// filter array of all rules and remove duplicates
// during the process of creating Rule[], duplicate rules might occur
// (e.g. when in a keylayout file the same modifiers occur in several behaviors thus producing the same rules).
// This is to filter out those duplicate Rule objects
const unique_data_Rules: Rule[] = data_ukelele.arrayOf_Rules.filter((curr) => {
return (!(curr.output === new TextEncoder().encode("") || curr.output === undefined)
&& (curr.key !== "")
@ -377,7 +380,8 @@ export class KmnFileWriter {
/**
* @brief member function to review rules for acceptable modifiers, duplicate or ambiguous rules and return an array containing possible warnings.
* Definition of comparisons e.g. 1-1, 2-4, 6-6
* Keyman can not handle duplicate rules so we need to make sure a rule is written only once by either omitting a duplicate rule or commenting out an ambiguous rule.
* Omitting rules and definition of comparisons e.g. 1-1, 2-4, 6-6
* see https://docs.google.com/document/d/12J3NGO6RxIthCpZDTR8FYSRjiMgXJDLwPY2z9xqKzJ0/edit?tab=t.0#heading=h.pcz8rjyrl5ug
* @param rule : Rule[] - an array of all rules
* @param index the index of a rule in array[rule]

View file

@ -113,7 +113,7 @@ describe('KmnFileWriter', function () {
});
describe("writeData_Stores() ", function () {
describe("write_KmnFileHeader() ", function () {
const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions);
const sut_r = new KeylayoutFileReader(compilerTestCallbacks);
const sut_w = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions);
@ -148,18 +148,18 @@ describe('KmnFileWriter', function () {
+ "group(main) using keys\n\n"
+ "\n";
it(('writeData_Stores should return store text with filename ').padEnd(62, " ") + 'on correct input', async function () {
const written_correctName = sut_w.writeData_Stores(converted);
it(('write_KmnFileHeader should return store text with filename ').padEnd(62, " ") + 'on correct input', async function () {
const written_correctName = sut_w.write_KmnFileHeader(converted);
assert.equal(written_correctName, (out_expected_first + converted.keylayout_filename + out_expected_last));
});
it(('writeData_Stores should return store text without filename ').padEnd(62, " ") + 'on empty input', async function () {
const written_emptyName = sut_w.writeData_Stores(converted_empty);
it(('write_KmnFileHeader should return store text without filename ').padEnd(62, " ") + 'on empty input', async function () {
const written_emptyName = sut_w.write_KmnFileHeader(converted_empty);
assert.equal(written_emptyName, (out_expected_first + out_expected_last));
});
it(('writeData_Stores should return store text without filename ').padEnd(62, " ") + 'on only filename as input', async function () {
const written_onlyName = sut_w.writeData_Stores(converted_unavailable);
it(('write_KmnFileHeader should return store text without filename ').padEnd(62, " ") + 'on only filename as input', async function () {
const written_onlyName = sut_w.write_KmnFileHeader(converted_unavailable);
assert.equal(written_onlyName, (out_expected_first + converted_unavailable.keylayout_filename + out_expected_last));
});
});

View file

@ -6,7 +6,7 @@ TODO: implement additional interfaces:
*/
// TODO: rename wasm-host?
import { VisualKeyboard, KvkFileReader, LdmlKeyboardTypes, KeymanFileTypes, KvkFileWriter } from '@keymanapp/common-types';
import { VisualKeyboard, KvkFileReader, LdmlKeyboardTypes, KeymanFileTypes, KvkFileWriter, ObjectWithCompileContext } from '@keymanapp/common-types';
import {
CompilerCallbacks, CompilerEvent, CompilerOptions, KeymanCompiler, KeymanCompilerArtifacts,
KeymanCompilerArtifactOptional, KeymanCompilerResult, KeymanCompilerArtifact, KvksFileReader,
@ -530,7 +530,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
* @param rangeCount - number of ranges to allocate
* @returns UnicodeSet accessor object, or null on failure
*/
public parseUnicodeSet(pattern: string, rangeCount: number, x?: any) : LdmlKeyboardTypes.UnicodeSet | null {
public parseUnicodeSet(pattern: string, rangeCount: number, compileContext?: any) : LdmlKeyboardTypes.UnicodeSet | null {
if(!this.verifyInitialized()) {
/* c8 ignore next 2 */
// verifyInitialized will set a callback if needed
@ -563,7 +563,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
// rc is negative: it's an error code.
this.wasmExports.free(buf);
// translate error code into callback
this.callbacks.reportMessage(getUnicodeSetError(rc, x));
this.callbacks.reportMessage(getUnicodeSetError(rc, compileContext));
return null;
}
}
@ -571,7 +571,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
/**
* @internal
*/
public sizeUnicodeSet(pattern: string, x?: any) : number {
public sizeUnicodeSet(pattern: string, compileContext?: any) : number {
if(!this.verifyInitialized()) {
/* c8 ignore next 2 */
return null;
@ -583,7 +583,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
if (rc >= 0) {
return rc;
} else {
this.callbacks.reportMessage(getUnicodeSetError(rc, x));
this.callbacks.reportMessage(getUnicodeSetError(rc, compileContext));
return -1;
}
}
@ -594,7 +594,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
* @param rc parseUnicodeSet error code
* @returns the compiler event
*/
function getUnicodeSetError(rc: number, x?: any) : CompilerEvent {
function getUnicodeSetError(rc: number, compileContext?: ObjectWithCompileContext) : CompilerEvent {
// from kmcmplib.h
const KMCMP_ERROR_SYNTAX_ERR = -1;
const KMCMP_ERROR_HAS_STRINGS = -2;
@ -602,16 +602,16 @@ function getUnicodeSetError(rc: number, x?: any) : CompilerEvent {
const KMCMP_FATAL_OUT_OF_RANGE = -4;
switch(rc) {
case KMCMP_ERROR_SYNTAX_ERR:
return KmnCompilerMessages.Error_UnicodeSetSyntaxError(x);
return KmnCompilerMessages.Error_UnicodeSetSyntaxError(compileContext);
case KMCMP_ERROR_HAS_STRINGS:
return KmnCompilerMessages.Error_UnicodeSetHasStrings(x);
return KmnCompilerMessages.Error_UnicodeSetHasStrings(compileContext);
case KMCMP_ERROR_UNSUPPORTED_PROPERTY:
return KmnCompilerMessages.Error_UnicodeSetHasProperties(x);
return KmnCompilerMessages.Error_UnicodeSetHasProperties(compileContext);
case KMCMP_FATAL_OUT_OF_RANGE:
return KmnCompilerMessages.Fatal_UnicodeSetOutOfRange(x);
return KmnCompilerMessages.Fatal_UnicodeSetOutOfRange(compileContext);
default:
/* c8 ignore next */
return CompilerError.setFromMetadata(KmnCompilerMessages.Fatal_UnexpectedException({e: `Unexpected UnicodeSet error code ${rc}`}), x);
return CompilerError.setFromMetadata(KmnCompilerMessages.Fatal_UnexpectedException({e: `Unexpected UnicodeSet error code ${rc}`}), compileContext);
}
}

View file

@ -1,7 +1,7 @@
import { KeyAddress } from "../kmw-compiler/validate-layout-file.js";
import { kmnfile } from "../kmw-compiler/compiler-globals.js";
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec as m, CompilerMessageObjectSpec as mx, CompilerMessageDef as def, CompilerMessageSpecWithException, KeymanUrls, ObjectWithMetadata } from "@keymanapp/developer-utils";
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec as m, CompilerMessageObjectSpec as mx, CompilerMessageDef as def, CompilerMessageSpecWithException, KeymanUrls } from "@keymanapp/developer-utils";
import { ObjectWithCompileContext } from "@keymanapp/common-types";
const Namespace = CompilerErrorNamespace.KmnCompiler;
const SevInfo = CompilerErrorSeverity.Info | Namespace;
const SevHint = CompilerErrorSeverity.Hint | Namespace;
@ -107,8 +107,8 @@ export class KmnCompilerMessages {
);
static FATAL_UnicodeSetOutOfRange = SevFatal | 0x904;
static Fatal_UnicodeSetOutOfRange = (x?: ObjectWithMetadata) => mx(
this.FATAL_UnicodeSetOutOfRange, x,
static Fatal_UnicodeSetOutOfRange = (compileContext?: ObjectWithCompileContext) => mx(
this.FATAL_UnicodeSetOutOfRange, compileContext,
`UnicodeSet buffer was too small`,
`Raised when caller to UnicodeSet functions provides an invalid buffer. If
you experience this error, it should be reported to the Keyman team for
@ -118,8 +118,8 @@ export class KmnCompilerMessages {
// TODO: rename the following functions to Error_UsetHasStrings etc
static ERROR_UnicodeSetHasStrings = SevError | 0x905;
static Error_UnicodeSetHasStrings = (x?: ObjectWithMetadata) => mx(
this.ERROR_UnicodeSetHasStrings, x,
static Error_UnicodeSetHasStrings = (compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_UnicodeSetHasStrings, compileContext,
`uset contains strings, not allowed`,
`The provided uset uses multi-character strings, (\`{}\` notation, e.g.
\`[żġħ{ie}{għ}]\`. ). Although full UnicodeSets support strings, LDML
@ -130,8 +130,8 @@ export class KmnCompilerMessages {
);
static ERROR_UnicodeSetHasProperties = SevError | 0x906;
static Error_UnicodeSetHasProperties = (x?: ObjectWithMetadata) => mx(
this.ERROR_UnicodeSetHasProperties, x,
static Error_UnicodeSetHasProperties = (compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_UnicodeSetHasProperties, compileContext,
`uset contains properties, not allowed`,
`The provided uset uses property notation (\`\\p{…}\` or \`[:…:]\`). LDML
keyboards do not support Unicode properties in usets, because that would
@ -142,8 +142,8 @@ export class KmnCompilerMessages {
);
static ERROR_UnicodeSetSyntaxError = SevError | 0x907;
static Error_UnicodeSetSyntaxError = (x?: ObjectWithMetadata) => mx(
this.ERROR_UnicodeSetSyntaxError, x,
static Error_UnicodeSetSyntaxError = (compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_UnicodeSetSyntaxError, compileContext,
`uset had a Syntax Error while parsing`,
`The provided uset has a syntax error and could not be parsed. Verify the
format of the uset against the specification.

View file

@ -62,7 +62,7 @@ export class DispCompiler extends SectionCompiler {
const result = new Disp();
// displayOptions
result.baseCharacter = sections.strs.allocString(this.keyboard3.displays?.displayOptions?.baseCharacter, { unescape: true, x: this.keyboard3?.displays?.displayOptions });
result.baseCharacter = sections.strs.allocString(this.keyboard3.displays?.displayOptions?.baseCharacter, { unescape: true, compileContext:this.keyboard3?.displays?.displayOptions });
// displays
result.disps = this.keyboard3.displays?.display.map(display => ({
@ -70,13 +70,13 @@ export class DispCompiler extends SectionCompiler {
stringVariables: true,
markers: true,
unescape: true,
x: display,
compileContext: display,
}, sections),
id: sections.strs.allocString(display.keyId, { x: display }), // not escaped, not substituted
id: sections.strs.allocString(display.keyId, { compileContext: display }), // not escaped, not substituted
display: sections.strs.allocString(display.display, {
stringVariables: true,
unescape: true,
x: display,
compileContext: display,
}, sections),
})) || []; // TODO-LDML: need coverage for the []

View file

@ -1,7 +1,8 @@
import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants';
import { SectionCompiler } from "./section-compiler.js";
import { util, KMXPlus, LdmlKeyboardTypes } from "@keymanapp/common-types";
import { CompilerCallbacks, LDMLKeyboard, ObjectWithMetadata } from "@keymanapp/developer-utils";
import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils";
import { ObjectWithCompileContext } from "@keymanapp/common-types";
import { VarsCompiler } from './vars.js';
import { LdmlCompilerMessages } from './ldml-compiler-messages.js';
@ -34,7 +35,7 @@ export class StrsCompiler extends EmptyCompiler {
const strs = <KMXPlus.Strs>section;
/** attempt to find a context object for the string */
function findContextForString(s: string): ObjectWithMetadata {
function findContextForString(s: string): ObjectWithCompileContext {
// try exact match
for(const str of strs.strings) {
if (str.value == s) return str.context;

View file

@ -324,16 +324,16 @@ export class KeysCompiler extends SectionCompiler {
// allocate the in-memory <flick id=…>
const flicks: KeysFlicks = new KeysFlicks(
sections.strs.allocString(flickId, { x: flick })
sections.strs.allocString(flickId, { compileContext: flick })
);
// add data from each segment
for (const flickSegment of flick.flickSegment) {
const { keyId, directions } = flickSegment;
const keyIdStr = sections.strs.allocString(keyId, { x: flickSegment });
const keyIdStr = sections.strs.allocString(keyId, { compileContext: flickSegment });
const directionsList: ListItem = sections.list.allocListFromSpaces(
directions,
{ x: flickSegment },
{ compileContext: flickSegment },
sections);
flicks.flicks.push({
directions: directionsList,
@ -370,20 +370,20 @@ export class KeysCompiler extends SectionCompiler {
if (!!gap) {
flags |= constants.keys_key_flags_gap;
}
const id = sections.strs.allocString(key.id, { x: key });
const id = sections.strs.allocString(key.id, { compileContext: key });
const longPress: ListItem = sections.list.allocListFromSpaces(
longPressKeyIds, { x: key },
longPressKeyIds, { compileContext: key },
sections);
const longPressDefault = sections.strs.allocString(longPressDefaultKeyId,
{ x: key },
{ compileContext: key },
sections);
const multiTap: ListItem = sections.list.allocListFromSpaces(
multiTapKeyIds,
{ x: key },
{ compileContext: key },
sections);
const keySwitch = sections.strs.allocString(layerId, { x: key }); // 'switch' is a reserved word
const keySwitch = sections.strs.allocString(layerId, { compileContext: key }); // 'switch' is a reserved word
const toRaw = output;
@ -396,7 +396,7 @@ export class KeysCompiler extends SectionCompiler {
unescape: true,
singleOk: true,
nfd: true,
x: key,
compileContext: key,
},
sections);
if (!to.isOneChar) {

View file

@ -60,7 +60,7 @@ export class LayrCompiler extends SectionCompiler {
if (totalLayerCount === 0) { // TODO-LDML: does not validate touch layers yet
// no layers seen anywhere
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(this.keyboard3?.layers[0]));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(this.keyboard3));
}
return valid;
}
@ -69,13 +69,13 @@ export class LayrCompiler extends SectionCompiler {
const sect = new Layr();
sect.lists = this.keyboard3.layers.map((layers) => {
const hardware = sections.strs.allocString(layers.formId, {x:layers});
const hardware = sections.strs.allocString(layers.formId, {compileContext: layers});
// Already validated in validate
const layerEntries = [];
for (const layer of layers.layer) {
const rows = layer.row.map((row) => {
const erow: LayrRow = {
keys: row.keys.trim().split(/[ \t]+/).map((id) => sections.strs.allocString(id, { x: row })),
keys: row.keys.trim().split(/[ \t]+/).map((id) => sections.strs.allocString(id, { compileContext: row })),
};
// include linenumber info for row
return SectionCompiler.copySymbols(erow, row);
@ -84,7 +84,7 @@ export class LayrCompiler extends SectionCompiler {
// push a layer entry for each modifier set
for (const mod of mods) {
layerEntries.push({
id: sections.strs.allocString(layer.id, {x:layer}),
id: sections.strs.allocString(layer.id, {compileContext: layer}),
mod,
rows,
});

View file

@ -1,4 +1,5 @@
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageObjectSpec as mx, CompilerMessageSpec as m, CompilerMessageDef as def, ObjectWithMetadata } from '@keymanapp/developer-utils';
import { ObjectWithCompileContext } from "@keymanapp/common-types";
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageObjectSpec as mx, CompilerMessageSpec as m, CompilerMessageDef as def } from '@keymanapp/developer-utils';
// const SevInfo = CompilerErrorSeverity.Info | CompilerErrorNamespace.LdmlKeyboardCompiler;
const SevHint = CompilerErrorSeverity.Hint | CompilerErrorNamespace.LdmlKeyboardCompiler;
const SevWarn = CompilerErrorSeverity.Warn | CompilerErrorNamespace.LdmlKeyboardCompiler;
@ -13,38 +14,38 @@ const SevErrorTransform = SevError | 0xF00;
*/
export class LdmlCompilerMessages {
static HINT_NormalizationDisabled = SevHint | 0x0001;
static Hint_NormalizationDisabled = (x?: ObjectWithMetadata) => mx(
this.HINT_NormalizationDisabled, x,
static Hint_NormalizationDisabled = (compileContext?: ObjectWithCompileContext) => mx(
this.HINT_NormalizationDisabled, compileContext,
`normalization=disabled is not recommended.`,
);
static ERROR_InvalidLocale = SevError | 0x0002;
static Error_InvalidLocale = (o:{tag: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidLocale, x,
static Error_InvalidLocale = (o:{tag: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_InvalidLocale, compileContext,
`Invalid BCP 47 locale form '${def(o.tag)}'`,
);
static ERROR_HardwareLayerHasTooManyRows = SevError | 0x0003;
static Error_HardwareLayerHasTooManyRows = (x?: ObjectWithMetadata) => mx(
this.ERROR_HardwareLayerHasTooManyRows, x,
static Error_HardwareLayerHasTooManyRows = (compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_HardwareLayerHasTooManyRows, compileContext,
`'hardware' layer has too many rows`,
);
static ERROR_RowOnHardwareLayerHasTooManyKeys = SevError | 0x0004;
static Error_RowOnHardwareLayerHasTooManyKeys = (o: { row: number, hardware: string, modifiers: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_RowOnHardwareLayerHasTooManyKeys, x,
static Error_RowOnHardwareLayerHasTooManyKeys = (o: { row: number, hardware: string, modifiers: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_RowOnHardwareLayerHasTooManyKeys, compileContext,
`Row #${def(o.row)} on 'hardware' ${def(o.hardware)} layer for modifier ${def(o.modifiers)} has too many keys`,
);
static ERROR_KeyNotFoundInKeyBag = SevError | 0x0005;
static Error_KeyNotFoundInKeyBag = (o: { keyId: string, col: number, row: number, layer: string, form: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_KeyNotFoundInKeyBag, x,
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`,
);
static HINT_OneOrMoreRepeatedLocales = SevHint | 0x0006;
static Hint_OneOrMoreRepeatedLocales = (x?: ObjectWithMetadata) => mx(
this.HINT_OneOrMoreRepeatedLocales, x,
static Hint_OneOrMoreRepeatedLocales = (compileContext?: ObjectWithCompileContext) => mx(
this.HINT_OneOrMoreRepeatedLocales, compileContext,
`After minimization, one or more locales is repeated and has been removed`,
);
@ -54,38 +55,38 @@ export class LdmlCompilerMessages {
m(this.ERROR_InvalidFile, `The source file has an invalid structure: ${def(o.errorText)}`);
static HINT_LocaleIsNotMinimalAndClean = SevHint | 0x0008;
static Hint_LocaleIsNotMinimalAndClean = (o:{sourceLocale: string, locale: string}, x?: ObjectWithMetadata) => mx(
this.HINT_LocaleIsNotMinimalAndClean, x,
static Hint_LocaleIsNotMinimalAndClean = (o:{sourceLocale: string, locale: string}, compileContext?: ObjectWithCompileContext) => mx(
this.HINT_LocaleIsNotMinimalAndClean, compileContext,
`Locale '${def(o.sourceLocale)}' is not minimal or correctly formatted and should be '${def(o.locale)}'`,
);
static ERROR_InvalidScanCode = SevError | 0x0009;
static Error_InvalidScanCode = (o: { codes: string, id: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidScanCode, x,
static Error_InvalidScanCode = (o: { codes: string, id: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_InvalidScanCode, compileContext,
`Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}'`,
);
static WARN_CustomForm = SevWarn | 0x000A;
static Warn_CustomForm = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.WARN_CustomForm, x,
static Warn_CustomForm = (o: { id: string }, compileContext?: ObjectWithCompileContext) => mx(
this.WARN_CustomForm, compileContext,
`Custom <form id="${def(o.id)}"> element. Key layout may not be as expected.`,
);
static ERROR_GestureKeyNotFoundInKeyBag = SevError | 0x000B;
static Error_GestureKeyNotFoundInKeyBag = (o:{keyId: string, parentKeyId: string, attribute: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_GestureKeyNotFoundInKeyBag, x,
static Error_GestureKeyNotFoundInKeyBag = (o:{keyId: string, parentKeyId: string, attribute: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_GestureKeyNotFoundInKeyBag, compileContext,
`Key '${def(o.keyId)}' not found in key bag, referenced from other '${def(o.parentKeyId)}' in ${def(o.attribute)}`,
);
static HINT_NoDisplayForMarker = SevHint | 0x000C;
static Hint_NoDisplayForMarker = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.HINT_NoDisplayForMarker, x,
static Hint_NoDisplayForMarker = (o: { id: string }, compileContext?: ObjectWithCompileContext) => mx(
this.HINT_NoDisplayForMarker, compileContext,
`Key element with id "${def(o.id)}" has only marker output, but there is no matching display element by output or keyId. Keycap may be blank.`,
);
static ERROR_InvalidVersion = SevError | 0x000D;
static Error_InvalidVersion = (o: { version: string; }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidVersion, x,
static Error_InvalidVersion = (o: { version: string; }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_InvalidVersion, compileContext,
`Version number '${def(o.version)}' must be a semantic version format string with 'major.minor.patch' components.`,
`The version number in the LDML keyboard file must be a [semantic
version](https://semver.org) (semver) string. This string has a format of three
@ -98,186 +99,186 @@ export class LdmlCompilerMessages {
);
static ERROR_MustBeAtLeastOneLayerElement = SevError | 0x000E;
static Error_MustBeAtLeastOneLayerElement = (x?: ObjectWithMetadata) => mx(
this.ERROR_MustBeAtLeastOneLayerElement, x,
static Error_MustBeAtLeastOneLayerElement = (compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_MustBeAtLeastOneLayerElement, compileContext,
`The source file must contain at least one layer element.`,
);
static HINT_NoDisplayForSwitch = SevHint | 0x000F;
static Hint_NoDisplayForSwitch = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.HINT_NoDisplayForSwitch, x,
static Hint_NoDisplayForSwitch = (o: { id: string }, compileContext?: ObjectWithCompileContext) => mx(
this.HINT_NoDisplayForSwitch, compileContext,
`Key element with id "${def(o.id)}" is a layer switch key, but there is no matching display element by keyId. Keycap may be blank.`,
);
static ERROR_DisplayIsRepeated = SevError | 0x0010;
static Error_DisplayIsRepeated = (o:{display?: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_DisplayIsRepeated, x,
static Error_DisplayIsRepeated = (o:{display?: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_DisplayIsRepeated, compileContext,
`display display='${def(o.display)}' refers to the same keyId or output as another entry.`,
);
static ERROR_KeyMissingToGapOrSwitch = SevError | 0x0011;
static Error_KeyMissingToGapOrSwitch = (o:{keyId: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_KeyMissingToGapOrSwitch, x,
static Error_KeyMissingToGapOrSwitch = (o:{keyId: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_KeyMissingToGapOrSwitch, compileContext,
`key id='${def(o.keyId)}' must have either output=, gap=, or layerId=.`,
);
static ERROR_ExcessHardware = SevError | 0x0012;
static Error_ExcessHardware = (o:{formId: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_ExcessHardware, x,
static Error_ExcessHardware = (o:{formId: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_ExcessHardware, compileContext,
`layers formId=${def(o.formId)}: Can only have one non-'touch' element`,
);
static ERROR_InvalidHardware = SevError | 0x0013;
static Error_InvalidHardware = (o: { formId: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidHardware, x,
static Error_InvalidHardware = (o: { formId: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_InvalidHardware, compileContext,
`layers has invalid value formId=${def(o.formId)}`,
);
static ERROR_InvalidModifier = SevError | 0x0014;
static Error_InvalidModifier = (o:{modifiers: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidModifier, x,
static Error_InvalidModifier = (o:{modifiers: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_InvalidModifier, compileContext,
`layer has invalid modifiers='${def(o.modifiers)}'`,
);
static ERROR_MissingFlicks = SevError | 0x0015;
static Error_MissingFlicks = (o: {id: string, flickId: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_MissingFlicks, x,
static Error_MissingFlicks = (o: {id: string, flickId: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_MissingFlicks, compileContext,
`key id=${def(o.id)} refers to missing flickId=${def(o.flickId)}`,
);
static ERROR_DuplicateVariable = SevError | 0x0016;
static Error_DuplicateVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_DuplicateVariable, x,
static Error_DuplicateVariable = (o:{id: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_DuplicateVariable, compileContext,
`duplicate variable: id=${def(o.id)}`,
);
// Not hit due to XML parsing
static ERROR_InvalidTransformsType = SevError | 0x0018;
static Error_InvalidTransformsType = (o:{type: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidTransformsType, x,
static Error_InvalidTransformsType = (o:{type: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_InvalidTransformsType, compileContext,
`Invalid transforms type: '${def(o.type)}'`,
);
static ERROR_DuplicateTransformsType = SevError | 0x0019;
static Error_DuplicateTransformsType = (o:{type: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_DuplicateTransformsType, x,
static Error_DuplicateTransformsType = (o:{type: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_DuplicateTransformsType, compileContext,
`Duplicate transforms type: '${def(o.type)}'`,
);
static ERROR_MixedTransformGroup = SevError | 0x001A;
static Error_MixedTransformGroup = (x?: ObjectWithMetadata) => mx(
this.ERROR_MixedTransformGroup, x,
static Error_MixedTransformGroup = (compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_MixedTransformGroup, compileContext,
`transformGroup cannot contain both reorder and transform elements`,
);
static ERROR_EmptyTransformGroup = SevError | 0x001B;
static Error_EmptyTransformGroup = (x?: ObjectWithMetadata) => mx(
this.ERROR_EmptyTransformGroup, x,
static Error_EmptyTransformGroup = (compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_EmptyTransformGroup, compileContext,
`transformGroup must have either reorder or transform elements`,
);
static ERROR_MissingStringVariable = SevError | 0x001C;
static Error_MissingStringVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_MissingStringVariable, x,
static Error_MissingStringVariable = (o:{id: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_MissingStringVariable, compileContext,
`Reference to undefined string variable: \${${def(o.id)}}`,
);
static ERROR_MissingSetVariable = SevError | 0x001D;
static Error_MissingSetVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_MissingSetVariable, x,
static Error_MissingSetVariable = (o:{id: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_MissingSetVariable, compileContext,
`Reference to undefined set variable: \$[${def(o.id)}]`,
);
static ERROR_MissingUnicodeSetVariable = SevError | 0x001E;
static Error_MissingUnicodeSetVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_MissingUnicodeSetVariable, x,
static Error_MissingUnicodeSetVariable = (o:{id: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_MissingUnicodeSetVariable, compileContext,
`Reference to undefined UnicodeSet variable: \$[${def(o.id)}]`,
);
static ERROR_NeedSpacesBetweenSetVariables = SevError | 0x001F;
static Error_NeedSpacesBetweenSetVariables = (o:{item: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_NeedSpacesBetweenSetVariables, x,
static Error_NeedSpacesBetweenSetVariables = (o:{item: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_NeedSpacesBetweenSetVariables, compileContext,
`Need spaces between set variables: ${def(o.item)}`,
);
static ERROR_CantReferenceSetFromUnicodeSet = SevError | 0x0020;
static Error_CantReferenceSetFromUnicodeSet = (o:{id: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_CantReferenceSetFromUnicodeSet, x,
static Error_CantReferenceSetFromUnicodeSet = (o:{id: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_CantReferenceSetFromUnicodeSet, compileContext,
`Illegal use of set variable from within UnicodeSet: \$[${def(o.id)}]`
);
static ERROR_MissingMarkers = SevError | 0x0021;
static Error_MissingMarkers = (o: { ids: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_MissingMarkers, x,
static Error_MissingMarkers = (o: { ids: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_MissingMarkers, compileContext,
`Markers used for matching but not defined: ${def(o.ids)}`
);
static ERROR_DisplayNeedsToOrId = SevError | 0x0022;
static Error_DisplayNeedsToOrId = (o:{display?: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_DisplayNeedsToOrId, x,
static Error_DisplayNeedsToOrId = (o:{display?: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_DisplayNeedsToOrId, compileContext,
`display display='${def(o.display)}' needs output= or keyId=, but not both`,
);
static HINT_PUACharacters = SevHint | 0x0023;
static Hint_PUACharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx(
this.HINT_PUACharacters, x,
static Hint_PUACharacters = (o: { count: number, lowestCh: string }, compileContext?: ObjectWithCompileContext) => mx(
this.HINT_PUACharacters, compileContext,
`File contains ${def(o.count)} PUA character(s), including ${def(o.lowestCh)}`,
);
static WARN_UnassignedCharacters = SevWarn | 0x0024;
static Warn_UnassignedCharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx(
this.WARN_UnassignedCharacters, x,
static Warn_UnassignedCharacters = (o: { count: number, lowestCh: string }, compileContext?: ObjectWithCompileContext) => mx(
this.WARN_UnassignedCharacters, compileContext,
`File contains ${def(o.count)} unassigned character(s), including ${def(o.lowestCh)}`,
);
static ERROR_IllegalCharacters = SevError | 0x0025;
static Error_IllegalCharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalCharacters, x,
static Error_IllegalCharacters = (o: { count: number, lowestCh: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_IllegalCharacters, compileContext,
`File contains ${def(o.count)} illegal character(s), including ${def(o.lowestCh) }`,
);
static HINT_CharClassImplicitDenorm = SevHint | 0x0026;
static Hint_CharClassImplicitDenorm = (o: { lowestCh: number }, x?: ObjectWithMetadata) => mx(
this.HINT_CharClassImplicitDenorm, x,
static Hint_CharClassImplicitDenorm = (o: { lowestCh: number }, compileContext?: ObjectWithCompileContext) => mx(
this.HINT_CharClassImplicitDenorm, compileContext,
`File has character classes which span non-NFD character(s), including ${def(o.lowestCh)}. These will not match any text.`,
);
static WARN_CharClassExplicitDenorm = SevWarn | 0x0027;
static Warn_CharClassExplicitDenorm = (o: { lowestCh: number }, x?: ObjectWithMetadata) => mx(
this.WARN_CharClassExplicitDenorm, x,
static Warn_CharClassExplicitDenorm = (o: { lowestCh: number }, compileContext?: ObjectWithCompileContext) => mx(
this.WARN_CharClassExplicitDenorm, compileContext,
`File has character classes which include non-NFD characters(s), including ${def(o.lowestCh)}. These will not match any text.`,
);
// Available: 0x0028
static ERROR_InvalidVariableIdentifier = SevError | 0x0029;
static Error_InvalidVariableIdentifier = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidVariableIdentifier, x,
static Error_InvalidVariableIdentifier = (o: { id: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_InvalidVariableIdentifier, compileContext,
`Invalid variable identifier "${def(o.id)}". Identifiers must be between 1 and 32 characters, and can use A-Z, a-z, 0-9, and _.`,
);
static ERROR_InvalidMarkerIdentifier = SevError | 0x002A;
static Error_InvalidMarkerIdentifier = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidMarkerIdentifier, x,
static Error_InvalidMarkerIdentifier = (o: { id: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_InvalidMarkerIdentifier, compileContext,
`Invalid marker identifier "\m{${def(o.id)}}". Identifiers must be between 1 and 32 characters, and can use A-Z, a-z, 0-9, and _.`,
);
static WARN_StringDenorm = SevWarn | 0x002B;
static Warn_StringDenorm = (o: { s: string }, x?: ObjectWithMetadata) => mx(
this.WARN_StringDenorm, x,
static Warn_StringDenorm = (o: { s: string }, compileContext?: ObjectWithCompileContext) => mx(
this.WARN_StringDenorm, compileContext,
`File contains string "${def(o.s)}" that is neither NFC nor NFD.`,
);
static ERROR_DuplicateLayerWidth = SevError | 0x002C;
static Error_DuplicateLayerWidth = (o: { minDeviceWidth: number }, x?: ObjectWithMetadata) => mx(
this.ERROR_DuplicateLayerWidth, x,
static Error_DuplicateLayerWidth = (o: { minDeviceWidth: number }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_DuplicateLayerWidth, compileContext,
`Two or more layers have minDeviceWidth=${def(o.minDeviceWidth)}`,
`Touch layers must have distinct widths.`
);
static ERROR_InvalidLayerWidth = SevError | 0x002D;
static Error_InvalidLayerWidth = (o: { minDeviceWidth: number }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidLayerWidth, x,
static Error_InvalidLayerWidth = (o: { minDeviceWidth: number }, 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)
);
@ -285,8 +286,8 @@ export class LdmlCompilerMessages {
// Available: 0x02E-0x2F
static ERROR_InvalidQuadEscape = SevError | 0x0030;
static Error_InvalidQuadEscape = (o: { cp: string, recommended: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidQuadEscape, x,
static Error_InvalidQuadEscape = (o: { cp: string, recommended: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_InvalidQuadEscape, compileContext,
`Invalid escape "${def(o.cp)}"`,
`**Hint**: Use "${def(o.recommended)}"`,
);
@ -296,8 +297,8 @@ export class LdmlCompilerMessages {
// This is a bit of a catch-all and represents messages bubbling up from the underlying regex engine
static ERROR_UnparseableTransformFrom = SevErrorTransform | 0x00;
static Error_UnparseableTransformFrom = (o: { from: string, message: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_UnparseableTransformFrom, x,
static Error_UnparseableTransformFrom = (o: { from: string, message: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_UnparseableTransformFrom, compileContext,
`Invalid transform from="${def(o.from)}": "${def(o.message)}"`,
);
@ -306,8 +307,8 @@ export class LdmlCompilerMessages {
//------------------------------------------------------------------------------|
static ERROR_IllegalTransformDollarsign = SevErrorTransform | 0x01;
static Error_IllegalTransformDollarsign = (o: { from: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalTransformDollarsign, x,
static Error_IllegalTransformDollarsign = (o: { from: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_IllegalTransformDollarsign, compileContext,
`Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, `
**Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a
variable name, the variable name may not be valid (A-Z, a-z, 0-9, _, 32
@ -315,36 +316,36 @@ export class LdmlCompilerMessages {
`);
static ERROR_TransformFromMatchesNothing = SevErrorTransform | 0x02;
static Error_TransformFromMatchesNothing = (o: { from: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_TransformFromMatchesNothing, x,
static Error_TransformFromMatchesNothing = (o: { from: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_TransformFromMatchesNothing, compileContext,
`Invalid transfom from="${def(o.from)}": Matches an empty string.`
);
static ERROR_IllegalTransformPlus = SevErrorTransform | 0x03;
static Error_IllegalTransformPlus = (o: { from: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalTransformPlus, x,
static Error_IllegalTransformPlus = (o: { from: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_IllegalTransformPlus, compileContext,
`Invalid transform from="${def(o.from)}": Unescaped plus (+) is not valid transform syntax.`, `
**Hint**: Use \`\\+\` to match a literal plus.
`);
static ERROR_IllegalTransformAsterisk = SevErrorTransform | 0x04;
static Error_IllegalTransformAsterisk = (o: { from: string }, x?: ObjectWithMetadata) =>mx(
this.ERROR_IllegalTransformAsterisk, x,
static Error_IllegalTransformAsterisk = (o: { from: string }, compileContext?: ObjectWithCompileContext) =>mx(
this.ERROR_IllegalTransformAsterisk, compileContext,
`Invalid transform from="${def(o.from)}": Unescaped asterisk (*) is not valid transform syntax.`, `
**Hint**: Use \`\\*\` to match a literal asterisk.
`);
static ERROR_IllegalTransformToUset = SevErrorTransform | 0x05;
static Error_IllegalTransformToUset = (o: { to: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalTransformToUset, x,
static Error_IllegalTransformToUset = (o: { to: string }, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_IllegalTransformToUset, compileContext,
`Invalid transform to="${def(o.to)}": Set variable (\\$[…]) cannot be used in 'to=' unless part of a map.`, `
**Hint**: If a map was meant, must use the form
\`<transform from="($[fromSet])" to="$[1:toSet]"/>\`.
`);
static ERROR_UnparseableTransformTo = SevErrorTransform | 0x06;
static Error_UnparseableTransformTo = (o: {to: string, message: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_UnparseableTransformTo, x,
static Error_UnparseableTransformTo = (o: {to: string, message: string}, compileContext?: ObjectWithCompileContext) => mx(
this.ERROR_UnparseableTransformTo, compileContext,
`Invalid transform to="${def(o.to)}": "${def(o.message)}"`,
);
}

View file

@ -1,6 +1,7 @@
import { constants } from "@keymanapp/ldml-keyboard-constants";
import { KMXPlus } from '@keymanapp/common-types';
import { LDMLKeyboard, ObjectWithMetadata } from '@keymanapp/developer-utils';
import { LDMLKeyboard } from '@keymanapp/developer-utils';
import { ObjectWithCompileContext } from "@keymanapp/common-types";
import { SectionCompiler } from "./section-compiler.js";
import { LdmlCompilerMessages } from "./ldml-compiler-messages.js";
@ -28,7 +29,7 @@ export class LocaCompiler extends SectionCompiler {
* @param loc locale tag
* @returns context metadata or null
*/
private contextForLocale(loc: string) : ObjectWithMetadata {
private contextForLocale(loc: string) : ObjectWithCompileContext {
if (loc == this.keyboard3.locale) return this.keyboard3;
return this.keyboard3?.locales;
}
@ -71,7 +72,7 @@ export class LocaCompiler extends SectionCompiler {
// yet include `getCanonicalLocales` but node 16 does include it so we can
// safely use it. Also well supported in modern browsers.
const canonicalLocales = (Intl as any).getCanonicalLocales(locales) as string[];
result.locales = canonicalLocales.map(locale => sections.strs.allocString(locale, {x: this.contextForLocale(locale)}));
result.locales = canonicalLocales.map(locale => sections.strs.allocString(locale, {compileContext: this.contextForLocale(locale)}));
if(result.locales.length < locales.length) {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales(this.keyboard3?.locales));

View file

@ -54,17 +54,17 @@ export class MetaCompiler extends SectionCompiler {
public compile(sections: DependencySections): Meta {
const result = new Meta();
result.author = sections.strs.allocString(this.keyboard3.info?.author,
{x: this.keyboard3.info});
{compileContext: this.keyboard3.info});
result.conform = sections.strs.allocString(this.keyboard3.conformsTo,
{x: this.keyboard3});
{compileContext: this.keyboard3});
result.layout = sections.strs.allocString(this.keyboard3.info?.layout,
{x: this.keyboard3.info});
{compileContext: this.keyboard3.info});
result.name = sections.strs.allocString(this.keyboard3.info?.name,
{x: this.keyboard3.info});
{compileContext: this.keyboard3.info});
result.indicator = sections.strs.allocString(this.keyboard3.info?.indicator,
{x: this.keyboard3.info});
{compileContext: this.keyboard3.info});
result.version = sections.strs.allocString(this.keyboard3.version?.number ?? "0.0.0",
{x: this.keyboard3.version});
{compileContext: this.keyboard3.version});
result.settings =
(this.keyboard3.settings?.normalization == "disabled" ? KeyboardSettings.normalizationDisabled : 0);
return result;

View file

@ -1,5 +1,5 @@
import { LdmlKeyboardTypes } from "@keymanapp/common-types";
import { ObjectWithMetadata } from "@keymanapp/developer-utils";
import { ObjectWithCompileContext } from '@keymanapp/common-types';
/**
* Verb for SubstitutionTracker.add()
@ -15,7 +15,7 @@ export enum SubstitutionUse {
variable,
}
type SubstitutionSet = Map<string, ObjectWithMetadata>;
type SubstitutionSet = Map<string, ObjectWithCompileContext>;
/** Tracks usage of markers */
export class SubstitutionTracker {
@ -29,10 +29,10 @@ export class SubstitutionTracker {
all: SubstitutionSet;
constructor() {
this.emitted = new Map<string, ObjectWithMetadata>();
this.consumed = new Map<string, ObjectWithMetadata>();
this.matched = new Map<string, ObjectWithMetadata>();
this.all = new Map<string, ObjectWithMetadata>();
this.emitted = new Map<string, ObjectWithCompileContext>();
this.consumed = new Map<string, ObjectWithCompileContext>();
this.matched = new Map<string, ObjectWithCompileContext>();
this.all = new Map<string, ObjectWithCompileContext>();
}
/**
@ -40,33 +40,33 @@ export class SubstitutionTracker {
* @param verb what kind of use we are adding
* @param markers list of substitutions to add
*/
add(verb: SubstitutionUse, markers: string[], x?: ObjectWithMetadata) {
add(verb: SubstitutionUse, markers: string[], compileContext?: ObjectWithCompileContext) {
if (!markers.length) {
return; // skip if empty
}
x = x || {}; // need at least an empty object
compileContext = compileContext || {}; // need at least an empty object
if (verb == SubstitutionUse.emit) {
markers.forEach((m) => {
this.emitted.set(m, x);
this.all.set(m,x);
this.emitted.set(m, compileContext);
this.all.set(m, compileContext);
});
} else if (verb == SubstitutionUse.consume) {
markers.forEach((m) => {
this.consumed.set(m,x);
this.all.set(m,x);
this.consumed.set(m, compileContext);
this.all.set(m, compileContext);
});
} else if (verb == SubstitutionUse.match) {
markers.forEach((m) => {
this.matched.set(m,x);
this.all.set(m,x);
this.matched.set(m, compileContext);
this.all.set(m, compileContext);
});
} else if (verb == SubstitutionUse.variable) {
markers.forEach((m) => {
// we don't know, so add it to all three
this.matched.set(m,x);
this.emitted.set(m,x);
this.consumed.set(m,x);
this.all.set(m,x);
this.matched.set(m, compileContext);
this.emitted.set(m, compileContext);
this.consumed.set(m, compileContext);
this.all.set(m, compileContext);
});
/* c8 skip next 3 */
} else {
@ -77,22 +77,22 @@ export class SubstitutionTracker {
/** rollup of several substitution types */
export class Substitutions {
addSetAndStringSubtitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.set.add(verb, LdmlKeyboardTypes.VariableParser.allSetReferences(str), x);
this.addStringAndMarkerSubstitution(verb, str, x);
addSetAndStringSubtitution(verb: SubstitutionUse, str?: string, compileContext?: ObjectWithCompileContext) {
this.set.add(verb, LdmlKeyboardTypes.VariableParser.allSetReferences(str), compileContext);
this.addStringAndMarkerSubstitution(verb, str, compileContext);
}
/** add a string that can have string var substitutions or markers */
addStringAndMarkerSubstitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.addMarkers(verb, str, x);
this.addStringSubstitution(verb, str, x);
addStringAndMarkerSubstitution(verb: SubstitutionUse, str?: string, compileContext?: ObjectWithCompileContext) {
this.addMarkers(verb, str, compileContext);
this.addStringSubstitution(verb, str, compileContext);
}
addStringSubstitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.string.add(verb, LdmlKeyboardTypes.VariableParser.allStringReferences(str), x);
addStringSubstitution(verb: SubstitutionUse, str?: string, compileContext?: ObjectWithCompileContext) {
this.string.add(verb, LdmlKeyboardTypes.VariableParser.allStringReferences(str), compileContext);
}
/** add a string that's just markers */
addMarkers(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.markers.add(verb, LdmlKeyboardTypes.MarkerParser.allReferences(str), x);
LdmlKeyboardTypes.MarkerParser.allBrokenReferences(str).forEach(m => this.badMarkers.set(m, x));
addMarkers(verb: SubstitutionUse, str?: string, compileContext?: ObjectWithCompileContext) {
this.markers.add(verb, LdmlKeyboardTypes.MarkerParser.allReferences(str), compileContext);
LdmlKeyboardTypes.MarkerParser.allBrokenReferences(str).forEach(m => this.badMarkers.set(m, compileContext));
}
// all valid markers
markers: SubstitutionTracker;
@ -110,6 +110,6 @@ export class Substitutions {
this.set = new SubstitutionTracker();
this.string = new SubstitutionTracker();
this.uset = new SubstitutionTracker();
this.badMarkers = new Map<string, ObjectWithMetadata>();
this.badMarkers = new Map<string, ObjectWithCompileContext>();
}
}

View file

@ -1,6 +1,7 @@
import { constants, SectionIdent } from "@keymanapp/ldml-keyboard-constants";
import { KMXPlus, LdmlKeyboardTypes, util } from '@keymanapp/common-types';
import { CompilerCallbacks, LDMLKeyboard, ObjectWithMetadata } from "@keymanapp/developer-utils";
import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils";
import { ObjectWithCompileContext } from "@keymanapp/common-types";
import { SectionCompiler } from "./section-compiler.js";
import Bksp = KMXPlus.Bksp;
@ -142,7 +143,7 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
private compileTransform(sections: DependencySections, transform: LKTransform) : TranTransform {
// we have lots of strings to allocate, that will all have these options
const stropts : StrsOptions = { x: transform };
const stropts : StrsOptions = { compileContext: transform };
const result = new TranTransform();
// setup for serializing
result._from = transform.from;
@ -219,7 +220,7 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
// cookedFrom is cooked above, since there's some special treatment
result.from = sections.strs.allocString(cookedFrom, {
unescape: false,
x: transform,
compileContext: transform,
}, sections);
// 'to' is handled via allocString
result.to = sections.strs.allocString(transform.to, {
@ -227,7 +228,7 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
markers: true,
unescape: true,
nfd: true,
x: transform,
compileContext: transform,
}, sections);
return result;
}
@ -237,9 +238,9 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
* We have already checked that it's not a mapTo,
* so there should not be any illegal substitutions.
*/
private isValidTo(to: string, x?: ObjectWithMetadata) : boolean {
private isValidTo(to: string, compileContext?: ObjectWithCompileContext) : boolean {
if (/(?<!\\)(?:\\\\)*\$\[/.test(to)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformToUset({ to }, x));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformToUset({ to }, compileContext));
return false;
}
return true;
@ -251,18 +252,18 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
* @param from the original from - for error reporting
* @returns true if OK
*/
private isValidRegex(cookedFrom: string, from: string, x?: ObjectWithMetadata) : boolean {
private isValidRegex(cookedFrom: string, from: string, compileContext?: ObjectWithCompileContext) : boolean {
// check for any unescaped dollar sign here
if (/(?<!\\)(?:\\\\)*\$/.test(cookedFrom)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformDollarsign({ from }, x));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformDollarsign({ from }, compileContext));
return false;
}
if (/(?<!\\)(?:\\\\)*\*/.test(cookedFrom)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformAsterisk({ from }, x));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformAsterisk({ from }, compileContext));
return false;
}
if (/(?<!\\)(?:\\\\)*\+/.test(cookedFrom)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformPlus({ from }, x));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformPlus({ from }, compileContext));
return false;
}
// Verify that the regex is syntactically valid
@ -272,14 +273,14 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
// does it match an empty string?
if (rg.test('')) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_TransformFromMatchesNothing({ from }, x));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_TransformFromMatchesNothing({ from }, compileContext));
return false;
}
} catch (e) {
// We're exposing the internal regex error message here.
// In the future, CLDR plans to expose the EBNF for the transform,
// at which point we would have more precise validation prior to getting to this point.
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformFrom({ from, message: e.message }, x));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformFrom({ from, message: e.message }, compileContext));
return false;
}
return true;
@ -307,8 +308,8 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
if (reorder.before && this.checkEscapes(reorder.before, reorder) === null) {
return null; // error'ed
}
result.elements = sections.elem.allocElementString(sections, {x:reorder}, reorder.from, reorder.order, reorder.tertiary, reorder.tertiaryBase, reorder.preBase);
result.before = sections.elem.allocElementString(sections, {x:reorder}, reorder.before);
result.elements = sections.elem.allocElementString(sections, {compileContext: reorder}, reorder.from, reorder.order, reorder.tertiary, reorder.tertiaryBase, reorder.preBase);
result.before = sections.elem.allocElementString(sections, {compileContext: reorder}, reorder.before);
if (!result.elements || !result.before) {
return null; // already error'ed
} else {
@ -344,7 +345,7 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
* @param cookedFrom the original string
* @returns the original string, or null if an error was reported
*/
private checkEscapes(cookedFrom: string, transform?: ObjectWithMetadata): string | null {
private checkEscapes(cookedFrom: string, transform?: ObjectWithCompileContext): string | null {
if (!cookedFrom) return cookedFrom;
// should not follow marker prefix, nor marker prefix with range

View file

@ -1,6 +1,7 @@
import { SectionIdent, constants } from "@keymanapp/ldml-keyboard-constants";
import { KMXPlus, LdmlKeyboardTypes } from '@keymanapp/common-types';
import { LDMLKeyboard, CompilerCallbacks, ObjectWithMetadata } from '@keymanapp/developer-utils';
import { ObjectWithCompileContext } from "@keymanapp/common-types";
import { LDMLKeyboard, CompilerCallbacks } from '@keymanapp/developer-utils';
import { SectionCompiler } from "./section-compiler.js";
import Vars = KMXPlus.Vars;
import StringVarItem = KMXPlus.StringVarItem;
@ -40,9 +41,9 @@ export class VarsCompiler extends SectionCompiler {
return valid;
}
private validateIdentifier(id: string, x?: ObjectWithMetadata) {
private validateIdentifier(id: string, compileContext?: ObjectWithCompileContext) {
if(!id.match(LdmlKeyboardTypes.VariableParser.ID)) { // From <string> DTD
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVariableIdentifier({id}, x));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVariableIdentifier({id}, compileContext));
return false;
}
return true;
@ -65,11 +66,11 @@ export class VarsCompiler extends SectionCompiler {
* @param id id
* @param x context
*/
function addId(id: string, x: LDMLKeyboard.Variable): void {
function addId(id: string, compileContext: LDMLKeyboard.Variable): void {
if (allIds.has(id)) {
dups.add(id);
} else {
allIds.set(id, x);
allIds.set(id, compileContext);
}
}
@ -97,7 +98,7 @@ export class VarsCompiler extends SectionCompiler {
// Sets
for (const e of variables.set) {
const { id, value } = e;
if(!this.validateIdentifier(id)) {
if(!this.validateIdentifier(id, e)) {
valid = false;
continue;
}
@ -124,7 +125,7 @@ export class VarsCompiler extends SectionCompiler {
// UnicodeSets
for (const e of variables.uset) {
const { id, value } = e;
if(!this.validateIdentifier(id)) {
if(!this.validateIdentifier(id, e)) {
valid = false;
continue;
}
@ -270,7 +271,7 @@ export class VarsCompiler extends SectionCompiler {
const allMarkers : string[] = Array.from(mt.all.keys()).filter(m => m !== LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID).sort();
result.markers = sections.list.allocList(allMarkers, {
// pass the first object in the listr
x: mt.all.get(allMarkers[0]||'')
compileContext: mt.all.get(allMarkers[0]||'')
// however, this is the string with the marker *id*. So, from the StrsCompiler
// point of view, the value of this context would be if the marker ID had, say,
// invalid Unicode in it for ERROR_IllegalCharacters - but, that wouldn't be a
@ -291,7 +292,7 @@ export class VarsCompiler extends SectionCompiler {
let { value } = e;
// fix any variables
value = result.substituteStrings(value, sections);
result.strings.push(new StringVarItem(id, value, sections));
result.strings.push(new StringVarItem(id, value, sections, e));
}
addSet(result: Vars, e: LDMLKeyboard.LKSet, sections: DependencySections): void {
const { id } = e;
@ -306,14 +307,14 @@ export class VarsCompiler extends SectionCompiler {
// this is not 'forMatch', all variables are to be assumed as string literals, not regex
// content.
const cookedItems: string[] = rawItems.map(v => result.substituteMarkerString(v, false));
result.sets.push(new SetVarItem(id, cookedItems, sections));
result.sets.push(new SetVarItem(id, cookedItems, sections, e));
}
addUnicodeSet(result: Vars, e: LDMLKeyboard.LKUSet, sections: DependencySections): void {
const { id } = e;
let { value } = e;
value = result.substituteStrings(value, sections);
value = result.substituteUnicodeSets(value, sections);
result.usets.push(new UnicodeSetItem(id, value, sections, sections.usetparser));
result.usets.push(new UnicodeSetItem(id, value, sections, sections.usetparser, e));
}
// routines for using/substituting variables have been moved to the Vars class and its
// properties

View file

@ -1,11 +1,12 @@
import 'mocha';
import {assert} from 'chai';
import { DispCompiler } from '../src/compiler/disp.js';
import { compilerTestCallbacks, loadSectionFixture, testCompilationCases, withOffset } from './helpers/index.js';
import { compilerTestCallbacks, loadSectionFixture, testCompilationCases } from './helpers/index.js';
import { KMXPlus } from '@keymanapp/common-types';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import Disp = KMXPlus.Disp;
import { withOffset } from '@keymanapp/developer-utils';
describe('disp', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while

View file

@ -8,7 +8,7 @@
<key id="one" output="1" />
</keys>
<layers formId="touch">
<layers formId="touch" minDeviceWidth="123">
<!-- INVALID: need at least one layer. -->
</layers>
</keyboard3>

View file

@ -1,6 +1,7 @@
import {assert} from 'chai';
import {readFileSync} from 'node:fs';
import { KeymanXMLReader, SymbolUtils } from "@keymanapp/developer-utils";
import { ObjectWithCompileContext } from '@keymanapp/common-types';
/**
*
@ -8,10 +9,10 @@ import { KeymanXMLReader, SymbolUtils } from "@keymanapp/developer-utils";
* @param expect path to expected XML
* @param mutator optional function that will be applied to the parsed object
*/
export function compareXml(actual : string, expect: string, mutator?: (input: any) => any) {
export function compareXml(actual : string, expect: string, mutator?: (input: ObjectWithCompileContext) => ObjectWithCompileContext) {
if (!mutator) {
// no-op
mutator = (x: any) => x;
mutator = (compileContext: ObjectWithCompileContext) => compileContext;
}
const reader = new KeymanXMLReader('keyboard3');

View file

@ -8,7 +8,7 @@ import * as path from 'path';
import { fileURLToPath } from 'url';
import { SectionCompiler, SectionCompilerNew } from '../../src/compiler/section-compiler.js';
import { util, KMXPlus, LdmlKeyboardTypes } from '@keymanapp/common-types';
import { CompilerEvent, compilerEventFormat, CompilerCallbacks, LDMLKeyboardXMLSourceFileReader, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboard, KeymanXMLMetadata, KeymanXMLReader, CompilerError } from "@keymanapp/developer-utils";
import { CompilerEvent, compilerEventFormat, CompilerCallbacks, LDMLKeyboardXMLSourceFileReader, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboard, CompilerError } from "@keymanapp/developer-utils";
import { LdmlKeyboardCompiler } from '../../src/main.js'; // make sure main.js compiles
import { assert } from 'chai';
import { KMXPlusMetadataCompiler } from '../../src/compiler/metadata-compiler.js';
@ -358,17 +358,3 @@ export function hex_str(s?: string) : string {
return [...s].map(ch => dontEscape.test(ch) ? ch : util.escapeRegexChar(ch)).join('');
}
/**
* Return an object simulating an XML object with an offset number
* For use in calling message functions
* @param c number for the offset setting
* @param x if set, this object will be used as the base object instead of {}
*/
export function withOffset(c: number, x?: any) : KeymanXMLMetadata {
// set metadata on an empty object
const o = Object.assign({}, x);
KeymanXMLReader.setMetaData(o, {
startIndex: c
});
return o;
}

View file

@ -1,7 +1,7 @@
import 'mocha';
import { assert } from 'chai';
import { KeysCompiler } from '../src/compiler/keys.js';
import { assertCodePoints, compilerTestCallbacks, loadSectionFixture, testCompilationCases, withOffset } from './helpers/index.js';
import { assertCodePoints, compilerTestCallbacks, loadSectionFixture, testCompilationCases } from './helpers/index.js';
import { KMXPlus, Constants, LdmlKeyboardTypes } from '@keymanapp/common-types';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import { constants } from '@keymanapp/ldml-keyboard-constants';
@ -9,7 +9,7 @@ import { MetaCompiler } from '../src/compiler/meta.js';
const keysDependencies = [ ...BASIC_DEPENDENCIES, MetaCompiler ];
import Keys = KMXPlus.Keys;
import { BASIC_DEPENDENCIES } from '../src/compiler/empty-compiler.js';
import { LDMLKeyboard } from '@keymanapp/developer-utils';
import { LDMLKeyboard, withOffset } from '@keymanapp/developer-utils';
const K = Constants.USVirtualKeyCodes;
describe('keys', function () {

View file

@ -2,12 +2,13 @@ import 'mocha';
import { assert } from 'chai';
import { LayrCompiler } from '../src/compiler/layr.js';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import { compilerTestCallbacks, testCompilationCases, withOffset } from './helpers/index.js';
import { compilerTestCallbacks, testCompilationCases } from './helpers/index.js';
import { KMXPlus } from '@keymanapp/common-types';
import { constants } from '@keymanapp/ldml-keyboard-constants';
import Layr = KMXPlus.Layr;
import LayrRow = KMXPlus.LayrRow;
import { withOffset } from '@keymanapp/developer-utils';
function allKeysOk(row : LayrRow, str : string, msg? : string) {
const split = str.split(' ');
@ -107,13 +108,13 @@ describe('layr', function () {
{
// missing layer element
subpath: 'sections/layr/invalid-missing-layer.xml',
errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(withOffset(258))],
errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(withOffset(40))],
retainOffsetInMessages: true,
},
{
// missing layer element
subpath: 'sections/layr/invalid-missing-layer2.xml',
errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement()],
errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(withOffset(40))],
retainOffsetInMessages: true,
},
{

View file

@ -1,11 +1,12 @@
import 'mocha';
import { assert } from 'chai';
import { LocaCompiler } from '../src/compiler/loca.js';
import { compilerTestCallbacks, loadSectionFixture, withOffset } from './helpers/index.js';
import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { KMXPlus } from '@keymanapp/common-types';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import Loca = KMXPlus.Loca;
import { withOffset } from '@keymanapp/developer-utils';
describe('loca', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while

View file

@ -2,8 +2,7 @@ import 'mocha';
import {expect} from 'chai';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
import { CompilerErrorNamespace, CompilerEvent } from '@keymanapp/developer-utils';
import { withOffset } from './helpers/index.js';
import { CompilerErrorNamespace, CompilerEvent, withOffset } from '@keymanapp/developer-utils';
describe('LdmlCompilerMessages', function () {
it('should have a valid LdmlCompilerMessages object', function() {

View file

@ -1,13 +1,13 @@
import 'mocha';
import {assert} from 'chai';
import { MetaCompiler } from '../src/compiler/meta.js';
import { compilerTestCallbacks, loadSectionFixture, withOffset } from './helpers/index.js';
import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { KMXPlus } from '@keymanapp/common-types';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import KeyboardSettings = KMXPlus.KeyboardSettings;
import Meta = KMXPlus.Meta;
import { LDMLKeyboard } from '@keymanapp/developer-utils';
import { LDMLKeyboard, withOffset } from '@keymanapp/developer-utils';
describe('meta', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while

View file

@ -85,11 +85,14 @@ function do_bundle() {
mkdir -p build/dist
node build-bundler.js
sentry-cli sourcemaps inject \
--org keyman \
--project keyman-developer \
--release "$KEYMAN_VERSION_GIT_TAG" \
build/dist/ "${SOURCEMAP_PATHS[@]}"
if builder_is_ci_build && builder_is_ci_build_level_release; then
# Only inject sourcemaps for release builds
sentry-cli sourcemaps inject \
--org keyman \
--project keyman-developer \
--release "$KEYMAN_VERSION_GIT_TAG" \
build/dist/ "${SOURCEMAP_PATHS[@]}"
fi
# Manually copy over kmcmplib module
cp ../kmc-kmn/build/src/import/kmcmplib/wasm-host.wasm build/dist/

View file

@ -126,7 +126,7 @@ do_command() {
# ----------------------------------------------------------------------------
build_meson_cross_file_for_wasm() {
if [ $BUILDER_OS == win ]; then
if builder_is_windows; then
local R=$(cygpath -w $(echo $EMSCRIPTEN_BASE) | sed 's_\\_\\\\_g')
else
local R=$(echo $EMSCRIPTEN_BASE | sed 's_/_\\/_g')
@ -140,4 +140,4 @@ should_do_full_test() {
fi
return 1
}
}

View file

@ -8,7 +8,7 @@ project('kmcmplib', 'cpp', 'c',
version: files('../../../VERSION.md'),
license: 'MIT',
default_options : ['buildtype=release',
'cpp_std=c++14',
'cpp_std=c++17',
'b_vscrt=static_from_buildtype',
'default_library=static',
'debug=true'])

View file

@ -3863,17 +3863,68 @@ bool hasPreamble(std::u16string result) {
return result.size() > 0 && result[0] == 0xFEFF;
}
bool isValidUtf8(KMX_BYTE* str, int sz) {
int i = 0;
while (i < sz) {
int remaining = sz - i;
if (str[i] <= 0x7F) {
// ASCII
i++;
} else if ((str[i] & 0xE0) == 0xC0) {
// 2-byte sequence
if (remaining < 2 ||
(str[i + 1] & 0xC0) != 0x80 ||
str[i] == 0xC0 || // C0 and C1 are illegal values
str[i] == 0xC1) {
return false;
}
i += 2;
} else if ((str[i] & 0xF0) == 0xE0) {
// 3-byte sequence
if (remaining < 3 ||
(str[i+1] & 0xC0) != 0x80 ||
(str[i+2] & 0xC0) != 0x80) {
return false;
}
if (str[i] == 0xE0 && (str[i + 1] & 0xE0) == 0x80) {
return false;
}
if (str[i] == 0xED && (str[i + 1] & 0xE0) == 0xA0) {
return false;
}
i += 3;
} else if ((str[i] & 0xF8) == 0xF0) {
// 4-byte sequence
if (remaining < 4 ||
(str[i+1] & 0xC0) != 0x80 ||
(str[i+2] & 0xC0) != 0x80 ||
(str[i+3] & 0xC0) != 0x80) {
return false;
}
if (str[i] == 0xF0 && (str[i + 1] & 0xF0) == 0x80) {
return false;
}
if (str[i] > 0xF4 || (str[i] == 0xF4 && str[i + 1] > 0x8F)) {
return false;
}
i += 4;
} else {
return false;
}
}
return true;
}
bool UTF16TempFromUTF8(KMX_BYTE* infile, int sz, KMX_BYTE** tempfile, int *sz16) {
if(sz == 0) {
return FALSE;
}
std::u16string result;
try {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> converter;
result = converter.from_bytes((char*)infile, (char*)infile+sz);
} catch(std::range_error& e) {
if (isValidUtf8(infile, sz)) {
std::string infileStr(reinterpret_cast<const char*>(infile), sz);
result = u16string_from_string(infileStr);
} else {
ReportCompilerMessage(KmnCompilerMessages::HINT_NonUnicodeFile);
result.resize(sz);
for(int i = 0; i < sz; i++) {

View file

@ -24,6 +24,8 @@ KMX_DWORD GetRHS(PFILE_KEYBOARD fk, PKMX_WCHAR p, PKMX_WCHAR buf, int bufsize, i
bool isIntegerWstring(PKMX_WCHAR p);
bool hasPreamble(std::u16string result);
KMX_DWORD ProcessKeyLineImpl(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnicode, PKMX_WCHAR pklIn, PKMX_WCHAR pklKey, PKMX_WCHAR pklOut);
extern bool UTF16TempFromUTF8(KMX_BYTE* infile, int sz, KMX_BYTE** tempfile, int* sz16);
extern bool isValidUtf8(KMX_BYTE* str, int sz);
namespace kmcmp {
extern int nErrors;
@ -2101,5 +2103,156 @@ TEST_F(CompilerTest, hasPreamble_test) {
EXPECT_FALSE(hasPreamble(u"a\uFEFF"));
}
// bool UTF16TempFromUTF8(KMX_BYTE* infile, int sz, KMX_BYTE** tempfile, int *sz16)
// PFILE_STORE FindSystemStore(PFILE_KEYBOARD fk, KMX_DWORD dwSystemID)
// Helper to convert UTF-16 buffer to std::u16string
static std::u16string buffer_to_u16string(const KMX_BYTE* buf, int sz16) {
std::u16string out;
for (int i = 0; i < sz16; i += 2) {
char16_t ch = buf[i] | (buf[i+1] << 8);
out.push_back(ch);
}
return out;
}
TEST(UTF16TempFromUTF8, HandlesValidUTF8WithoutBOM) {
const char* utf8 = "abc \xE2\x82\xAC"; // "abc €"
int sz = strlen(utf8);
KMX_BYTE* tempfile = nullptr;
int sz16 = 0;
ASSERT_TRUE(UTF16TempFromUTF8((KMX_BYTE*)utf8, sz, &tempfile, &sz16));
ASSERT_EQ(sz16, 10); // 5 UTF-16 code units * 2 bytes
std::u16string result = buffer_to_u16string(tempfile, sz16);
EXPECT_EQ(result, u"abc \u20AC");
delete[] tempfile;
}
TEST(UTF16TempFromUTF8, HandlesValidUTF8WithBOM) {
// UTF-8 BOM + "A"
const char utf8[] = { char(0xEF), char(0xBB), char(0xBF), 'A', 0 };
int sz = 4;
KMX_BYTE* tempfile = nullptr;
int sz16 = 0;
ASSERT_TRUE(UTF16TempFromUTF8((KMX_BYTE*)utf8, sz, &tempfile, &sz16));
ASSERT_EQ(sz16, 2); // Only 'A' (1 code unit) * 2 bytes
std::u16string result = buffer_to_u16string(tempfile, sz16);
EXPECT_EQ(result, u"A");
delete[] tempfile;
}
TEST(UTF16TempFromUTF8, HandlesInvalidUTF8FallbacksToCP1252) {
// 0x80 is invalid in UTF-8, should fallback to CP1252 (U+20AC)
const char invalid_utf8[] = { char(0x80), 0 };
int sz = 1;
KMX_BYTE* tempfile = nullptr;
int sz16 = 0;
ASSERT_TRUE(UTF16TempFromUTF8((KMX_BYTE*)invalid_utf8, sz, &tempfile, &sz16));
ASSERT_EQ(sz16, 2);
std::u16string result = buffer_to_u16string(tempfile, sz16);
EXPECT_EQ(result, u"\u20AC");
delete[] tempfile;
}
TEST(UTF16TempFromUTF8, HandlesEmptyInput) {
KMX_BYTE* tempfile = nullptr;
int sz16 = 0;
ASSERT_FALSE(UTF16TempFromUTF8(nullptr, 0, &tempfile, &sz16));
// tempfile should remain nullptr, sz16 should be 0
EXPECT_EQ(tempfile, nullptr);
EXPECT_EQ(sz16, 0);
}
TEST(IsValidUtf8Test, ValidAscii) {
std::vector<KMX_BYTE> v = {'h', 'e', 'l', 'l', 'o'};
EXPECT_TRUE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, Valid2Byte) {
std::vector<KMX_BYTE> v = {0xC2, 0xA2}; // U+00A2: ¢
EXPECT_TRUE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, Valid3Byte) {
std::vector<KMX_BYTE> v = {0xE2, 0x82, 0xAC}; // U+20AC: €
EXPECT_TRUE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, Valid4Byte) {
std::vector<KMX_BYTE> v = {0xF0, 0x9F, 0x98, 0x80}; // U+1F600: 😀
EXPECT_TRUE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, InvalidOverlongEncoding) {
std::vector<KMX_BYTE> v = {0xC1, 0x81}; // Overlong encoding for ASCII
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, InvalidMissingContinuation) {
std::vector<KMX_BYTE> v = {0xE2, 0x82}; // Missing one continuation byte
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, InvalidLoneContinuation) {
std::vector<KMX_BYTE> v = {0x80}; // Continuation byte without starter
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, Invalid4ByteOutOfRange) {
std::vector<KMX_BYTE> v = {0xF4, 0x90, 0x80, 0x80}; // > U+10FFFF
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, ValidMixedAsciiAndMultiByte) {
std::vector<KMX_BYTE> v = {'A', 0xC2, 0xA2, 0xE2, 0x82, 0xAC, 0xF0, 0x9F, 0x98, 0x80};
EXPECT_TRUE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, Invalid2ByteBadSecondByte) {
std::vector<KMX_BYTE> v = {0xD2, 0x28};
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, Invalid2ByteBadSecondByteTooHigh) {
std::vector<KMX_BYTE> v = {0xD2, 0xC8};
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, Invalid3ByteBadSecondByte) {
std::vector<KMX_BYTE> v = {0xE2, 0x28, 0xA1};
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, Invalid3ByteBadThirdByte) {
std::vector<KMX_BYTE> v = {0xE2, 0xA8, 0xD1};
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, Invalid4ByteBadSecondByte) {
std::vector<KMX_BYTE> v = {0xF2, 0xC8, 0xB2, 0x88};
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, Invalid4ByteBadThirdByte) {
std::vector<KMX_BYTE> v = {0xF2, 0xB8, 0x02, 0x88};
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, Invalid4ByteBadFourthByte) {
std::vector<KMX_BYTE> v = {0xF2, 0xB8, 0xA2, 0xD8};
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, InvalidByteC0) {
std::vector<KMX_BYTE> v = {0xC1};
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, InvalidByteF5) {
std::vector<KMX_BYTE> v = {0xF5};
EXPECT_FALSE(isValidUtf8(v.data(), v.size()));
}
TEST(IsValidUtf8Test, ValidEmptyInput) {
std::vector<KMX_BYTE> v = {};
EXPECT_TRUE(isValidUtf8(v.data(), v.size()));
}

View file

@ -33,7 +33,7 @@ function do_build() {
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
cp "$WIN32_TARGET_PATH/kmconvert.dbg" "$DEVELOPER_DEBUGPATH"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmconvert.dbg" "$DEVELOPER_DEBUGPATH"
rm -rf "$DEVELOPER_PROGRAM/projects/templates"
mkdir -p "$DEVELOPER_PROGRAM/projects/templates"

View file

@ -32,9 +32,7 @@ function do_build() {
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
if [[ -f "$WIN32_TARGET_PATH/kmdbrowserhost.dbg" ]]; then
cp "$WIN32_TARGET_PATH/kmdbrowserhost.dbg" "$DEVELOPER_DEBUGPATH"
fi
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmdbrowserhost.dbg" "$DEVELOPER_DEBUGPATH"
}
function do_publish() {

View file

@ -33,7 +33,7 @@ function do_build() {
build_version.res
vs_msbuild kmdecomp.sln //t:Build "//p:Platform=Win32"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
cp "$WIN32_TARGET_PATH/kmdecomp.pdb" "$DEVELOPER_DEBUGPATH"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmdecomp.pdb" "$DEVELOPER_DEBUGPATH"
}
function do_publish() {

View file

@ -32,11 +32,11 @@ function do_build() {
build_version.res
vs_msbuild imsample.sln //t:Build "//p:Platform=Win32"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
cp "$WIN32_TARGET_PATH/imsample.pdb" "$DEVELOPER_DEBUGPATH"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/imsample.pdb" "$DEVELOPER_DEBUGPATH"
vs_msbuild imsample.sln //t:Build "//p:Platform=x64"
cp "$X64_TARGET" "$DEVELOPER_PROGRAM"
cp "$X64_TARGET_PATH/imsample.x64.pdb" "$DEVELOPER_DEBUGPATH"
builder_if_release_build_level cp "$X64_TARGET_PATH/imsample.x64.pdb" "$DEVELOPER_DEBUGPATH"
}
# TODO

View file

@ -13,7 +13,7 @@
"@sentry/node": "^7.57.0",
"chalk": "^4.1.2",
"express": "^4.20.0",
"multer": "^2.0.1",
"multer": "^2.0.2",
"ngrok": "^5.0.0-beta.2",
"open": "^8.4.0",
"restructure": "^3.0.1",

View file

@ -33,13 +33,8 @@ function do_build() {
sentrytool_delphiprep "$WIN32_TARGET" setup.dpr
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
if [[ -f "$WIN32_TARGET_PATH/setup.dbg" ]]; then
cp "$WIN32_TARGET_PATH/setup.dbg" "$DEVELOPER_DEBUGPATH/devsetup.dbg"
fi
rm -f "$WIN32_TARGET_PATH/devsetup.dbg"
if [[ -f "$WIN32_TARGET_PATH/setup.dbg" ]]; then
mv "$WIN32_TARGET_PATH/setup.dbg" "$WIN32_TARGET_PATH/devsetup.dbg"
fi
builder_if_release_build_level cp "$WIN32_TARGET_PATH/setup.dbg" "$DEVELOPER_DEBUGPATH/devsetup.dbg"
builder_if_release_build_level mv "$WIN32_TARGET_PATH/setup.dbg" "$WIN32_TARGET_PATH/devsetup.dbg"
}
function do_publish() {

View file

@ -76,11 +76,10 @@ function do_build() {
cp kmlmp.cmd "$DEVELOPER_PROGRAM"
cp kmc.cmd "$DEVELOPER_PROGRAM"
cp "$KEYMAN_ROOT/core/build/x86/$TARGET_PATH/src/$KEYMANCORE_DLL" "$DEVELOPER_PROGRAM"
if [[ -f "$WIN32_TARGET_PATH/tike.dbg" ]]; then
cp "$WIN32_TARGET_PATH/tike.dbg" "$DEVELOPER_DEBUGPATH"
fi
builder_if_release_build_level cp "$WIN32_TARGET_PATH/tike.dbg" "$DEVELOPER_DEBUGPATH"
cp "$KEYMAN_ROOT/core/build/x86/$TARGET_PATH/src/$KEYMANCORE_DLL" "$WIN32_TARGET_PATH"
cp "$KEYMAN_ROOT/core/build/x86/$TARGET_PATH/src/$KEYMANCORE_PDB" "$WIN32_TARGET_PATH"
builder_if_release_build_level cp "$KEYMAN_ROOT/core/build/x86/$TARGET_PATH/src/$KEYMANCORE_PDB" "$WIN32_TARGET_PATH"
cp "$KEYMAN_ROOT/common/windows/delphi/ext/sentry/sentry.dll" "$DEVELOPER_PROGRAM/"
cp "$KEYMAN_ROOT/common/windows/delphi/ext/sentry/sentry.x64.dll" "$DEVELOPER_PROGRAM/"

229
docs/build-bot.md Normal file
View file

@ -0,0 +1,229 @@
# Controlling builds in CI with the Keyman build bot
The Keyman CI test infrastructure can be controlled by use of a `Build-bot:`
commit trailer, or a PR body `Build-bot:` trailer. This allows you to specify
what is built for any given platform. The primary purpose of the build bot is to
reduce the build agent load, but it can also be used to ensure that specific
artifacts are available for the purposes of testing.
The Keyman build bot is only available for test builds on pull requests. It is
not used for any other builds, either test builds on target branches (master,
beta, stable-x.y), or for release builds.
The default set of platform builds is determined by the files touched in the
pull request; see /resources/build/trigger-definitions.inc.sh. The default build
level for this set is 'build' (see [Build Level] section). This is known as the
'build set'.
You may choose to increase or decrease what is built -- for example, for a
documentation-only change, you may decide that nothing needs to be built at all,
with:
```
Build-bot: skip
```
Or you may want to ensure that an artifact is built for Windows:
```
Build-bot: release:windows
```
## The Build-bot command
The build bot is controlled through the `Build-bot` command, which can be put
into commit trailers or the PR body (not PR comments). The format of the command
is:
```
Build-bot: <BuildLevel>:[Platforms] ...
```
* `BuildLevel` can be `skip`, `build`, or `release`. See [Build Level] for more
details.
* `Platforms` can be omitted, in which case the command applies to all
previously-specified platforms (which is not equivalent to specifying `all` --
if no platform is specified, it will only update platforms that were already
in the build set). If specified, it must be a comma-separated list, without
spaces, of one or more of the following platform identifiers:
* `all`: apply to all platforms listed below
* `android`
* `developer`
* `ios`
* `linux`
* `mac`
* `web`
* `windows`
* `common`: build common components, on all three build platforms (note:
`release` is equivalent to `build` buildLevel for `common`)
* `core`: build Keyman Core, on all three build platforms, and also WASM
(note: `release` is equivalent to `build` buildLevel for `core`)
The list of platforms can be found in the `available_platforms` variable in
trigger-definitions.inc.sh.
## Build Level
The build level specifies what we want to be run for a test build on a PR.
There is a bit of nomenclature overlap with a 'release build'. A 'release'
buildLevel for a 'test build' is roughly equivalent to what is performed in a
'release build', however, a test build is only ever uploaded to test endpoints
(i.e. TestFlight, Play Store 'test' streams), and never to *.keyman.com, or to
other release distribution endpoints.
The build level is controlled by the Build-bot commit trailer and PR body
Build-bot/Test-bot trailers. The default build level will be 'build'.
For target branch (master, beta, stable-x.y) builds, the build level will always
be 'build', and Build-bot: commit trailers are ignored.
### 'skip' build level
Don't do a build at all. This would be appropriate for documentation PRs, for
example, or changes only to comments in source files.
### 'build' build level
Build the code and run unit tests, but don't create artifacts. What this looks
like will vary from platform to platform, but there are some common things we
won't do:
* we won't upload artifacts to TeamCity or *.keyman.com
* we won't upload artifacts to any endpoint such as Play Store
* we won't upload symbols to Sentry
* we won't codesign
However, we _will_ still build an installer (skipping codesigning), as this is
part of the 'build' buildLevel rather than the 'release' buildLevel. The
installer will be thrown away for 'build' build level -- it will not be
available for download as an artifact.
For a platform-specific example, on macOS we will also skip notarizing, as this
is costly and depends on external network resources, making it fragile.
### 'release' build level
A full test build will be run, roughly equivalent to a release build. We will do
the following steps:
* codesign (many platforms) and notarization (macOS)
* upload artifacts to TeamCity (all platforms)
* upload builds to TestFlight / Play Store 'test' endpoints (iOS/Android)
* upload symbols to Sentry
For a 'release' build level:
* we won't upload artifacts to *.keyman.com
* we won't upload artifacts to any release endpoint such as Debian,
packages.sil.org, etc, or to the release areas for Play Store or App Store
(uploading to *.keyman.com and to release endpoints happens in the release TC
build config/GHA)
## Controlling the build bot with trailers
The build bot respects commit trailers and trailers in the PR body. The commands
are cumulative and applied in order; the Build-bot trailer in the PR body is
applied after any trailers in commit messages.
If no platform is specified, then the command applies to all platforms in the
current build set, overriding any previous Build-bot commands. Any platforms not
in the current build set will remain 'skipped'.
If a platform is specified, then the command will apply only to that platform,
and the platform will be added to the build set if not already present (and
thus, subsequent Build-bot commands will be applied to this new platform as
well).
It is important to note that the Build-bot trailers are read when builds are
triggered, which happens within 2 minutes of a PR being opened or commits being
pushed. Editing the PR body after the build trigger has run will have no effect
on existing builds, until another commit is pushed, or the test build trigger is
run manually from TeamCity. Thus, do not rely on editing the PR body after PR
creation to control the build bot; either include your build bot trailers in
commit messages, or include them in the PR body submitted when creating the PR.
## Interactions with Test Bot
The Build-bot has limited interactions with the Keyman test bot (aka
keymanapp-test-bot): if a 'User Testing' section is found in the PR body, the
default build level will be upgraded from 'build' to 'release'.
Build bot trailers found in either the commits or in the PR body are applied
after the test bot command.
WARNING: The build bot does not check PR comments for Test-bot commands or a
'User Testing' section.
## Example Build-bot interactions
Say we have a PR that touches `/android/build.sh`. The default build set will be
`(android:build)`.
The PR body has a User Testing section: `# User Testing`. The build set is
upgraded to: `(android:release)`.
The first commit includes a Build-bot command: `Build-bot: build:ios`. The build
set is now `(android:release ios:build)`.
In a subsequent commit, the PR author decides that nothing needs to be built,
after all: `Build-bot: skip`. The build set is now `(android:skip ios:skip)`.
Note that other platforms are still 'skip' but not included in the build set.
Finally, the PR author pushes another commit, with `Build-bot: release:windows`.
The build set is now: `(android:skip ios:skip windows:release)`.
# FAQ
* How do I specify commands for multiple platforms, e.g. building on Windows and Linux?
```
Build-bot: build:windows,linux
```
* If I modify a file that would cause a build on all platforms, does `Build-bot:
build android` then cause a build only on Android and skip all others? Or is
it only additive?
The commands are additive. To skip all others, you would do:
```
Build-bot: skip
Build-bot: build:android
```
* What happens in the following scenario: I have a PR that I only want to build
on Android and I add `Build-bot: build android`. Later on I merge in the
changes from `master` and add another commit. If I still only want to build on
Android, do I have to add `Build-bot: build android` on my new commit again?
Or what happens with merges that touch other files which would cause builds
for other platforms?
First, you would need to specify `skip` for all other platforms in order to only
build on Android:
```
Build-bot: skip
Build-bot: build:android
```
Then, the build bot scans all the commits in the PR, and additively builds the
final build set from the `Build-bot` commands from all the commits (up to 2000
in one PR). Thus, earlier bot commands commits will continue to be honoured.
This makes it easier to apply merge commits, for example, or small fixups,
without needing to think about what needs to be built again.
* How can I re-trigger builds after changing my build bot commands?
In [TeamCity - Trigger Test Builds](https://build.palaso.org/buildConfiguration/Keyman_Test),
select a custom Run (small button attached to right of Run button), and in the
"Changes" tab, "Build branch" field, enter the PR number to re-run the builds.
Any test builds currently running against that PR will be cancelled and
restarted.
Note: if you reduce the build set (with `skip`), and re-run builds in this way
without pushing a new commit, you will end up with stale status checks on the
latest commit in your PR for the builds that are now skipped on the new run, so
in this scenario it is safest to push a new commit with the updated build bot
commands.
[Build Level]: #Build_Level

View file

@ -123,6 +123,13 @@ package.json (`npm run <script>`).
passed in, or `release` otherwise, which corresponds to the output folder
names for many projects.
* `BUILDER_OS` will be set to `linux`, `mac`, or `win`, depending on the platform
the builder script is currently running on.
**NOTE:** Only use `BUILDER_OS` in a `case` statement. Use one of the
`builder_is_macos`, `builder_is_windows`, or `builder_is_linux` functions
to check if the script is running on a particular platform.
Other environment variables and paths will probably be added over time.
## Split
@ -182,6 +189,8 @@ a user or called by another script:
An option will be inherited by child scripts if you append a `+` to the option
name.
Other standard options are listed in [standard builder options].
* **module dependencies**: these are other builder scripts which must be
configured and built before the actions in this script can continue. Only
`configure` and `build` actions are ever passed to dependency scripts; these
@ -320,20 +329,33 @@ across targets. Use this judiciously; for example, Keyman Core uses this to
build both x86_64 and arm64 targets, and test only the appropriate architecture
on macOS.
# Standard builder parameters
# Standard builder options
The following parameters are pre-defined and should not be overridden:
The following options are pre-defined and should not be overridden:
* `--help`, `-h`: displays help on using this script
* `--color`: forces on ANSI color output for the script
* `--no-color`: forces off ANSI color output for the script
* `--verbose`, `-v`: verbose mode, sets the [`$builder_verbose`] variable
* `--debug`, `-d`: debug build; see [`builder_is_debug_build`] for more detail
* `--debug`, `-d`: debug build (default for local builds); see
[`builder_is_debug_build`] for more detail
* `--release`: release build, inverse of `--debug`, to force a non-debug build
* `--offline`: allow to build while offline. This might fail if not all
dependencies are cached.
* `--deps`: Build dependencies if required (default)
* `--no-deps`: Skip build of dependencies
* `--force-deps`: Reconfigure and rebuild all dependencies
* `--builder-ignore-unknown-options`: Ignore any subsequent unrecognized `-` or
`--` prefixed options in the parameter list. This is intended for use in
transitional situations where scripts may need to temporarily support passing
additional parameters; see also: #14200 for more background,
[`builder_ignore_unknown_options`], and [`$builder_ignored_options`].
## Internal builder options
The following options are used internally in builder and should never be included
as parameters otherwise: `--builder-dep-parent`, `--builder-child`,
`--builder-report-dependencies`, `--builder-completion-describe`
--------------------------------------------------------------------------------
@ -482,7 +504,7 @@ There is one standard option: `--debug`. You should not include `--debug` in the
`builder_describe` call, as it is always available. See
[`builder_is_debug_build`] for more details.
Note that you should not include any of the [standard builder parameters] here.
Note that you should not include any of the [standard builder options] here.
**Dependencies** are defined with a `@` prefix, for example:
@ -842,6 +864,47 @@ fi
--------------------------------------------------------------------------------
## `builder_ignore_unknown_options` function
Returns `true` (aka 0) if the `--builder-ignore-unknown-options` standard option
was passed in.
### Usage
```bash
if builder_ignore_unknown_options; then
builder_echo "These options were ignored: ${builder_ignored_options[@]}"
fi
```
### Description
This function should probably never be used in a builder script; it is included
for completeness and testing. See discussion in [standard builder options].
--------------------------------------------------------------------------------
## `$builder_ignored_options` variable
Lists all the unrecognized options passed in; only used if the
`--builder-ignore-unknown-options` option was set (because if not set, the
script would have aborted in `builder_parse` with an unknown option error).
### Usage
```bash
if builder_ignore_unknown_options; then
builder_echo "These options were ignored: ${builder_ignored_options[@]}"
fi
```
### Description
This variable should probably never be used in a builder script; it is included
for completeness and testing. See discussion in [standard builder options].
--------------------------------------------------------------------------------
## `builder_is_debug_build` function
Returns `true` (aka 0) if the `--debug` standard option was passed in. This
@ -1185,6 +1248,20 @@ builder_run_action test builder_do_typescript_tests [coverage_threshold]
--------------------------------------------------------------------------------
## `builder_is_windows`, `builder_is_macos`, and `builder_is_linux` functions
Returns 0 (true) if building on the particular platform.
### Usage
```bash
if builder_is_windows; then
builder_echo "We're building on a Windows machine"
fi
```
--------------------------------------------------------------------------------
## Formatting variables
These helper variables define ANSI color escapes when running in color mode, and
@ -1208,7 +1285,7 @@ Note: it is often cleaner to use [`builder_echo`] than to use these variables di
Note: it is recommended that you use `$(builder_term text)` instead of
`${BUILDER_TERM_START}text${BUILDER_TERM_END}`.
[standard builder parameters]: #standard-builder-parameters
[standard builder options]: #standard-builder-options
[`builder_describe`]: #builder_describe-function
[`builder_describe_internal_dependency`]: #builder_describe_internal_dependency-function
[`builder_describe_outputs`]: #builder_describe_outputs-function
@ -1218,6 +1295,9 @@ Note: it is recommended that you use `$(builder_term text)` instead of
[`builder_finish_action`]: #builder_finish_action-function
[`builder_has_action`]: #builder_has_action-function
[`builder_has_option`]: #builder_has_option-function
[`$builder_ignored_options`]: #builder_ignored_options-variable
[`builder_ignore_unknown_options`]: #builder_ignore_unknown_options-function
[`builder_is_debug_build`]: #builder_is_debug_build-function
[`builder_parse`]: #builder_parse-function
[`builder_start_action`]: #builder_start_action-function
[`builder_use_color`]: #builder_use_color-function
@ -1228,4 +1308,3 @@ Note: it is recommended that you use `$(builder_term text)` instead of
[`builder_echo`]: #builder_echo-function
[`builder_die`]: #builder_die-function
[`builder_echo_debug`]: #builder_echo_debug-function
[`builder_is_debug_build`]: #builder_is_debug_build-function

View file

@ -50,7 +50,7 @@ https://help.keyman.com/developer/engine/android/latest-version/
|-----------------------------------|--------------|
| KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER | noble |
| KEYMAN_MIN_TARGET_VERSION_ANDROID | 5.0 |
| KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME | 53.0 |
| KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME | 95.0 |
| KEYMAN_MIN_TARGET_VERSION_IOS | 15.6 |
| KEYMAN_MIN_TARGET_VERSION_MAC | 10.13 |
| KEYMAN_MIN_TARGET_VERSION_UBUNTU | 22.04 |

View file

@ -16,8 +16,8 @@ builder_describe "Builds Keyman Engine and the Keyman app for use on iOS devices
":engine Builds KeymanEngine.xcframework, usable by our main app and by third-party apps" \
":app=keyman Builds the Keyman app for iOS platforms" \
":help Online documentation" \
":sample1=Samples/KMSample1 Builds the first KeymanEngine sample app" \
":sample2=Samples/KMSample2 Builds the second KeymanEngine sample app" \
":sample1=samples/KMSample1 Builds the first KeymanEngine sample app" \
":sample2=samples/KMSample2 Builds the second KeymanEngine sample app" \
":fv=../oem/firstvoices/ios Builds OEM FirstVoices for iOS platforms" \
"--sim-artifact+ Also outputs a simulator-friendly test artifact corresponding to the build"

View file

@ -153,6 +153,7 @@ class KeyboardScaleMap {
scalings[KeyboardScaleMap.hashKey(for: Device.iPad8)] = Scaling(portrait: KeyboardSize(w: 810, h: 265, b: 55), landscape: KeyboardSize(w: 1080, h: 353, b: 55))
scalings[KeyboardScaleMap.hashKey(for: Device.iPad9)] = Scaling(portrait: KeyboardSize(w: 810, h: 265, b: 55), landscape: KeyboardSize(w: 1080, h: 353, b: 55))
scalings[KeyboardScaleMap.hashKey(for: Device.iPad10)] = Scaling(portrait: KeyboardSize(w: 820, h: 262-5, b: 55), landscape: KeyboardSize(w: 1180, h: 347-5, b: 55)) // has negative 'system inset' (-5), which causes a mismatch in final height balanced by the subtraction
scalings[KeyboardScaleMap.hashKey(for: Device.iPadAir2)] = Scaling(portrait: KeyboardSize(w: 768, h: 258, b: 55), landscape: KeyboardSize(w: 1024, h: 343, b: 55)) // Banner always shows; merges copy ctrls with pred-banner.
scalings[KeyboardScaleMap.hashKey(for: Device.iPadAir3)] = Scaling(portrait: KeyboardSize(w: 834, h: 265, b: 55), landscape: KeyboardSize(w: 1112, h: 353, b: 55))
scalings[KeyboardScaleMap.hashKey(for: Device.iPadAir4)] = Scaling(portrait: KeyboardSize(w: 820, h: 262-5, b: 55), landscape: KeyboardSize(w: 1180, h: 347-5, b: 55))
scalings[KeyboardScaleMap.hashKey(for: Device.iPadAir5)] = Scaling(portrait: KeyboardSize(w: 820, h: 262-5, b: 55), landscape: KeyboardSize(w: 1180, h: 347-5, b: 55))

View file

@ -129,11 +129,9 @@ fi
# Write download info files
#
cd "${UPLOAD_DIR}"
write_download_info "${UPLOAD_DIR}" "${KMEI_DST_NAME}" "Keyman Engine for iOS" zip ios
write_download_info "${UPLOAD_DIR}" "${KEYMANAPP_IPA_DST}" "Keyman for iPhone and iPad" ipa ios
write_download_info "Keyman Engine for iOS" "${KMEI_DST_NAME}" "${BUILD_NUMBER}" "${KEYMAN_TIER}" "ios"
write_download_info "Keyman for iPhone and iPad" "${KEYMANAPP_IPA_DST}" "${BUILD_NUMBER}" "${KEYMAN_TIER}" "ios"
if [ ${RELEASE_OEM_FIRSTVOICES} = true ]; then
write_download_info "FirstVoices Keyboards" "${FIRSTVOICESAPP_IPA_DST}" "${BUILD_NUMBER}" "${KEYMAN_TIER}" "ios"
if [[ ${RELEASE_OEM_FIRSTVOICES} = true ]]; then
write_download_info "${UPLOAD_DIR}" "${FIRSTVOICESAPP_IPA_DST}" "FirstVoices Keyboards" ipa ios
fi

View file

@ -96,6 +96,7 @@ Depends:
python3-fonttools,
python3-gi,
python3-packaging,
python3-pkg-resources,
python3-sentry-sdk (>= 1.1),
python3-xdg,
${misc:Depends},

View file

@ -1,5 +1,6 @@
project('ibus-keyman', 'c', 'cpp',
version: files('../../VERSION.md'),
default_options: ['cpp_std=c++17'],
license: 'GPL-2+',
meson_version: '>=1.0')
@ -55,6 +56,10 @@ core_include_dirs = [
include_directories('../../core/build/arch' / get_option('buildtype') / 'include'),
]
linux_include_dirs = [
include_directories('../include')
]
subdir('icons')
subdir('src')
subdir('tests')

View file

@ -5,11 +5,9 @@
#else
#include <basu/sd-bus.h>
#endif
#include <km_linux_common.h>
#include "KeymanSystemServiceClient.h"
#define KEYMAN_BUS_NAME "com.keyman.SystemService1"
#define KEYMAN_INTERFACE_NAME "com.keyman.SystemService1.System"
#define KEYMAN_OBJECT_PATH "/com/keyman/SystemService1/System"
extern gboolean testing;

View file

@ -32,6 +32,8 @@
#include <keyman/keyman_core_api.h>
#include <keyman/keyman_core_api_consts.h>
#include <km_linux_common.h>
#include "config.h"
#include "keymanutil.h"
#include "keyman-service.h"
@ -51,10 +53,6 @@
#define KEYMAN_RCTRL 97 // 0x61
#define KEYMAN_RALT 100 // 0x64
// TODO: Move to a common header file. Also defined in tests/testfixture.h
// and keyman-system-service/src/OrderedOutputDevice.cpp
#define KEYMAN_F24_KEYCODE_OUTPUT_SENTINEL 194 // 0xC2
typedef struct _IBusKeymanEngine IBusKeymanEngine;
typedef struct _IBusKeymanEngineClass IBusKeymanEngineClass;

View file

@ -13,6 +13,7 @@ engine_files = files(
include_dirs = [
core_include_dirs,
linux_include_dirs,
include_directories('..'),
include_directories(meson.current_build_dir() / '..'),
]

View file

@ -2,18 +2,13 @@
#include <glib-object.h>
#include <glib.h>
#include <kmx/kmx_processevent.h>
#include <km_linux_test_common.h>
#include "EventSource.h"
#include "KeyHandling.h"
#include "KeymanSystemServiceClient.h"
#include "kmx_test_source.hpp"
#include "DbusTestHelper.h"
// TODO: move these to a common header. Also defined in
// keyman-system-service/tests/OrderedOutputDeviceMock.cpp
#define KEYMAN_TESTHELPER_BUS_NAME "com.keyman.TestService"
#define KEYMAN_TESTHELPER_OBJECT_PATH "/com/keyman/TestService/TestHelper"
#define KEYMAN_TESTHELPER_INTERFACE_NAME "com.keyman.TestService.TestHelper"
static sd_bus* bus = NULL;
static EventSource *source = NULL;

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