Merge branch 'stable-17.0' into fix/web/cherrypick-longpress-modifier-multitap-warning

This commit is contained in:
Joshua Horton 2025-01-24 08:19:49 +07:00
commit b6d95f5efe
112 changed files with 1724 additions and 887 deletions

View file

@ -1,5 +1,60 @@
# Keyman Version History
## 17.0.333 stable 2025-01-16
* fix(core): permanently disable logging (#12674)
* fix(linux): pushing of updated changelog branch (#12819)
* fix(linux): work around Lintian errors (#12817)
* fix(core): implement ldml_processor::get_key_list() (#12816)
* chore(linux): Update debian changelog (#12022)
* fix(android): use main looper to dispatch key events when OSK is hidden (#12875)
* chore: use GitHub PR titles when writing HISTORY.md (#12908)
* fix(developer): filter incorrect fonts out of .keyboard_info (#12913)
## 17.0.332 stable 2024-11-06
* fix(developer): create Server config directory before options save (#12609)
* fix(developer): handle merge commits when checking git log date (#12628)
* fix(linux): set environment variable for rendering of downloads dialog (#12617)
## 17.0.331 stable 2024-10-30
* fix(android): Hide suggestion banner on password fields (#12466)
* fix(common): declare dep on @keymanapp/ldml-keyboard-constants (#12475)
* fix(oem/fv): Update keyboard versions and names for fv_all.kmp (#12504)
* chore(ios): renew certificate (#12513)
* fix(developer): prevent invalid string ids (#12524)
* fix(developer): ignore excess whitespace in `<row keys>` attribute (#12523)
## 17.0.330 stable 2024-09-16
* refactor(android): Move Sentry and APK to publish task (#12392)
* fix(developer): rewrite ldml visual keyboard compiler (#12406)
* fix(developer): check vars string usage before definition (#12407)
## 17.0.329 stable 2024-09-09
* chore(android,ios): Add ojibwa ifinal/rdot keyboards to FirstVoices (#12020)
* change(web): revert #11174, which loads keyboards before initializing the OSK (#12040)
* fix(web): unrevert #11258, leaving OSK hidden before instructed to display (#12058)
* chore(common): use `nvm` to select version of node for builds (#12074)
* fix(developer): ignore scan code if zero in debugger (#12182)
* fix(developer): enforce presence of Version field when FollowKeyboardVersion is not set, in package compiler (#12206)
* fix(developer): enforce presence of kps Info.Description field in info compilers (#12207)
* fix(web): disable fat-finger data use when mayCorrect = false (#12226)
* chore(common): allow build agents to automatically select emsdk version, and enable support for 3.1.60+ (#12245)
* fix(web): fix documentation-keyboard spacebar-text scaling (#12240)
* fix(core): set mac build version for meson cli build to 10.13 (#12246)
* change(ios): defer registration of fonts past initialization (#12241)
* chore(android,ios): Update FirstVoices keyboards to 12.15 (#12301)
* fix(core): properly support 'other' modifier state with `uint32_t` type (#12285)
* fix(developer): find last matching key in LDML key bag when building KVK (#12284)
* fix(android): check in material-stepper as internal Maven dependency (#12324)
* fix(linux): add `keymanFacename` to .ldml file (#12283)
* chore(oem/fv): Update to fv_all 13.0 (#12363)
* fix(mac): avoid crash on startup with macOS 10.15 (Catalina) (#12364)
* fix(android): skip language counts for lexical-model packages (#12368)
## 17.0.328 stable 2024-07-27
* fix(web): add nullish test in setOsk (#12041)

View file

@ -1 +1 @@
17.0.329
17.0.334

View file

@ -2,8 +2,10 @@
buildscript {
repositories {
google()
jcenter()
mavenCentral()
flatDir {
dirs "kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/"
}
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.0'
@ -11,16 +13,16 @@ buildscript {
classpath 'io.sentry:sentry-android-gradle-plugin:2.1.2'
classpath 'name.remal:gradle-plugins:1.5.0'
// From jcenter() which could be sunset in future
// From jcenter() which was deprecated August 2024
// https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/
classpath 'com.stepstone.stepper:material-stepper:4.3.1'
classpath 'com.stepstone.stepper:material-stepper:4.3.1@aar'
}
}
allprojects {
repositories {
maven { url uri("${projectDir}/libs") }
google()
jcenter()
mavenCentral()
maven { url "https://jitpack.io" }
}

View file

@ -14,6 +14,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "$KEYMAN_ROOT/resources/build/build-help.inc.sh"
. "$KEYMAN_ROOT/resources/build/build-download-resources.sh"
. "$KEYMAN_ROOT/android/KMAPro/build-play-store-notes.inc.sh"
# This script runs from its own folder
cd "$THIS_SCRIPT_PATH"
@ -32,6 +34,7 @@ builder_describe "Builds Keyman for Android app." \
"configure" \
"build" \
"test Runs lint and unit tests." \
"publish Publishes symbols to Sentry and the APK to the Play Store." \
"--ci Don't start the Gradle daemon. For CI" \
"--upload-sentry Upload to sentry"
@ -116,3 +119,13 @@ if builder_start_action test; then
builder_finish_action success test
fi
if builder_start_action publish; then
# Copy Release Notes
generateReleaseNotes
# Publish symbols and Keyman for Android to Play Store
./gradlew $DAEMON_FLAG publishSentry publishReleaseApk
builder_finish_action success publish
fi

View file

@ -105,12 +105,17 @@ android {
}
// how to configure the sentry android gradle plugin
sentry {
// Disables or enables the automatic configuration of Native symbols
uploadNativeSymbols = true
task publishSentry {
doLast {
println 'Publishing Keyman symbols to Sentry'
sentry {
// Disables or enables the automatic configuration of Native symbols
uploadNativeSymbols = true
// Does or doesn't include the source code of native code for Sentry
includeNativeSources = true
// Does or doesn't include the source code of native code for Sentry
includeNativeSources = true
}
}
}
String env_keys_json_file = System.getenv("keys_json_file")
@ -145,7 +150,6 @@ repositories {
dirs 'libs'
}
google()
jcenter()
}
dependencies {

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.stepstone.stepper</groupId>
<artifactId>material-stepper</artifactId>
<version>4.3.1</version>
<packaging>aar</packaging>
<name>Android Material Stepper</name>
<description>This library allows to use Material steppers inside Android applications.</description>
<url>https://github.com/stepstone-tech/android-material-stepper</url>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<developers>
<developer>
<id>zawadz88</id>
<name>Piotr Zawadzki</name>
<email>piotr.zawadzki@stepstone.com</email>
</developer>
</developers>
<scm>
<connection>https://github.com/stepstone-tech/android-material-stepper.git</connection>
<developerConnection>https://github.com/stepstone-tech/android-material-stepper.git</developerConnection>
<url>https://github.com/stepstone-tech/android-material-stepper</url>
</scm>
<dependencies>
<dependency>
<groupId>com.android.support</groupId>
<artifactId>appcompat-v7</artifactId>
<version>25.4.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.7.21</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.assertj</groupId>
<artifactId>assertj-android</artifactId>
<version>1.1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.robolectric</groupId>
<artifactId>robolectric</artifactId>
<version>3.3.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>httpclient</artifactId>
<groupId>org.apache.httpcomponents</groupId>
</exclusion>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jre7</artifactId>
<version>1.1.4-3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>1.1.4-3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.nhaarman</groupId>
<artifactId>mockito-kotlin</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

@ -7,10 +7,6 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- API 33 and above -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Devices running up to Android 12L
Starting in API level 33, this permission has no effect:
https://developer.android.com/reference/android/Manifest.permission#READ_EXTERNAL_STORAGE -->

View file

@ -35,8 +35,8 @@ public class CheckPermissions {
checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE);
} else {
// API 33+
//https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions
// Manifest.permission.READ_MEDIA_AUDIO doesn't seem to be needed
// We had to remove these MEDIA permissions from AndroidManifest.xml so these will end up failing
// https://support.google.com/googleplay/android-developer/answer/14115180?hl=en
permissionsOK = permissionsOK && checkPermission(activity, Manifest.permission.READ_MEDIA_IMAGES);
permissionsOK = permissionsOK && checkPermission(activity, Manifest.permission.READ_MEDIA_VIDEO);
}

View file

@ -112,7 +112,8 @@ public class PackageActivity extends AppCompatActivity implements
// Number of languages associated with the first keyboard in a keyboard package.
// lexical-model packages will be 0
final int languageCount = kmpProcessor.getLanguageCount(pkgInfo, PackageProcessor.PP_KEYBOARDS_KEY, 0);
final int languageCount = (keyboardCount > 0) ?
kmpProcessor.getLanguageCount(pkgInfo, PackageProcessor.PP_KEYBOARDS_KEY, 0) : 0;
// Sanity check for keyboard packages
if (pkgTarget.equals(PackageProcessor.PP_TARGET_KEYBOARDS)) {

View file

@ -31,6 +31,7 @@ import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.GestureDetector;
@ -70,6 +71,10 @@ final class KMKeyboard extends WebView {
protected KeyboardType keyboardType = KeyboardType.KEYBOARD_TYPE_UNDEFINED;
protected ArrayList<String> javascriptAfterLoad = new ArrayList<>();
// .getMainLooper() returns the looper associated with the main UI thread.
// https://stackoverflow.com/questions/13974661/runonuithread-vs-looper-getmainlooper-post-in-android
private Handler jsQueuer = new Handler(Looper.getMainLooper());
private static String currentKeyboard = null;
/**
@ -368,7 +373,7 @@ final class KMKeyboard extends WebView {
if(this.javascriptAfterLoad.size() > 0) {
// Don't call this WebView method on just ANY thread - run it on the main UI thread.
// https://stackoverflow.com/a/22611010
this.postDelayed(new Runnable() {
jsQueuer.postDelayed(new Runnable() {
@Override
public void run() {
StringBuilder allCalls = new StringBuilder();

View file

@ -164,7 +164,7 @@ public final class KMKeyboardWebViewClient extends WebViewClient {
// appContext instead of context?
SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
boolean modelPredictionPref = false;
if (KMManager.currentLexicalModel != null) {
if (!KMManager.getMayPredictOverride() && KMManager.currentLexicalModel != null) {
modelPredictionPref = prefs.getBoolean(KMManager.getLanguagePredictionPreferenceKey(KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), true);
}
KMManager.setBannerOptions(modelPredictionPref);

View file

@ -1,87 +0,0 @@
#!/usr/bin/env bash
# CI script to publish specified app APKs to the Play Store.
# The APKs should already have been built from a separate script
# set -x: Debugging use, print each statement
# set -x
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../resources/build/build-utils.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "$KEYMAN_ROOT/android/KMAPro/build-play-store-notes.inc.sh"
echo Publishing APKs to Play Store
#
# Prevents 'clear' on exit of mingw64 bash shell
#
SHLVL=0
display_usage ( ) {
echo "build-publish.sh [-no-daemon] [-kmapro] [-fv]"
echo
echo "Publish app to the Play Store"
echo " -no-daemon Don't start the Gradle daemon. Use for CI"
echo " -kmapro Keyman for Android"
echo " -fv First Voices"
exit 1
}
NO_DAEMON=false
DO_KMAPRO=false
DO_FV=false
# Parse args
while [[ $# -gt 0 ]] ; do
key="$1"
case $key in
-no-daemon)
NO_DAEMON=true
;;
-kmapro)
DO_KMAPRO=true
;;
-fv)
DO_FV=true
;;
-h|-\?)
display_usage
;;
esac
shift # past argument
done
# Override JAVA_HOME to OpenJDK 11
set_java_home
echo
echo "NO_DAEMON: $NO_DAEMON"
echo "DO_KMAPRO: $DO_KMAPRO"
echo "DO_FV: $DO_FV"
echo
if [ "$NO_DAEMON" = true ]; then
DAEMON_FLAG=--no-daemon
else
DAEMON_FLAG=
fi
BUILD_FLAGS="publishReleaseApk"
echo "BUILD_FLAGS $BUILD_FLAGS"
# Publish Keyman for Android
if [ "$DO_KMAPRO" = true ]; then
# Copy Release Notes
generateReleaseNotes
cd "$KEYMAN_ROOT/android/KMAPro/"
./gradlew $DAEMON_FLAG $BUILD_FLAGS
fi
# Publish FV app
if [ "$DO_FV" = true ]; then
cd "$KEYMAN_ROOT/oem/firstvoices/android/"
./gradlew $DAEMON_FLAG $BUILD_FLAGS
fi

View file

@ -25,7 +25,7 @@ builder_describe \
configure \
build \
test \
"publish Publishes the APKs to the Play Store." \
"publish Publishes symbols to Sentry and the APKs to the Play Store." \
--ci+ \
--upload-sentry+ \
":engine=KMEA Keyman Engine for Android" \

View file

@ -19,13 +19,7 @@ permission to access storage / "file and media". The screenshot below is from An
### Android 13.0+ Devices
Step 1)
Go to Android Settings.
Due to Google Play Store's "Photo and Video Permissions policy",
Keyman for Android must use a system picker to browse for .kmp files from within the Keyman app.
Step 2)
Depending on your device, click "Apps" and grant Keyman permission to the following:
* Photos and videos --> Always allow all
The screenshot below is from Android 14.0.
![](../android_images/keyman-storage-permission-34b.png)
Follow the steps in [Installing Custom Keyboards/Dictionaries](../basic/installing-custom-packages).

View file

@ -30,6 +30,7 @@
},
"dependencies": {
"@keymanapp/keyman-version": "*",
"@keymanapp/ldml-keyboard-constants": "*",
"restructure": "git+https://github.com/keymanapp/dependency-restructure.git#7a188a1e26f8f36a175d95b67ffece8702363dfc",
"semver": "^7.5.2",
"xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829"

View file

@ -0,0 +1,33 @@
/*
* Keyman is copyright (C) SIL International. MIT License.
*
* Modifier key bit-flags
*/
export const ModifierKeyConstants = {
// Define Keyman Developer modifier bit-flags (exposed for use by other modules)
// Compare against /common/include/kmx_file.h. CTRL+F "#define LCTRLFLAG" to find the secton.
LCTRLFLAG: 0x0001, // Left Control flag
RCTRLFLAG: 0x0002, // Right Control flag
LALTFLAG: 0x0004, // Left Alt flag
RALTFLAG: 0x0008, // Right Alt flag
K_SHIFTFLAG: 0x0010, // Either shift flag
K_CTRLFLAG: 0x0020, // Either ctrl flag
K_ALTFLAG: 0x0040, // Either alt flag
K_METAFLAG: 0x0080, // Either Meta-key flag (tentative). Not usable in keyboard rules;
// Used internally (currently, only by KMW) to ensure Meta-key
// shortcuts safely bypass rules
// Meta key = Command key on macOS, Windows key on Windows/Linux
CAPITALFLAG: 0x0100, // Caps lock on
NOTCAPITALFLAG: 0x0200, // Caps lock NOT on
NUMLOCKFLAG: 0x0400, // Num lock on
NOTNUMLOCKFLAG: 0x0800, // Num lock NOT on
SCROLLFLAG: 0x1000, // Scroll lock on
NOTSCROLLFLAG: 0x2000, // Scroll lock NOT on
ISVIRTUALKEY: 0x4000, // It is a Virtual Key Sequence
VIRTUALCHARKEY: 0x8000, // Keyman 6.0: Virtual Key Cap Sequence NOT YET
// Note: OTHER_MODIFIER = 0x10000, used by KMX+ for the
// other modifier flag in layers, > 16 bit so not available here.
// See keys_mod_other in keyman_core_ldml.ts
}

View file

@ -285,7 +285,7 @@ export class Vars extends Section {
});
}
findStringVariableValue(id: string): string {
return Vars.findVariable(this.strings, id)?.value?.value; // Unwrap: Variable, StrsItem
return Vars.findVariable(this.strings, id)?.value?.value ?? null; // Unwrap: Variable, StrsItem
}
substituteSetRegex(str: string, sections: DependencySections): string {
return str.replaceAll(VariableParser.SET_REFERENCE, (_entire, id) => {

View file

@ -113,6 +113,7 @@ export class LDMLKeyboardXMLSourceFileReader {
for (const sub of obj) {
// retain the same subtag
if (!this.boxImportsAndSpecials(sub, subtag)) {
// resolveImports has already logged a message
return false;
}
}
@ -125,6 +126,7 @@ export class LDMLKeyboardXMLSourceFileReader {
boxXmlArray(obj, key);
// Now, resolve the import
if (!this.resolveImports(obj, subtag)) {
// resolveImports has already logged a message
return false;
}
// now delete the import array we so carefully constructed, the caller does not
@ -132,6 +134,7 @@ export class LDMLKeyboardXMLSourceFileReader {
delete obj['import'];
} else {
if (!this.boxImportsAndSpecials(obj[key], key)) {
// resolveImports has already logged a message
return false;
}
}
@ -151,6 +154,7 @@ export class LDMLKeyboardXMLSourceFileReader {
// first, the explicit imports
for (const asImport of ([...obj['import'] as LKImport[]].reverse())) {
if (!this.resolveOneImport(obj, subtag, asImport)) {
// resolveOneImport has already logged a message
return false;
}
}
@ -161,6 +165,7 @@ export class LDMLKeyboardXMLSourceFileReader {
base: constants.cldr_import_base,
path: constants.cldr_implied_keys_import
}, true)) {
// resolveOneImport has already logged a message
return false;
}
} else if (subtag === 'forms') {
@ -169,6 +174,7 @@ export class LDMLKeyboardXMLSourceFileReader {
base: constants.cldr_import_base,
path: constants.cldr_implied_forms_import
}, true)) {
// resolveOneImport has already logged a message
return false;
}
}
@ -266,7 +272,8 @@ export class LDMLKeyboardXMLSourceFileReader {
// An alternative fix would be to pull xml2js directly from github
// rather than using the version tagged on npmjs.com.
});
parser.parseString(file, (e: unknown, r: unknown) => { a = r as LDMLKeyboardXMLSourceFile }); // TODO-LDML: isn't 'e' the error?
const data = new TextDecoder().decode(file);
parser.parseString(data, (e: unknown, r: unknown) => { if(e) throw e; a = r as LDMLKeyboardXMLSourceFile }); // TODO-LDML: isn't 'e' the error?
return a;
})();
return source;
@ -278,14 +285,23 @@ export class LDMLKeyboardXMLSourceFileReader {
*/
public load(file: Uint8Array): LDMLKeyboardXMLSourceFile | null {
if (!file) {
throw new Error(`file parameter must not be null`);
}
let source: LDMLKeyboardXMLSourceFile = null;
try {
source = this.loadUnboxed(file);
} catch(e) {
this.callbacks.reportMessage(CommonTypesMessages.Error_InvalidXml({e}));
return null;
}
const source = this.loadUnboxed(file);
if(this.boxArrays(source)) {
if (this.boxArrays(source)) {
return source;
} else {
return null;
}
// boxArrays ... resolveImports has already logged a message
return null;
}
loadTestDataUnboxed(file: Uint8Array): any {

View file

@ -20,6 +20,7 @@ export { VariableParser, MarkerParser } from './ldml-keyboard/pattern-parser.js'
export { LDMLKeyboardXMLSourceFileReader, LDMLKeyboardXMLSourceFileReaderOptions } from './ldml-keyboard/ldml-keyboard-xml-reader.js';
export * as Constants from './consts/virtual-key-constants.js';
export { ModifierKeyConstants } from './consts/modifier-key-constants.js';
export { defaultCompilerOptions, CompilerBaseOptions, CompilerCallbacks, CompilerOptions, CompilerEvent, CompilerErrorNamespace,
CompilerErrorSeverity, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerCallbackOptions,

View file

@ -1,3 +1,6 @@
/*
* Keyman is copyright (C) SIL International. MIT License.
*/
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m } from './compiler-interfaces.js';
import { constants } from '@keymanapp/ldml-keyboard-constants';
@ -43,4 +46,8 @@ export class CommonTypesMessages {
static Error_TestDataUnexpectedArray = (o: {subtag: string}) =>
m(this.ERROR_TestDataUnexpectedArray,
`Problem reading test data: expected single ${def(o.subtag)} element, found multiple`);
static ERROR_InvalidXml = SevError | 0x0008;
static Error_InvalidXml = (o:{e: any}) =>
m(this.ERROR_InvalidXml, `The XML file could not be read: ${(o.e ?? '').toString()}`);
};

View file

@ -5,9 +5,9 @@ cpu = 'arm64'
endian = 'little'
[binaries]
c = ['clang', '-arch', 'arm64']
objc = ['clang', '-arch', 'arm64']
cpp = ['clang++', '-arch', 'arm64']
c = ['clang', '-arch', 'arm64', '-mmacosx-version-min=10.13']
objc = ['clang', '-arch', 'arm64', '-mmacosx-version-min=10.13']
cpp = ['clang++', '-arch', 'arm64', '-mmacosx-version-min=10.13']
ar = 'ar'
ld = 'ld'
strip = 'strip'

View file

@ -5,9 +5,9 @@ cpu = 'x86_64'
endian = 'little'
[binaries]
c = ['clang', '-arch', 'x86_64']
objc = ['clang', '-arch', 'x86_64']
cpp = ['clang++', '-arch', 'x86_64']
c = ['clang', '-arch', 'x86_64', '-mmacosx-version-min=10.13']
objc = ['clang', '-arch', 'x86_64', '-mmacosx-version-min=10.13']
cpp = ['clang++', '-arch', 'x86_64', '-mmacosx-version-min=10.13']
ar = 'ar'
ld = 'ld'
strip = 'strip'

View file

@ -16,6 +16,7 @@
#pragma once
enum km_core_modifier_state {
KM_CORE_MODIFIER_NONE = 0,
KM_CORE_MODIFIER_LCTRL = 1 << 0,
KM_CORE_MODIFIER_RCTRL = 1 << 1,
KM_CORE_MODIFIER_LALT = 1 << 2,

View file

@ -455,7 +455,13 @@ public:
bool setLayr(const COMP_KMXPLUS_LAYR *newLayr);
bool valid() const;
/**
* @param list index from 0 to layr->listCount
*/
const COMP_KMXPLUS_LAYR_LIST *getList(KMX_DWORD list) const;
/**
* @param entry index value: COMP_KMXPLUS_LAYR_LIST.layer but less than COMP_KMXPLUS_LAYR_LIST.layer+COMP_KMXPLUS_LAYR_LIST.count
*/
const COMP_KMXPLUS_LAYR_ENTRY *getEntry(KMX_DWORD entry) const;
const COMP_KMXPLUS_LAYR_ROW *getRow(KMX_DWORD row) const;
const COMP_KMXPLUS_LAYR_KEY *getKey(KMX_DWORD key) const;

View file

@ -12,7 +12,7 @@ using namespace kmx;
/* Globals */
KMX_BOOL km::core::kmx::g_debug_ToConsole = FALSE;
KMX_BOOL km::core::kmx::g_debug_KeymanLog = TRUE;
KMX_BOOL km::core::kmx::g_debug_KeymanLog = FALSE; // workaround for #12661
KMX_BOOL km::core::kmx::g_silent = FALSE;
/*

View file

@ -85,7 +85,7 @@ ldml_processor::ldml_processor(path const & kb_path, const std::vector<uint8_t>
} else {
str = keyEntry->get_to_string();
}
keys.add((km_core_virtual_key)kmapEntry->vkey, (uint16_t)kmapEntry->mod, str);
keys.add((km_core_virtual_key)kmapEntry->vkey, kmapEntry->mod, str);
}
} // else: no keys! but still valid. Just, no keys.
@ -348,8 +348,7 @@ km_core_attr const & ldml_processor::attributes() const {
}
km_core_keyboard_key * ldml_processor::get_key_list() const {
km_core_keyboard_key* key_list = new km_core_keyboard_key(KM_CORE_KEYBOARD_KEY_LIST_END);
return key_list;
return keys.get_key_list();
}
km_core_keyboard_imx * ldml_processor::get_imx_list() const {

View file

@ -8,6 +8,8 @@
#include "ldml_vkeys.hpp"
#include "kmx_file.h"
#include <ldml/keyman_core_ldml.h>
#include <set>
#include <assert.h>
namespace km {
namespace core {
@ -17,11 +19,119 @@ vkeys::vkeys() : vkey_to_string() {
}
void
vkeys::add(km_core_virtual_key vk, uint16_t modifier_state, std::u16string output) {
vkeys::add(km_core_virtual_key vk, km_core_ldml_modifier_state modifier_state, std::u16string output) {
// construct key
const vkey_id id(vk, modifier_state);
// assign the string
vkey_to_string[id] = output;
// includes all keys - including gaps.
all_vkeys.insert(id);
}
km_core_keyboard_key *
vkeys::get_key_list() const {
// prescan to find out which modifier flags are used
std::size_t other_key_count = 0; // number of 'OTHER' keys, which will need to expand to all of the other_state set ( so other_state.size())
std::set<km::core::ldml::km_core_ldml_modifier_state> all_modifiers;
for (const auto &k : all_vkeys) {
const auto mod = k.second;
if (mod == LDML_KEYS_MOD_OTHER) {
other_key_count++;
}
all_modifiers.insert(mod);
}
std::set<km_core_modifier_state> other_state;
// Alt
if (all_modifiers.count(LDML_KEYS_MOD_ALT) == 0 && all_modifiers.count(LDML_KEYS_MOD_ALTL) == 0 && all_modifiers.count(LDML_KEYS_MOD_ALTR) == 0) {
// no ALT keys were seen, so OTHER includes ALT
other_state.insert(KM_CORE_MODIFIER_ALT);
} else if(all_modifiers.count(LDML_KEYS_MOD_ALTL) == 0) {
other_state.insert(KM_CORE_MODIFIER_RALT);
} else if(all_modifiers.count(LDML_KEYS_MOD_ALTR) == 0) {
other_state.insert(KM_CORE_MODIFIER_LALT);
}
// ctrl
if (all_modifiers.count(LDML_KEYS_MOD_CTRL) == 0 && all_modifiers.count(LDML_KEYS_MOD_CTRLL) == 0 && all_modifiers.count(LDML_KEYS_MOD_CTRLR) == 0) {
// no CTRL keys were seen, so OTHER includes CTRL
other_state.insert(KM_CORE_MODIFIER_CTRL);
} else if(all_modifiers.count(LDML_KEYS_MOD_CTRLL) == 0) {
other_state.insert(KM_CORE_MODIFIER_RCTRL);
} else if(all_modifiers.count(LDML_KEYS_MOD_CTRLR) == 0) {
other_state.insert(KM_CORE_MODIFIER_LCTRL);
}
// shift
if (all_modifiers.count(LDML_KEYS_MOD_SHIFT) == 0) {
other_state.insert(KM_CORE_MODIFIER_SHIFT);
}
// caps
if (all_modifiers.count(LDML_KEYS_MOD_CAPS) == 0) {
other_state.insert(KM_CORE_MODIFIER_CAPS);
}
// none- it's possible there is no 'none' layer
if (all_modifiers.count(LDML_KEYS_MOD_NONE) == 0) {
other_state.insert(KM_CORE_MODIFIER_NONE);
}
// We need ALL combinations of the other_state, except for 'all off'.
// The number of additions will be (2**(other_state.size())-1
// Also, since the LDML_KEYS_MOD_OTHER modifier is excluded, we
// will need to subtract 1 when calculating new_list_size
const std::size_t other_expanded_count = (1 << other_state.size()) - 1;
std::vector<uint32_t> other_expanded_mods(other_expanded_count);
// populate the expanded list.
// we start at 1 because 0 is "all bits off" (00000b)
for (std::size_t expansion = 1; expansion <= other_expanded_count; expansion++) {
uint32_t &expanded_mod = other_expanded_mods.at(expansion-1) = KM_CORE_MODIFIER_NONE;
std::size_t bit_mask = 1;
for (const auto mod : other_state) {
// Check if this modifier is on in this iteration of the expansion
// bit_mask will be 2^0 … 2^(other_state().size()-1)
if (bit_mask & expansion) {
// do we include this entry? check if this bit is on
expanded_mod |= mod;
}
// shift the bitmask over
assert(expanded_mod <= KM_CORE_MODIFIER_MASK_CAPS);
bit_mask <<= 1;
}
}
const std::size_t new_list_size = all_vkeys.size() // original size
+ (other_key_count * (other_expanded_count - 1))// number of additional entries needed
+ 1; // terminator
km_core_keyboard_key *list = new km_core_keyboard_key[new_list_size];
std::size_t n = 0;
for (const auto &k : all_vkeys) {
const auto vkey = k.first;
const auto mod = k.second;
if (mod == LDML_KEYS_MOD_OTHER) {
// expand to all of other_state
for (const auto expanded_mod : other_expanded_mods) {
list[n].key = vkey;
list[n++].modifier_flag = expanded_mod;
assert(n <= new_list_size);
}
} else {
assert(mod <= KM_CORE_MODIFIER_MASK_CAPS); // that no LDMLisms escape
list[n].key = vkey;
list[n++].modifier_flag = mod;
assert(n <= new_list_size);
}
}
// add the list terminator
list[n++] = KM_CORE_KEYBOARD_KEY_LIST_END;
assert(n == new_list_size);
return list;
}
static const uint16_t BOTH_ALT = LALTFLAG | RALTFLAG;

View file

@ -12,6 +12,7 @@
#include <unordered_map>
#include <utility>
#include <vector>
#include <set>
#include "keyman_core.h"
@ -19,10 +20,17 @@ namespace km {
namespace core {
namespace ldml {
/**
* LDML keyboards have 32-bit modifier flags in order to support
* LDML_KEYS_MOD_OTHER (0x10000), unlike the Core APIs which have only 16 bit
* modifier flags.
*/
typedef uint32_t km_core_ldml_modifier_state;
/**
* identifier for keybag lookup
*/
typedef std::pair<km_core_virtual_key, uint16_t> vkey_id;
typedef std::pair<km_core_virtual_key, km_core_ldml_modifier_state> vkey_id;
/**
* LDML Class to manage all things key related: vkey remapping and vkey to string
@ -30,14 +38,15 @@ typedef std::pair<km_core_virtual_key, uint16_t> vkey_id;
class vkeys {
private:
std::map<vkey_id, std::u16string> vkey_to_string;
std::set<vkey_id> all_vkeys;
public:
vkeys();
/**
* add a vkey to the bag
* add a vkey to the bag.
*/
void add(km_core_virtual_key vk, uint16_t modifier_state, std::u16string output);
void add(km_core_virtual_key vk, km_core_ldml_modifier_state ldml_modifier_state, std::u16string output);
/**
* Lookup a vkey, returns an empty string if not found
@ -46,6 +55,11 @@ public:
std::u16string
lookup(km_core_virtual_key vk, uint16_t modifier_state, bool &found) const;
/**
* For implementing ldml_processor::get_key_list()
*/
km_core_keyboard_key* get_key_list() const;
private:
/**
* Non-recursive internal lookup of a specific ID

View file

@ -2,8 +2,9 @@
<!--
@@keys: [SHIFT K_BKQUOTE][K_1][K_BKQUOTE]
@@expected: \u0037\u1790\u17B6\u0127
@@keys: [SHIFT K_BKQUOTE][K_1][K_BKQUOTE][LCTRL K_BKQUOTE][RALT K_BKQUOTE]
@@expected: \u0037\u1790\u17B6\u0127\u1790\u17B6\u0065
@@keylist: [SHIFT K_BKQUOTE][SHIFT K_1][K_BKQUOTE][K_1][CTRL-do-not-use K_1][ALT-do-not-use K_BKQUOTE][CAPS K_BKQUOTE][ALT-do-not-use CAPS K_BKQUOTE]
-->
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
@ -23,5 +24,11 @@
<layer id="shift" modifiers="shift">
<row keys="seven eee" /> <!-- number row -->
</layer>
<layer id="control" modifiers="ctrl">
<row keys="that gap" /> <!-- number row -->
</layer>
<layer id="catchall" modifiers="other">
<row keys="eee gap" /> <!-- number row -->
</layer>
</layers>
</keyboard3>

View file

@ -16,6 +16,7 @@
#include <sstream>
#include <string>
#include <type_traits>
#include <set>
#include "path.hpp"
#include "state.hpp"
@ -266,6 +267,63 @@ verify_context(std::u16string &text_store, km_core_state *&test_state, std::vect
delete[] buf;
}
/**
* @param actual the list from get_key_list()
* @param expected optional list with keys to check, can be empty - not exhaistive
* @returns true if passing
*/
bool
verify_key_list(std::set<km::tests::key_event> &actual, std::set<km::tests::key_event> &expected) {
bool equals = true;
// error if any bad modifier keys
for(const auto &akey : actual) {
if (akey.modifier_state > KM_CORE_MODIFIER_MASK_CAPS) {
equals = false;
std::u16string dump = convert<char, char16_t>(akey.dump()); // akey.dump()
std::wcout << console_color::fg(console_color::BRIGHT_RED) << "- FAIL - key_map had key with bad modifier " << akey.modifier_state << ": " << dump << console_color::reset() << std::endl;
}
}
// error if any expected keys missing (note expected may be empty)
for(const auto &ekey : expected) {
if (actual.count(ekey) == 0) {
equals = false;
std::u16string dump = convert<char, char16_t>(ekey.dump()); // akey.dump()
std::wcout << console_color::fg(console_color::BRIGHT_RED) << "- FAIL - key_map had missing key " << dump << console_color::reset() << std::endl;
}
}
if (equals) {
std::wcout << console_color::fg(console_color::GREEN) << " " << actual.size() << " vkeys OK, verified " << expected.size() << console_color::reset() << std::endl;
}
return equals;
}
/**
* @param actual_list the list from get_key_list()
* @param keylist optional string with keys to check, can be empty
* @param test the LDML test source, for additional data
* @returns true if passing
*/
bool
verify_key_list(const km_core_keyboard_key *actual_list, const std::u16string &expected_list, const km::tests::LdmlTestSource &test) {
std::set<km::tests::key_event> actual, expected;
// convert actual list
while (actual_list != nullptr && !(actual_list->key == 0 && actual_list->modifier_flag == 0)) {
km::tests::key_event k(actual_list->key, (uint16_t)actual_list->modifier_flag);
actual.insert(k);
actual_list++; // advance pointer
}
// parse the expected list
std::string keylist = convert<char16_t, char>(expected_list);
while (!keylist.empty() && keylist[0] == '[') {
const km::tests::key_event k = km::tests::LdmlEmbeddedTestSource::parse_next_key(keylist);
if (!k.empty()) {
expected.emplace(k);
}
}
return verify_key_list(actual, expected);
}
int
run_test(const km::core::path &source, const km::core::path &compiled, km::tests::LdmlTestSource& test_source) {
km_core_keyboard * test_kb = nullptr;
@ -371,6 +429,17 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests
errorLine = __LINE__;
}
} break;
case km::tests::LDML_ACTION_CHECK_KEYLIST: {
std::cout << "- checking keylist" << std::endl;
// get keylist from kbd
const km_core_keyboard_key* actual_list = test_kb->get_key_list();
if (!verify_key_list(actual_list, action.string, test_source)) {
errorLine = __LINE__;
} else {
std::cout << " .. passes." << std::endl;
}
delete [] actual_list;
} break;
case km::tests::LDML_ACTION_FAIL: {
// test requested failure
std::wcout << console_color::fg(console_color::BRIGHT_RED) << "- FAIL: " << action.string << console_color::reset()
@ -421,7 +490,7 @@ int run_all_tests(const km::core::path &source, const km::core::path &compiled,
std::vector<std::string> failures; // track failures for summary
int embedded_result = embedded_test_source.load_source(source);
int embedded_result = embedded_test_source.load_source(source, compiled);
if (!filter.empty()) {
// Always skip the embedded test if there's a filter.

View file

@ -122,6 +122,50 @@ bool LdmlTestSource::get_expected_beep() const {
return false;
}
int LdmlTestSource::load_kmx_plus(const km::core::path &compiled) {
// check and load the KMX (yes, once again)
if(!km::core::ldml_processor::is_kmxplus_file(compiled, rawdata)) {
std::cerr << "Reading KMX for test purposes failed: " << compiled << std::endl;
return __LINE__;
}
auto comp_keyboard = (const km::core::kmx::COMP_KEYBOARD*)rawdata.data();
// initialize the kmxplus object with our copy
kmxplus.reset(new km::core::kmx::kmx_plus(comp_keyboard, rawdata.size()));
if (!kmxplus->is_valid()) {
std::cerr << "kmx_plus invalid" << std::endl;
return __LINE__;
}
if (!kmxplus->key2Helper.valid()) {
std::cerr << "kmx_plus invalid" << std::endl;
return __LINE__;
}
return 0; // success
}
bool LdmlTestSource::get_vkey_table(std::set<key_event> &fillin) const {
if (!kmxplus || !kmxplus->is_valid()) {
return false; // fail
}
// just dump the kmap table
for (KMX_DWORD kmapIdx = 0; kmapIdx < kmxplus->key2->kmapCount; kmapIdx++) {
const km::core::kmx::COMP_KMXPLUS_KEYS_KMAP *kmap = kmxplus->key2Helper.getKmap(kmapIdx);
if (kmap == nullptr) {
return false;
}
if (kmap->vkey > 0xFF) {
continue; // synthetic key- skip
}
fillin.insert(key_event(kmap->vkey, kmap->mod));
}
return true;
}
// String trim functions from https://stackoverflow.com/a/217605/1836776
// trim from start (in place)
static inline void
@ -259,12 +303,13 @@ LdmlEmbeddedTestSource::is_token(const std::string token, std::string &line) {
}
int
LdmlEmbeddedTestSource::load_source( const km::core::path &path ) {
LdmlEmbeddedTestSource::load_source( const km::core::path &path, const km::core::path &compiled ) {
const std::string s_keys = "@@keys: ";
const std::string s_expected = "@@expected: ";
const std::string s_context = "@@context: ";
const std::string s_capsLock = "@@capsLock: ";
const std::string s_expecterror = "@@expect-error: ";
const std::string s_keylist = "@@keylist: ";
// Parse out the header statements in file.kmn that tell us (a) environment, (b) key sequence, (c) start context, (d) expected
// result
@ -296,6 +341,10 @@ LdmlEmbeddedTestSource::load_source( const km::core::path &path ) {
context = parse_source_string(line);
} else if (is_token(s_capsLock, line)) {
set_caps_lock_on(parse_source_string(line).compare(u"1") == 0);
} else if (is_token(s_keylist, line)) {
set_keylist(line);
} else if (line[0] == '@') {
std::cerr << path << " warning, unknown @-command " << line << std::endl;
}
}
@ -311,7 +360,13 @@ LdmlEmbeddedTestSource::load_source( const km::core::path &path ) {
return __LINE__;
}
return 0;
// and load KMX+ for keylist
if (!expected_error) {
// don't attempt to load KMX+ on expected error
return load_kmx_plus(compiled);
} else {
return 0;
}
}
km_core_status
@ -352,7 +407,7 @@ LdmlTestSource::char_to_event(char ch) {
}
uint16_t
LdmlTestSource::get_modifier(std::string const m) {
LdmlTestSource::get_modifier(std::string const &m) {
for (int i = 0; km::core::kmx::s_modifier_names[i].name; i++) {
if (m == km::core::kmx::s_modifier_names[i].name) {
return km::core::kmx::s_modifier_names[i].modifier;
@ -361,6 +416,12 @@ LdmlTestSource::get_modifier(std::string const m) {
return 0;
}
std::string key_event::dump() const {
std::stringstream f;
f << "Key: {" << km::core::kmx::Debug_VirtualKey(vk) << ", " << km::core::kmx::Debug_ModifierName(modifier_state) << "}";
return f.str();
}
key_event
LdmlEmbeddedTestSource::vkey_to_event(std::string const &vk_event) {
// vkey format is MODIFIER MODIFIER K_NAME
@ -396,34 +457,37 @@ LdmlEmbeddedTestSource::vkey_to_event(std::string const &vk_event) {
void
LdmlEmbeddedTestSource::next_action(ldml_action &fillin) {
if (is_done || keys.empty()) {
// We were already done. return done.
fillin.type = LDML_ACTION_DONE;
return;
if (keys.empty()) {
// #3 we are almost done, let's run the key check
if (check_keylist) {
fillin.type = LDML_ACTION_CHECK_KEYLIST;
fillin.string = expected_keylist; // could be empty
check_keylist = false;
} else {
fillin.type = LDML_ACTION_DONE;
}
} else if(keys[0].empty()) {
// #2. Then, when we finish a key set, we check the 'expected' at the end of it.
// Got to the end of a key set. time to check
fillin.type = LDML_ACTION_CHECK_EXPECTED;
fillin.string = expected[0]; // copy expected
expected.pop_front();
keys.pop_front();
if (keys.empty()) {
is_done = true; // so we get DONE next time
}
} else {
// #1 First, we process each key
fillin.type = LDML_ACTION_KEY_EVENT;
fillin.k = next_key();
}
}
key_event
LdmlEmbeddedTestSource::next_key() {
// mutate this->keys
return next_key(keys[0]);
return parse_next_key(keys[0]);
}
key_event
LdmlEmbeddedTestSource::next_key(std::string &keys) {
LdmlEmbeddedTestSource::parse_next_key(std::string &keys) {
// Parse the next element of the string, chop it off, and return it
// mutates keys
if (keys.length() == 0)
@ -448,10 +512,10 @@ LdmlEmbeddedTestSource::next_key(std::string &keys) {
class LdmlJsonTestSource : public LdmlTestSource {
public:
LdmlJsonTestSource(const std::string &path, km::core::kmx::kmx_plus *kmxplus);
LdmlJsonTestSource(const std::string &path);
virtual ~LdmlJsonTestSource();
virtual const std::u16string &get_context();
int load(const nlohmann::json &test);
int load(const nlohmann::json &test, const km::core::path &compiled);
virtual void next_action(ldml_action &fillin);
private:
std::string path;
@ -462,14 +526,14 @@ private:
* Which action are we on?
*/
std::size_t action_index = -1;
const km::core::kmx::kmx_plus *kmxplus;
/** @return false if not found */
bool set_key_from_id(key_event& k, const std::u16string& id);
bool loaded_context = false;
bool check_keys = true;
};
LdmlJsonTestSource::LdmlJsonTestSource(const std::string &path, km::core::kmx::kmx_plus *k)
:path(path), kmxplus(k) {
LdmlJsonTestSource::LdmlJsonTestSource(const std::string &path)
:path(path) {
}
@ -514,6 +578,13 @@ bool LdmlJsonTestSource::set_key_from_id(key_event& k, const std::u16string& id)
void
LdmlJsonTestSource::next_action(ldml_action &fillin) {
if ((action_index+1) >= data["/actions"_json_pointer].size()) {
// add check keylist
if (check_keys) {
fillin.type = LDML_ACTION_CHECK_KEYLIST;
fillin.string.clear();
check_keys = false;
return;
}
// at end, done
fillin.type = LDML_ACTION_DONE;
return;
@ -581,19 +652,21 @@ LdmlJsonTestSource::get_context() {
return context;
}
int LdmlJsonTestSource::load(const nlohmann::json &data) {
int LdmlJsonTestSource::load(const nlohmann::json &data, const km::core::path &compiled) {
this->data = data;
// TODO-LDML: validate here?
return 0;
// TODO-LDML: validate JSON here?
// load up the kmx_plus
return load_kmx_plus(compiled);
}
#if defined(HAVE_ICU4C)
class LdmlJsonRepertoireTestSource : public LdmlTestSource {
public:
LdmlJsonRepertoireTestSource(const std::string &path, km::core::kmx::kmx_plus *kmxplus);
LdmlJsonRepertoireTestSource(const std::string &path );
virtual ~LdmlJsonRepertoireTestSource();
virtual const std::u16string &get_context();
int load(const nlohmann::json &test);
int load(const nlohmann::json &test, const km::core::path &compiled);
virtual void next_action(ldml_action &fillin);
private:
std::string path;
@ -605,11 +678,10 @@ private:
std::unique_ptr<icu::UnicodeSet> uset;
std::unique_ptr<icu::UnicodeSetIterator> iterator;
bool need_check = false; // set this after each char
const km::core::kmx::kmx_plus *kmxplus;
};
LdmlJsonRepertoireTestSource::LdmlJsonRepertoireTestSource(const std::string &path, km::core::kmx::kmx_plus *k)
:path(path), kmxplus(k){
LdmlJsonRepertoireTestSource::LdmlJsonRepertoireTestSource(const std::string &path)
:path(path) {
}
@ -697,7 +769,7 @@ LdmlJsonRepertoireTestSource::get_context() {
return context; // no context needed
}
int LdmlJsonRepertoireTestSource::load(const nlohmann::json &data) {
int LdmlJsonRepertoireTestSource::load(const nlohmann::json &data, const km::core::path &compiled) {
this->data = data; // TODO-LDML
// Need an update to json.hpp to use contains()
// if (data.contains("/type"_json_pointer)) {
@ -728,7 +800,7 @@ int LdmlJsonRepertoireTestSource::load(const nlohmann::json &data) {
// }
// #endif
iterator = std::unique_ptr<icu::UnicodeSetIterator>(new icu::UnicodeSetIterator(*uset));
return 0;
return load_kmx_plus(compiled);
}
#endif // HAVE_ICU4C
@ -752,25 +824,6 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co
return __LINE__; // empty
}
// check and load the KMX (yes, once again)
if(!km::core::ldml_processor::is_kmxplus_file(compiled, rawdata)) {
std::cerr << "Reading KMX for test purposes failed: " << compiled << std::endl;
return __LINE__;
}
auto comp_keyboard = (const km::core::kmx::COMP_KEYBOARD*)rawdata.data();
// initialize the kmxplus object with our copy
kmxplus.reset(new km::core::kmx::kmx_plus(comp_keyboard, rawdata.size()));
if (!kmxplus->is_valid()) {
std::cerr << "kmx_plus invalid" << std::endl;
return __LINE__;
}
if (!kmxplus->key2Helper.valid()) {
std::cerr << "kmx_plus invalid" << std::endl;
return __LINE__;
}
auto conformsTo = data["/keyboardTest3/conformsTo"_json_pointer].get<std::string>();
assert_or_return(std::string(LDML_CLDR_TEST_VERSION_LATEST) == conformsTo);
@ -793,8 +846,8 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co
test_path.append(info_name).append("/tests/").append(tests_name).append("/").append(test_name);
// std::cout << "JSON: reading " << info_name << "/" << test_path << std::endl;
std::unique_ptr<LdmlJsonTestSource> subtest(new LdmlJsonTestSource(test_path, kmxplus.get()));
assert_or_return(subtest->load(test) == 0);
std::unique_ptr<LdmlJsonTestSource> subtest(new LdmlJsonTestSource(test_path));
assert_or_return(subtest->load(test, compiled) == 0);
test_map[test_path] = std::unique_ptr<LdmlTestSource>(subtest.release());
}
}
@ -809,8 +862,8 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co
std::string test_path;
test_path.append(info_name).append("/repertoire/").append(rep_name);
std::unique_ptr<LdmlJsonRepertoireTestSource> reptest(new LdmlJsonRepertoireTestSource(test_path, kmxplus.get()));
assert_or_return(reptest->load(rep) == 0);
std::unique_ptr<LdmlJsonRepertoireTestSource> reptest(new LdmlJsonRepertoireTestSource(test_path));
assert_or_return(reptest->load(rep, compiled) == 0);
test_map[test_path] = std::unique_ptr<LdmlTestSource>(reptest.release());
}

View file

@ -4,6 +4,7 @@
#include "path.hpp"
#include <map>
#include <set>
#include <memory>
#include "kmx/kmx_plus.h"
@ -14,6 +15,34 @@ namespace tests {
struct key_event {
km_core_virtual_key vk;
uint16_t modifier_state;
public:
key_event() : vk(0), modifier_state(0) {
}
key_event(km_core_virtual_key k, uint16_t m) :vk(k), modifier_state(m) {
}
std::string dump() const;
int compare(const key_event &other) const {
if (vk < other.vk) return -1;
if (vk > other.vk) return 1;
if (modifier_state < other.modifier_state) return -1;
if (modifier_state > other.modifier_state) return 1;
return 0;
}
bool operator<(const key_event &other) const {
return compare(other) < 0;
}
bool operator>(const key_event &other) const {
return compare(other) > 0;
}
bool operator==(const key_event &other) const {
return compare(other) == 0;
}
/** true if unset (null) key */
bool empty() const {
return vk == 0 && modifier_state == 0;
}
};
enum ldml_action_type {
@ -37,6 +66,10 @@ enum ldml_action_type {
* expected text
*/
LDML_ACTION_CHECK_EXPECTED,
/**
* string - keylist to check
*/
LDML_ACTION_CHECK_KEYLIST,
// TODO-LDML: gestures, etc? Depends on touch.
/**
@ -77,10 +110,17 @@ public:
virtual km_core_status get_expected_load_status();
virtual const std::u16string &get_context() = 0;
virtual bool get_expected_beep() const;
/**
* fillin a list
* @param fillin key list to be filled in with all vkeys in the hardware map.
* @param modifier modifier to load key list for
* @returns false on load fail, true on OK
*/
bool get_vkey_table(std::set<key_event> &fillin) const;
// helper functions
static key_event char_to_event(char ch);
static uint16_t get_modifier(std::string const m);
static uint16_t get_modifier(std::string const &m);
static std::u16string parse_source_string(std::string const &s);
static std::u16string parse_u8_source_string(std::string const &s);
@ -102,6 +142,12 @@ private:
private:
bool _caps_lock_on = false;
protected:
/** populate rawdata and kmxplus */
int load_kmx_plus(const km::core::path &compiled);
// copy of the kbd data, for lookups
std::vector<uint8_t> rawdata;
std::unique_ptr<km::core::kmx::kmx_plus> kmxplus;
};
typedef std::map<std::string, std::unique_ptr<km::tests::LdmlTestSource>> JsonTestMap;
@ -120,9 +166,6 @@ class LdmlJsonTestSourceFactory {
const JsonTestMap& get_tests() const;
private:
JsonTestMap test_map;
// copy of the kbd data, for lookups
std::vector<uint8_t> rawdata;
std::unique_ptr<km::core::kmx::kmx_plus> kmxplus;
};
@ -134,7 +177,7 @@ public:
/**
* Load the test_source from comments in the .xml source
*/
int load_source(const km::core::path &path);
int load_source(const km::core::path &path, const km::core::path &compiled);
virtual km_core_status get_expected_load_status();
virtual const std::u16string &get_context();
@ -143,10 +186,6 @@ public:
virtual void next_action(ldml_action &fillin);
private:
bool is_token(const std::string token, std::string &line);
key_event vkey_to_event(std::string const &vk_event);
key_event next_key(std::string &keys);
key_event next_key();
std::deque<std::string> keys;
@ -155,6 +194,24 @@ private:
bool expected_beep = false;
bool expected_error = false;
bool is_done = false;
/** did we check the keylist yet? */
bool check_keylist = true;
/** set the expected keys in the keylist */
void set_keylist(std::string const& s) {
expected_keylist = parse_source_string(s);
}
std::u16string expected_keylist;
/** returns false on fail and updates the message */
bool handle_check_keylist(std::string &message) const;
// utility
static key_event vkey_to_event(std::string const &vk_event);
static bool is_token(const std::string token, std::string &line);
public:
static key_event parse_next_key(std::string &keys);
};
} // namespace tests

View file

@ -1,4 +1,4 @@
[binaries]
c = ['$EMSCRIPTEN_BASE/emcc.py']
cpp = ['$EMSCRIPTEN_BASE/em++.py']
ar = ['$EMSCRIPTEN_BASE/emar.py']
c = ['$EMSCRIPTEN_BASE/emcc']
cpp = ['$EMSCRIPTEN_BASE/em++']
ar = ['$EMSCRIPTEN_BASE/emar']

View file

@ -1,4 +1,4 @@
[binaries]
c = ['$EMSCRIPTEN_BASE/emcc.py']
cpp = ['$EMSCRIPTEN_BASE/em++.py']
ar = ['$EMSCRIPTEN_BASE/emar.py']
c = ['$EMSCRIPTEN_BASE/emcc']
cpp = ['$EMSCRIPTEN_BASE/em++']
ar = ['$EMSCRIPTEN_BASE/emar']

View file

@ -1,7 +1,7 @@
[binaries]
c = ['emcc.py']
cpp = ['em++.py']
ar = ['emar.py']
c = ['emcc']
cpp = ['em++']
ar = ['emar']
exe_wrapper = 'node'
[properties]

View file

@ -493,9 +493,6 @@ export class KeyboardInfoCompiler implements KeymanCompiler {
keyboard_info.languages[language] = {};
}
const fontSource = [].concat(...kmpJsonData.keyboards.map(e => e.displayFont ? [e.displayFont] : []), ...kmpJsonData.keyboards.map(e => e.webDisplayFonts ?? []));
const oskFontSource = [].concat(...kmpJsonData.keyboards.map(e => e.oskFont ? [e.oskFont] : []), ...kmpJsonData.keyboards.map(e => e.webOskFonts ?? []));
for(const bcp47 of Object.keys(keyboard_info.languages)) {
const language = keyboard_info.languages[bcp47];
@ -520,6 +517,20 @@ export class KeyboardInfoCompiler implements KeymanCompiler {
// do it right now.
//
// The code below:
// 1. Only includes fonts associated with keyboards which support the current bcp47 (filter)
// 2. Joins the displayFont and webDisplayFonts data, and removes duplicates (...new Set())
const supportedKeyboards = kmpJsonData.keyboards.filter(k => k.languages.find(lang => lang.id == bcp47));
const fontSource = [...new Set([].concat(
...supportedKeyboards.map(e => e.displayFont ? [e.displayFont] : []),
...supportedKeyboards.map(e => e.webDisplayFonts ?? [])
))];
const oskFontSource = [...new Set([].concat(
...supportedKeyboards.map(e => e.oskFont ? [e.oskFont] : []),
...supportedKeyboards.map(e => e.webOskFonts ?? [])
))];
if(fontSource.length) {
language.font = await this.fontSourceToKeyboardInfoFont(kpsFilename, kmpJsonData, fontSource);
if(language.font == null) {

View file

@ -35,12 +35,14 @@
"@keymanapp/developer-test-helpers": "*",
"@keymanapp/resources-gosh": "*",
"@types/chai": "^4.1.7",
"@types/common-tags": "^1.8.4",
"@types/mocha": "^5.2.7",
"@types/node": "^20.4.1",
"@types/semver": "^7.3.12",
"c8": "^7.12.0",
"chai": "^4.3.4",
"chalk": "^2.4.2",
"common-tags": "^1.8.2",
"mocha": "^8.4.0",
"ts-node": "^9.1.1",
"typescript": "^4.9.5"

View file

@ -1,4 +1,4 @@
import { LDMLKeyboardXMLSourceFileReader, LDMLKeyboard, KMXPlus, CompilerCallbacks, LDMLKeyboardTestDataXMLSourceFile, UnicodeSetParser, KeymanCompiler, KeymanCompilerResult, KeymanCompilerArtifacts, defaultCompilerOptions, KMXBuilder, KvkFileWriter, KeymanCompilerArtifactOptional } from '@keymanapp/common-types';
import { KMX, LDMLKeyboardXMLSourceFileReader, LDMLKeyboard, KMXPlus, CompilerCallbacks, LDMLKeyboardTestDataXMLSourceFile, UnicodeSetParser, KeymanCompiler, KeymanCompilerResult, KeymanCompilerArtifacts, defaultCompilerOptions, KMXBuilder, KvkFileWriter, KeymanCompilerArtifactOptional } from '@keymanapp/common-types';
import { LdmlCompilerOptions } from './ldml-compiler-options.js';
import { CompilerMessages } from './messages.js';
import { BkspCompiler, TranCompiler } from './tran.js';
@ -125,18 +125,34 @@ export class LdmlKeyboardCompiler implements KeymanCompiler {
return null;
}
outputFilename = outputFilename ?? inputFilename.replace(/\.xml$/, '.kmx');
// In order for the KMX file to be loaded by non-KMXPlus components, it is helpful
// to duplicate some of the metadata
KMXPlusMetadataCompiler.addKmxMetadata(kmx.kmxplus, kmx.keyboard, compilerOptions);
// Use the builder to generate the binary output file
const builder = new KMXBuilder(kmx, compilerOptions.saveDebug);
const kmx_binary = builder.compile();
const kmxBuilder = new KMXBuilder(kmx, compilerOptions.saveDebug);
const keyboardId = this.callbacks.path.basename(outputFilename, '.kmx');
const vkCompiler = new LdmlKeyboardVisualKeyboardCompiler(this.callbacks);
const vkCompilerResult = vkCompiler.compile(kmx.kmxplus, keyboardId);
if(vkCompilerResult === null) {
return null;
}
const vkData = typeof vkCompilerResult == 'object' ? vkCompilerResult : null;
const vkcompiler = new LdmlKeyboardVisualKeyboardCompiler(this.callbacks);
const vk = vkcompiler.compile(source);
const writer = new KvkFileWriter();
const kvk_binary = writer.write(vk);
if(vkData) {
kmx.keyboard.stores.push({
dpName: '',
dpString: keyboardId + '.kvk',
dwSystemID: KMX.KMXFile.TSS_VISUALKEYBOARD
});
}
const kmxBinary = kmxBuilder.compile();
const kvkWriter = new KvkFileWriter();
const kvkBinary = vkData ? kvkWriter.write(vkData) : null;
// Note: we could have a step of generating source files here
// KvksFileWriter()...
@ -149,12 +165,10 @@ export class LdmlKeyboardCompiler implements KeymanCompiler {
//KMW17.0: const encoder = new TextEncoder();
//KMW17.0: const kmw_binary = encoder.encode(kmw_string);
outputFilename = outputFilename ?? inputFilename.replace(/\.xml$/, '.kmx');
return {
artifacts: {
kmx: { data: kmx_binary, filename: outputFilename },
kvk: { data: kvk_binary, filename: outputFilename.replace(/\.kmx$/, '.kvk') },
kmx: { data: kmxBinary, filename: outputFilename },
kvk: kvkBinary ? { data: kvkBinary, filename: outputFilename.replace(/\.kmx$/, '.kvk') } : null,
//KMW17.0: js: { data: kmw_binary, filename: outputFilename.replace(/\.kmx$/, '.js') },
}
};

View file

@ -5,7 +5,7 @@ import { VarsCompiler } from './vars.js';
import { CompilerMessages } from './messages.js';
/**
* Compiler for typrs that don't actually consume input XML
* Compiler for types that don't actually consume input XML
*/
export abstract class EmptyCompiler extends SectionCompiler {
private _id: SectionIdent;

View file

@ -1,113 +0,0 @@
import { CompilerCallbacks, VisualKeyboard, LDMLKeyboard, TouchLayoutFileWriter, KeymanFileTypes } from "@keymanapp/common-types";
import { LdmlCompilerOptions } from "./ldml-compiler-options.js";
import { TouchLayoutCompiler } from "./touch-layout-compiler.js";
import { LdmlKeyboardVisualKeyboardCompiler } from "./visual-keyboard-compiler.js";
const MINIMUM_KMW_VERSION = '16.0';
export class LdmlKeyboardKeymanWebCompiler {
private readonly options: LdmlCompilerOptions;
private readonly nl: string;
private readonly tab: string;
constructor(private callbacks: CompilerCallbacks, options?: LdmlCompilerOptions) {
this.options = { ...options };
this.nl = this.options.saveDebug ? "\n" : '';
this.tab = this.options.saveDebug ? " " : '';
}
public compileVisualKeyboard(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile) {
const nl = this.nl, tab = this.tab;
const vkc = new LdmlKeyboardVisualKeyboardCompiler(this.callbacks);
const vk: VisualKeyboard.VisualKeyboard = vkc.compile(source);
let result =
`{F: ' 1em ${JSON.stringify(vk.header.unicodeFont.name)}', `+
`K102: ${vk.header.flags & VisualKeyboard.VisualKeyboardHeaderFlags.kvkh102 ? 1 : 0}};${nl}` + // TODO-LDML: escape ' and " in font name correctly
`${tab}this.KV.KLS={${nl}` +
`${tab}${tab}TODO_LDML: ${vk.keys.length}${nl}` +
// TODO-LDML: fill in KLS
`${tab}}`;
return result;
}
public compileTouchLayout(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile) {
const tlcompiler = new TouchLayoutCompiler();
const layout = tlcompiler.compileToJavascript(source);
const writer = new TouchLayoutFileWriter({formatted: this.options.saveDebug});
return writer.compile(layout);
}
private cleanName(name: string): string {
let result = this.callbacks.path.basename(name, KeymanFileTypes.Source.LdmlKeyboard);
if(!result) {
throw new Error(`Invalid file name ${name}`);
}
result = result.replaceAll(/[^a-z0-9]/g, '_');
if(result.match(/^[0-9]/)) {
// Can't have a digit as initial
result = '_' + result;
}
return result;
}
public compile(name: string, source: LDMLKeyboard.LDMLKeyboardXMLSourceFile): string {
const nl = this.nl, tab = this.tab;
const sName = 'Keyboard_'+this.cleanName(name);
const displayUnderlying = true; // TODO-LDML
const modifierBitmask = 0; // TODO-LDML: define the modifiers used by this keyboard
const vkDictionary = ''; // TODO-LDML: vk dictionary for touch keys
const hasSupplementaryPlaneChars = false; // TODO-LDML
const isRTL = false; // TODO-LDML
let result =
`if(typeof keyman === 'undefined') {${nl}` +
`${tab}console.error('Keyboard requires KeymanWeb ${MINIMUM_KMW_VERSION} or later');${nl}` +
`} else {${nl}` +
`${tab}KeymanWeb.KR(new ${sName}());${nl}` +
`}${nl}` +
`function ${sName}() {${nl}` +
// `${tab}${this.setupDebug()}${nl}` + ? we may use this for modifierBitmask in future
// `${tab}this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9;${nl}` + ? we probably don't need this, it's for back-compat
`${tab}this.KI="${sName}";${nl}` +
`${tab}this.KN=${JSON.stringify(source.keyboard3.info.name)};${nl}` +
`${tab}this.KMINVER=${JSON.stringify(MINIMUM_KMW_VERSION)};${nl}` +
`${tab}this.KV=${this.compileVisualKeyboard(source)};${nl}` +
`${tab}this.KDU=${displayUnderlying ? '1' : '0'};${nl}` +
`${tab}this.KH="";${nl}` + // TODO-LDML: help text not supported
`${tab}this.KM=0;${nl}` + // TODO-LDML: mnemonic layout not supported for LDML keyboards
`${tab}this.KBVER=${JSON.stringify(source.keyboard3.version?.number || '0.0')};${nl}` +
`${tab}this.KMBM=${modifierBitmask};${nl}`;
if(isRTL) {
result += `${tab}this.KRTL=1;${nl}`;
}
if(hasSupplementaryPlaneChars) {
result += `${tab}this.KS=1;${nl}`;
}
if(vkDictionary != '') {
result += `${tab}this.KVKD=${JSON.stringify(vkDictionary)};${nl}`;
}
let layoutFile = this.compileTouchLayout(source);
if(layoutFile != '') {
result += `${tab}this.KVKL=${layoutFile};${nl}`;
}
// TODO-LDML: KCSS not supported
// TODO-LDML: embed binary keyboard for loading into Core
// A LDML keyboard has a no-op for its gs() (begin Unicode) function,
// because the functionality is embedded in Keyman Core
result += `${tab}this.gs=function(t,e){${nl}`+
`${tab}${tab}return 0;${nl}`+ // TODO-LDML: we will start by embedding call into Keyman Core here
`${tab}};${nl}`;
result += `}${nl}`;
return result;
}
}

View file

@ -63,7 +63,7 @@ export class LayrCompiler extends SectionCompiler {
for (const layer of layers.layer) {
const rows = layer.row.map((row) => {
const erow: LayrRow = {
keys: row.keys.split(' ').map((id) => sections.strs.allocString(id)),
keys: row.keys.trim().split(/[ \t]+/).map((id) => sections.strs.allocString(id)),
};
return erow;
});

View file

@ -174,7 +174,13 @@ export class CompilerMessages {
static Error_UnparseableReorderSet = (o: { from: string, set: string }) =>
m(this.ERROR_UnparseableReorderSet, `Illegal UnicodeSet "${def(o.set)}" in reorder "${def(o.from)}`);
// Available: 0x029
static ERROR_InvalidVariableIdentifer = SevError | 0x0029;
static Error_InvalidVariableIdentifer = (o: { id: string }) => m(
this.ERROR_InvalidVariableIdentifer,
`Invalid variable identifier "\\u${def(o.id)}". Identifiers must be between 1 and 32 characters, and can use A-Z, a-z, 0-9, and _.`,
);
// Available: 0x02A-0x2F
static ERROR_InvalidQuadEscape = SevError | 0x0030;
static Error_InvalidQuadEscape = (o: { cp: number }) =>
@ -189,9 +195,12 @@ export class CompilerMessages {
m(this.ERROR_UnparseableTransformFrom, `Invalid transform from="${def(o.from)}": "${def(o.message)}"`);
static ERROR_IllegalTransformDollarsign = SevErrorTransform | 0x01;
static Error_IllegalTransformDollarsign = (o: { from: string }) =>
m(this.ERROR_IllegalTransformDollarsign, `Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`,
'**Hint**: Use `\\$` to match a literal dollar-sign.');
static Error_IllegalTransformDollarsign = (o: { from: string }) => m(
this.ERROR_IllegalTransformDollarsign,
`Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`,
`**Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a variable name, `+
`the variable name may not be valid (A-Z, a-z, 0-9, _, 32 character maximum).`
);
static ERROR_TransformFromMatchesNothing = SevErrorTransform | 0x02;
static Error_TransformFromMatchesNothing = (o: { from: string }) =>

View file

@ -1,112 +0,0 @@
import { TouchLayout, LDMLKeyboard } from "@keymanapp/common-types";
export class TouchLayoutCompiler {
public compileToJavascript(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile): TouchLayout.TouchLayoutFile {
let result: TouchLayout.TouchLayoutFile = {};
// start with desktop to mimic vk emit
result.desktop = {
defaultHint: "none", // TODO-LDML this should be optional
layer: []
};
for(let layers of source.keyboard3.layers) {
for(let layer of layers.layer) {
const resultLayer = this.compileHardwareLayer(source, result, layer);
result.desktop.layer.push(resultLayer);
}
}
return result;
}
private compileHardwareLayer(
source: LDMLKeyboard.LDMLKeyboardXMLSourceFile,
file: TouchLayout.TouchLayoutFile,
layer: LDMLKeyboard.LKLayer
) {
// TODO-LDML: consider consolidation with keys.ts?
let fileLayer: TouchLayout.TouchLayoutLayer = {
id: this.translateLayerIdToTouchLayoutLayerId(layer.id, layer.modifiers),
row: []
};
let y = -1;
for(let row of layer.row) {
y++;
let fileRow: TouchLayout.TouchLayoutRow = {id: y, key: []};
fileLayer.row.push(fileRow);
const keys = row.keys.split(' ');
for(let key of keys) {
const keydef = source.keyboard3.keys?.key?.find(x => x.id == key);
if(keydef) {
const fileKey: TouchLayout.TouchLayoutKey = {
id: this.translateKeyIdentifierToTouch(keydef.id) as TouchLayout.TouchLayoutKeyId,
text: keydef.output || '',
// TODO-LDML: additional properties
};
fileRow.key.push(fileKey);
} else {
// TODO-LDML: consider logging missing keys
}
}
}
return fileLayer;
}
private translateLayerIdToTouchLayoutLayerId(id: string, modifier: string): string {
// Touch layout layers have a set of reserved names that correspond to
// hardware modifiers. We want to map these identifiers first before falling
// back to the layer ids
// The set of recognized layer identifiers is:
//
// touch | LDML
// ---------------+-------------
// default | none
// shift | shift
// caps | caps
// rightalt | altR
// rightalt-shift | altR shift
//
const map = {
none: 'default',
shift: 'shift',
caps: 'caps',
altR: 'rightalt',
"altR shift": 'rightalt-shift'
};
// canonicalize modifier string, alphabetical
// TODO-LDML: need to support multiple here
if (modifier && modifier.indexOf(',') !== -1) {
throw Error(`Internal error: TODO-LDML: multiple modifiers ${modifier} not yet supported.`);
}
modifier = (modifier||'').split(/\b/).sort().join(' ').trim();
if(Object.hasOwn(map, modifier)) {
return (map as any)[modifier];
}
// TODO-LDML: Other layer names will be used unmodified, is this sufficient?
return id;
}
private translateKeyIdentifierToTouch(id: string): string {
// Note: keys identifiers in kmx were traditionally case-insensitive, but we
// are going to use them as case-insensitive for LDML keyboards. The set of
// standard key identifiers a-z, A-Z, 0-9 will be used, where possible, and
// all other keys will be mapped to `T_key`.
if(id.match(/^[0-9a-zA-Z]$/)) {
return 'K_'+id;
}
// Not a standard key
return 'T_'+id;
}
}

View file

@ -40,6 +40,14 @@ export class VarsCompiler extends SectionCompiler {
return valid;
}
private validateIdentifier(id: string) {
if(!id.match(VariableParser.ID)) { // From <string> DTD
this.callbacks.reportMessage(CompilerMessages.Error_InvalidVariableIdentifer({id}));
return false;
}
return true;
}
private validateVars(st: Substitutions): boolean {
let valid = true;
const variables = this.keyboard3?.variables;
@ -67,13 +75,28 @@ export class VarsCompiler extends SectionCompiler {
if (variables) {
// Strings
for (const { id, value } of variables.string) {
if(!this.validateIdentifier(id)) {
valid = false;
continue;
}
addId(id);
allStrings.add(id);
const stringrefs = VariableParser.allStringReferences(value);
for(const ref of stringrefs) {
if(!allStrings.has(ref)) {
valid = false;
this.callbacks.reportMessage(CompilerMessages.Error_MissingStringVariable({id: ref}));
allStrings.add(ref); // avoids multiple reports of same missing variable
}
}
st.string.add(SubstitutionUse.variable, stringrefs);
allStrings.add(id);
}
// Sets
for (const { id, value } of variables.set) {
if(!this.validateIdentifier(id)) {
valid = false;
continue;
}
addId(id);
allSets.add(id);
// check for illegal references, here.
@ -96,6 +119,10 @@ export class VarsCompiler extends SectionCompiler {
}
// UnicodeSets
for (const { id, value } of variables.uset) {
if(!this.validateIdentifier(id)) {
valid = false;
continue;
}
addId(id);
allUnicodeSets.add(id);
const stringrefs = VariableParser.allStringReferences(value);

View file

@ -1,84 +1,164 @@
import { VisualKeyboard, LDMLKeyboard, CompilerCallbacks } from "@keymanapp/common-types";
import { KeysCompiler } from "./keys.js";
/*
* Keyman is copyright (C) SIL International. MIT License.
*
* Export LDML data (https://www.unicode.org/reports/tr35/tr35-keyboards.html)
* to .kvk format. This is an interim solution until Keyman Core supports
* interrogation of the KMX+ data for OSK.
*/
import { ModifierKeyConstants, KMXPlus } from "@keymanapp/common-types";
import { VisualKeyboard, CompilerCallbacks } from "@keymanapp/common-types";
import { CompilerMessages } from "./messages.js";
// This is a partial polyfill for findLast, so not polluting Array.prototype
//
// TODO: remove and replace with Array.prototype.findLast when it is
// well-supported
function findLast(arr: any, callback: any) {
if (!arr) {
return undefined;
}
const len = arr.length >>> 0;
for (let i = len - 1; i >= 0; i--) {
if (callback(arr[i], i, arr)) {
return arr[i];
}
}
return undefined;
}
const LDML_MODIFIER_TO_KVK_MODIFIER = new Map<number, number>();
LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.LCTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_LCTRL);
LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.RCTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_RCTRL);
LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.LALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_LALT);
LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.RALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT);
LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_SHIFTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT);
LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_CTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_CTRL);
LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_ALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_ALT);
export class LdmlKeyboardVisualKeyboardCompiler {
public constructor(private callbacks: CompilerCallbacks) {
}
public compile(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile): VisualKeyboard.VisualKeyboard {
/**
* Generate a visual keyboard
* @param source Compiled KMX+ data; note that this is modified to add
* &VISUALKEYBOARD system store on success
* @param keyboardId Basename of keyboard, without file extension
* @returns Visual keyboard data on success, null on failure, or
* false if no VK was generated for this keyboard
*/
public compile(source: KMXPlus.KMXPlusData, keyboardId: string): VisualKeyboard.VisualKeyboard | boolean | null {
let result = new VisualKeyboard.VisualKeyboard();
/* TODO-LDML: consider VisualKeyboardHeaderFlags.kvkhUseUnderlying kvkhDisplayUnderlying kvkhAltGr kvkh102 */
result.header.flags = 0;
result.header.version = 0x0600;
/* TODO-LDML: consider associatedKeyboard: this _must_ be set to id (aka basename sans ext) of keyboard .kmx file */
result.header.associatedKeyboard = '';
result.header.associatedKeyboard = keyboardId;
result.header.ansiFont = {...VisualKeyboard.DEFAULT_KVK_FONT};
result.header.unicodeFont = {...VisualKeyboard.DEFAULT_KVK_FONT};
for(let layers of source.keyboard3.layers) {
const { formId } = layers;
for(let layer of layers.layer) {
this.compileHardwareLayer(source, result, layer, formId);
let hasVisualKeyboard = false;
for(let layersList of source.layr.lists) {
const formId = layersList.hardware.value;
if(formId == 'touch') {
continue;
}
for(let layer of layersList.layers) {
const res = this.compileHardwareLayer(source, result, layer, formId);
if(res === false) {
// failed to compile the layer
return null;
}
if(res === null) {
// not a supported layer type, but not an error
continue;
}
hasVisualKeyboard = true;
}
}
if(!hasVisualKeyboard) {
return false;
}
return result;
}
private compileHardwareLayer(
source: KMXPlus.KMXPlusData,
vk: VisualKeyboard.VisualKeyboard,
layer: KMXPlus.LayrEntry,
hardware: string,
) {
const layerId = layer.id.value;
hardware = 'us'; // TODO-LDML: US Only. We need to clean this up for other hardware forms
const shift = this.translateLayerModifiersToVisualKeyboardShift(layer.mod);
if(shift === null) {
// Caps (num, scroll) is not a supported shift state in .kvk
return null;
}
let result = true;
let y = -1;
for(let row of layer.rows) {
y++;
let x = -1;
for(let key of row.keys) {
x++;
const keydef: KMXPlus.KeysKeys = findLast(source.keys?.keys, (kd: KMXPlus.KeysKeys) => kd.id.value == key.value);
const kmap = source.keys.kmap.find(k => k.key == keydef.id.value && k.mod == layer.mod);
const text = this.getDisplayFromKey(keydef, source) ?? null;
if (!keydef || !kmap || text === null) {
this.callbacks.reportMessage(
CompilerMessages.Error_KeyNotFoundInKeyBag({ keyId: key.value, layer: layerId, row: y, col: x, form: hardware })
);
result = false;
} else {
vk.keys.push({
flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode,
shift,
text,
vkey: kmap.vkey
});
}
}
}
return result;
}
private compileHardwareLayer(
source: LDMLKeyboard.LDMLKeyboardXMLSourceFile,
vk: VisualKeyboard.VisualKeyboard,
layer: LDMLKeyboard.LKLayer,
hardware: string,
) {
const layerId = layer.id;
if (hardware === 'touch') {
hardware = 'us'; // TODO-LDML: US Only. Do something different here?
private getDisplayFromKey(keydef: KMXPlus.KeysKeys, source: KMXPlus.KMXPlusData) {
const display = source.disp?.disps?.find(d => d.id.value == keydef.id.value || d.to.value == keydef.to.value);
return display?.display.value ?? keydef.to.value;
}
private translateLayerModifiersToVisualKeyboardShift(modifiers: number): VisualKeyboard.VisualKeyboardShiftState {
if(modifiers == 0) {
return VisualKeyboard.VisualKeyboardShiftState.KVKS_NORMAL;
}
const keymap = KeysCompiler.getKeymapFromForms(source.keyboard3?.forms?.form, hardware);
if (!keymap) {
this.callbacks.reportMessage(
CompilerMessages.Error_InvalidHardware({ formId: hardware })
);
return;
if(modifiers &
(ModifierKeyConstants.CAPITALFLAG | ModifierKeyConstants.NUMLOCKFLAG | ModifierKeyConstants.SCROLLFLAG)
) {
// Caps/Num/Scroll are not supported in .kvk, in combination or alone
return null;
}
const shift = this.translateLayerIdToVisualKeyboardShift(layer.id);
let y = -1;
for(let row of layer.row) {
y++;
let shift: VisualKeyboard.VisualKeyboardShiftState = 0;
const keys = row.keys.split(' ');
let x = -1;
for(let key of keys) {
const keyId = key;
x++;
let keydef = source.keyboard3.keys?.key?.find(x => x.id == key);
if (!keydef) {
this.callbacks.reportMessage(
CompilerMessages.Error_KeyNotFoundInKeyBag({ keyId, layer: layerId, row: y, col: x, form: hardware })
);
} else {
vk.keys.push({
flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode,
shift: shift,
text: keydef.output, // TODO-LDML: displays
vkey: keymap[y][x],
});
}
for(const mod of LDML_MODIFIER_TO_KVK_MODIFIER.keys()) {
if(modifiers & mod) {
shift |= LDML_MODIFIER_TO_KVK_MODIFIER.get(mod);
}
}
}
private translateLayerIdToVisualKeyboardShift(id: string) {
if(id == 'base') {
return 0;
}
// TODO-LDML: other modifiers
return 0;
return shift;
}
}

View file

@ -1,4 +1,6 @@
#
# Keyman is copyright (C) SIL International. MIT License.
#
# basic-kvk.txt describes the expected output of running kmc against basic.xml to generate
# a .kvk file. It is used in the end-to-end test test-visual-keyboard-compiler-e2e.ts.
#
@ -10,8 +12,8 @@ block(kvkheader)
00 06 00 00 # version 0x0600
00 # flags
block(associated_keyboard)
01 00 # string len in UTF-16 code units incl zterm
00 00 # '\0'
06 00 # string len in UTF-16 code units incl zterm
62 00 61 00 73 00 69 00 63 00 00 00 # 'basic\0'
block(ansi_font)
06 00 # string len in UTF-16 code units incl zterm

View file

@ -1 +0,0 @@
if(typeof keyman === 'undefined') {console.error('Keyboard requires KeymanWeb 16.0 or later');} else {KeymanWeb.KR(new Keyboard_basic());}function Keyboard_basic() {this.KI="Keyboard_basic";this.KN="TestKbd";this.KMINVER="16.0";this.KV={F: ' 1em "Arial"', K102: 0};this.KV.KLS={TODO_LDML: 2};this.KDU=1;this.KH="";this.KM=0;this.KBVER="1.0.0";this.KMBM=0;this.KVKL={"desktop":{"defaultHint":"none","layer":[{"id":"base","row":[{"id":"0","key":[{"id":"T_hmaqtugha","text":"ħ"},{"id":"T_that","text":"ថា"}]}]}],"displayUnderlying":false}};this.gs=function(t,e){return 0;};}

View file

@ -1,48 +0,0 @@
if(typeof keyman === 'undefined') {
console.error('Keyboard requires KeymanWeb 16.0 or later');
} else {
KeymanWeb.KR(new Keyboard_basic());
}
function Keyboard_basic() {
this.KI="Keyboard_basic";
this.KN="TestKbd";
this.KMINVER="16.0";
this.KV={F: ' 1em "Arial"', K102: 0};
this.KV.KLS={
TODO_LDML: 2
};
this.KDU=1;
this.KH="";
this.KM=0;
this.KBVER="1.0.0";
this.KMBM=0;
this.KVKL={
"desktop": {
"defaultHint": "none",
"layer": [
{
"id": "base",
"row": [
{
"id": "0",
"key": [
{
"id": "T_hmaqtugha",
"text": "ħ"
},
{
"id": "T_that",
"text": "ថា"
}
]
}
]
}
],
"displayUnderlying": false
}
};
this.gs=function(t,e){
return 0;
};
}

View file

@ -1,4 +1,6 @@
#
# Keyman is copyright (C) SIL International. MIT License.
#
# basic.txt describes the expected output of running kmc against basic.xml. It is used in
# the end-to-end test test-compiler-e2e.ts.
#
@ -402,7 +404,7 @@ block(layr) # struct COMP_KMXPLUS_LAYR {
01 00 00 00 # count
7B 00 00 00 # KMX_DWORD minDeviceWidth; // 123
# layers 0
index(strNull,strBase,2) # KMXPLUS_STR id;
00 00 00 00 # KMXPLUS_STR id;
00 00 00 00 # KMX_DWORD mod
00 00 00 00 # KMX_DWORD row index
01 00 00 00 # KMX_DWORD count
@ -502,7 +504,6 @@ block(strs) # struct COMP_KMXPLUS_STRS {
diff(strs,strSet) sizeof(strSet,2)
diff(strs,strSet2) sizeof(strSet2,2)
diff(strs,strTranTo) sizeof(strTranTo,2)
diff(strs,strBase) sizeof(strBase,2)
diff(strs,strElemBkspFrom2) sizeof(strElemBkspFrom2,2)
diff(strs,strGapReserved) sizeof(strGapReserved,2)
diff(strs,strHmaqtugha) sizeof(strHmaqtugha,2)
@ -538,7 +539,6 @@ block(strs) # struct COMP_KMXPLUS_STRS {
#str #0A
block(strSet2) 61 00 62 00 63 00 block(x) 00 00 # 'abc'
block(strTranTo) 61 00 02 03 block(x) 00 00 # 'â' (U+0061 U+0302)
block(strBase) 62 00 61 00 73 00 65 00 block(x) 00 00 # 'base'
block(strElemBkspFrom2) 65 00 block(x) 00 00 # 'e'
block(strGapReserved) 67 00 61 00 70 00 20 00 28 00 72 00 65 00 73 00 65 00 72 00 76 00 65 00 64 00 29 00 block(x) 00 00 # 'gap (reserved)'
block(strHmaqtugha) 68 00 6d 00 61 00 71 00 74 00 75 00 67 00 68 00 61 00 block(x) 00 00 # 'hmaqtugha'

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Keyman is copyright (C) SIL International. MIT License. -->
<!--
@@keys: [K_Q][K_W][K_Q]
@ -23,7 +24,8 @@
</keys>
<layers formId="us" minDeviceWidth="123">
<layer id="base">
<!-- TODO-LDML: unit test for <layers formId="us" with <layer id="base" should be illegal -->
<layer modifiers="none">
<row keys="hmaqtugha that" />
</layer>
</layers>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
<info name="keys-minimal"/>
<keys>
<key id="grave" output="🪦" />
<key id="mistake" output="oops" />
</keys>
<layers formId="us">
<layer id="base">
<row keys=" grave mistake" />
<row keys=" grave mistake " />
</layer>
</layers>
</keyboard3>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
<info name="vars-fail"/>
<keys />
<!-- from spec -->
<variables>
<string id="y" value="${usedBeforeDefinition}" /> <!-- FAIL: reference before definition -->
<string id="usedBeforeDefinition" value="yes" />
</variables>
</keyboard3>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
<info name="vars-minimal"/>
<keys />
<!-- from spec -->
<variables>
<string id="invalid-string" value="yes" /> <!-- ERROR_InvalidVariableIdentifer -->
<set id="invalid-set" value="A B C D E FF" /> <!-- ERROR_InvalidVariableIdentifer -->
<uset id="invalid-uset" value="[कसतनमह]" /> <!-- ERROR_InvalidVariableIdentifer -->
<set id="a_marker_name_more_than_32_chars_long" value="Y E S" /> <!-- ERROR_InvalidVariableIdentifer -->
<string id="😡" value="UPPERCASE" /> <!-- ERROR_InvalidVariableIdentifer -->
</variables>
</keyboard3>

View file

@ -1,16 +1,17 @@
/**
/*
* Keyman is copyright (C) SIL International. MIT License.
*
* Helpers and utilities for the Mocha tests.
*/
import 'mocha';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { SectionCompiler, SectionCompilerNew } from '../../src/compiler/section-compiler.js';
import { util, KMXPlus, LDMLKeyboardXMLSourceFileReader, VisualKeyboard, CompilerEvent, LDMLKeyboardTestDataXMLSourceFile, compilerEventFormat, LDMLKeyboard, UnicodeSetParser, CompilerCallbacks } from '@keymanapp/common-types';
import { util, KMXPlus, LDMLKeyboardXMLSourceFileReader, CompilerEvent, LDMLKeyboardTestDataXMLSourceFile, compilerEventFormat, LDMLKeyboard, UnicodeSetParser, CompilerCallbacks } from '@keymanapp/common-types';
import { LdmlKeyboardCompiler } from '../../src/main.js'; // make sure main.js compiles
import { assert } from 'chai';
import { KMXPlusMetadataCompiler } from '../../src/compiler/metadata-compiler.js';
import { LdmlCompilerOptions } from '../../src/compiler/ldml-compiler-options.js';
import { LdmlKeyboardVisualKeyboardCompiler } from '../../src/compiler/visual-keyboard-compiler.js';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import KMXPlusFile = KMXPlus.KMXPlusFile;
@ -20,7 +21,6 @@ import Section = KMXPlus.Section;
import { ElemCompiler, ListCompiler, StrsCompiler } from '../../src/compiler/empty-compiler.js';
import { KmnCompiler } from '@keymanapp/kmc-kmn';
import { VarsCompiler } from '../../src/compiler/vars.js';
// import Vars = KMXPlus.Vars;
/**
* Builds a path to the fixture with the given path components.
@ -47,7 +47,7 @@ beforeEach(function() {
afterEach(function() {
if (this.currentTest.state !== 'passed') {
compilerTestCallbacks.messages.forEach(message => console.log(message.message));
compilerTestCallbacks.printMessages();
}
});
@ -160,28 +160,7 @@ export async function compileKeyboard(inputFilename: string, options: LdmlCompil
return kmx;
}
export async function compileVisualKeyboard(inputFilename: string, options: LdmlCompilerOptions): Promise<VisualKeyboard.VisualKeyboard> {
const k = new LdmlKeyboardCompiler();
assert.isTrue(await k.init(compilerTestCallbacks, options));
const source = k.load(inputFilename);
checkMessages();
assert.isNotNull(source, 'k.load should not have returned null');
const valid = await k.validate(source);
checkMessages();
assert.isTrue(valid, 'k.validate should not have failed');
const vk = (new LdmlKeyboardVisualKeyboardCompiler(compilerTestCallbacks)).compile(source);
checkMessages();
assert.isNotNull(vk, 'LdmlKeyboardVisualKeyboardCompiler.compile should not have returned null');
return vk;
}
export function checkMessages() {
if(compilerTestCallbacks.messages.length > 0) {
console.log(compilerTestCallbacks.messages);
}
assert.isEmpty(compilerTestCallbacks.messages, compilerEventFormat(compilerTestCallbacks.messages));
}

View file

@ -1,50 +0,0 @@
import 'mocha';
import { assert } from 'chai';
import { checkMessages, compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './helpers/index.js';
import { LdmlKeyboardKeymanWebCompiler } from '../src/compiler/keymanweb-compiler.js';
import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js';
import * as fs from 'fs';
describe('LdmlKeyboardKeymanWebCompiler', function() {
it('should build a .js file', async function() {
// Let's build basic.xml
// It should generate content identical to basic.js
const inputFilename = makePathToFixture('basic.xml');
const outputFilename = makePathToFixture('basic.js');
const outputFilenameNoDebug = makePathToFixture('basic-no-debug.js');
// Load input data; we'll use the LDML keyboard compiler loader to save us
// effort here
const k = new LdmlKeyboardCompiler();
await k.init(compilerTestCallbacks, {...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false});
const source = k.load(inputFilename);
checkMessages();
assert.isNotNull(source, 'k.load should not have returned null');
// Sanity check ... this is also checked in other tests
const valid = await k.validate(source);
checkMessages();
assert.isTrue(valid, 'k.validate should not have failed');
// Actual test: compile to javascript
const jsCompiler = new LdmlKeyboardKeymanWebCompiler(compilerTestCallbacks, {...compilerTestOptions, saveDebug: true});
const output = jsCompiler.compile('basic.xml', source);
assert.isNotNull(output);
// Does the emitted js match?
const outputFixture = fs.readFileSync(outputFilename, 'utf-8').replaceAll(/\r\n/g, '\n');
assert.strictEqual(output, outputFixture);
// Second test: compile to javascript without debug formatting
const jsCompilerNoDebug = new LdmlKeyboardKeymanWebCompiler(compilerTestCallbacks, {...compilerTestOptions, saveDebug: false});
const outputNoDebug = jsCompilerNoDebug.compile('basic.xml', source);
assert.isNotNull(outputNoDebug);
// Does the emitted js match? The nodebug has no newline at end, but allow one in the fixture
const outputFixtureNoDebug = fs.readFileSync(outputFilenameNoDebug, 'utf-8').replaceAll(/\r\n/g, '\n').trim();
assert.strictEqual(outputNoDebug, outputFixtureNoDebug);
// TODO(lowpri): consider using Typescript parser to generate AST for further validation
});
});

View file

@ -129,5 +129,30 @@ describe('layr', function () {
]);
},
},
{
subpath: 'sections/layr/row-keys-whitespace.xml',
callback(sect) {
const layr = <Layr> sect;
assert.ok(layr);
assert.equal(compilerTestCallbacks.messages.length, 0);
assert.equal(layr.lists?.length, 1);
const list0 = layr.lists[0];
assert.ok(list0);
assert.equal(list0.layers.length, 1);
assert.equal(list0.hardware.value, 'us');
const layer0 = list0.layers[0];
assert.ok(layer0);
assert.equal(layer0.rows.length, 2);
assert.equal(layer0.id.value, 'base');
assert.equal(layer0.mod, constants.keys_mod_none);
for(const row of layer0.rows) {
assert.ok(row);
assert.equal(row.keys.length, 2);
assert.equal(row.keys[0]?.value, 'grave');
assert.equal(row.keys[1]?.value, 'mistake');
}
},
},
]);
});

View file

@ -121,6 +121,16 @@ describe('vars', function () {
CompilerMessages.Error_DuplicateVariable({ids: 'upper, y'})
],
},
{
subpath: 'sections/vars/fail-invalid-identifiers.xml',
errors: [
CompilerMessages.Error_InvalidVariableIdentifer({id: 'invalid-string'}),
CompilerMessages.Error_InvalidVariableIdentifer({id: 'invalid-set'}),
CompilerMessages.Error_InvalidVariableIdentifer({id: 'invalid-uset'}),
CompilerMessages.Error_InvalidVariableIdentifer({id: 'a_marker_name_more_than_32_chars_long'}),
CompilerMessages.Error_InvalidVariableIdentifer({id: '😡'}),
],
},
{
subpath: 'sections/vars/fail-uset-props1.xml',
errors: [
@ -187,7 +197,14 @@ describe('vars', function () {
CompilerMessages.Error_MissingStringVariable({id: 'missingStringInSet'})
],
},
], varsDependencies);
{
subpath: 'sections/vars/fail-badref-7.xml',
errors: [
CompilerMessages.Error_MissingStringVariable({id: 'usedBeforeDefinition'})
],
strictErrors: true
},
], varsDependencies);
describe('should match some marker constants', () => {
// neither of these live here, but, common/web/types does not import ldml-keyboard-constants otherwise.

View file

@ -1,31 +0,0 @@
import 'mocha';
import {assert} from 'chai';
import hextobin from '@keymanapp/hextobin';
import { KvkFileWriter } from '@keymanapp/common-types';
import {checkMessages, compilerTestOptions, compileVisualKeyboard, makePathToFixture} from './helpers/index.js';
describe('visual-keyboard-compiler', function() {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should build fixtures', async function() {
// Let's build basic.xml
// It should match basic.kvk (built from basic-kvk.txt)
const inputFilename = makePathToFixture('basic.xml');
const binaryFilename = makePathToFixture('basic-kvk.txt');
// Compile the visual keyboard
const vk = await compileVisualKeyboard(inputFilename, {...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false});
assert.isNotNull(vk);
// Use the builder to generate the binary output file
const writer = new KvkFileWriter();
const code = writer.write(vk);
checkMessages();
assert.isNotNull(code);
// Compare output
let expected = await hextobin(binaryFilename, undefined, {silent:true});
assert.deepEqual<Uint8Array>(code, expected);
});
});

View file

@ -0,0 +1,235 @@
/*
* Keyman is copyright (C) SIL International. MIT License.
*/
import 'mocha';
import * as path from 'path';
import {assert} from 'chai';
import { stripIndent } from 'common-tags';
import { KMX, KvkFileWriter, LDMLKeyboardXMLSourceFileReader, VisualKeyboard } from '@keymanapp/common-types';
import hextobin from '@keymanapp/hextobin';
import { checkMessages, compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './helpers/index.js';
import { LdmlKeyboardVisualKeyboardCompiler } from '../src/compiler/visual-keyboard-compiler.js';
import { LdmlKeyboardCompiler } from '../src/main.js';
describe('visual-keyboard-compiler', function() {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should build fixtures', async function() {
// Let's build basic.xml
// It should match basic.kvk (built from basic-kvk.txt)
const inputFilename = makePathToFixture('basic.xml');
const binaryFilename = makePathToFixture('basic-kvk.txt');
// Compile the visual keyboard
const k = new LdmlKeyboardCompiler();
assert.isTrue(await k.init(compilerTestCallbacks, {...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false}));
const source = k.load(inputFilename);
checkMessages();
assert.isNotNull(source, 'k.load should not have returned null');
const valid = await k.validate(source);
checkMessages();
assert.isTrue(valid, 'k.validate should not have failed');
let kmx = await k.compile(source);
assert(kmx, 'k.compile should not have failed');
const keyboardId = path.basename(inputFilename, '.xml');
const vk = (new LdmlKeyboardVisualKeyboardCompiler(compilerTestCallbacks)).compile(kmx.kmxplus, keyboardId);
checkMessages();
assert.isNotNull(vk, 'LdmlKeyboardVisualKeyboardCompiler.compile should not have returned null');
assert.isNotNull(kmx.keyboard.stores.find(store =>
store.dwSystemID == KMX.KMXFile.TSS_VISUALKEYBOARD &&
store.dpString == keyboardId + '.kvk'
));
assert(typeof vk == 'object');
// Use the builder to generate the binary output file
const writer = new KvkFileWriter();
const code = writer.write(vk);
assert.isEmpty(compilerTestCallbacks.messages);
assert.isNotNull(code);
// Compare output
let expected = await hextobin(binaryFilename, undefined, {silent:true});
assert.deepEqual<Uint8Array>(code, expected);
});
it('should support various modifiers', async function() {
const xml = stripIndent`
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
<info name="minimal"/>
<keys>
<key id="x" output="y" />
</keys>
<layers formId="us">
<layer modifiers="none"><row keys="x" /></layer>
<layer modifiers="shift"><row keys="x" /></layer>
<layer modifiers="altR"><row keys="x" /></layer>
<layer modifiers="ctrlR altR, shift altR"><row keys="x" /></layer>
<layer modifiers="caps"><row keys="x" /></layer>
</layers>
</keyboard3>
`;
const vk = await loadVisualKeyboardFromXml(xml, 'test');
assert.equal(vk.keys.length, 5);
assert.equal(vk.keys[0].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_NORMAL);
assert.equal(vk.keys[1].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT);
assert.equal(vk.keys[2].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT);
assert.equal(vk.keys[3].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_RCTRL | VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT);
assert.equal(vk.keys[4].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT | VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT);
});
it('should emit the correct associated keyboard id', async function() {
const xml = stripIndent`
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
<info name="minimal"/>
<keys>
<key id="x" output="y" />
</keys>
<layers formId="us">
<layer modifiers="none"><row keys="x" /></layer>
</layers>
</keyboard3>
`;
const vk = await loadVisualKeyboardFromXml(xml, 'test');
assert.equal(vk.header.associatedKeyboard, 'test');
});
it('should support <display>', async function() {
const xml = stripIndent`
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
<info name="minimal"/>
<keys>
<key id="a" output="X" />
<key id="b" output="Y" />
</keys>
<displays>
<display keyId="a" display="C" />
<display output="Y" display="D" />
</displays>
<layers formId="us">
<layer modifiers="none"><row keys="a b" /></layer>
</layers>
</keyboard3>
`;
const vk = await loadVisualKeyboardFromXml(xml, 'test');
assert.equal(vk.keys.length, 2);
assert.equal(vk.keys[0].text, 'C');
assert.equal(vk.keys[1].text, 'D');
});
it('should correctly decode \\u{xxxx}', async function() {
const xml = stripIndent`
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
<info name="minimal"/>
<keys>
<key id="x" output="\\u{0e80}" />
<key id="z" output="y" />
</keys>
<displays>
<display output="y" display="\\u{0e81}" />
</displays>
<layers formId="us">
<layer modifiers="none"><row keys="x z" /></layer>
</layers>
</keyboard3>
`;
const vk = await loadVisualKeyboardFromXml(xml, 'test');
assert.equal(vk.keys.length, 2);
assert.equal(vk.keys[0].text, '\u{0e80}');
assert.equal(vk.keys[1].text, '\u{0e81}');
});
it('should read string variables in key.output', async function() {
const xml = stripIndent`
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
<info name="minimal"/>
<keys>
<key id="x" output="\${one}" />
</keys>
<layers formId="us">
<layer modifiers="none"><row keys="x" /></layer>
</layers>
<variables>
<string id="two" value="2" />
<string id="one" value="\${two}" />
</variables>
</keyboard3>
`;
const vk = await loadVisualKeyboardFromXml(xml, 'test');
assert.equal(vk.keys.length, 1);
assert.equal(vk.keys[0].text, '2');
});
it('should read string variables in display.display', async function() {
const xml = stripIndent`
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
<info name="minimal"/>
<keys>
<key id="x" output="y" />
</keys>
<displays>
<display output="y" display="\${one}" />
</displays>
<layers formId="us">
<layer modifiers="none"><row keys="x" /></layer>
</layers>
<variables>
<string id="two" value="2" />
<string id="one" value="\${two}" />
</variables>
</keyboard3>
`;
const vk = await loadVisualKeyboardFromXml(xml, 'test');
assert.equal(vk.keys.length, 1);
assert.equal(vk.keys[0].text, '2');
});
});
async function loadVisualKeyboardFromXml(xml: string, id: string) {
const data = new TextEncoder().encode(xml);
assert.isOk(data);
const reader = new LDMLKeyboardXMLSourceFileReader(compilerTestOptions.readerOptions, compilerTestCallbacks);
const source = reader.load(data);
assert.isEmpty(compilerTestCallbacks.messages);
assert.isOk(source);
const k = new LdmlKeyboardCompiler();
assert.isTrue(await k.init(compilerTestCallbacks, compilerTestOptions));
const kmx = await k.compile(source);
assert(kmx, 'k.compile should not have failed');
const vk = (new LdmlKeyboardVisualKeyboardCompiler(compilerTestCallbacks)).compile(kmx.kmxplus, id);
assert(typeof vk == 'object');
assert.isEmpty(compilerTestCallbacks.messages);
assert.isOk(vk);
return vk;
}

View file

@ -10,21 +10,53 @@ export const expectedGitDateFormat = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\
*/
export function getLastGitCommitDate(path: string): string {
// TZ=UTC0 git log -1 --no-merges --date=format:%Y-%m-%dT%H:%M:%SZ --format=%ad
let result = null;
let result = callGitLog([
'log', // git log
'-1', // one commit only
'--no-merges', // we're only interested in 'real' commits
'--format=%at', // emit only the commit date as a UNIX timestamp
'--',
path
]);
try {
result = execFileSync('git', [
if(typeof result != 'string') {
return null;
}
if(result == '') {
// #12626: no history was found, but not an error. We may have only a merge
// commit in history for that file, allow merges in the log
result = callGitLog([
'log', // git log
'-1', // one commit only
'--no-merges', // we're only interested in 'real' commits
'--format=%at', // emit only the commit date as a UNIX timestamp
'--',
path
], {
]);
if(typeof result != 'string') {
return null;
}
}
const sec = Number.parseInt(result);
if(Number.isNaN(sec)) {
// We received invalid data, perhaps a git error string so just ignore
return null;
}
// We receive a timestamp in seconds but we need milliseconds
return new Date(sec * 1000).toISOString();
}
function callGitLog(params: string[]) {
try {
const result = execFileSync('git', params, {
encoding: 'utf-8', // force a string result rather than Buffer
windowsHide: true, // on windows, we may need this to suppress a console window popup
stdio: ['pipe', 'pipe', 'pipe'] // all output via pipe, so we don't get git errors on console
});
return result.trim();
} catch (e) {
// If git is not available, or the file is not in-repo, then it is probably
// fine to just silently return null, as the only machines where this is
@ -34,9 +66,4 @@ export function getLastGitCommitDate(path: string): string {
// seems low.
return null;
}
// We receive a timestamp in seconds but we need milliseconds
const msec = Number.parseInt(result.trim()) * 1000;
return new Date(msec).toISOString();
}

View file

@ -90,9 +90,16 @@ struct BindingType<std::vector<T, Allocator>> {
using ValBinding = BindingType<val>;
using WireType = ValBinding::WireType;
#if __EMSCRIPTEN_major__ == 3 && __EMSCRIPTEN_minor__ == 1 && __EMSCRIPTEN_tiny__ >= 60
// emscripten-core/emscripten#21692
static WireType toWireType(const std::vector<T, Allocator> &vec, rvp::default_tag) {
return ValBinding::toWireType(val::array(vec), rvp::default_tag{});
}
#else
static WireType toWireType(const std::vector<T, Allocator> &vec) {
return ValBinding::toWireType(val::array(vec));
}
#endif
static std::vector<T, Allocator> fromWireType(WireType value) {
return vecFromJSArray<T>(ValBinding::fromWireType(value));

View file

@ -1,4 +1,4 @@
[binaries]
c = ['$EMSCRIPTEN_BASE/emcc.py']
cpp = ['$EMSCRIPTEN_BASE/em++.py']
ar = ['$EMSCRIPTEN_BASE/emar.py']
c = ['$EMSCRIPTEN_BASE/emcc']
cpp = ['$EMSCRIPTEN_BASE/em++']
ar = ['$EMSCRIPTEN_BASE/emar']

View file

@ -1,4 +1,4 @@
[binaries]
c = ['$EMSCRIPTEN_BASE/emcc.py']
cpp = ['$EMSCRIPTEN_BASE/em++.py']
ar = ['$EMSCRIPTEN_BASE/emar.py']
c = ['$EMSCRIPTEN_BASE/emcc']
cpp = ['$EMSCRIPTEN_BASE/em++']
ar = ['$EMSCRIPTEN_BASE/emar']

View file

@ -83,6 +83,7 @@ type
procedure optWriteInt(const nm: string; value: Integer);
procedure WriteServerConfigurationJson;
class function Default_DefaultProjectPath: string; static;
function BackOffAndSaveJson(const Filename: string; const JSON: TJSONObject): Boolean;
public
procedure Read;
procedure Write;
@ -406,8 +407,6 @@ begin
end;
procedure TKeymanDeveloperOptions.Write;
var
count: Integer;
begin
// If the output file does not exist, this creates it. Note that we load
// from the existing file before writing out our set of options, which is
@ -464,33 +463,7 @@ begin
ForceDirectories(TKeymanDeveloperPaths.OptionsPath);
try
count := 0;
repeat
try
SaveJSONToFile(TKeymanDeveloperPaths.OptionsPath + TKeymanDeveloperPaths.S_OptionsJson, json);
Break;
except
on E:EOSError do
begin
if E.ErrorCode = ERROR_ACCESS_DENIED then
begin
// Back off and retry 0.5 sec later, it's probably another
// instance happening to read or write the settings file
Inc(count);
Sleep(500);
end
else
raise;
end
else
raise;
end;
until count >= Max_Retries;
if count >= Max_Retries then
begin
if TKeymanSentryClient.Instance <> nil then
TKeymanSentryClient.Instance.ReportMessage('TKeymanDeveloperOptions: Failed to write settings after 5 tries');
end;
BackOffAndSaveJson(TKeymanDeveloperPaths.OptionsPath + TKeymanDeveloperPaths.S_OptionsJson, json);
except
on E:Exception do
begin
@ -507,6 +480,39 @@ begin
WriteServerConfigurationJson;
end;
function TKeymanDeveloperOptions.BackOffAndSaveJson(const Filename: string; const JSON: TJSONObject): Boolean;
var
count: Integer;
begin
count := 0;
repeat
try
SaveJSONToFile(Filename, JSON);
Exit(True);
except
on E:EOSError do
begin
if E.ErrorCode = ERROR_ACCESS_DENIED then
begin
// Back off and retry 0.5 sec later, it's probably another
// instance happening to read or write the settings file
Inc(count);
Sleep(500);
end
else
raise;
end
else
raise;
end;
until count >= Max_Retries;
if TKeymanSentryClient.Instance <> nil then
TKeymanSentryClient.Instance.ReportMessage('TKeymanDeveloperOptions: Failed to write '+Filename+' after 5 tries');
Result := False;
end;
procedure TKeymanDeveloperOptions.WriteServerConfigurationJson;
var
o: TJSONObject;
@ -517,7 +523,8 @@ begin
o.AddPair('ngrokToken', FServerNgrokToken);
o.AddPair('useNgrok', TJSONBool.Create(FServerUseNgrok));
o.AddPair('ngrokVisible', TJSONBool.Create(FServerServerShowConsoleWindow));
SaveJSONToFile(TKeymanDeveloperPaths.ServerDataPath + TKeymanDeveloperPaths.S_ServerConfigJson, o);
ForceDirectories(TKeymanDeveloperPaths.ServerDataPath);
BackOffAndSaveJSON(TKeymanDeveloperPaths.ServerDataPath + TKeymanDeveloperPaths.S_ServerConfigJson, o);
finally
o.Free;
end;

View file

@ -831,12 +831,22 @@ public class Manager: NSObject, UIGestureRecognizerDelegate {
shared.copyUserDefaults(to: nonShared, withKeys: keysToCopy, shouldOverwrite: true)
do {
try shared.copyFiles(to: nonShared)
FontManager.shared.registerCustomFonts()
} catch {
let message = ("Failed to copy from shared container: \(error)")
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message)
SentryManager.capture(error, message:message)
}
// This operation has a surprisingly high cost and isn't critical
// for getting the keyboard loaded and available to the OS.
//
// We certainly want it done _soon_... but we don't want it to cause
// the primary, synchronous initialization call from the OS to run so
// long that the keyboard fails to start due to its enforced time
// constraints.
DispatchQueue.main.async {
FontManager.shared.registerCustomFonts()
}
}
}

View file

@ -7,9 +7,9 @@
<key>teamID</key>
<string>3YE4W86L3G</string>
<key>signingCertificate</key>
<string>BB842A0081298BBA6D034C85451750B08FC9EE8A</string>
<string>683E4A3CDF5277C13004E84D63E31D2F720AB063</string>
<key>installerSigningCertificate</key>
<string>BB842A0081298BBA6D034C85451750B08FC9EE8A</string>
<string>683E4A3CDF5277C13004E84D63E31D2F720AB063</string>
<key>provisioningProfiles</key>
<dict>
<key>Tavultesoft.Keyman</key>

View file

@ -1,4 +1,4 @@
# Copyright (c) 2022-2023 SIL International. All rights reserved.
# Copyright (c) 2022-2023 SIL Global. All rights reserved.
#
# builder image for a linux build
# see ../docs/build/linux-ubuntu.md
@ -7,7 +7,7 @@ ARG OS_VERSION=latest
ARG OS_PLATFORM=amd64
FROM --platform=${OS_PLATFORM} ubuntu:${OS_VERSION}
LABEL org.opencontainers.image.authors="SIL International."
LABEL org.opencontainers.image.authors="SIL Global."
LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git"
LABEL org.opencontainers.image.title="Keyman Linux Build Image"

View file

@ -1,3 +1,18 @@
keyman (17.0.333-1) unstable; urgency=medium
* New upstream release.
-- Eberhard Beilharz <eb1@sil.org> Thu, 16 Jan 2025 16:29:44 +0100
keyman (17.0.332-1) unstable; urgency=medium
* set environment variable for rendering of downloads dialog (#12617)
* add `keymanFacename` to .ldml file (#12283)
* ignore exceptions trying to install cache (#11885)
* New upstream release.
-- Eberhard Beilharz <eb1@sil.org> Wed, 11 Dec 2024 15:53:59 +0100
keyman (17.0.326-1) unstable; urgency=medium
* New upstream release

View file

@ -5,10 +5,15 @@
<metadata_license>MIT</metadata_license>
<project_license>MIT</project_license>
<project_group>Keyman</project_group>
<developer_name>SIL International</developer_name>
<developer id="org.sil.keyman">
<name>SIL Global</name>
</developer>
<icon type="remote" width="96" height="96">https://keyman.com/cdn/dev/img/keyman-logo.png</icon>
<name>Keyman for Linux</name>
<summary>Keyman makes it possible for you to type in over 2,000 languages</summary>
<content_rating type="oars-1.1">
<content_attribute id="social-info">mild</content_attribute>
</content_rating>
<description>
<p>

View file

@ -2,9 +2,11 @@
<component type="inputmethod">
<id>com.keyman.ibus_keyman</id>
<metadata_license>MIT</metadata_license>
<project_license>GPL-2+</project_license>
<project_license>GPL-2.0+</project_license>
<project_group>Keyman</project_group>
<developer_name>SIL International</developer_name>
<developer id="org.sil.keyman">
<name>SIL Global</name>
</developer>
<icon type="remote" width="96" height="96">https://keyman.com/cdn/dev/img/keyman-logo.png</icon>
<name>ibus-keyman</name>

View file

@ -26,6 +26,7 @@ Build-Depends:
pkgconf,
python3-all (>= 3.5),
python3-dbus,
python3-fonttools,
python3-gi,
python3-lxml,
python3-magic,
@ -90,6 +91,7 @@ Depends:
gir1.2-webkit2-4.1 | gir1.2-webkit2-4.0,
keyman-engine,
python3-bs4,
python3-fonttools,
python3-gi,
python3-packaging,
python3-sentry-sdk (>= 1.1),

View file

@ -4,24 +4,24 @@ Upstream-Contact: Keyman team <support@keyman.com>
Source: https://github.com/keymanapp/keyman
Files: *
Copyright: 2018-2024 SIL International
Copyright: 2018-2025 SIL Global
License: MIT
Files: linux/ibus-keyman/*
Copyright: 2004-2024 SIL International
Copyright: 2004-2025 SIL Global
License: GPL-2+
Files: linux/ibus-keyman/src/keymanutil.c
linux/ibus-keyman/src/keymanutil.h
linux/ibus-keyman/src/kmpdetails.c
linux/ibus-keyman/src/kmpdetails.h
Copyright: 2009-2024 SIL International
Copyright: 2009-2025 SIL Global
License: GPL-2+ or MIT
Files: linux/ibus-keyman/src/keyman-service.c
linux/ibus-keyman/src/keyman-service.h
linux/keyman-config/buildtools/help2md
Copyright: 2018-2024 SIL International
Copyright: 2018-2025 SIL Global
License: GPL-3+
Files: linux/ibus-keyman/tests/ibusimcontext.c
@ -31,7 +31,7 @@ Copyright: 2008-2010, Peng Huang <shawn.p.huang@gmail.com>
2008-2013, Peng Huang <shawn.p.huang@gmail.com>
2008-2021, Red Hat, Inc.
2015-2021, Takao Fujiwara <takao.fujiwara1@gmail.com>
2021-2024, SIL International
2021-2025, SIL Global
License: LGPL-2.1+
Files: linux/keyman-config/buildtools/help2man
@ -41,7 +41,7 @@ License: GPL-3+
Files: debian/com.keyman.config.appdata.xml
debian/com.keyman.ibus_keyman.metainfo.xml
Copyright: 2019 Daniel Glassey <wdg@debian.org>
2022-2024 SIL International
2022-2025 SIL Global
License: MIT
License: MIT

View file

@ -1,7 +1,8 @@
libkeymancore.so.1 libkeymancore1 #MINVER#
* Build-Depends-Package: libkeymancore-dev
(c++|optional)"typeinfo name for std::codecvt_utf8_utf16<char16_t, 1114111ul, (std::codecvt_mode)0>@Base" 17.0.244
# We only use a C API, so ignore all C++ symbols in the std namespace that bleed through
(c++|regex|optional)"std::" 17.0.332
km_core_context_clear@Base 17.0.195
km_core_context_get@Base 17.0.195
km_core_context_item_list_size@Base 17.0.195

View file

@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
* Copyright (C) 2009-2023 SIL International
* Copyright (C) 2009-2023 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public

View file

@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
* Copyright (C) 2018 SIL International
* Copyright (C) 2018 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public

View file

@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
* Copyright (C) 2018-2023 SIL International
* Copyright (C) 2018-2023 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public

View file

@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
* Copyright (C) 2018-2023 SIL International
* Copyright (C) 2018-2023 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public

View file

@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
* Copyright (C) 2018 SIL International
* Copyright (C) 2018 SIL Global
*
* keymanutil is dual licensed under the MIT or GPL licenses as described below.
*

View file

@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
* Copyright (C) 2018-2023 SIL International
* Copyright (C) 2018-2023 SIL Global
*
* keymanutil is dual licensed under the MIT or GPL licenses as described below.
*

View file

@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
* Copyright (C) 2018 SIL International
* Copyright (C) 2018 SIL Global
*
* kmpdetails is dual licensed under the MIT or GPL licenses as described below.
*

View file

@ -3,7 +3,7 @@
/*
* Keyman Input Method for IBUS (The Input Bus)
*
* Copyright (C) 2018 SIL International
* Copyright (C) 2018 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public

View file

@ -14,7 +14,7 @@
"description": "LIBTRALO"
},
"copyright": {
"description": "\u00A9 2008-2018 SIL International"
"description": "\u00A9 2008-2018 SIL Global"
},
"author": {
"description": "support@keyman.com",

View file

@ -14,7 +14,7 @@
"description": "LIBTRALO"
},
"copyright": {
"description": "\u00A9 2008-2018 SIL International"
"description": "\u00A9 2008-2018 SIL Global"
},
"author": {
"description": "support@keyman.com",

View file

@ -14,7 +14,7 @@
"description": "LIBTRALO"
},
"copyright": {
"description": "\u00A9 2008-2018 SIL International"
"description": "\u00A9 2008-2018 SIL Global"
},
"author": {
"description": "support@keyman.com",

View file

@ -4,7 +4,7 @@
* Copyright (C) 2008-2013 Peng Huang <shawn.p.huang@gmail.com>
* Copyright (C) 2015-2022 Takao Fujiwara <takao.fujiwara1@gmail.com>
* Copyright (C) 2008-2022 Red Hat, Inc.
* Copyright (C) 2021-2022 SIL International
* Copyright (C) 2021-2022 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public

View file

@ -3,7 +3,7 @@
/* ibus - The Input Bus
* Copyright (C) 2008-2010 Peng Huang <shawn.p.huang@gmail.com>
* Copyright (C) 2008-2010 Red Hat, Inc.
* Copyright (C) 2021-2022 SIL International
* Copyright (C) 2021-2022 SIL Global
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public

View file

@ -1,4 +1,4 @@
Copyright (c) 2018 SIL International
Copyright (c) 2018 SIL Global
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -48,7 +48,7 @@ MOFILES := $(POFILES:.po=/LC_MESSAGES/keyman-config.mo)
update-template: version
xgettext --package-name "keyman" --package-version "$(VERSION)" \
--msgid-bugs-address "<support@keyman.com>" --copyright-holder "SIL International" \
--msgid-bugs-address "<support@keyman.com>" --copyright-holder "SIL Global" \
--language=Python --directory=. --output-dir=locale --output=keyman-config.pot \
--add-comments=i18n: --sort-by-file --width=98 \
keyman_config/*.py km-*

View file

@ -5,7 +5,7 @@
# Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2009,
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2020, 2021 Free Software
# Foundation, Inc.
# Copyright (C) 2021 SIL International
# Copyright (C) 2021 SIL Global
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -76,7 +76,7 @@ my $version_info = enc_user sprintf _(<<'EOT'), $this_program, $this_version;
Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2009,
2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2020, 2021 Free Software
Foundation, Inc.
Copyright (C) 2021 SIL International
Copyright (C) 2021 SIL Global
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

View file

@ -1,5 +1,6 @@
import atexit
import gettext
import importlib
import logging
import os
import pathlib
@ -53,6 +54,14 @@ def initialize_sentry():
SentryErrorHandling().initialize_sentry()
def are_requirements_missing():
try:
ttLib = importlib.import_module('fontTools.ttLib')
except ImportError:
return True
return False
class FileCleanup():
"""
Allow to register files that will be deleted when the process exits

View file

@ -177,8 +177,8 @@ class InstallKmp():
# Special handling to convert kvk into LDML
logging.info("Converting %s to LDML and installing both as as keyman file",
f['name'])
ldml = convert_kvk_to_ldml(fpath)
name, ext = os.path.splitext(f['name'])
ldml = convert_kvk_to_ldml(name, fpath)
ldmlfile = os.path.join(self.packageDir, f"{name}.ldml")
output_ldml(ldmlfile, ldml)
elif ftype == KMFileTypes.KM_ICON:

View file

@ -1,11 +1,15 @@
#!/usr/bin/python3
import logging
import os
import struct
import sys
from fontTools import ttLib
from lxml import etree
from keyman_config.kmpmetadata import parsemetadata
# .kvk file format
# KVK files are variable length files with variable sized structures.
@ -311,7 +315,18 @@ def get_modifer(key):
return modifier
def convert_ldml(kvkData):
def _fontFacename(fontFilename):
"""Get the facename from fontFilename's TTF tables"""
# From https://github.com/mcfletch/ttfquery/blob/master/ttfquery/describe.py
FONT_SPECIFIER_NAME_ID = 4
font = ttLib.TTFont(fontFilename)
for record in font['name'].names:
if record.nameID == FONT_SPECIFIER_NAME_ID:
return record.toUnicode()
return None
def convert_ldml(keyboardName, kvkData, kmpJsonFilename):
keymaps = {}
for key in kvkData.Keys:
@ -349,7 +364,19 @@ def convert_ldml(kvkData):
else:
keymaps["shift"] = (uskey,)
info, system, options, keyboards, files = parsemetadata(kmpJsonFilename)
ldml = etree.Element("keyboard", locale="zzz-keyman")
for keyboard in keyboards:
if keyboard['id'] != keyboardName:
continue
if 'oskFont' in keyboard:
fontFile = os.path.join(os.path.dirname(kmpJsonFilename), keyboard['oskFont'])
font = _fontFacename(fontFile)
if font:
ldml.set('keymanFacename', font)
break
etree.SubElement(ldml, "version", platform="11")
names = etree.SubElement(ldml, "names")
names.append(etree.Element("name", value="ZZZ"))
@ -402,6 +429,7 @@ def parse_kvk_file(kvkfile):
return kvkData
def convert_kvk_to_ldml(kvkfile):
def convert_kvk_to_ldml(name, kvkfile):
kvkData = parse_kvk_file(kvkfile)
return convert_ldml(kvkData)
kmpJsonFilename = os.path.join(os.path.dirname(kvkfile), 'kmp.json')
return convert_ldml(name, kvkData, kmpJsonFilename)

View file

@ -1,4 +1,9 @@
#!/usr/bin/python3
'''
Keyman is copyright (C) SIL Global. MIT License.
Implements the Sentry error handling
'''
import getpass
import hashlib
import importlib

View file

@ -2,6 +2,7 @@
import argparse
import logging
import os
import sys
import gi
@ -9,7 +10,10 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from keyman_config import __versionwithtag__, __pkgversion__, add_standard_arguments, initialize_logging, initialize_sentry, verify_dbus_running
from keyman_config import (
_, __versionwithtag__, __pkgversion__, add_standard_arguments,
are_requirements_missing, initialize_logging, initialize_sentry,
verify_dbus_running)
from keyman_config.handle_install import download_and_install_package
from keyman_config.ibus_util import verify_ibus_daemon
from keyman_config.view_installed import ViewInstalledWindow
@ -42,6 +46,18 @@ if __name__ == '__main__':
initialize_sentry()
verify_dbus_running()
if are_requirements_missing():
if args.install or args.url:
logging.error('Missing requirements. Please install python3-fonttools.')
else:
dialog = Gtk.MessageDialog(
None, 0, Gtk.MessageType.ERROR,
Gtk.ButtonsType.OK, _("Missing requirements. Please install python3-fonttools."))
dialog.run()
dialog.destroy()
sys.exit(1)
logging.info('Keyman version %s %s', __versionwithtag__, __pkgversion__)
verify_ibus_daemon(False)
@ -51,6 +67,10 @@ if __name__ == '__main__':
elif args.url:
download_and_install_package(args.url)
else:
# Workaround for bug in webkit2gtk (#12587)
if not 'WEBKIT_DISABLE_DMABUF_RENDERER' in os.environ:
os.environ['WEBKIT_DISABLE_DMABUF_RENDERER'] = '1'
w = ViewInstalledWindow()
try:
w.run()

View file

@ -5,7 +5,9 @@ import logging
import os
import sys
from keyman_config import add_standard_arguments, initialize_logging, initialize_sentry, verify_dbus_running
from keyman_config import (
are_requirements_missing, add_standard_arguments, initialize_logging,
initialize_sentry, verify_dbus_running)
from keyman_config.kvk2ldml import parse_kvk_file, print_kvk, convert_ldml, output_ldml
@ -25,6 +27,10 @@ def main():
initialize_sentry()
verify_dbus_running()
if are_requirements_missing():
logging.error('km-kvk2ldml: Missing requirements. Please install python3-fonttools.')
sys.exit(1)
name, ext = os.path.splitext(args.kvkfile)
# Check if input file extension is kvk
if ext != ".kvk":
@ -54,7 +60,8 @@ def main():
try:
with open(outputfile, 'wb') as ldmlfile:
ldml = convert_ldml(kvkData)
kmpJsonFilename = os.path.join(os.path.dirname(args.kvkfile), 'kmp.json')
ldml = convert_ldml(name, kvkData, kmpJsonFilename)
output_ldml(ldmlfile, ldml)
except PermissionError:
logging.error(f'km-kvk2ldml: error, permission denied writing file `{outputfile}`')

View file

@ -1,5 +1,5 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR SIL International
# Copyright (C) YEAR SIL Global
# This file is distributed under the same license as the keyman package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#

View file

@ -0,0 +1,57 @@
import os
import shutil
import sys
import tempfile
import unittest
from keyman_config.kvk2ldml import KVKData, NFont, convert_ldml
class Kvk2LdmlTests(unittest.TestCase):
def _createKmpJson(self, packagedir):
kmpJsonFilename = os.path.join(packagedir, 'kmp.json')
with open(kmpJsonFilename, 'w') as file:
file.write('''{
"system": {
"keymanDeveloperVersion": "18.0",
"fileVersion": "7.0"
},
"files": [ {
"name": "khmer_angkor.kmx",
"description": "Keyboard Khmer Angkor"
}, {
"name": "kmp.json",
"description": "Package information (JSON)"
} ],
"keyboards": [ {
"name": "Khmer Angkor",
"id": "khmer_angkor",
"version": "1.5",
"oskFont": "keymanweb-osk.ttf",
"languages": [ {
"name": "Central Khmer (Khmer, Cambodia)",
"id": "km"
} ]}
]}''')
return kmpJsonFilename
def test_convert_ldml__adds_keymanFacename(self):
# Setup
keyboardName = 'khmer_angkor'
kvkData = KVKData()
kvkData.AssociatedKeyboard = keyboardName
kvkData.UnicodeFont = NFont()
kvkData.UnicodeFont.name='DontUseThis!'
workdir = tempfile.TemporaryDirectory()
kmpJsonFilename = self._createKmpJson(workdir.name)
shutil.copy2(os.path.join(sys.path[0], '../../../common/resources/fonts/keymanweb-osk.ttf'),
workdir.name)
# Execute
ldml = convert_ldml(keyboardName, kvkData, kmpJsonFilename)
# Verify
self.assertEqual(ldml.get('locale'), "zzz-keyman")
self.assertEqual(ldml.get('keymanFacename'), 'SymChar')
workdir.cleanup()

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