chore(windows): Merge branch 'master' into fix/windows/7870/add-is-text-selected-flag-api

# Keyman Conventional Commit suggestions:
#
# - Link to a Sentry issue with git trailer:
#     Fixes: _MODULE_-_ID_
# - Give credit to co-authors:
#     Co-authored-by: _Name_ <_email_>
# - Use imperative, present tense ('attach' not 'attaches', 'attached' etc)
# - Don't include a period at the end of the title
# - Always include a blank line before trailers
# - More: https://github.com/keymanapp/keyman/wiki/Pull-Request-and-Commit-workflow-notes
This commit is contained in:
rc-swag 2025-01-23 11:23:12 +10:00
commit 1bcafb1a72
256 changed files with 10439 additions and 1076 deletions

View file

@ -1,5 +1,120 @@
# Keyman Version History
## 18.0.175 alpha 2025-01-22
* feat(windows): Fixes check now to use SM and LastUpdateCheckTime key on first run. (#12922)
* chore(developer): validate keyboard and model IDs in kmc-generate (#12958)
* fix(developer): use correct terms in kmc-generate lexical model help (#12960)
* fix(developer): use 1.0.0 as default for ldml keyboards in kmc-generate (#12962)
* chore: adds h1 tags (#12967)
* fix(web): prevent warning that occurs when multitapping longpressable modifier keys (#12964)
* chore(developer): add test for basic transform to kmc-generate (#12966)
* fix(android/engine): Revert how keyboard picker menu launches (#12986)
## 18.0.174 alpha 2025-01-21
* chore(developer): validate targets parameter in kmc-generate (#12955)
* fix(developer): support `&displayMap` font in web debugger (#12929)
## 18.0.173 alpha 2025-01-20
* feat(developer): rewrite font data in .kvk from package metadata (#12949)
* docs(ios): document gestures within iOS app help (#12900)
* fix(developer): include developer-utils deps in server build (#12951)
## 18.0.172 alpha 2025-01-19
* chore(common): Add 17.0.333 to version history (#12926)
## 18.0.171 alpha 2025-01-18
* fix(common/web): add StrsItem.isEqual() method (#12868)
* fix(common/web): handle invalid order and tertiary arguments to ElementString.fromString() (#12882)
## 18.0.170 alpha 2025-01-17
* chore(linux): update copyright year (#12918)
* fix(web): patch unit test with execution dependent on autocorrect state (#12940)
* test(common/web): unit tests for element-string (#12811)
* chore(linux): Update debian changelog (#12917)
## 18.0.169 alpha 2025-01-17
* chore(windows): remove `postinstall` state from mermaid diagram (#12923)
## 18.0.168 alpha 2025-01-16
* chore: update macOS environment-variable shell script (#12878)
* fix(android): use main looper to dispatch key events when OSK is hidden (#12871)
* chore(web): integrates predictive-text builds to top level script, reconnects headless tests (#12866)
* chore(ios): Update crowdin strings for Khmer (#12910)
* chore(windows): merge master epic windows updates (#12904)
* fix(developer): detect invalid key ids in touch layout files (#12895)
* chore: use GitHub PR titles when writing HISTORY.md (#12907)
* fix(developer): filter incorrect fonts out of .keyboard_info (#12909)
* change(web): make 'keep' transform pattern match standard suggestion pattern by including the prefix string (#12906)
* chore: Add docker images for building the different platforms (#11397)
## 18.0.167 alpha 2025-01-15
* chore: changes to use https and removes anchor in docs (#12838)
* fix(developer): ensure license parameter is required and add tests (#12879)
* fix(web): Improve predictive-text handling of text wordbreaking transitions (#12864)
* chore(common): Update crowdin strings for Spanish (#12886)
* docs(developer): Clarify square brackets in `U_####[_####]` shortcut (#12876)
* feat: add builder tab-completion script (#12296)
* docs(android): document longpress up-flick to quick-display shortcut (#12899)
* docs(web): fix `adding-keyboards.md` (#12890)
## 18.0.166 alpha 2025-01-10
* fix(web): numpad + and - with zoom shortcut use (#12865)
## 18.0.165 alpha 2025-01-09
* feat(web): add KeymanWeb API for auto-correct (#12857)
* chore(web): disable auto-correct (#12858)
* refactor(web): modernize `web/ci.sh` script (#12862)
## 18.0.164 alpha 2025-01-08
* fix(windows): add language and close button fit inside pop-window (#12855)
* chore(windows): add bcp47 code and tips to diagnostic (#12843)
* fix(android/engine): Cleanup KMManager API keys and calls (#12861)
* docs(developer,mac): add playable video links to the help documentation (#12828)
## 18.0.163 alpha 2025-01-06
* docs: clarify that sections in KMX file might not be aligned (#12856)
## 18.0.162 alpha 2024-12-19
* fix(mac): standardize interaction between OSK and physical keyboard (#12836)
## 18.0.161 alpha 2024-12-17
* fix(mac): OSK layers displayed consistently for hardware and OSK modifiers (#12829)
## 18.0.160 alpha 2024-12-16
* feat(ios): configurable keyboard height (#12571)
* chore(common/web): remove .c8rc.json from types and merge exclusions into package.json (#12813)
* fix(android/engine): Initialize index when resuming KeyboardPicker (#12832)
## 18.0.159 alpha 2024-12-13
* chore(core): remove meson warnings for wasm builds (#12827)
* chore(linux): Update debian changelog (#12023)
## 18.0.158 alpha 2024-12-11
* fix(linux): pushing of updated changelog branch (#12818)
* fix(linux): work around Lintian errors (#12815)
## 18.0.157 alpha 2024-12-10
* test(common/web): unit tests for kvk-file-writer (#12734)
## 18.0.156 alpha 2024-12-09
* fix(developer): remove platforms from kmc-generate LM readme (#12803)
@ -153,7 +268,7 @@
## 18.0.134 alpha 2024-11-04
* (#12606)
* fix(developer): ldml don't allow a uset as right-hand-side variable (#12606)
## 18.0.133 alpha 2024-11-01
@ -876,7 +991,7 @@
## 18.0.41 alpha 2024-05-22
* fix(developer): handle `KM_CORE_IT_INVALIDATE_CONTEXT` in debugger (#11488)
* chore(linux): Trigger GHA packaging for stable builds :cherries: (#11495)
* chore(linux): Trigger GHA packaging for stable builds (#11495)
* chore(android,mac,windows): Update crowdin strings for DE (#11497)
* feat(web): custom infrastructure for @web/test-runner use (#11403)
* chore(web): conversion of lm-worker browser-test for @web/test-runner use (#11404)
@ -1072,6 +1187,17 @@
* chore(common): move to 18.0 alpha (#10713)
* chore: move to 18.0 alpha
## 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)

View file

@ -1 +1 @@
18.0.157
18.0.176

View file

@ -26,7 +26,6 @@
<!-- Context: Textfield prompt (&#8230; is the ellipsis character "...") -->
<string name="textview_hint" comment="Prompt to start typing">Escriba aquí&#8230;</string>
<!-- Context: Splash screen (version/copyright info currently disabled -->
<string name="splash_blank" translatable="false" comment="Blank text while version info on splash page is disabled"></string>
<!-- Context: Splash screen (version/copyright info currently disabled -->
<!-- Context: Text Size dialog -->
<string name="text_size" comment="Current text size (number)">Tamaño del texto: %1$d</string>
@ -34,6 +33,8 @@
<string name="ic_text_size_up" comment="Make text bigger">Agrandar texto</string>
<!-- Context: Text Size dialog -->
<string name="ic_text_size_down" comment="Make text smaller">Achicar texto</string>
<!-- Context: Text Size dialog -->
<string name="ic_text_size_slider" comment="Adjust text size">Control deslizante de tamaño de texto</string>
<!-- Context: Clear Text dialog -->
<string name="all_text_will_be_cleared" comment="Erase all the text">\nTodo el texto será borrado\n</string>
<!-- Context: Get Started menu -->
@ -52,6 +53,7 @@
<string name="request_storage_permission" comment="Request storage permission to access keyboard packages"> Para instalar los paquetes de teclado, permita que Keyman acceda al almacenamiento externo.</string>
<!-- Context: Android Storage Permission -->
<string name="storage_permission_denied" comment="Keyboard package installation may fail since Android storage permission not granted"> Se ha denegado la solicitud de permiso de almacenamiento. La instalación del paquete de teclado podría fallar</string>
<string name="storage_permission_denied2" comment="Keyboard package installation failed. Recommend install from local file"> Fallo en la solicitud de permiso de almacenamiento. Pruebe Configuración de Keyman - Instalar desde archivo local</string>
<!-- Context: Keyman Settings menu -->
<string name="keyman_settings" comment="Settings menu title">Configuración</string>
<!-- Context: Keyman Settings menu -->
@ -66,6 +68,8 @@
<!-- Context: Keyman Settings menu -->
<string name="adjust_keyboard_height" comment="Menu action to adjust keyboard height">Ajustar altura del teclado</string>
<!-- Context: Keyman Settings menu -->
<string name="adjust_longpress_delay" comment="Menu action to adjust longpress delay duration">Ajustar demora de pulsación larga</string>
<!-- Context: Keyman Settings menu -->
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">Leyenda del teclado</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_keyboard" comment="Spacebar caption option - keyboard">Teclado</string>
@ -85,11 +89,11 @@
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">No mostrar leyenda en la barra espaciadora</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
<string name="haptic_feedback" comment="haptic feedback on key press">Vibrar al escribir</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner" comment="text suggestions banner">Mostrar siempre el banner</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_on" comment="Description when toggle is on">Próximamente implementado</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_off" comment="Description when toggle is off">Cuando esté desactivado, sólo mostrado cuando el texto predictivo esté habilitado</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report" comment="permission for sending crash reports">Permitir el envío de informes de fallos a través de la red</string>
@ -121,6 +125,14 @@
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Rotar el dispositivo para ajustar a modo vertical u horizontal</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Restablecer valores predeterminados</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Tiempo de retraso: %1$.1f segundos</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_up" comment="Make longpress delay longer">Tiempo de retraso más largo</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_down" comment="Make longpress delay shorter">Tiempo de retraso más corto</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_longpress_delay_slider" comment="Adjust longpress delay time">Deslizador de tiempo de retraso de pulsación larga</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_text" comment="Prompt to type in the browser search bar">Buscar o escribir una dirección web</string>
<!-- Context: Keyman Web Browser -->

View file

@ -241,11 +241,11 @@ function enableSuggestions(model, suggestionType) {
[true, true, false], // 2 = SuggestionType.PREDICTIONS_WITH_CORRECTIONS
[true, true, true], // 3 = SuggestionType.PREDICTIONS_WITH_AUTO_CORRECT
];
const t = suggestionSettings[suggestionType]
const t = suggestionSettings[suggestionType]
? suggestionSettings[suggestionType] : suggestionSettings[0];
keyman.core.languageProcessor.mayPredict = t[0];
keyman.core.languageProcessor.maySuggest = t[1];
// keyman.core.languageProcessor.mayAutoCorrect = t[2];
keyman.core.languageProcessor.mayAutoCorrect = t[2];
registerModel(model);
}

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

@ -249,7 +249,7 @@ public final class KMManager {
public final static String predictionPrefSuffix = ".mayPredict";
public final static String correctionPrefSuffix = ".mayCorrect";
public final static String autoCorrectionPrefSuffix = ".mayAutoCorect";
public final static String autoCorrectionPrefSuffix = ".mayAutoCorrect";
// Special override for when the keyboard may have haptic feedback when typing.
// haptic feedback disabled for hardware keystrokes
@ -305,11 +305,6 @@ public final class KMManager {
public static final String KMKey_LexicalModelVersion = "lmVersion";
public static final String KMKey_LexicalModelPackageFilename = "kmpPackageFilename";
// DEPRECATED keys
public static final String KMKey_CustomKeyboard = "CustomKeyboard";
public static final String KMKey_CustomModel = "CustomModel";
public static final String KMKey_HelpLink = "helpLink";
// Keyman internal keys
protected static final String KMKey_ShouldShowHelpBubble = "ShouldShowHelpBubble";
@ -1714,26 +1709,6 @@ public final class KMManager {
return KeyboardPickerActivity.addKeyboard(context, keyboardInfo);
}
// Intend to deprecate in Keyman 15.0
public static boolean addKeyboard(Context context, HashMap<String, String> keyboardInfo) {
String packageID = keyboardInfo.get(KMManager.KMKey_PackageID);
String keyboardID = keyboardInfo.get(KMManager.KMKey_KeyboardID);
String keyboardName = keyboardInfo.get(KMManager.KMKey_KeyboardName);
String languageID = keyboardInfo.get(KMManager.KMKey_LanguageID);
String languageName = keyboardInfo.get(KMManager.KMKey_LanguageName);
String version = keyboardInfo.get(KMManager.KMKey_KeyboardVersion);
String helpLink = keyboardInfo.get(KMManager.KMKey_CustomHelpLink);
String kmpLink = MapCompat.getOrDefault(keyboardInfo, KMManager.KMKey_KMPLink, "");
String font = keyboardInfo.get(KMManager.KMKey_Font);
String oskFont = keyboardInfo.get(KMManager.KMKey_OskFont);
boolean isNewKeyboard = true;
Keyboard k = new Keyboard(packageID, keyboardID, keyboardName,
languageID, languageName, version, helpLink, kmpLink,
isNewKeyboard, font, oskFont);
return addKeyboard(context, k);
}
public static boolean removeKeyboard(Context context, int position) {
return KeyboardPickerActivity.removeKeyboard(context, position);
}
@ -2292,6 +2267,7 @@ public final class KMManager {
if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
}
i.putExtra(KMKey_DisplayKeyboardSwitcher, kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM);

View file

@ -25,6 +25,7 @@ import com.keyman.engine.util.MapCompat;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@ -252,8 +253,17 @@ public final class KeyboardPickerActivity extends BaseActivity {
adapter.notifyDataSetChanged();
}
int curKbPos = KeyboardController.getInstance().getKeyboardIndex(KMKeyboard.currentKeyboard());
setSelection(curKbPos);
// Determine the index to the current keyboard position to highlight as the selected keyboard
int currentKeyboardIndex;
String currentKeyboardKey = KMKeyboard.currentKeyboard();
if (currentKeyboardKey == null) {
SharedPreferences prefs = this.getSharedPreferences(this.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
currentKeyboardIndex = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0);
} else {
currentKeyboardIndex = KeyboardController.getInstance().getKeyboardIndex(currentKeyboardKey);
}
setSelection(currentKeyboardIndex);
imeList = getIMEList(this);
BaseAdapter imeAdapter = (BaseAdapter)imeListAdapter;

View file

@ -107,7 +107,7 @@
<string name="dictionary_download_start_in_background" comment="Notification that a dictionary download has started">La descarga del diccionario inició en segundo plano</string>
<!-- Context: Background download messages-->
<string name="dictionary_download_is_running_in_background" comment="Notification that a dictionary download is still running">El diccionario seleccionado ya se está descargando; ¡por favor, inténtalo de nuevo en un momento!</string>
<!-- Context: Background download messages-->
<!-- Context: Background download messages. Removed in Keyman 17-->
<string name="dictionary_download_finished" comment="Notification that a dictionary download has finished">La descarga del diccionario ha finalizado.</string>
<!-- Context: Background download messages -->
<string name="download_failed" comment="Notification that a download failed">Descargar falló</string>
@ -119,7 +119,7 @@
<string name="update_check_current" comment="Notification that all resources are up to date">"¡Todos los recursos están actualizados!"</string>
<!-- Context: General Updates -->
<string name="update_failed" comment="Notification that a resource update failed">¡Uno o más recursos no se han actualizado!</string>
<!-- Context: General Updates -->
<!-- Context: General Updates. Removed in Keyman 17 -->
<string name="update_success" comment="Notification that a resource update successfully updated">¡Recursos actualizados con éxito!</string>
<!-- Context: Model Info -->
<string name="model_version" comment="Title for a dictionary version">Versión del diccionario</string>
@ -133,10 +133,15 @@
<string name="keyboard_install_toast" comment="Notification when a keyboard is installed">Teclado %1$s instalado</string>
<!-- Context: Language Settings Keyboard uninstall strings -->
<string name="keyboard_deleted_toast" comment="Notification when a keyboard is deleted">Teclado eliminado</string>
<!-- Context: Language Settings menu strings -->
<!-- Context: Language Settings menu strings. Removed in Keyman 18 -->
<string name="enable_corrections" comment="Enable corrections from the suggestion banner">Habilitar correcciones</string>
<!-- Context: Language Settings menu strings -->
<!-- Context: Language Settings menu strings. Removed in Keyman 18 -->
<string name="enable_predictions" comment="Enable predictions from the suggestion banner">Habilitar predicciones</string>
<!-- Context: Language Settings menu strings - radio buttons -->
<string name="suggestions_radio_0" comment="Predictions and corrections disabled">Desactivar sugerencias</string>
<string name="suggestions_radio_1" comment="Suggestions Enabled: Predictions with no corrections">Sólo predicciones</string>
<string name="suggestions_radio_2" comment="Suggestions Enabled: Predictions with corrections">Predicciones con correcciones</string>
<string name="suggestions_radio_3" comment="Suggestions Enabled: Predictions with auto-corrections">Predicciones con autocorrecciones</string>
<!-- Context: Language Settings menu strings -->
<string name="model_label">Diccionario</string>
<plurals name="model_count">

View file

@ -126,7 +126,6 @@ public class KMManagerTest {
HashMap<String, String> kmInfo = new HashMap<String, String>();
kmInfo.put(KMManager.KMKey_PackageID, "khmer_angkor");
kmInfo.put(KMManager.KMKey_KeyboardID, "khmer_angkor");
kmInfo.put(KMManager.KMKey_CustomKeyboard, "Y");
kmInfo.put(KMManager.KMKey_LanguageID, "km");
kmInfo.put(KMManager.KMKey_KeyboardName, "Khmer Angkor");
kmInfo.put(KMManager.KMKey_LanguageName, "Central Khmer");

View file

@ -66,7 +66,21 @@ public class FunctionalTestHelper {
Assert.assertEquals(installedKbds.size(),1);
KMManager.addKeyboard(ApplicationProvider.getApplicationContext(),(HashMap<String, String>) installedKbds.get(0));
HashMap<String, String> map = (HashMap<String, String>)installedKbds.get(0);
Keyboard keyboard = new Keyboard(
map.get(KMManager.KMKey_PackageID),
map.get(KMManager.KMKey_KeyboardID),
map.get(KMManager.KMKey_KeyboardName),
map.get(KMManager.KMKey_LanguageID),
map.get(KMManager.KMKey_LanguageName),
map.get(KMManager.KMKey_KeyboardVersion),
map.get(KMManager.KMKey_CustomHelpLink),
map.get(KMManager.KMKey_KMPLink),
true,
null,
null
);
KMManager.addKeyboard(ApplicationProvider.getApplicationContext(), keyboard);
}

View file

@ -8,6 +8,8 @@ There are now several gestures available to interact with some keys on the keybo
![](../android_images/touch-hold-ap.png)
* **Long press**: Press and hold a key, and after a moment a submenu will appear. Slide the finger to the submenu to select a key. Release the finger, and the highlighted key from the submenu will be output.
* **Flick**: Press and hold a key, and then slide the finger in various directions on a key to reach alternate outputs. The key will animate to show the expected output when the finger gets released. A common flick is to slide down to reach numerals on the top row of the keyboard.
* **Multitap**: Some keys can be pressed repeatedly and rapidly to reach alternate outputs. Tapping rapidly twice on <kbd>Shift</kbd> will activate <kbd>Caps Lock</kbd> on many keyboards.
* **Flick**: Press and hold a key, and then slide the finger in various directions on a key to reach alternate outputs. The key will animate to show the expected output when the finger gets released. A common flick is to slide down to reach numerals on the top row of the keyboard.
An up-flick on keys that support longpresses but do not have a defined up-flick will skip the wait and immediately display their longpress submenu.
* **Multitap**: Some keys can be pressed repeatedly and rapidly to reach alternate outputs. Tapping rapidly twice on <kbd>Shift</kbd> will activate <kbd>Caps Lock</kbd> on many keyboards.

View file

@ -1,12 +0,0 @@
{
"exclude": [
"src/keyboard-object.ts",
"src/lexical-model-types.ts",
"src/outputTarget.interface.ts",
"src/*.d.ts",
"src/main.ts",
"src/schemas/*",
"src/schema-validators.ts",
"src/schemas.ts"
]
}

View file

@ -92,7 +92,7 @@ function do_test() {
readonly C8_THRESHOLD=60
# Excludes are defined in .c8rc.json
# Excludes are defined in package.json
c8 -skip-full --reporter=lcov --reporter=text --lines $C8_THRESHOLD --statements $C8_THRESHOLD --branches $C8_THRESHOLD --functions $C8_THRESHOLD mocha ${MOCHA_FLAGS} "${builder_extra_params[@]}"
builder_echo warning "Coverage thresholds are currently $C8_THRESHOLD%, which is lower than ideal."
builder_echo warning "Please increase threshold in build.sh as test coverage improves."

View file

@ -76,7 +76,13 @@
"src/keyman-touch-layout/keyman-touch-layout-file-writer.ts",
"src/osk/osk.ts",
"src/schemas/*",
"tests/"
"tests/",
"src/keyboard-object.ts",
"src/outputTarget.interface.ts",
"src/*.d.ts",
"src/main.ts",
"src/schema-validators.ts",
"src/schemas.ts"
]
},
"sideEffects": false

View file

@ -19,8 +19,8 @@ export class ElemElement {
order: number; // -128 to +127; used only by reorder element values
tertiary: number; // -128 to +127; used only by reorder element values
flags: ElemElementFlags;
isEqual(a: ElemElement) {
return a.value === this.value &&
isEqual(a: ElemElement): boolean {
return a.value.isEqual(this.value) &&
a.order === this.order &&
a.tertiary === this.tertiary &&
a.flags === this.flags;
@ -104,8 +104,8 @@ export class ElementString extends Array<ElemElement> {
typeFlag |= constants.elem_flags_type_str;
}
}
elem.order = orders.length ? parseInt(orders[i], 10) : 0;
elem.tertiary = tertiaries.length ? parseInt(tertiaries[i], 10) : 0;
elem.order = orders.length ? this.parseIntOrZero(orders[i]) : 0;
elem.tertiary = tertiaries.length ? this.parseIntOrZero(tertiaries[i]) : 0;
elem.flags = ElemElementFlags.none |
(ElemElementFlags.type & typeFlag) |
(tertiary_bases?.[i] == '1' /* TODO-LDML: or 'true'? */ ? ElemElementFlags.tertiary_base : 0) |
@ -125,5 +125,9 @@ export class ElementString extends Array<ElemElement> {
}
return true;
}
private static parseIntOrZero(str: string): number {
const num = parseInt(str, 10);
return !Number.isNaN(num) ? num : 0;
}
}
;

View file

@ -131,6 +131,10 @@ export class StrsItem {
get isOneChar() {
return this.char !== undefined;
}
isEqual(a: StrsItem): boolean {
return a.value === this.value && a.char === this.char;
}
};
/**

View file

@ -0,0 +1,498 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Dr Mark C. Sinclair on 2024-12-09
*
* Test code for element-string.ts
*/
import 'mocha';
import { assert } from 'chai';
import { constants } from '@keymanapp/ldml-keyboard-constants';
import { ElemElementFlags, ElemElement, ElementString } from '../../../src/kmx/kmx-plus/element-string.js';
import { StrsItem, UsetItem, Strs, StrsOptions, DependencySections, CharStrsItem, Uset } from '../../../src/kmx/kmx-plus/kmx-plus.js';
import { UnicodeSet, UnicodeSetParser } from '../../../src/ldml-keyboard/unicodeset-parser-api.js';
import { ElementParser, ElementSegment, ElementType } from '../../../src/ldml-keyboard/pattern-parser.js';
const GOTHIC_A = new StrsItem("𐌰", 0x10330);
const GOTHIC_B = new StrsItem("𐌱", 0x10331);
const GOTHIC_C = new StrsItem("𐌲", 0x10332);
const GOTHIC_D = new StrsItem("𐌳", 0x10333);
const HI_GOTHIC_A = new StrsItem('\ud800', 0xd800);
const LO_GOTHIC_A = new StrsItem('\udf30', 0xdf30);
const GOTHIC_PATTERN = "[𐌰-𐍊]";
const GOTHIC_STRSITEM = new StrsItem(GOTHIC_PATTERN);
const GOTHIC_SET = new UnicodeSet(GOTHIC_PATTERN, [[0x10330,0x1034A]]);
const GOTHIC_USETITEM = new UsetItem(GOTHIC_SET, GOTHIC_STRSITEM);
const UGARITIC_PATTERN = "[𐎀-𐎟]";
const UGARITIC_STRSITEM = new StrsItem(UGARITIC_PATTERN);
const UGARITIC_SET = new UnicodeSet(UGARITIC_PATTERN, [[0x10380,0x1039F]]);
const UGARITIC_USETITEM = new UsetItem(UGARITIC_SET, UGARITIC_STRSITEM);
class TestUnicodeSetParser implements UnicodeSetParser {
parseUnicodeSet = (pattern: string, rangeCount: number) : UnicodeSet | null => GOTHIC_SET;
sizeUnicodeSet = (pattern: string) : number => 1;
};
const origElementParserSegment = ElementParser.segment;
let sections: DependencySections = null;
describe('Test of ElementString file', () => {
describe('Test of ElemElement', () => {
describe('Test of isEqual()', () => {
it('returns true when elems identical', () => {
const one = initElemElement();
const two = initElemElement();
assert.isTrue(one.isEqual(two));
});
it('returns true when elems are clones', () => {
const one = initElemElement(new StrsItem("𐌰", 0x10330));
const two = initElemElement(new StrsItem("𐌰", 0x10330));
assert.isTrue(one.isEqual(two));
});
it('returns false when value differs', () => {
const one = initElemElement(GOTHIC_A);
const two = initElemElement(GOTHIC_B);
assert.isFalse(one.isEqual(two));
});
it('returns false when order differs', () => {
const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0);
const two = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 1);
assert.isFalse(one.isEqual(two));
});
it('returns false when tertiary differs', () => {
const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 0);
const two = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 1);
assert.isFalse(one.isEqual(two));
});
it('returns false when flags differs', () => {
const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 0, ElemElementFlags.none);
const two = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 0, ElemElementFlags.type);
assert.isFalse(one.isEqual(two));
});
it('returns true even though uset differs', () => {
const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM);
const two = initElemElement(GOTHIC_A, UGARITIC_USETITEM);
assert.isTrue(one.isEqual(two));
});
});
});
describe('Test of ElementString class', () => {
beforeEach(() => {
sections = {
strs: new Strs(),
uset: new Uset(),
usetparser: new TestUnicodeSetParser(),
};
ElementParser.segment = stubElementParserSegment_CodePoint;
});
afterEach(() => {
ElementParser.segment = origElementParserSegment;
});
describe('Test of fromStrings()', () => {
it('returns an empty ElementString if source is null', () => {
const es = ElementString.fromStrings({}, null);
assert.deepEqual(es, new ElementString());
});
it('can create an ElementString from a string array', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]);
const expected = [
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
];
assert.deepEqual(actual, expected);
});
it('can create an ElementString from a string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(sections, "𐌰𐌱𐌲");
const expected = [
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
];
assert.deepEqual(actual, expected);
});
it('can apply order string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
"1 2 3",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 1),
initElemElement(GOTHIC_B, undefined, 2),
initElemElement(GOTHIC_C, undefined, 3),
];
assert.deepEqual(actual, expected);
});
it('can apply single order to all', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
"1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 1),
initElemElement(GOTHIC_B, undefined, 1),
initElemElement(GOTHIC_C, undefined, 1),
];
assert.deepEqual(actual, expected);
});
it('can handle order string that is too short', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
"1 2",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 1),
initElemElement(GOTHIC_B, undefined, 2),
initElemElement(GOTHIC_C, undefined, 0),
];
assert.deepEqual(actual, expected);
});
it('can handle non-number in order string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
"1 A 3",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 1),
initElemElement(GOTHIC_B, undefined, 0),
initElemElement(GOTHIC_C, undefined, 3),
];
assert.deepEqual(actual, expected);
});
it('can apply tertiary string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
"1 2 3",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 1),
initElemElement(GOTHIC_B, undefined, 0, 2),
initElemElement(GOTHIC_C, undefined, 0, 3),
];
assert.deepEqual(actual, expected);
});
it('can apply single tertiary to all', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
"1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 1),
initElemElement(GOTHIC_B, undefined, 0, 1),
initElemElement(GOTHIC_C, undefined, 0, 1),
];
assert.deepEqual(actual, expected);
});
it('can handle tertiary string that is too short', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
"1 2",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 1),
initElemElement(GOTHIC_B, undefined, 0, 2),
initElemElement(GOTHIC_C, undefined, 0, 0),
];
assert.deepEqual(actual, expected);
});
it('can handle non-number in tertiary string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
"1 A 3",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 1),
initElemElement(GOTHIC_B, undefined, 0, 0),
initElemElement(GOTHIC_C, undefined, 0, 3),
];
assert.deepEqual(actual, expected);
});
it('can apply tertiary_base string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
"1 0 1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.tertiary_base),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.tertiary_base),
];
assert.deepEqual(actual, expected);
});
it('can apply single tertiary_base to all', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
"1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.tertiary_base),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.tertiary_base),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.tertiary_base),
];
assert.deepEqual(actual, expected);
});
it('can handle tertiary_base string that is too short', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
"1 0",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.tertiary_base),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.none),
];
assert.deepEqual(actual, expected);
});
it('can apply prebase string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
null,
"1 0 1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.prebase),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.prebase),
];
assert.deepEqual(actual, expected);
});
it('can apply single prebase to all', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
null,
"1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.prebase),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.prebase),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.prebase),
];
assert.deepEqual(actual, expected);
});
it('can handle prebase string that is too short', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
null,
"1 0",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.prebase),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.none),
];
assert.deepEqual(actual, expected);
});
it('can create an ElementString from a uset string', () => {
ElementParser.segment = stubElementParserSegment_Uset;
sections.strs.allocString = stubStrsAllocString_Str;
sections.uset.allocUset = stubUsetAllocUset;
const actual = ElementString.fromStrings(sections, "[𐌰-𐍊]");
const expected = [
initElemElement(
new StrsItem(''),
GOTHIC_USETITEM,
0,
0,
constants.elem_flags_type_uset,
),
];
assert.deepEqual(actual, expected);
});
it('returns null for an invalid unicode set size', () => {
ElementParser.segment = stubElementParserSegment_Uset;
sections.usetparser.sizeUnicodeSet = (pattern: string) : number => -1;
assert.isNull(ElementString.fromStrings(sections, "[𐌰-𐍊]"));
});
it('returns null if it cannot parse the unicode set', () => {
ElementParser.segment = stubElementParserSegment_Uset;
sections.usetparser.parseUnicodeSet = (pattern: string, rangeCount: number) : UnicodeSet | null => null;
assert.isNull(ElementString.fromStrings(sections, "[𐌰-𐍊]"));
});
it('can handle quad strings', () => {
sections.strs.allocString = stubStrsAllocString_Char;
ElementParser.segment = stubElementParserSegment_Escaped;
const actual = ElementString.fromStrings(sections, "\\ud800\\udf30");
const expected = [
initElemElement(HI_GOTHIC_A),
initElemElement(LO_GOTHIC_A),
];
assert.deepEqual(actual, expected);
});
it('can handle ElemElement of string type', () => {
sections.strs.allocString = stubStrsAllocString_Str;
const actual = ElementString.fromStrings(sections, ["𐌰𐌱𐌲",]);
const expected = [
initElemElement(
new StrsItem("𐌰𐌱𐌲"),
undefined,
0,
0,
constants.elem_flags_type_str,
),
];
assert.deepEqual(actual, expected);
});
});
describe('Test of isEqual()', () => {
it('returns true when ElementStrings are identical', () => {
const es = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
]);
assert.isTrue(es.isEqual(es));
});
it('returns true when ElementStrings are clones', () => {
const one = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
]);
const two = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
]);
assert.isTrue(one.isEqual(two));
});
it('returns false when ElementStrings are different lengths', () => {
const one = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
]);
const two = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
]);
assert.isFalse(one.isEqual(two));
});
it('returns false when ElementStrings have different ElemElements', () => {
const one = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
]);
const two = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_D),
]);
assert.isFalse(one.isEqual(two));
});
});
describe('Test of parseIntOrZero()', () => {
it('returns a number for a valid string', () => {
const num = ElementString['parseIntOrZero']('1');
assert.equal(num, 1);
});
it('returns zero for an invalid string', () => {
const num = ElementString['parseIntOrZero']('A');
assert.equal(num, 0);
});
it('returns zero for undefined', () => {
const num = ElementString['parseIntOrZero'](undefined);
assert.equal(num, 0);
});
it('returns zero for a null string', () => {
const num = ElementString['parseIntOrZero'](null);
assert.equal(num, 0);
});
});
});
});
function initElemElement(
value: StrsItem = GOTHIC_A,
uset: UsetItem = undefined,
order: number = 0,
tertiary: number = 0,
flags: ElemElementFlags = ElemElementFlags.none,
): ElemElement {
const ee = new ElemElement();
ee.value = value;
ee.uset = uset;
ee.order = order;
ee.tertiary = tertiary;
ee.flags = flags;
return ee;
};
function initElementString(elemElements: ElemElement[]): ElementString {
const es: ElementString = new ElementString();
elemElements.forEach((ee) => {es.push(ee)});
return es;
};
function stubStrsAllocString_Char(s?: string, opts?: StrsOptions, sections?: DependencySections): StrsItem {
return new CharStrsItem(s);
};
function stubStrsAllocString_Str(s?: string, opts?: StrsOptions, sections?: DependencySections): StrsItem {
return new StrsItem(s);
};
function stubElementParserSegment_CodePoint(str: string): ElementSegment[] {
return [...str].map(s => new ElementSegment(s, ElementType.codepoint));
};
function stubElementParserSegment_Uset(str: string): ElementSegment[] {
return [new ElementSegment(str, ElementType.uset)];
};
function stubElementParserSegment_Escaped(str: string): ElementSegment[] {
const strs = str.match(/\\u[0-9a-fA-F]{4}/g);
return strs.map((s) => new ElementSegment(s, ElementType.escaped));
};
function stubUsetAllocUset(set: UnicodeSet, sections: DependencySections) : UsetItem {
return new UsetItem(set, new StrsItem(set.pattern));
};

View file

@ -0,0 +1,325 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Dr Mark C. Sinclair on 2024-12-05
*
* Test code for kvk-file-writer.ts
*/
import 'mocha';
import { assert } from 'chai';
import { KvkFileWriter } from '../../src/main.js';
import { VisualKeyboard,
VisualKeyboardKey,
VisualKeyboardFont,
DEFAULT_KVK_FONT
} from '../../src/kvk/visual-keyboard.js';
import { BUILDER_KVK_FILE,
BUILDER_KVK_HEADER_FLAGS,
BUILDER_KVK_STRING,
BUILDER_KVK_HEADER_IDENTIFIER,
BUILDER_KVK_HEADER_VERSION,
BUILDER_KVK_KEY,
BUILDER_KVK_KEY_FLAGS,
BUILDER_KVK_SHIFT_STATE,
} from '../../src/kvk/kvk-file.js';
const VISUAL_KEYBOARD_TEXT_COLOR = 0xFF000008;
describe('Test of KVK-File-Writer', () => {
describe('Test of write()', () => {
it('can create a visual keyboard', () => {
const writer = new KvkFileWriter;
const vk = initVisualKeyboard([
initVisualKeyboardKey(0),
initVisualKeyboardKey(1),
initVisualKeyboardKey(2),
]);
const file = writer.write(vk);
assert.isNotNull(file);
});
});
describe('Test of build()', () => {
it('can build a BUILDER_KVK_FILE', () => {
const vk = initVisualKeyboard([
initVisualKeyboardKey(0),
initVisualKeyboardKey(1),
initVisualKeyboardKey(2),
]);
const writer = new KvkFileWriter;
const binary: BUILDER_KVK_FILE = writer['build'](vk);
checkBuilderKvkFile(binary, vk);
});
it('does not take account of the VisualKeyboard version', () => {
const vk_version = (BUILDER_KVK_HEADER_VERSION + 0x0100);
const vk = initVisualKeyboard([
initVisualKeyboardKey(0),
initVisualKeyboardKey(1),
initVisualKeyboardKey(2),
],
vk_version,
BUILDER_KVK_HEADER_FLAGS.kvkhNone,
"associatedKeyboard",
DEFAULT_KVK_FONT,
DEFAULT_KVK_FONT,
undefined,
);
const writer = new KvkFileWriter;
const binary: BUILDER_KVK_FILE = writer['build'](vk);
checkBuilderKvkFile(binary, vk);
});
it.skip('can handle a null associatedKeyboard', () => {
const vk_associatedKeyboard: string = null;
const vk = initVisualKeyboard([
initVisualKeyboardKey(0),
initVisualKeyboardKey(1),
initVisualKeyboardKey(2),
],
undefined,
BUILDER_KVK_HEADER_FLAGS.kvkhNone,
vk_associatedKeyboard,
DEFAULT_KVK_FONT,
DEFAULT_KVK_FONT,
undefined,
);
const writer = new KvkFileWriter;
const binary: BUILDER_KVK_FILE = writer['build'](vk);
checkBuilderKvkFile(binary, vk);
});
it.skip('can handle a null ansiFont name', () => {
const vk_ansiFont_name: string = null;
const vk = initVisualKeyboard([
initVisualKeyboardKey(0),
initVisualKeyboardKey(1),
initVisualKeyboardKey(2),
],
undefined,
BUILDER_KVK_HEADER_FLAGS.kvkhNone,
"associatedKeyboard",
{ name: vk_ansiFont_name, size: -12 },
DEFAULT_KVK_FONT,
undefined,
);
const writer = new KvkFileWriter;
const binary: BUILDER_KVK_FILE = writer['build'](vk);
checkBuilderKvkFile(binary, vk);
});
it.skip('can handle a null unicodeFont name', () => {
const vk_unicodeFont_name: string = null;
const vk = initVisualKeyboard([
initVisualKeyboardKey(0),
initVisualKeyboardKey(1),
initVisualKeyboardKey(2),
],
undefined,
BUILDER_KVK_HEADER_FLAGS.kvkhNone,
"associatedKeyboard",
DEFAULT_KVK_FONT,
{ name: vk_unicodeFont_name, size: -12 },
undefined,
);
const writer = new KvkFileWriter;
const binary: BUILDER_KVK_FILE = writer['build'](vk);
checkBuilderKvkFile(binary, vk);
});
it('can handle a null vkey', () => {
const vkk_vkey: number = null;
const vk = initVisualKeyboard([
initVisualKeyboardKey(
vkk_vkey,
BUILDER_KVK_KEY_FLAGS.kvkkBitmap,
BUILDER_KVK_SHIFT_STATE.KVKS_NORMAL,
"text",
null,
),
initVisualKeyboardKey(1),
initVisualKeyboardKey(2),
]);
const writer = new KvkFileWriter;
const binary: BUILDER_KVK_FILE = writer['build'](vk);
checkBuilderKvkFile(binary, vk);
});
it('can handle a null key flags', () => {
const vkk_flags: BUILDER_KVK_KEY_FLAGS = null;
const vk = initVisualKeyboard([
initVisualKeyboardKey(
0,
vkk_flags,
BUILDER_KVK_SHIFT_STATE.KVKS_NORMAL,
"text",
null,
),
initVisualKeyboardKey(1),
initVisualKeyboardKey(2),
]);
const writer = new KvkFileWriter;
const binary: BUILDER_KVK_FILE = writer['build'](vk);
checkBuilderKvkFile(binary, vk);
});
it('can handle a null key shift', () => {
const vkk_shift: BUILDER_KVK_SHIFT_STATE = null;
const vk = initVisualKeyboard([
initVisualKeyboardKey(
0,
BUILDER_KVK_KEY_FLAGS.kvkkBitmap,
vkk_shift,
"text",
null,
),
initVisualKeyboardKey(1),
initVisualKeyboardKey(2),
]);
const writer = new KvkFileWriter;
const binary: BUILDER_KVK_FILE = writer['build'](vk);
checkBuilderKvkFile(binary, vk);
});
it('can handle a null key text', () => {
const vkk_text: string = null;
const vk = initVisualKeyboard([
initVisualKeyboardKey(
0,
BUILDER_KVK_KEY_FLAGS.kvkkBitmap,
BUILDER_KVK_SHIFT_STATE.KVKS_NORMAL,
vkk_text,
null,
),
initVisualKeyboardKey(1),
initVisualKeyboardKey(2),
]);
const writer = new KvkFileWriter;
const binary: BUILDER_KVK_FILE = writer['build'](vk);
checkBuilderKvkFile(binary, vk);
});
it('can handle a non-null key bitmap', () => {
const vkk_bitmap: number[] = [0];
const vk = initVisualKeyboard([
initVisualKeyboardKey(
0,
BUILDER_KVK_KEY_FLAGS.kvkkBitmap,
BUILDER_KVK_SHIFT_STATE.KVKS_NORMAL,
"text",
vkk_bitmap,
),
initVisualKeyboardKey(1),
initVisualKeyboardKey(2),
]);
const writer = new KvkFileWriter;
const binary: BUILDER_KVK_FILE = writer['build'](vk);
checkBuilderKvkFile(binary, vk);
});
});
describe('Test of setString()', () => {
it('can set a BUILDER_KVK_STRING', () => {
const bks: BUILDER_KVK_STRING = { len: 0, str: null };
const writer = new KvkFileWriter;
const str = "hello";
writer['setString'](bks, str);
assert.equal(bks.len, str.length + 1);
assert.deepEqual(bks.str, str);
});
it('can handle an empty string', () => {
const bks: BUILDER_KVK_STRING = { len: 0, str: null };
const writer = new KvkFileWriter;
const str = "";
writer['setString'](bks, str);
assert.equal(bks.len, str.length + 1);
assert.deepEqual(bks.str, str);
});
it('throws TypeError for a null BUILDER_KVK_STRING', () => {
assert.throws(() => {
const writer = new KvkFileWriter;
writer['setString'](null, "");
}, TypeError);
});
it('throws TypeError for a null value', () => {
assert.throws(() => {
const bks: BUILDER_KVK_STRING = { len: 0, str: null };
const writer = new KvkFileWriter;
writer['setString'](bks, null);
}, TypeError);
});
});
});
function initVisualKeyboard(
vkks: VisualKeyboardKey[],
version: number = undefined,
flags: BUILDER_KVK_HEADER_FLAGS = BUILDER_KVK_HEADER_FLAGS.kvkhNone,
associatedKeyboard: string = "associatedKeyboard",
ansiFont: VisualKeyboardFont = DEFAULT_KVK_FONT,
unicodeFont: VisualKeyboardFont = DEFAULT_KVK_FONT,
underlyingLayout: string = undefined,
): VisualKeyboard {
const vkh = {
version: version,
flags: flags,
associatedKeyboard: associatedKeyboard,
ansiFont: ansiFont,
unicodeFont: unicodeFont,
underlyingLayout: underlyingLayout,
};
return { header: vkh, keys: vkks };
};
function initVisualKeyboardKey(
vkey: number,
flags: BUILDER_KVK_KEY_FLAGS = BUILDER_KVK_KEY_FLAGS.kvkkBitmap,
shift: BUILDER_KVK_SHIFT_STATE = BUILDER_KVK_SHIFT_STATE.KVKS_NORMAL,
text: string = "text",
bitmap: number[] = null,
): VisualKeyboardKey {
const vkk: VisualKeyboardKey = {
vkey,
flags,
shift,
text,
bitmap: bitmap ? new Uint8Array(bitmap) : null,
};
return vkk;
}
function checkBuilderKvkFile(binary: BUILDER_KVK_FILE, vk: VisualKeyboard) {
assert.equal(binary.header.identifier, BUILDER_KVK_HEADER_IDENTIFIER);
assert.equal(binary.header.version, BUILDER_KVK_HEADER_VERSION);
if (vk.header.associatedKeyboard != null) {
assert.deepEqual(binary.header.associatedKeyboard.str, vk.header.associatedKeyboard);
} else {
assert.deepEqual(binary.header.associatedKeyboard.str, '');
}
assert.equal(binary.header.flags, vk.header.flags);
assert.equal(binary.header.ansiFont.color, VISUAL_KEYBOARD_TEXT_COLOR);
assert.equal(binary.header.ansiFont.size, vk.header.ansiFont.size);
if (vk.header.ansiFont.name != null) {
assert.deepEqual(binary.header.ansiFont.name.str, vk.header.ansiFont.name);
} else {
assert.deepEqual(binary.header.ansiFont.name.str, '');
}
assert.equal(binary.header.unicodeFont.color, VISUAL_KEYBOARD_TEXT_COLOR);
assert.equal(binary.header.unicodeFont.size, vk.header.unicodeFont.size);
if (vk.header.unicodeFont.name != null) {
assert.deepEqual(binary.header.unicodeFont.name.str, vk.header.unicodeFont.name);
} else {
assert.deepEqual(binary.header.unicodeFont.name.str, '');
}
for (let idx=0; idx<binary.keys.length; idx++) {
checkBuilderKvkKey(binary.keys[idx], vk.keys[idx])
}
}
function checkBuilderKvkKey(target: BUILDER_KVK_KEY, source: VisualKeyboardKey) {
assert.equal(target.flags, source.flags);
assert.equal(target.shift, source.shift);
assert.equal(target.vkey, source.vkey);
if (source.text != null) {
assert.deepEqual(target.text.str, source.text);
} else {
assert.deepEqual(target.text.str, '');
}
if (source.bitmap != null) {
assert.equal(target.bitmapSize, source.bitmap.byteLength);
assert.deepEqual(target.bitmapData, Array.from(source.bitmap));
} else {
assert.equal(target.bitmapSize, 0);
assert.deepEqual(target.bitmapData, []);
}
}

View file

@ -0,0 +1,52 @@
{
Keyman is copyright (C) SIL Global. MIT License.
This module provides functionality to track the execution state of the Keyman
engine. It uses a global atom to record whether Keyman has started during the
current session and checks if it has previously run.
}
unit Keyman.System.ExecutionHistory;
interface
const
AtomName = 'KeymanSessionFlag';
function RecordKeymanStarted : Boolean;
function HasKeymanRun : Boolean;
implementation
uses
System.SysUtils,
Winapi.Windows,
KLog;
function RecordKeymanStarted : Boolean;
var
atom: WORD;
begin
atom := GlobalAddAtom(AtomName);
if atom = 0 then
begin
// TODO-WINDOWS-UPDATES: #10210 log to sentry
Result := False;
end
else
Result := True;
end;
function HasKeymanRun : Boolean;
begin
Result := GlobalFindAtom(AtomName) <> 0;
if not Result then
begin
if GetLastError <> ERROR_FILE_NOT_FOUND then
begin
// TODO-WINDOWS-UPDATES: log to Sentry
end;
end;
end;
end.

View file

@ -35,6 +35,7 @@ type
TUpdateCheckResponse = record
private
FOriginalData: string;
FInstallSize: Int64;
FInstallURL: string;
FNewVersion: string;
@ -46,9 +47,13 @@ type
FFileName: string;
function ParseKeyboards(nodes: TJSONObject): Boolean;
function ParseLanguages(i: Integer; v: TJSONValue): Boolean;
function DoParse(const message, app, currentVersion: string): Boolean;
public
function Parse(const message: AnsiString; const app, currentVersion: string): Boolean;
procedure SaveToFile(const Filename: string);
function LoadFromFile(const Filename, app, currentVersion: string): Boolean;
property CurrentVersion: string read FCurrentVersion;
property NewVersion: string read FNewVersion;
property NewVersionWithTag: string read FNewVersionWithTag;
@ -58,25 +63,32 @@ type
property ErrorMessage: string read FErrorMessage;
property Status: TUpdateCheckResponseStatus read FStatus;
property Packages: TUpdateCheckResponsePackages read FPackages;
property OriginalData: string read FOriginalData;
end;
implementation
uses
System.Classes,
System.Generics.Collections,
versioninfo;
{ TUpdateCheckResponse }
function TUpdateCheckResponse.Parse(const message: AnsiString; const app, currentVersion: string): Boolean;
begin
Result := DoParse(string(UTF8String(message)), app, currentVersion);
end;
function TUpdateCheckResponse.DoParse(const message, app, currentVersion: string): Boolean;
var
node, doc: TJSONObject;
begin
FOriginalData := message;
FCurrentVersion := currentVersion;
FStatus := ucrsNoUpdate;
// TODO: test with UTF8 characters in response
doc := TJSONObject.ParseJSONValue(UTF8String(message)) as TJSONObject;
doc := TJSONObject.ParseJSONValue(UTF8String(FOriginalData)) as TJSONObject;
if doc = nil then
begin
FErrorMessage := Format('Invalid response:'#13#10'%s', [string(message)]);
@ -168,4 +180,29 @@ begin
Result := True;
end;
function TUpdateCheckResponse.LoadFromFile(const Filename, app, currentVersion: string): Boolean;
var
ss: TStringStream;
begin
ss := TStringStream.Create('', TEncoding.UTF8);
try
ss.LoadFromFile(Filename);
Result := DoParse(ss.DataString, app, currentVersion);
finally
ss.Free;
end;
end;
procedure TUpdateCheckResponse.SaveToFile(const Filename: string);
var
ss: TStringStream;
begin
ss := TStringStream.Create(FOriginalData, TEncoding.UTF8);
try
ss.SaveToFile(Filename);
finally
ss.Free;
end;
end;
end.

View file

@ -17,6 +17,8 @@ type
const S_CEF_SubProcess = 'kmbrowserhost.exe';
const S_CEF_SubProcess_Developer = 'kmdbrowserhost.exe';
const S_CustomisationFilename = 'desktop_pro.pxx';
const S_KeymanAppData_UpdateCache = 'Keyman\UpdateCache\';
public
const S_KMShell = 'kmshell.exe';
const S_TSysInfoExe = 'tsysinfo.exe';
@ -27,8 +29,10 @@ type
const S_FallbackKeyboardPath = 'Keyboards\';
const S__Package = '_Package\';
const S_MCompileExe = 'mcompile.exe';
const S_UpdateCache_Metadata = 'cache.json';
class function ErrorLogPath(const app: string = ''): string; static;
class function KeymanHelpPath(const HelpFile: string): string; static;
class function KeymanUpdateCachePath(const filename: string = ''): string; static;
class function KeymanDesktopInstallPath(const filename: string = ''): string; static;
class function KeymanEngineInstallPath(const filename: string = ''): string; static;
class function KeymanDesktopInstallDir: string; static;
@ -423,6 +427,11 @@ begin
Result := '';
end;
class function TKeymanPaths.KeymanUpdateCachePath(const filename: string): string;
begin
Result := GetFolderPath(CSIDL_LOCAL_APPDATA) + S_KeymanAppData_UpdateCache + filename;
end;
class function TKeymanPaths.RunningFromSource(var keyman_root: string): Boolean;
begin
// On developer machines, if we are running within the source repo, then use

View file

@ -175,8 +175,10 @@ const
SRegValue_CharMapSourceData = 'charmap source data'; // LM
SRegValue_AvailableLanguages = 'available languages'; //CU
SRegValue_CurrentLanguage = 'current language'; //CU
SRegValue_AvailableLanguages = 'available languages'; // CU
SRegValue_CurrentLanguage = 'current language'; // CU
SRegValue_Update_State = 'update state'; // CU
{ Privacy }
@ -312,8 +314,10 @@ const
SRegValue_ActiveProject_Filename = 'project filename';
SRegValue_ActiveProject_SourcePath = 'source path';
SRegValue_AutomaticUpdates = 'automatic updates'; //CU
SRegValue_CheckForUpdates = 'check for updates'; // CU
SRegValue_LastUpdateCheckTime = 'last update check time'; // CU
SRegValue_ApplyNow = 'apply now'; // CU Start the install now even though it will require an restart
SRegValue_UpdateCheck_UseProxy = 'update check use proxy'; // CU
SRegValue_UpdateCheck_ProxyHost = 'update check proxy host'; // CU

View file

@ -230,7 +230,8 @@ type
ssPlatform = 31, ssBaseLayout = 32, ssLayer = 33, ssVKDictionary = 34, ssLayoutFile = 35, // I3438 // I3483
ssKeyboardVersion = 36, ssKMW_EmbedCSS = 37, ssTargets = 38,
ssCasedKeys = 39,
ssBegin_NewContext = 40, ssBegin_PostKeystroke = 41, ssNewLayer = 42, ssOldLayer = 43); // I4140 // I4368 // I4504
ssBegin_NewContext = 40, ssBegin_PostKeystroke = 41, ssNewLayer = 42, ssOldLayer = 43, // I4140 // I4368 // I4504
ssDisplayMap = 44);
const
SystemStoreNames: array[TSystemStore] of WideString = (
@ -245,7 +246,8 @@ const
'', '', '', '', 'LAYOUTFILE', 'KEYBOARDVERSION', 'KMW_EMBEDCSS',
'TARGETS', //9.0 // I3483 // I4140 // I4368 // I4504
'CASEDKEYS', //14.0,
'', '', 'NEWLAYER', 'OLDLAYER'); //15.0
'', '', 'NEWLAYER', 'OLDLAYER', //15.0
'DISPLAYMAP'); //17.0
implementation

View file

@ -17,7 +17,7 @@ value.
-------------------------------------------------------------------------------
# km_core_option_scope enum {#km_core_option_scope}
# km_core_option_scope enum
## Description
@ -48,7 +48,7 @@ enum km_core_option_scope {
-------------------------------------------------------------------------------
# km_core_option_item struct {#km_core_option_item}
# km_core_option_item struct
## Description
@ -78,7 +78,7 @@ struct km_core_option_item {
-------------------------------------------------------------------------------
# km_core_options_list_size() {#km_core_options_list_size}
# km_core_options_list_size()
## Description
Return the length of a terminated [km_core_option_item] array (options
@ -150,7 +150,7 @@ km_core_state_option_lookup(km_core_state const *state,
-------------------------------------------------------------------------------
# km_core_state_options_update() {#km_core_state_options_update}
# km_core_state_options_update()
## Description
@ -188,7 +188,7 @@ km_core_state_options_update(km_core_state *state,
-------------------------------------------------------------------------------
# km_core_state_options_to_json() {#km_core_state_options_to_json}
# km_core_state_options_to_json()
## Description

View file

@ -56,10 +56,8 @@ TEST_F(KmCoreKeyboardApiTests, LoadFromBlobNull) {
// Setup
km::core::path kmxfile = "";
std::unique_ptr<uint8_t> data(new uint8_t[0]);
// Execute
auto status = km_core_keyboard_load_from_blob(kmxfile.stem().c_str(), data.get(), 0, &this->keyboard);
auto status = km_core_keyboard_load_from_blob(kmxfile.stem().c_str(), nullptr, 0, &this->keyboard);
// Verify
EXPECT_EQ(status, KM_CORE_STATUS_INVALID_ARGUMENT);

View file

@ -16,11 +16,7 @@ binary_tests = [
]
foreach kbd : binary_tests
configure_file(
input: kbd + '.kmn',
output: kbd + '.kmn',
copy: true
)
fs.copyfile(kbd + '.kmn', kbd + '.kmn')
configure_file(
command: hextobin_cmd + ['@INPUT@', '@OUTPUT@'],

View file

@ -139,7 +139,10 @@ foreach kbd : tests
cfg.set('CONTENT', content)
kbd_kmn = configure_file(
copy: true,
# not really a file that needs to be configured, but the `copy` parameter
# is deprecated and `fs.copyfile` doesn't work with files that are not in
# the source directory
configuration: cfg,
input: kbd_src_path,
output: kbd + '.kmn'
)

View file

@ -11,11 +11,7 @@ invalid_tests = [
# Build all keyboards in output folder
foreach kbd : invalid_tests
configure_file(
input: kbd + '.xml',
output: kbd + '.xml',
copy: true
)
fs.copyfile(kbd + '.xml', kbd + '.xml')
configure_file(
command: hextobin_cmd + ['@INPUT@', '@OUTPUT@'],

View file

@ -65,11 +65,7 @@ kmc_cmd = [node, '--enable-source-maps', kmc_root]
# Build all keyboards in output folder
foreach kbd : tests_needing_copy
configure_file(
copy: true,
input: kbd + '.xml',
output: kbd + '.xml'
)
fs.copyfile(kbd + '.xml', kbd + '.xml')
configure_file(
command: kmc_cmd + ['build', '@INPUT@', '--out-file', '@OUTPUT@'],
@ -86,13 +82,8 @@ foreach kbd : tests_with_testdata
)
endforeach
foreach kbd : tests_from_cldr
configure_file(
copy: true,
input: ldml_data / kbd + '.xml',
output: kbd + '.xml'
)
fs.copyfile(ldml_data / kbd + '.xml', kbd + '.xml')
configure_file(
command: kmc_cmd + ['build', '@INPUT@', '--out-file', '@OUTPUT@'],
input: join_paths(ldml_data, kbd + '.xml'),

View file

@ -43,16 +43,8 @@ else
endif
# copy package.json into build dir for test use
configure_file(
copy: true,
input: '../../../../package.json',
output: 'package.json',
)
configure_file(
copy: true,
input: '../../../../resources/standards-data/unicode-character-database/Blocks.txt',
output: 'Blocks.txt',
)
fs.copyfile('../../../../package.json', 'package.json')
fs.copyfile('../../../../resources/standards-data/unicode-character-database/Blocks.txt', 'Blocks.txt')
configure_file(
command: [node, meson.current_source_dir() / 'write_node_versions.js','@OUTPUT@'],

View file

@ -2,3 +2,4 @@
c = ['$EMSCRIPTEN_BASE/emcc']
cpp = ['$EMSCRIPTEN_BASE/em++']
ar = ['$EMSCRIPTEN_BASE/emar']
strip = ['$EMSCRIPTEN_BASE/emstrip']

View file

@ -2,3 +2,4 @@
c = ['$EMSCRIPTEN_BASE/emcc']
cpp = ['$EMSCRIPTEN_BASE/em++']
ar = ['$EMSCRIPTEN_BASE/emar']
strip = ['$EMSCRIPTEN_BASE/emstrip']

View file

@ -2,5 +2,6 @@
c = ['python.exe', '$EMSCRIPTEN_BASE/emcc.py']
cpp = ['python.exe', '$EMSCRIPTEN_BASE/em++.py']
ar = ['python.exe', '$EMSCRIPTEN_BASE/emar.py']
strip = ['python.exe', '$EMSCRIPTEN_BASE/emstrip.py']
[properties]

View file

@ -2,6 +2,7 @@
c = ['emcc']
cpp = ['em++']
ar = ['emar']
strip = ['emstrip']
exe_wrapper = 'node'
[properties]

View file

@ -382,22 +382,24 @@ ID
required, and a default code will usually be generated automatically
by Keyman Developer.
- `K_xxxx` is used for a standard Keyman for Windows's key name, e.g.
`K_W`, `K_ENTER`. You cannot make up your own `K_xxxx` names.
Many of the `K_` ids have overloaded output behaviour, for
instance, if no rule is matched for `K_W`, Keyman will output
'w' when it is touched. The standard key names are listed in
[Virtual Keys and Virtual Character Keys](/developer/language/guide/virtual-keys). Typically,
you would use only the "common" virtual ID.
- `T_xxxx` is used for any user defined names, e.g. `T_SCHWA`.
If you wanted to use it, `T_ENTER` would also be valid. If no
rule matches it, the key will have no output behaviour.
- `U_####[_####]` is used as a shortcut for a key that
will output those Unicode values, if no rule matches it. This is
similar to the overloaded behaviour for `K_` ids. Thus `####`
must be valid Unicode characters. E.g. `U_0259` would generate a
schwa if no rule matches. It is still valid to have a rule such
as `+ [U_0259] > ...`
- `K_xxxx` is used for a standard Keyman for Windows's key name, e.g.
`K_W`, `K_ENTER`. You cannot make up your own `K_xxxx` names.
Many of the `K_` ids have overloaded output behaviour, for
instance, if no rule is matched for `K_W`, Keyman will output
'w' when it is touched. The standard key names are listed in
[Virtual Keys and Virtual Character Keys](/developer/language/guide/virtual-keys). Typically,
you would use only the "common" virtual ID.
- `T_xxxx` is used for any user defined names, e.g. `T_SCHWA`.
If you wanted to use it, `T_ENTER` would also be valid. If no
rule matches it, the key will have no output behaviour.
- `U_####[_####]` is used as a shortcut for a key that
will output those Unicode values, if no rule matches it. This is
similar to the overloaded behaviour for `K_` ids. Thus `####`
must be valid Unicode characters.
The square bracket characters `[` and `]` indicate an optional portion of the sequence
and are not to be included in the shortcut. E.g. `U_0259` would generate a
schwa if no rule matches. It is still valid to have a rule such
as `+ [U_0259] > ...`
Padding Left
: Padding to the left of each key can be adjusted, and specified as a

View file

@ -131,7 +131,9 @@ Character Keys"). Typically, you would use only the "common" virtual key
- `U_####[_####]` is used as a shortcut for a key that will output those
Unicode values, if no rule matches it. This is similar to the overloaded
behaviour for `K_` ids. Thus `####` must be valid Unicode characters. E.g.
behaviour for `K_` ids. Thus `####` must be valid Unicode characters.
The square bracket characters `[` and `]` indicate an optional portion of the sequence
and are not to be included in the shortcut. E.g.
`U_0259` would generate a schwa if no rule matches. It is still valid to
have a rule such as `+ [U_0259] > ...`

View file

@ -58,7 +58,7 @@ welcome.htm
> ### Tip
If you want links to your website to open in the user's preferred
browser, preface the href link with `link:`, e.g.
`<a href="http://keyman.com/">website</a>`
`<a href="https://keyman.com/">website</a>`
The `link:` sceheme will open the referred file in the default
application - that is, a web browser for URLs and links, Notepad for
.txt files, Adobe Reader for PDFs. You can use `link:` to open any

View file

@ -54,7 +54,7 @@ Website
: Enter the name of the website where you will have information about
this keyboard. If you want to host it on keyman.com, you could enter
`http://www.keyman.com/`
`https://keyman.com/`
Image file
: Select the splash image file that you created in Step 2 from the

View file

@ -12,8 +12,9 @@ Linux.
There are two core applications included in Keyman Developer: the Keyman
Developer IDE (formerly called TIKE), and the command line compiler kmc.
[Watch a video](https://youtu.be/kwhgx_eX4Es) that highlights some of
the Keyman Developer features.
Watch a video that highlights some of the Keyman Developer 14.0 features (57:23)
:<img class="video-thumbnail" data-video="https://www.youtube.com/embed/kwhgx_eX4Es" src="https://img.youtube.com/vi/kwhgx_eX4Es/maxresdefault.jpg" width="600px" alt="Keyman Developer 14.0 Webinar">
## Keyman Developer IDE

View file

@ -38,7 +38,7 @@ recommendations:
ZIP archive file!) and you won't save much space by recompressing
it.
3. Include a link to the Keyman download page:
`http://keyman.com/downloads/`
`https://keyman.com/downloads/`
## Distributing a package by email

View file

@ -97,7 +97,9 @@ Character Keys"). Typically, you would use only the "common" virtual key
- `U_####[_####]` is used as a shortcut for a key that will output those
Unicode values, if no rule matches it. This is similar to the overloaded
behaviour for `K_` ids. Thus `####` must be valid Unicode characters. E.g.
behaviour for `K_` ids. Thus `####` must be valid Unicode characters.
The square bracket characters `[` and `]` indicate an optional portion of the sequence
and are not to be included in the shortcut. E.g.
`U_0259` would generate a schwa if no rule matches. It is still valid to
have a rule such as `+ [U_0259] > ...`

View file

@ -21,7 +21,8 @@ unit KeyboardFonts;
interface
type
TKeyboardFont = (kfontCode, kfontChar, kfontOSK, kfontTouchLayoutPhone, kfontTouchLayoutTablet, kfontTouchLayoutDesktop);
TKeyboardFont = (kfontCode, kfontChar, kfontOSK, kfontTouchLayoutPhone, kfontTouchLayoutTablet, kfontTouchLayoutDesktop,
kfontDisplayMap);
TKeyboardFontArray = array[TKeyboardFont] of string; // I4409
@ -34,7 +35,7 @@ type
const
KeyboardFontId: array[TKeyboardFont] of string = (
'Code', 'Char', 'Osk', 'TouchLayoutPhone', 'TouchLayoutTablet', 'TouchLayoutDesktop'
'Code', 'Char', 'Osk', 'TouchLayoutPhone', 'TouchLayoutTablet', 'TouchLayoutDesktop', 'DisplayMap'
);
implementation

View file

@ -24,5 +24,32 @@ export async function getFontFamily(source: Uint8Array) {
return null;
}
return font.meta.property.find(prop => prop.name == 'font-family')?.text ?? null;
}
/**
* Extracts the font-family from an in-memory TTF or WOFF blob in `source`
* parameter.
*
* @param source In-memory TTF or WOFF font blob
*
* @throws Uncaught exceptions from ttfMeta.promise if the font file is invalid.
*
* @returns If the file is invalid or cannot be parsed, returns `null`,
* otherwise returns the font family as a string.
*/
export function getFontFamilySync(source: Uint8Array) {
/* c8 ignore next 3 */
if(!source) {
return null;
}
const buffer = Buffer.from(source);
const font = ttfMeta.ttfInfoSync(buffer);
/* c8 ignore next 3 */
if(!font) {
return null;
}
return font.meta.property.find(prop => prop.name == 'font-family')?.text ?? null;
}

View file

@ -68,4 +68,8 @@ export * as SourceFilenamePatterns from './source-filename-patterns.js';
export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js';
export * as GitHubUrls from './github-urls.js';
export * as CloudUrls from './cloud-urls.js';
export * as CloudUrls from './cloud-urls.js';
export { getFontFamily, getFontFamilySync } from './font-family.js';
export * as ValidIds from './valid-ids.js';

View file

@ -31,6 +31,20 @@ export function ttfInfo(pathOrData, callback) {
}
}
/**
* @namespace
* @param {string | number | Buffer | URL} pathOrData
* @returns {typeof ttf.result}
*/
export function ttfInfoSync(pathOrData) {
if (pathOrData instanceof Buffer) {
return ttf.ttfInfoSync(ttf.view(pathOrData));
} else {
const data = fs.readFileSync(pathOrData);
return ttf.ttfInfoSync(ttf.view(data));
}
}
/**
* @param {string | number | Buffer | URL} pathOrData
* @returns {Promise<typeof ttf.result>}
@ -48,5 +62,5 @@ export function promise(pathOrData) {
}
/** @namespace */
export const ttfMeta = { ttfInfo, promise };
export const ttfMeta = { ttfInfo, ttfInfoSync, promise };
export default ttfMeta;

View file

@ -300,6 +300,16 @@ export function ttfInfo(data, callback) {
}
}
/**
* @namespace
* @param {*} data
* @returns {typeof result}
*/
export function ttfInfoSync(data) {
resultTables(data);
return result;
}
/**
* @param {string | number | Buffer | URL | DataView} pathOrData
* @returns {Promise<typeof result>}

View file

@ -66,6 +66,12 @@ export class KeymanSentry {
static async captureException(e: any): Promise<never> {
if(isInit) {
// For local development, we don't want to bury the trace; we need the cast to avoid
// TS2367 (comparison appears to be unintentional)
if((KEYMAN_VERSION.VERSION_ENVIRONMENT as string) == 'local') {
throw e;
}
const eventId = Sentry.captureException(e);
process.stderr.write(`
Fatal error: ${(e??'').toString()}
@ -73,11 +79,6 @@ export class KeymanSentry {
this.writeSentryMessage(eventId);
await this.close();
// For local development, we don't want to bury the trace; we need the cast to avoid
// TS2367 (comparison appears to be unintentional)
if((KEYMAN_VERSION.VERSION_ENVIRONMENT as string) == 'local') {
throw e;
}
process.exit(1);
} else {
throw e;
@ -86,7 +87,11 @@ export class KeymanSentry {
static async close() {
if(isInit) {
await Sentry.close(CLOSE_TIMEOUT);
try {
await Sentry.close(CLOSE_TIMEOUT);
} catch(e) {
// ignore any errors here
}
}
}
}

View file

@ -0,0 +1,38 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
/**
* Verifies that an id passed in is valid as an identifier for a Keyman .kmn
* keyboard: starts with alpha or underscore and remainder is alphanumeric
* @param id
* @returns
*/
export function isValidKeymanKeyboardId(id: string) {
return /^[a-z_][a-z0-9_]*$/.test(id);
}
/**
* Verifies that an id passed in is valid as an identifier for an LDML xml
* keyboard: starts with alpha or underscore and remainder is alphanumeric.
* Note that this test is not the same as the CLDR test for valid LDML xml
* filename, as this test has a narrower validity policy to match the Keyman
* implementation requirements.
* @param id
* @returns
*/
export function isValidLdmlKeyboardId(id: string) {
return /^[a-z_][a-z0-9_]*$/.test(id);
}
/**
* Verifies that an id passed in is valid as an identifier for a Keyman
* lexical model: starts with alpha or underscore and remainder is alphanumeric,
* in three components -- author.bcp47.uniq
* @param id
* @returns
*/
export function isValidLexicalModelId(id: string) {
return /^[a-z_][a-z0-9_]*\.[a-z][a-z0-9_]+\.[a-z_][a-z0-9_]*$/.test(id);
}

View file

@ -0,0 +1,76 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { assert } from 'chai';
import 'mocha';
import { isValidKeymanKeyboardId, isValidLdmlKeyboardId, isValidLexicalModelId } from '../src/valid-ids.js';
describe('valid-ids', function () {
it('should accept a valid Keyman keyboard ID', function() {
[
'khmer_angkor',
'm',
'fancypants1_2',
'keyboard___',
'_100',
].forEach(id => assert.isTrue(isValidKeymanKeyboardId(id), `expected '${id}' to be valid`));
});
it('should reject invalid Keyman keyboard IDs', function() {
[
'khmer_angkor@example.com',
'keyboard ',
'Khmer',
' keyboard',
'111',
'?',
'keyboard.version',
].forEach(id => assert.isFalse(isValidKeymanKeyboardId(id), `expected '${id}' to be invalid`));
});
it('should accept a valid LDML keyboard ID', function() {
[
'khmer_angkor',
'm',
'fancypants1_2',
'keyboard___',
'_100',
].forEach(id => assert.isTrue(isValidLdmlKeyboardId(id), `expected '${id}' to be valid`));
});
it('should reject invalid LDML keyboard IDs', function() {
[
'khmer_angkor@example.com',
'keyboard ',
' keyboard',
'Khmer',
'111',
'?',
'keyboard.version',
].forEach(id => assert.isFalse(isValidLdmlKeyboardId(id), `expected '${id}' to be invalid`));
});
it('should accept a valid lexical model ID', function() {
[
'khmer.km.khmer_angkor',
'nrc.en.mtnt',
].forEach(id => assert.isTrue(isValidLexicalModelId(id), `expected '${id}' to be valid`));
});
it('should reject invalid lexical model IDs', function() {
[
'khmer',
'en-us',
'khmer.km-KM.angkor',
'Khmer.km_km.angkor',
'model ',
' model',
'111',
'?',
'lexical.model',
'lexical..model',
'.en.model',
].forEach(id => assert.isFalse(isValidLexicalModelId(id), `expected '${id}' to be invalid`));
});
});

View file

@ -5,8 +5,12 @@
"rootDir": ".",
"outDir": "./build/",
"baseUrl": ".",
/** for ttfmeta */
"allowJs": true,
},
"include": [
"src/**/*.ts",
"src/ttfmeta/lib/*.js",
],
}

View file

@ -54,7 +54,8 @@
"exclude-after-remap": true,
"exclude": [
"test/",
"src/template/"
"src/template/",
"src/main.ts"
]
},
"repository": {

View file

@ -7,6 +7,7 @@
import { KeymanTargets } from "@keymanapp/common-types";
import KEYMAN_VERSION from "@keymanapp/keyman-version";
import { AbstractGenerator, GeneratorArtifacts } from "./abstract-generator.js";
import { GeneratorMessages } from "./generator-messages.js";
/**
* @internal
@ -28,7 +29,19 @@ export class BasicGenerator extends AbstractGenerator {
this.languageTags = this.options.languageTags.length
? this.options.languageTags.map(tag => new Intl.Locale(tag).minimize().toString())
: [this.DEFAULT_LOCALE];
//TODO-GENERATE: validate targets
// Verify that targets passed in are valid
let failed = false;
for(const target of this.options.targets) {
if(!KeymanTargets.AllKeymanTargets.includes(target)) {
this.callbacks.reportMessage(GeneratorMessages.Error_InvalidTarget({target}));
failed = true;
}
}
if(failed) {
return false;
}
if(this.options.targets.includes(KeymanTargets.KeymanTarget.any) || this.options.targets.length == 0) {
this.resolvedTargets = KeymanTargets.AllKeymanTargets.filter(target => target != KeymanTargets.KeymanTarget.any);
} else {
@ -50,6 +63,8 @@ export class BasicGenerator extends AbstractGenerator {
dt.getDate().toString().padStart(2, '0');
this.tokenMap['$PACKAGE_LANGUAGES'] = this.generateLanguageListForPackage();
this.tokenMap['$PLATFORMS_DOTLIST_README'] = this.getPlatformDotListForReadme();
return true;
}
protected generate(artifacts: GeneratorArtifacts): boolean {
@ -89,6 +104,8 @@ export class BasicGenerator extends AbstractGenerator {
sourceFile = this.callbacks.path.join(this.templateBasePath, this.templatePath, sourceFile);
const sourceData = this.callbacks.loadFile(sourceFile);
/* c8 ignore next 4 */
if(sourceData == null) {
// internal error -- source file should be readable
throw new Error(`source file ${sourceFile} does not exist`);

View file

@ -17,30 +17,65 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
* @internal
*/
export class GeneratorMessages {
static Fatal_UnexpectedException = (o:{e: any}) => m(this.FATAL_UnexpectedException, null, o.e ?? 'unknown error');
static FATAL_UnexpectedException = SevFatal | 0x0001;
static Fatal_UnexpectedException = (o:{e: any}) => m(
this.FATAL_UnexpectedException, null, o.e ?? 'unknown error'
);
static Info_GeneratingProject = (o:{type: string, id: string}) => m(this.INFO_GeneratingProject,
`Generating project of type ${def(o.type)} with id ${def(o.id)}`);
static INFO_GeneratingProject = SevInfo | 0x0002;
static Info_GeneratingProject = (o:{type: string, id: string}) => m(
this.INFO_GeneratingProject,
`Generating project of type ${def(o.type)} with id ${def(o.id)}`
);
static ERROR_CannotCreateFolder = SevError | 0x0003;
static Error_CannotCreateFolder = (o:{folderName:string, e: any}) => CompilerMessageSpecWithException(this.ERROR_CannotCreateFolder, null,
`Unable to create folder ${def(o.folderName)}: ${o.e ?? 'unknown error'}`);
static Error_CannotCreateFolder = (o:{folderName:string, e: any}) => CompilerMessageSpecWithException(
this.ERROR_CannotCreateFolder, null,
`Unable to create folder ${def(o.folderName)}: ${o.e ?? 'unknown error'}`
);
static ERROR_OutputPathAlreadyExists = SevError | 0x0004;
static Error_OutputPathAlreadyExists = (o:{outPath:string}) => m(
this.ERROR_OutputPathAlreadyExists,
`Output path ${def(o.outPath)} already exists, not overwriting`);
`Output path ${def(o.outPath)} already exists, not overwriting`
);
static ERROR_CannotWriteOutputFile = SevError | 0x0005;
static Error_CannotWriteOutputFile = (o:{filename:string, e: any}) => CompilerMessageSpecWithException(this.ERROR_CannotWriteOutputFile, null,
`Unable to write file ${def(o.filename)}: ${o.e ?? 'unknown error'}`);
static Error_CannotWriteOutputFile = (o:{filename:string, e: any}) => CompilerMessageSpecWithException(
this.ERROR_CannotWriteOutputFile, null,
`Unable to write file ${def(o.filename)}: ${o.e ?? 'unknown error'}`
);
// See also PackageCompilerMessages.WARN_PackageNameDoesNotFollowLexicalModelConventions
static WARN_ModelIdDoesNotFollowLexicalModelConventions = SevWarn | 0x0006;
static Warn_ModelIdDoesNotFollowLexicalModelConventions = (o:{id: string}) => m(this.WARN_ModelIdDoesNotFollowLexicalModelConventions,
static Warn_ModelIdDoesNotFollowLexicalModelConventions = (o:{id: string}) => m(
this.WARN_ModelIdDoesNotFollowLexicalModelConventions,
`The id ${def(o.id)} does not follow the recommended model id conventions. The id should be all lower case, `+
`include only alphanumeric characters and underscore (_), not start with a digit, and should have the structure `+
`<author>.<bcp47>.<uniq>`);
`<author>.<bcp47>.<uniq>`
);
static ERROR_InvalidTarget = SevError | 0x0007;
static Error_InvalidTarget = (o:{target:string}) => m(
this.ERROR_InvalidTarget,
`The specified target platform '${def(o.target)}' is not a valid target.`
);
static ERROR_InvalidKeymanKeyboardId = SevError | 0x0008;
static Error_InvalidKeymanKeyboardId = (o:{id:string}) => m(
this.ERROR_InvalidKeymanKeyboardId,
`The specified keyboard id '${def(o.id)}' contains characters that are not permitted for a .kmn keyboard id or filename.`
);
static ERROR_InvalidLdmlKeyboardId = SevError | 0x0009;
static Error_InvalidLdmlKeyboardId = (o:{id:string}) => m(
this.ERROR_InvalidLdmlKeyboardId,
`The specified keyboard id '${def(o.id)}' contains characters that are not permitted for an LDML .xml keyboard id or filename.`
);
static ERROR_InvalidLexicalModelId = SevError | 0x000A;
static Error_InvalidLexicalModelId = (o:{id:string}) => m(
this.ERROR_InvalidLexicalModelId,
`The specified lexical model id '${def(o.id)}' contains characters that are not permitted or does not match the required pattern of 'author.bcp47.uniq'.`
);
};

View file

@ -5,9 +5,10 @@
*/
import { KeymanFileTypes, KeymanTargets } from '@keymanapp/common-types';
import { KeymanCompiler, } from '@keymanapp/developer-utils';
import { KeymanCompiler, ValidIds, } from '@keymanapp/developer-utils';
import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js';
import { BasicGenerator } from './basic-generator.js';
import { GeneratorMessages } from './generator-messages.js';
/**
* @public
@ -39,7 +40,15 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom
* @returns Binary artifacts on success, null on failure.
*/
async run(): Promise<GeneratorResult> {
this.preGenerate();
if(!ValidIds.isValidKeymanKeyboardId(this.options.id)) {
this.callbacks.reportMessage(GeneratorMessages.Error_InvalidKeymanKeyboardId({id:this.options.id}));
return null;
}
if(!this.preGenerate()) {
// errors will have been reported in preGenerate
return null;
}
const artifacts: GeneratorArtifacts = this.defaultArtifacts();

View file

@ -5,9 +5,10 @@
*/
import { KeymanFileTypes } from '@keymanapp/common-types';
import { KeymanCompiler, } from '@keymanapp/developer-utils';
import { KeymanCompiler, ValidIds, } from '@keymanapp/developer-utils';
import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js';
import { BasicGenerator } from './basic-generator.js';
import { GeneratorMessages } from './generator-messages.js';
/**
* @public
@ -28,7 +29,15 @@ export class LdmlKeyboardGenerator extends BasicGenerator implements KeymanCompi
* @returns Binary artifacts on success, null on failure.
*/
async run(): Promise<GeneratorResult> {
this.preGenerate();
if(!ValidIds.isValidLdmlKeyboardId(this.options.id)) {
this.callbacks.reportMessage(GeneratorMessages.Error_InvalidLdmlKeyboardId({id:this.options.id}));
return null;
}
if(!this.preGenerate()) {
// errors will have been reported in preGenerate
return null;
}
const artifacts: GeneratorArtifacts = this.defaultArtifacts();
this.templatePath = 'ldml-keyboard';

View file

@ -5,7 +5,7 @@
*/
import { KeymanFileTypes } from '@keymanapp/common-types';
import { KeymanCompiler, SourceFilenamePatterns } from '@keymanapp/developer-utils';
import { KeymanCompiler, SourceFilenamePatterns, ValidIds } from '@keymanapp/developer-utils';
import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js';
import { BasicGenerator } from './basic-generator.js';
import { GeneratorMessages } from './generator-messages.js';
@ -31,7 +31,15 @@ export class LexicalModelGenerator extends BasicGenerator implements KeymanCompi
* @returns Binary artifacts on success, null on failure.
*/
async run(): Promise<GeneratorResult> {
this.preGenerate();
if(!ValidIds.isValidLexicalModelId(this.options.id)) {
this.callbacks.reportMessage(GeneratorMessages.Error_InvalidLexicalModelId({id:this.options.id}));
return null;
}
if(!this.preGenerate()) {
// errors will have been reported in preGenerate
return null;
}
const artifacts: GeneratorArtifacts = this.defaultArtifacts();

View file

@ -2,22 +2,71 @@
* Keyman is copyright (C) SIL International. MIT License.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import 'mocha';
import { assert } from 'chai';
import { BasicGenerator } from '../src/basic-generator.js';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import { BasicGenerator } from '../src/basic-generator.js';
import { AbstractGenerator, GeneratorArtifacts } from '../src/abstract-generator.js';
import { options } from './shared-options.js';
import { makePathToFixture } from './helpers/index.js';
// this class is here to make it possible to access protected members
class BasicGeneratorTest extends BasicGenerator {
static files() {
return [
'sample/' + AbstractGenerator.SFile_WelcomeHTM,
'sample/' + AbstractGenerator.SFile_ReadmeHTM,
'sample/' + AbstractGenerator.SFile_HistoryMD,
'sample/' + AbstractGenerator.SFile_LicenseMD,
'sample/' + AbstractGenerator.SFile_ReadmeMD,
'sample/' + AbstractGenerator.SFile_GitIgnore,
];
};
setup() {
this.templatePath = 'kmn-keyboard';
}
}
describe('BasicGenerator', function () {
it('should configure default settings', async function() {
const bg = new BasicGenerator();
const callbacks = new TestCompilerCallbacks();
assert(await bg.init(callbacks, options));
bg.test_preGenerate();
assert.isTrue(await bg.init(callbacks, options));
assert.isTrue(bg.test_preGenerate());
assert.equal(bg.test_tokenMap['$NAME'], 'Sample Project');
});
// TODO-GENERATE: test transform
it('should successfully transform common project files', async function() {
const bg = new BasicGeneratorTest();
bg.setup();
const callbacks = new TestCompilerCallbacks();
assert.isTrue(await bg.init(callbacks, options));
assert.isTrue(bg.test_preGenerate());
const artifacts: GeneratorArtifacts = { "kmc-generate:outputPath": { data: null, filename: '' } };
assert.isTrue(bg.test_generate(artifacts));
// standard artifacts are listed in AbstractGenerator
const keys = [].concat(
'kmc-generate:outputPath',
BasicGeneratorTest.files().map(f => f.replaceAll('/', path.sep))
);
assert.hasAllKeys(artifacts, keys);
// pick one file and compare it
const filename = BasicGeneratorTest.files()[0].replaceAll('/', path.sep); // welcome.htm
assert.equal(artifacts[filename].filename, filename);
const fixturePath = makePathToFixture('basic', 'welcome.htm');
const fixture = fs.readFileSync(fixturePath, 'utf-8').replace(/\r\n/g, '\n');
const actual = new TextDecoder().decode(artifacts[filename].data);
assert.equal(actual, fixture);
});
});

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Start Using Sample Project</title>
<style type="text/css">
p { font: 10pt Tahoma; }
h1 { font: bold 16pt Tahoma; color: #4444cc; margin-bottom: 2px }
h2 { font: bold 12pt Tahoma; color: #4444cc; }
</style>
</head>
<body>
<h1>Start Using Sample Project</h1>
<p>
# A mighty description
</p>
<h1>Keyboard Layout</h1>
<!-- Insert Keyboard Layout Images or HTML here -->
</body>
</html>

View file

@ -2,15 +2,24 @@
* Keyman is copyright (C) SIL International. MIT License.
*/
import * as path from 'node:path';
import * as fs from 'node:fs';
import 'mocha';
import * as path from 'path';
import * as fs from 'fs';
import { fileURLToPath } from 'url';
import { assert } from 'chai';
import { GeneratorMessages } from '../src/generator-messages.js';
import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
import { CompilerErrorNamespace } from '@keymanapp/developer-utils';
import { GeneratorMessages } from '../src/generator-messages.js';
import { AbstractGenerator, GeneratorOptions } from '../src/abstract-generator.js';
import { BasicGenerator } from '../src/basic-generator.js';
import { KeymanKeyboardGenerator } from '../src/keyman-keyboard-generator.js';
import { LdmlKeyboardGenerator } from '../src/ldml-keyboard-generator.js';
import { LexicalModelGenerator } from '../src/lexical-model-generator.js';
import { options } from './shared-options.js';
describe('GeneratorMessages', function () {
const callbacks = new TestCompilerCallbacks();
@ -62,4 +71,49 @@ describe('GeneratorMessages', function () {
assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_CannotWriteOutputFile),
`messageId ERROR_CannotWriteOutputFile not generated, instead got: `+JSON.stringify(callbacks.messages,null,2));
});
it('should generate ERROR_InvalidTarget if an invalid target is passed in', async function () {
const testOptions: GeneratorOptions = { ...options,
// @ts-ignore ('invalid' could be passed by a user, even if would be a compile error here)
targets: ['invalid']
};
const bg = new BasicGenerator();
const callbacks = new TestCompilerCallbacks();
assert(await bg.init(callbacks, testOptions));
assert.isFalse(bg.test_preGenerate());
assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidTarget));
});
it('should generate ERROR_InvalidKeymanKeyboardId if an invalid keyboard id is passed in', async function () {
const testOptions: GeneratorOptions = { ...options,
id: '???invalid',
};
const kkg = new KeymanKeyboardGenerator();
const callbacks = new TestCompilerCallbacks();
assert.isTrue(await kkg.init(callbacks, testOptions));
assert.isNull(await kkg.run());
assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidKeymanKeyboardId));
});
it('should generate ERROR_InvalidLdmlKeyboardId if an invalid keyboard id is passed in', async function () {
const testOptions: GeneratorOptions = { ...options,
id: '???invalid',
};
const lkg = new LdmlKeyboardGenerator();
const callbacks = new TestCompilerCallbacks();
assert.isTrue(await lkg.init(callbacks, testOptions));
assert.isNull(await lkg.run());
assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidLdmlKeyboardId));
});
it('should generate ERROR_InvalidLexicalModelId if an invalid lexical model id is passed in', async function () {
const testOptions: GeneratorOptions = { ...options,
id: 'example.???.invalid',
};
const lmg = new LexicalModelGenerator();
const callbacks = new TestCompilerCallbacks();
assert.isTrue(await lmg.init(callbacks, testOptions));
assert.isNull(await lmg.run());
assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidLexicalModelId));
});
});

View file

@ -12,7 +12,7 @@ import { CompilerCallbacks, KeymanCompiler, CompilerOptions, KeymanCompilerResul
import { KmpCompiler } from "@keymanapp/kmc-package";
import { SchemaValidators } from "@keymanapp/common-types";
import { getFontFamily } from "./font-family.js";
import { getFontFamily } from "@keymanapp/developer-utils";
const regionNames = new Intl.DisplayNames(['en'], { type: "region" });
const scriptNames = new Intl.DisplayNames(['en'], { type: "script" });
@ -499,9 +499,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 ?? []));
let commonScript = null;
for(const bcp47 of Object.keys(keyboard_info.languages)) {
@ -528,6 +525,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

@ -2,7 +2,7 @@ import * as fs from 'fs';
import { assert } from 'chai';
import 'mocha';
import { makePathToFixture } from './helpers/index.js';
import { getFontFamily } from '../src/font-family.js';
import { getFontFamily } from '@keymanapp/developer-utils';
const AFGHAN_TURKMEN_DISPLAY_FONT = makePathToFixture('afghan_turkmen', "Lateef-Regular.ttf");
const AFGHAN_TURKMEN_OSK_FONT = makePathToFixture('afghan_turkmen', "Lateef-Bold.ttf");

View file

@ -13,7 +13,6 @@
},
"include": [
"src/**/*.ts",
"src/ttfmeta/lib/*.js",
"src/imports/langtags.js",
],
"references": [

View file

@ -46,6 +46,16 @@ function GetKeyIdUnicodeType(value: string): TKeyIdType {
function KeyIdType(FId: string): TKeyIdType { // I4142
FId = FId.toUpperCase();
// Validate key id format:
// K_xxxx -- predefined virtual key - restricted character set
// T_xxxx -- custom 'touch' virtual key - touch key id
// U_ABCD_1234 -- Unicode key id (1+ chars)
// x00 -- "ISO" key identifier (not currently supported in touch layout files)
if(!/^((K_[A-Z0-9_?]+)|(T_\S+)|(U_[0-9A-F_]+))$/.test(FId)) {
// note: |[A-Z][0-9][0-9] -- ISO key identifiers not currently supported
return TKeyIdType.Key_Invalid;
}
switch(FId.charAt(0)) {
case 'T':
return TKeyIdType.Key_Touch;

View file

@ -0,0 +1,550 @@
{
"tablet": {
"displayUnderlying": false,
"layer": [
{
"id": "default",
"row": [
{
"id": 1,
"key": [
{
"id": "U_1E6B[_0307]",
"text": "១"
},
{
"id": "K_2",
"text": "២"
},
{
"id": "K_3",
"text": "៣"
},
{
"id": "K_4",
"text": "៤"
},
{
"id": "K_5",
"text": "៥"
},
{
"id": "K_6",
"text": "៦"
},
{
"id": "K_7",
"text": "៧"
},
{
"id": "K_8",
"text": "៨"
},
{
"id": "K_9",
"text": "៩"
},
{
"id": "K_0",
"text": "០"
},
{
"id": "K_HYPHEN",
"text": "ឥ"
},
{
"id": "K_EQUAL",
"text": "ឲ"
},
{
"id": "K_BKSP",
"text": "*BkSp*",
"width": "100",
"sp": "1"
}
]
},
{
"id": 2,
"key": [
{
"id": "K_Q",
"text": "ឆ",
"pad": "75"
},
{
"id": "K_W",
"text": ""
},
{
"id": "K_E",
"text": ""
},
{
"id": "K_R",
"text": "រ"
},
{
"id": "K_T",
"text": "ត"
},
{
"id": "K_Y",
"text": "យ"
},
{
"id": "K_U",
"text": ""
},
{
"id": "K_I",
"text": ""
},
{
"id": "K_O",
"text": ""
},
{
"id": "K_P",
"text": "ផ"
},
{
"id": "K_LBRKT",
"text": ""
},
{
"id": "K_RBRKT",
"text": "ឪ"
},
{
"id": "T_new_138",
"text": "",
"width": "10",
"sp": "10"
}
]
},
{
"id": 3,
"key": [
{
"id": "K_BKQUOTE",
"text": "«"
},
{
"id": "K_A",
"text": ""
},
{
"id": "K_S",
"text": "ស"
},
{
"id": "K_D",
"text": "ដ"
},
{
"id": "K_F",
"text": "ថ"
},
{
"id": "K_G",
"text": "ង"
},
{
"id": "K_H",
"text": "ហ"
},
{
"id": "K_J",
"text": ""
},
{
"id": "K_K",
"text": "ក"
},
{
"id": "K_L",
"text": "ល"
},
{
"id": "K_COLON",
"text": ""
},
{
"id": "K_QUOTE",
"text": ""
},
{
"id": "K_BKSLASH",
"text": "ឮ"
}
]
},
{
"id": 4,
"key": [
{
"id": "K_SHIFT",
"text": "*Shift*",
"width": "160",
"sp": "1",
"nextlayer": "shift"
},
{
"id": "K_oE2",
"text": ""
},
{
"id": "K_Z",
"text": "ឋ"
},
{
"id": "K_X",
"text": "ខ"
},
{
"id": "K_C",
"text": "ច"
},
{
"id": "K_V",
"text": "វ"
},
{
"id": "K_B",
"text": "ប"
},
{
"id": "K_N",
"text": "ន"
},
{
"id": "K_M",
"text": "ម"
},
{
"id": "K_COMMA",
"text": ""
},
{
"id": "K_PERIOD",
"text": "។"
},
{
"id": "K_SLASH",
"text": ""
},
{
"id": "T_new_164",
"text": "",
"width": "10",
"sp": "10"
}
]
},
{
"id": 5,
"key": [
{
"id": "K_LCONTROL",
"text": "*AltGr*",
"width": "160",
"sp": "1"
},
{
"id": "K_LOPT",
"text": "*Menu*",
"width": "160",
"sp": "1"
},
{
"id": "K_SPACE",
"text": "​",
"width": "930"
},
{
"id": "K_ENTER",
"text": "*Enter*",
"width": "160",
"sp": "1"
}
]
}
]
},
{
"id": "shift",
"row": [
{
"id": 1,
"key": [
{
"id": "K_1",
"text": "!"
},
{
"id": "K_2",
"text": "ៗ"
},
{
"id": "K_3",
"text": "\""
},
{
"id": "K_4",
"text": "៛"
},
{
"id": "K_5",
"text": "%"
},
{
"id": "K_6",
"text": ""
},
{
"id": "K_7",
"text": ""
},
{
"id": "K_8",
"text": ""
},
{
"id": "K_9",
"text": "("
},
{
"id": "K_0",
"text": ")"
},
{
"id": "K_HYPHEN",
"text": ""
},
{
"id": "K_EQUAL",
"text": "="
},
{
"id": "K_BKSP",
"text": "*BkSp*",
"width": "100",
"sp": "1"
}
]
},
{
"id": 2,
"key": [
{
"id": "K_Q",
"text": "ឈ",
"pad": "75"
},
{
"id": "K_W",
"text": ""
},
{
"id": "K_E",
"text": ""
},
{
"id": "K_R",
"text": "ឬ"
},
{
"id": "K_T",
"text": "ទ"
},
{
"id": "K_Y",
"text": ""
},
{
"id": "K_U",
"text": ""
},
{
"id": "K_I",
"text": ""
},
{
"id": "K_O",
"text": ""
},
{
"id": "K_P",
"text": "ភ"
},
{
"id": "K_LBRKT",
"text": ""
},
{
"id": "K_RBRKT",
"text": "ឧ"
},
{
"id": "T_new_364",
"text": "",
"width": "10",
"sp": "10"
}
]
},
{
"id": 3,
"key": [
{
"id": "K_BKQUOTE",
"text": "»"
},
{
"id": "K_A",
"text": ""
},
{
"id": "K_S",
"text": ""
},
{
"id": "K_D",
"text": "ឌ"
},
{
"id": "K_F",
"text": "ធ"
},
{
"id": "K_G",
"text": "អ"
},
{
"id": "K_H",
"text": "ះ"
},
{
"id": "K_J",
"text": "ញ"
},
{
"id": "K_K",
"text": "គ"
},
{
"id": "K_L",
"text": "ឡ"
},
{
"id": "K_COLON",
"text": ""
},
{
"id": "K_QUOTE",
"text": ""
},
{
"id": "K_BKSLASH",
"text": "ឭ"
}
]
},
{
"id": 4,
"key": [
{
"id": "K_SHIFT",
"text": "*Shift*",
"width": "160",
"sp": "2",
"nextlayer": "default"
},
{
"id": "K_oE2",
"text": ""
},
{
"id": "K_Z",
"text": "ឍ"
},
{
"id": "K_X",
"text": "ឃ"
},
{
"id": "K_C",
"text": "ជ"
},
{
"id": "K_V",
"text": ""
},
{
"id": "K_B",
"text": "ព"
},
{
"id": "K_N",
"text": "ណ"
},
{
"id": "K_M",
"text": ""
},
{
"id": "K_COMMA",
"text": ""
},
{
"id": "K_PERIOD",
"text": "៕"
},
{
"id": "K_SLASH",
"text": "?"
},
{
"id": "T_new_390",
"text": "",
"width": "10",
"sp": "10"
}
]
},
{
"id": 5,
"key": [
{
"id": "K_LCONTROL",
"text": "*AltGr*",
"width": "160",
"sp": "1"
},
{
"id": "K_LOPT",
"text": "*Menu*",
"width": "160",
"sp": "1"
},
{
"id": "K_SPACE",
"text": "",
"width": "930"
},
{
"id": "K_ENTER",
"text": "*Enter*",
"width": "160",
"sp": "1"
}
]
}
]
}
],
"font": "Arial"
}
}

View file

@ -0,0 +1,10 @@
store(&VERSION) '15.0'
store(&NAME) "error_touch_layout_invalid_identifier"
store(&COPYRIGHT) '© 2015-2024 SIL Global'
store(&TARGETS) 'any'
store(&LAYOUTFILE) 'error_touch_layout_invalid_identifier.keyman-touch-layout'
store(&KEYBOARDVERSION) '1.3'
begin Unicode > use(main)
group(main) using keys

View file

@ -36,7 +36,9 @@ describe('KeymanWeb Compiler', function() {
});
this.afterEach(function() {
callbacks.printMessages();
if(this.currentTest?.isFailed() || debug) {
callbacks.printMessages();
}
callbacks.clear();
});
@ -203,6 +205,19 @@ describe('KeymanWeb Compiler', function() {
assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel));
});
it('should give error ERROR_TouchLayoutInvalidIdentifier if a virtual key is badly formatted e.g. U_1234[_5678]', async function() {
// #12870
const filenames = generateTestFilenames('error_touch_layout_invalid_identifier');
let result = await kmnCompiler.run(filenames.source, null);
assert.isNull(result);
assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion));
assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion));
assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.ERROR_TouchLayoutInvalidIdentifier));
assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.ERROR_InvalidTouchLayoutFile));
assert.lengthOf(callbacks.messages, 2);
});
});
async function run_test_keyboard(kmnCompiler: KmnCompiler, id: string):

View file

@ -18,6 +18,8 @@ import { PackageKeyboardTargetValidator } from './package-keyboard-target-valida
import { PackageMetadataUpdater } from './package-metadata-updater.js';
import { markdownToHTML } from './markdown.js';
import { PackageValidation } from './package-validation.js';
import { getFontFamilySync } from "@keymanapp/developer-utils";
import { setKvkFontData } from './kvk-font-facename.js';
const KMP_JSON_FILENAME = 'kmp.json';
const KMP_INF_FILENAME = 'kmp.inf';
@ -483,36 +485,36 @@ export class KmpCompiler implements KeymanCompiler {
this.callbacks.reportMessage(PackageCompilerMessages.Warn_AbsolutePath({filename: filename}));
}
let memberFileData = this.getMemberFileData(kpsFilename, filename);
if(!memberFileData) {
failed = true;
return;
}
filename = this.callbacks.resolveFilename(kpsFilename, filename);
const basename = this.callbacks.path.basename(filename);
if(!this.callbacks.fs.existsSync(filename)) {
this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: filename}));
failed = true;
return;
if(KeymanFileTypes.filenameIs(filename, KeymanFileTypes.Binary.VisualKeyboard)) {
if(!this.isKvkFileBinary(memberFileData)) {
// warn the few users who are still doing this -- non-binary .kvk should not be included in package!
this.callbacks.reportMessage(PackageCompilerMessages.Warn_FileIsNotABinaryKvkFile({filename: filename}));
} else {
memberFileData = this.setKvkFontData(kpsFilename, data, filename, memberFileData);
}
}
let memberFileData;
try {
memberFileData = this.callbacks.loadFile(filename);
} catch(e) {
this.callbacks.reportMessage(PackageCompilerMessages.Error_FileCouldNotBeRead({filename: filename, e: e}));
failed = true;
return;
}
this.warnIfKvkFileIsNotBinary(filename, memberFileData);
zip.file(basename, memberFileData);
// Remove path data from files before JSON save
value.name = basename;
});
if(failed) {
return null;
}
data.files.forEach((value) => {
const basename = this.callbacks.path.basename(value.name);
// Remove path data from files before JSON save
value.name = basename;
});
// TODO #9477: transform .md to .htm
// Remove path data from file references in options
@ -557,26 +559,87 @@ export class KmpCompiler implements KeymanCompiler {
return transcodeToCP1252(s);
}
private getMemberFileData(kpsFilename: string, filename: string) {
filename = this.callbacks.resolveFilename(kpsFilename, filename);
if(!this.callbacks.fs.existsSync(filename)) {
this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: filename}));
return null;
}
try {
return this.callbacks.loadFile(filename);
} catch(e) {
this.callbacks.reportMessage(PackageCompilerMessages.Error_FileCouldNotBeRead({filename: filename, e: e}));
return null;
}
}
private setKvkFontData(kpsFilename: string, data: KmpJsonFile.KmpJsonFile, filename: string, kvk: Uint8Array) {
// find the appropriate font to set
const kvkId = this.callbacks.path.basename(filename.toLowerCase(), '.kvk');
const kbd = data.keyboards.find(k => k.id == kvkId);
if(!kbd) {
// cannot find a matching keyboard
this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindMatchingKeyboardForVisualKeyboard({filename}));
return kvk;
}
const fontFilename = kbd.oskFont || kbd.displayFont;
if(!fontFilename) {
// no font data to set
return kvk;
}
// Look up the full font filename
const fontFile = data.files.find(file => this.callbacks.path.basename(file.name.toLowerCase()) == fontFilename.toLowerCase());
if(!fontFile) {
// font cannot be found
this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindFontForKeyboard({id: kbd.id, fontFilename}));
return kvk;
}
// the font is a filename, included in the .kps
const fontData = this.getMemberFileData(kpsFilename, fontFile.name);
if(!fontData) {
// cannot find source font
this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindFontForKeyboard({id: kbd.id, fontFilename: fontFile.name}));
return kvk;
}
const fontFacename = getFontFamilySync(fontData);
if(!fontFacename) {
// Font facename could not be extracted from ttf
this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotReadFont({fontFilename: fontFile.name}));
return kvk;
}
const result = setKvkFontData(kvk, fontFacename);
if(!result) {
// KVK is invalid
this.callbacks.reportMessage(PackageCompilerMessages.Warn_VisualKeyboardFileIsInvalid({filename}));
return kvk;
}
return result;
}
/**
* Legacy .kmp compiler would transform xml-format .kvk files into a binary .kvk file; now
* we want that to remain the responsibility of the keyboard compiler, so we'll warn the
* few users who are still doing this
* we want that to remain the responsibility of the keyboard compiler.
*/
private warnIfKvkFileIsNotBinary(filename: string, data: Uint8Array) {
if(!KeymanFileTypes.filenameIs(filename, KeymanFileTypes.Binary.VisualKeyboard)) {
return;
}
private isKvkFileBinary(data: Uint8Array) {
if(data.byteLength < 4) {
// TODO: Not a valid .kvk file; should we be reporting this?
return;
return false;
}
// Must start with 'KVKF'
if(data[0] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[0] ||
data[1] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[1] ||
data[2] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[2] ||
data[3] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[3]) {
this.callbacks.reportMessage(PackageCompilerMessages.Warn_FileIsNotABinaryKvkFile({filename: filename}));
return false;
}
return true;
}
}

View file

@ -0,0 +1,16 @@
import { KvkFileReader, KvkFileWriter } from "@keymanapp/common-types";
export function setKvkFontData(fileData: Uint8Array, fontFacename: string) {
const reader = new KvkFileReader();
const kvk = reader.read(fileData);
if(!kvk) {
// kvk is invalid
return null;
}
kvk.header.ansiFont.name = fontFacename;
kvk.header.unicodeFont.name = fontFacename;
const writer = new KvkFileWriter();
return writer.write(kvk);
}

View file

@ -22,16 +22,21 @@ export class PackageCompilerMessages {
static Warn_AbsolutePath = (o:{filename: string}) => m(this.WARN_AbsolutePath, `File ${def(o.filename)} has an absolute path, which is not portable.`);
static ERROR_FileDoesNotExist = SevError | 0x0003;
static Error_FileDoesNotExist = (o:{filename: string}) => m(this.ERROR_FileDoesNotExist, `File ${def(o.filename)} does not exist.`);
static Error_FileDoesNotExist = (o:{filename: string}) => m(
this.ERROR_FileDoesNotExist, `File '${def(o.filename)}' does not exist.`
);
static ERROR_FileCouldNotBeRead = SevError | 0x0004;
static Error_FileCouldNotBeRead = (o:{filename: string; e: any}) => m(this.ERROR_FileCouldNotBeRead,
`File ${def(o.filename)} could not be read: ${(o.e ?? 'unknown error').toString()}.`);
static Error_FileCouldNotBeRead = (o:{filename: string; e: any}) => m(
this.ERROR_FileCouldNotBeRead,
`File '${def(o.filename)}' could not be read: ${(o.e ?? 'unknown error').toString()}.`
);
static WARN_FileIsNotABinaryKvkFile = SevWarn | 0x0005;
static Warn_FileIsNotABinaryKvkFile = (o:{filename: string}) => m(this.WARN_FileIsNotABinaryKvkFile,
`File ${def(o.filename)} does not appear to be a valid binary .kvk file; this may be an old package that includes an xml-format .kvk file. `+
`You must update the package to include the compiled .kvk file in the package.`);
`File '${def(o.filename)}' does not appear to be a valid binary .kvk file; this may be an old package that includes an xml-format .kvk file. `+
`You must update the package to include the compiled .kvk file in the package.`
);
static ERROR_FollowKeyboardVersionNotAllowedForModelPackages = SevError | 0x0006;
static Error_FollowKeyboardVersionNotAllowedForModelPackages = () => m(this.ERROR_FollowKeyboardVersionNotAllowedForModelPackages,
@ -43,15 +48,18 @@ export class PackageCompilerMessages {
static ERROR_KeyboardContentFileNotFound = SevError | 0x0008;
static Error_KeyboardContentFileNotFound = (o:{id:string}) => m(this.ERROR_KeyboardContentFileNotFound,
`Keyboard ${def(o.id)} was listed in <Keyboards> but a corresponding .kmx file was not found in <Files>`);
`Keyboard '${def(o.id)}' was listed in <Keyboards> but a corresponding .kmx file was not found in <Files>`
);
static ERROR_KeyboardFileNotValid = SevError | 0x0009;
static Error_KeyboardFileNotValid = (o:{filename:string, e:any}) => m(this.ERROR_KeyboardFileNotValid,
`Keyboard file ${def(o.filename)} is not a valid .kmx file: ${(o.e ?? 'unknown error').toString()}`);
`Keyboard file '${def(o.filename)}' is not a valid .kmx file: ${(o.e ?? 'unknown error').toString()}`
);
static INFO_KeyboardFileHasNoKeyboardVersion = SevInfo | 0x000A;
static Info_KeyboardFileHasNoKeyboardVersion = (o:{filename:string}) => m(this.INFO_KeyboardFileHasNoKeyboardVersion,
`Keyboard file ${def(o.filename)} has no &KeyboardVersion store, using default '0.0'`);
`Keyboard file '${def(o.filename)}' has no &KeyboardVersion store, using default '0.0'`
);
static ERROR_PackageCannotContainBothModelsAndKeyboards = SevError | 0x000B;
static Error_PackageCannotContainBothModelsAndKeyboards = () => m(this.ERROR_PackageCannotContainBothModelsAndKeyboards,
@ -63,19 +71,22 @@ export class PackageCompilerMessages {
static WARN_PackageNameDoesNotFollowLexicalModelConventions = SevWarn | 0x000D;
static Warn_PackageNameDoesNotFollowLexicalModelConventions = (o:{filename: string}) => m(this.WARN_PackageNameDoesNotFollowLexicalModelConventions,
`The package file ${def(o.filename)} does not follow the recommended model filename conventions. The name should be all lower case, `+
`The package file '${def(o.filename)}' does not follow the recommended model filename conventions. The name should be all lower case, `+
`include only alphanumeric characters and underscore (_), not start with a digit, and should have the structure `+
`<author>.<bcp47>.<uniq>.model.kps.`);
`<author>.<bcp47>.<uniq>.model.kps.`
);
static WARN_PackageNameDoesNotFollowKeyboardConventions = SevWarn | 0x000E;
static Warn_PackageNameDoesNotFollowKeyboardConventions = (o:{filename: string}) => m(this.WARN_PackageNameDoesNotFollowKeyboardConventions,
`The package file ${def(o.filename)} does not follow the recommended keyboard filename conventions. The name should be all lower case, `+
`include only alphanumeric characters and underscore (_), and not start with a digit.`);
`The package file '${def(o.filename)}' does not follow the recommended keyboard filename conventions. The name should be all lower case, `+
`include only alphanumeric characters and underscore (_), and not start with a digit.`
);
static WARN_FileInPackageDoesNotFollowFilenameConventions = SevWarn | 0x000F;
static Warn_FileInPackageDoesNotFollowFilenameConventions = (o:{filename: string}) => m(this.WARN_FileInPackageDoesNotFollowFilenameConventions,
`The file ${def(o.filename)} does not follow the recommended filename conventions. The extension should be all lower case, `+
`and the filename should include only alphanumeric characters, -, _, + and .`);
`The file '${def(o.filename)}' does not follow the recommended filename conventions. The extension should be all lower case, `+
`and the filename should include only alphanumeric characters, -, _, + and .`
);
static ERROR_PackageNameCannotBeBlank = SevError | 0x0010;
static Error_PackageNameCannotBeBlank = () => m(this.ERROR_PackageNameCannotBeBlank,
@ -83,7 +94,8 @@ export class PackageCompilerMessages {
static ERROR_KeyboardFileNotFound = SevError | 0x0011;
static Error_KeyboardFileNotFound = (o:{filename:string}) => m(this.ERROR_KeyboardFileNotFound,
`Keyboard file ${def(o.filename)} was not found. Has it been compiled?`);
`Keyboard file '${def(o.filename)}' was not found. Has it been compiled?`
);
static WARN_KeyboardVersionsDoNotMatch = SevWarn | 0x0012;
static Warn_KeyboardVersionsDoNotMatch = (o: {keyboard:string, version:string, firstKeyboard:string, firstVersion:string}) => m(this.WARN_KeyboardVersionsDoNotMatch,
@ -129,7 +141,8 @@ export class PackageCompilerMessages {
static HINT_PackageContainsSourceFile = SevHint | 0x001D;
static Hint_PackageContainsSourceFile = (o:{filename:string}) => m(this.HINT_PackageContainsSourceFile,
`The source file ${def(o.filename)} should not be included in the package; instead include the compiled result.`);
`The source file '${def(o.filename)}' should not be included in the package; instead include the compiled result.`
);
// 0x001E was ERROR_InvalidPackageFile, now CommonTypesMessages.Error_InvalidPackageFile
@ -146,5 +159,30 @@ export class PackageCompilerMessages {
this.ERROR_PackageFileHasEmptyVersion,
`Package version is not following keyboard version, but the package version field is blank.`
);
static ERROR_RequiredParameterMissing = SevError | 0x0022;
static Error_RequiredParameterMissing = (o:{param: string}) => m(
this.ERROR_RequiredParameterMissing, `Source parameter '${def(o.param)}' is required.`
);
static WARN_CannotFindMatchingKeyboardForVisualKeyboard = SevWarn | 0x0023;
static Warn_CannotFindMatchingKeyboardForVisualKeyboard = (o:{filename: string}) => m(
this.WARN_CannotFindMatchingKeyboardForVisualKeyboard, `Cannot find matching keyboard for visual keyboard file '${def(o.filename)}'.`
);
static WARN_CannotFindFontForKeyboard = SevWarn | 0x0024;
static Warn_CannotFindFontForKeyboard = (o:{id: string, fontFilename: string}) => m(
this.WARN_CannotFindFontForKeyboard, `Cannot find font file '${def(o.fontFilename)}' for keyboard '${def(o.id)}'.`
);
static WARN_CannotReadFont = SevWarn | 0x0025;
static Warn_CannotReadFont = (o:{fontFilename: string}) => m(
this.WARN_CannotReadFont, `Cannot parse font file '${def(o.fontFilename)}' to get facename.`
);
static WARN_VisualKeyboardFileIsInvalid = SevWarn | 0x0026;
static Warn_VisualKeyboardFileIsInvalid = (o:{filename: string}) => m(
this.WARN_VisualKeyboardFileIsInvalid, `Visual keyboard file '${def(o.filename)}' is invalid.`
);
}

View file

@ -130,7 +130,15 @@ export class WindowsPackageInstallerCompiler implements KeymanCompiler {
}
// Check existence of required files
for(const filename of [sources.licenseFilename, sources.msiFilename, sources.setupExeFilename]) {
for(const [param,filename] of [
['licenseFilename',sources.licenseFilename],
['msiFilename',sources.msiFilename],
['setupExeFilename',sources.setupExeFilename]
]) {
if(!filename) {
this.callbacks.reportMessage(PackageCompilerMessages.Error_RequiredParameterMissing({param}));
return null;
}
if(!this.callbacks.fs.existsSync(filename)) {
this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename}));
return null;
@ -186,6 +194,11 @@ export class WindowsPackageInstallerCompiler implements KeymanCompiler {
private async buildZip(kps: KpsFile.KpsFile, kpsFilename: string, sources: WindowsPackageInstallerSources): Promise<Uint8Array> {
const kmpJson: KmpJsonFile.KmpJsonFile = this.kmpCompiler.transformKpsFileToKmpObject(kpsFilename, kps);
if(!kmpJson) {
// error will have been reported in transformKpsFileToKmpObject
return null;
}
if(!kmpJson.info?.name?.description) {
this.callbacks.reportMessage(PackageCompilerMessages.Error_PackageNameCannotBeBlank());
return null;
@ -194,6 +207,10 @@ export class WindowsPackageInstallerCompiler implements KeymanCompiler {
const kmpFilename = this.callbacks.path.basename(kpsFilename, KeymanFileTypes.Source.Package) + KeymanFileTypes.Binary.Package;
const setupInfBuffer = this.buildSetupInf(sources, kmpJson, kmpFilename, kps);
const kmpBuffer = await this.kmpCompiler.buildKmpFile(kpsFilename, kmpJson);
if(!kmpBuffer) {
// error will have been reported in buildKmpFile
return null;
}
// Note that this does not technically generate a "valid" sfx according to
// the zip spec, because the offsets in the .zip are relative to the start

View file

@ -27,7 +27,7 @@
<FileType>.kvk</FileType>
</File>
<File>
<Name>../../invalid/basic.kmx</Name>
<Name>binary_kvk_file.kmx</Name>
<Description>Keyboard Basic</Description>
<CopyLocation>0</CopyLocation>
<FileType>.kmx</FileType>
@ -36,7 +36,7 @@
<Keyboards>
<Keyboard>
<Name>Basic</Name>
<ID>basic</ID>
<ID>binary_kvk_file</ID>
<Version>1.0</Version>
<Languages>
<Language ID="KM">Khmer</Language>

View file

@ -53,6 +53,13 @@ function do_build() {
#-------------------------------------------------------------------------------------------------------------------
function do_test() {
builder_do_typescript_tests 50
./test/command-line-tests.sh test
}
#-------------------------------------------------------------------------------------------------------------------
function do_api() {
rm -rf ./build/messages
mkdir -p ./build/messages
@ -99,7 +106,7 @@ function do_bundle() {
builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo
builder_run_action configure verify_npm_setup
builder_run_action build do_build
builder_run_action test builder_do_typescript_tests 50
builder_run_action test do_test
builder_run_action api do_api
builder_run_action bundle do_bundle
builder_run_action publish builder_publish_npm

View file

@ -60,9 +60,9 @@ If no input file is supplied, kmc will build the current folder.`)
buildCommand
.command('windows-package-installer <infile>')
.description('Build an executable installer for Windows for a Keyman package')
.option('--msi <msiFilename>', 'Location of keymandesktop.msi')
.option('--exe <exeFilename>', 'Location of setup.exe')
.option('--license <licenseFilename>', 'Location of license.txt')
.requiredOption('--msi <msiFilename>', 'Location of keymandesktop.msi')
.requiredOption('--exe <exeFilename>', 'Location of setup.exe')
.requiredOption('--license <licenseFilename>', 'Location of license.txt')
.option('--title-image [titleImageFilename]', 'Location of title image')
.option('--app-name [applicationName]', 'Installer property: name of the application to be installed', 'Keyman')
.option('--start-disabled', 'Installer property: do not enable keyboards after installation completes')

View file

@ -45,7 +45,7 @@ function declareGenerateLdmlKeyboard(command: Command) {
.option('-o, --out-path <path>', 'Output path (may exist)')
.option('-n, --name <name>', 'Keyboard descriptive name')
.option('-c, --copyright <copyright-name>', 'Copyright holder') /* © yyyy <copyright-name> */
.option('-v, --version <version-string>', 'Keyboard version', '1.0')
.option('-v, --version <version-string>', 'Keyboard version', '1.0.0')
.option('-L, --language-tag <bcp-47 tag>', 'BCP-47 language tag',
(value, previous) => previous.concat([value]), [])
.option('-a, --author <author-name>', 'Name of keyboard author')
@ -59,12 +59,12 @@ function declareGenerateLexicalModel(command: Command) {
modelSubCommand
.description('Generate a wordlist lexical model project')
.option('-o, --out-path <path>', 'Output path (may exist)')
.option('-n, --name <name>', 'Keyboard descriptive name')
.option('-n, --name <name>', 'Lexical model descriptive name')
.option('-c, --copyright <copyright-name>', 'Copyright holder') /* © yyyy <copyright-name> */
.option('-v, --version <version-string>', 'Keyboard version', '1.0')
.option('-v, --version <version-string>', 'Lexical model version', '1.0')
.option('-L, --language-tag <bcp-47 tag>', 'BCP-47 language tag',
(value, previous) => previous.concat([value]), [])
.option('-a, --author <author-name>', 'Name of keyboard author')
.option('-a, --author <author-name>', 'Name of lexical model author')
.option('-d, --description <description>', 'Short description of the project, Markdown')
.action(generateLexicalModel);

View file

@ -19,15 +19,23 @@ await TestKeymanSentry.runTestIfCLRequested(kmcSentryOptions);
if(KeymanSentry.isEnabled()) {
KeymanSentry.init(kmcSentryOptions);
}
try {
await run();
} catch(e) {
KeymanSentry.captureException(e);
}
// Ensure any messages reported to Sentry have had time to be uploaded before we
// exit. In most cases, this will be a no-op so should not affect performance.
await exitProcess(0);
run().then(async () => {
// Ensure any messages reported to Sentry have had time to be uploaded before we
// exit. In most cases, this will be a no-op so should not affect performance.
await exitProcess(0);
}, (reason: any) => {
KeymanSentry.captureException(reason);
// in local environment, captureException will
// return so we want to re-throw; note that this is
// a little noisy because it comes from an async function
console.error('Aborting due to fatal exception. Local development environment detected, so printing trace.');
const report: any = process.report?.getReport(reason);
console.log(report?.javascriptStack?.message);
console.log(report?.javascriptStack?.stack?.map((s: string) => ' '+s).join('\n'));
// This cannot be an async function because otherwise the stack trace gets captured
exitProcess(1);
});
async function run() {
await loadOptions();
@ -62,6 +70,5 @@ async function run() {
declarePublish(program);
*/
await program.parseAsync(process.argv)
.catch(reason => console.error(reason));
await program.parseAsync(process.argv);
}

View file

@ -0,0 +1,147 @@
#!/usr/bin/env bash
#
# Command line tests for kmc
#
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
# . "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh"
# . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh"
# . "$KEYMAN_ROOT/developer/src/packages.inc.sh"
builder_describe "Test Keyman Keyboard Compiler kmc command line e2e" \
"test" \
"--verbose,-v Verbose logging of output from kmc" \
"--filter,-f=FILTER Run tests that match FILTER" \
":basic Basic sanity tests" \
":analyze Test 'kmc analyze' command" \
":build Test 'kmc build' command" \
":copy Test 'kmc copy' command'" \
":generate Test 'kmc generate' command" \
":message Test 'kmc message' command"
builder_parse "$@"
if [[ -z ${KMC+x} ]]; then
KMC="node .."
fi
FAILED_TESTS=0
PASSED_TESTS=0
SKIPPED_TESTS=0
function run_kmc() {
local EXPECTED_EXIT=$1
local EXPECTED_MATCH="$2"
shift 2
local ACTUAL_EXIT=0
local MATCH_RESULT=0
local DATA
if builder_has_option --filter; then
if [[ ! "$@" =~ "$FILTER" ]]; then
builder_echo blue "🟦 SKIP 'kmc $@'"
SKIPPED_TESTS=$((SKIPPED_TESTS + 1))
return 0
fi
fi
DATA=$($KMC "$@" 2>&1) || ACTUAL_EXIT=$?
echo "$DATA" | grep -qF "$EXPECTED_MATCH" || MATCH_RESULT=$?
if [[ $ACTUAL_EXIT != $EXPECTED_EXIT ]]; then
builder_echo red "❌ FAIL 'kmc $@': expected exit code $EXPECTED_EXIT != actual exit code $ACTUAL_EXIT"
builder_echo grey "Actual output from 'kmc $@':"
echo "$DATA"
FAILED_TESTS=$(($FAILED_TESTS + 1))
elif [[ $MATCH_RESULT != 0 ]]; then
builder_echo red "❌ FAIL 'kmc $@': expected string '$EXPECTED_MATCH' not found in output"
builder_echo grey "Actual output from 'kmc $@':"
echo "$DATA"
FAILED_TESTS=$(($FAILED_TESTS + 1))
else
builder_echo "${COLOR_GREEN}✔${COLOR_RESET} PASS 'kmc $@'"
if builder_has_option --verbose; then
builder_echo grey "Actual output from 'kmc $@':"
echo "$DATA"
fi
PASSED_TESTS=$(($PASSED_TESTS + 1))
fi
}
function do_test_basic() {
run_kmc 0 "Keyman Developer Command Line Interface" --help
run_kmc 0 "kmc build [options] [command]" build --help
run_kmc 1 "error: unknown option '--bad-option'" --bad-option
}
function build_cleanup() {
rm -rf ./fixtures/command-line-tests/build/khmer_angkor/build/
}
function do_test_build() {
# project/file builds
build_cleanup
run_kmc 0 "." build ./fixtures/command-line-tests/build/khmer_angkor/khmer_angkor.kpj
build_cleanup
run_kmc 0 "." build file ./fixtures/command-line-tests/build/khmer_angkor/khmer_angkor.kpj
build_cleanup
run_kmc 0 "." build ./fixtures/command-line-tests/build/khmer_angkor/
# windows-package-installer tests
# TODO: windows-package-installer is currently silent but should emit a 'success' message
#build_cleanup
run_kmc 0 "" build windows-package-installer \
./fixtures/command-line-tests/build/khmer_angkor/source/khmer_angkor.kps \
--msi ./fixtures/command-line-tests/build/windows-installer/keymandesktop.txt \
--exe ./fixtures/command-line-tests/build/windows-installer/setup.txt \
--license ./fixtures/command-line-tests/build/windows-installer/license.txt \
--out-file ./fixtures/command-line-tests/build/khmer_angkor/build/khmer_angkor.exe
#build_cleanup
run_kmc 1 "" build windows-package-installer \
./fixtures/command-line-tests/build/khmer_angkor/source/khmer_angkor.kps \
--msi ./fixtures/command-line-tests/build/windows-installer/keymandesktop.txt \
--exe ./fixtures/command-line-tests/build/windows-installer/setup.txt \
--out-file ./fixtures/command-line-tests/build/khmer_angkor/build/khmer_angkor.exe
}
function do_test_analyze() {
builder_heading "kmc analyze"
}
function do_test_copy() {
builder_heading "kmc copy"
}
function do_test_generate() {
builder_heading "kmc generate"
}
function do_test_message() {
run_kmc 1 "Invalid parameter" message zap
run_kmc 0 "ERROR_FileDoesNotExist" message 4003
}
builder_run_action test:basic do_test_basic
builder_run_action test:analyze do_test_analyze
builder_run_action test:build do_test_build
builder_run_action test:copy do_test_copy
builder_run_action test:generate do_test_generate
builder_run_action test:message do_test_message
if builder_has_action test; then
builder_echo grey "$PASSED_TESTS test(s) passed"
builder_echo grey "$SKIPPED_TESTS test(s) skipped"
if [[ $FAILED_TESTS != 0 ]]; then
builder_echo red "$FAILED_TESTS test(s) failed"
exit 1
else
builder_echo "0 test(s) failed"
fi
fi

View file

@ -0,0 +1 @@
build/

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<KeymanDeveloperProject>
<Options>
<BuildPath>$PROJECTPATH\build</BuildPath>
<SourcePath>$PROJECTPATH\source</SourcePath>
<CompilerWarningsAsErrors>True</CompilerWarningsAsErrors>
<WarnDeprecatedCode>True</WarnDeprecatedCode>
<CheckFilenameConventions>False</CheckFilenameConventions>
<ProjectType>keyboard</ProjectType>
<Version>2.0</Version>
</Options>
</KeymanDeveloperProject>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,642 @@
store(&VERSION) '10.0'
store(&NAME) "Khmer Angkor"
store(&COPYRIGHT) '© 2015-2022 SIL International'
store(&MESSAGE) "More than just a Khmer Unicode keyboard."
store(&TARGETS) 'any'
store(&LAYOUTFILE) 'khmer_angkor.keyman-touch-layout'
store(&KEYBOARDVERSION) '1.3'
store(&BITMAP) 'khmer_angkor.ico'
store(&VISUALKEYBOARD) 'khmer_angkor.kvks'
begin Unicode > use(main)
c ==============================================STORES==============================================
c 33 consonants and two deprecated consonants
store(c_key) [K_K] [K_X] [SHIFT K_K] [SHIFT K_X] [K_G] \
[K_C] [K_Q] [SHIFT K_C] [SHIFT K_Q] [SHIFT K_J] \
[K_D] [K_Z] [SHIFT K_D] [SHIFT K_Z] [SHIFT K_N] \
[K_T] [K_F] [SHIFT K_T] [SHIFT K_F] [K_N] \
[K_B] [K_P] [SHIFT K_B] [SHIFT K_P] [K_M] \
[K_Y] [K_R] [K_L] [K_V] [K_S] [K_H] [SHIFT K_L] [SHIFT K_G] \
[RALT K_K] [RALT K_B]
store(c_out) U+1780 U+1781 U+1782 U+1783 U+1784 \
U+1785 U+1786 U+1787 U+1788 U+1789 \
U+178A U+178B U+178C U+178D U+178E \
U+178F U+1790 U+1791 U+1792 U+1793 \
U+1794 U+1795 U+1796 U+1797 U+1798 \
U+1799 U+179A U+179B U+179C U+179F U+17A0 U+17A1 U+17A2 \
U+179D U+179E c deprecated, but they are used in minority languages
c All genuine dependent vowels
store(v_gen_key) [K_A] [K_I] [SHIFT K_I] [K_W] [SHIFT K_W] [K_U] [SHIFT K_U] [SHIFT K_Y] \
[K_COLON] [SHIFT K_LBRKT] [K_LBRKT] [K_E] [SHIFT K_E] [SHIFT K_S] [K_O] [SHIFT K_O]
store(v_gen) U+17B6 U+17B7 U+17B8 U+17B9 U+17BA U+17BB U+17BC U+17BD \
U+17BE U+17BF U+17C0 U+17C1 U+17C2 U+17C3 U+17C4 U+17C5
c All pseudo dependent vowels
store(v_pseudo_key) [SHIFT K_M] [SHIFT K_H] [RALT K_QUOTE]
store(v_pseudo) U+17C6 U+17C7 U+17C8
c Both genuine and pseudo vowels
store(v_key) outs(v_gen_key) outs(v_pseudo_key)
store(v_out) outs(v_gen) outs(v_pseudo)
store(v_any) outs(v_out)
c These seven vowels can be concatenated with Reahmuk (U+17C7) to make េះ ោះ ុះ ិះ ីះ ឹះ and ែះ.
store(v_combo_R) U+17C1 U+17C4 U+17BB U+17B7 U+17B8 U+17B9 U+17C2
c these two vowels can be concatenated with Nikahit (U+17C6) to make ាំ and ុំ.
store(v_combo_N) U+17B6 U+17BB
store(v_combo) outs(v_combo_R) outs(v_combo_N)
c Independent vowels
store(ind_v_key) [K_HYPHEN] [RALT K_I] [SHIFT K_RBRKT] [RALT K_T] [RALT K_LBRKT] [K_RBRKT] [RALT K_R] [SHIFT K_R] \
[SHIFT K_BKSLASH] [K_BKSLASH] [RALT K_E] [RALT K_P] [RALT K_O] [K_EQUAL] [RALT K_RBRKT]
store(ind_v_out) U+17A5 U+17A6 U+17A7 U+17A8 U+17A9 U+17AA U+17AB U+17AC \
U+17AD U+17AE U+17AF U+17B0 U+17B1 U+17B2 U+17B3
c Diacritics
store(diacritic_key) [K_QUOTE] [SHIFT K_7] [SHIFT K_HYPHEN] [SHIFT K_8] [SHIFT K_6] [RALT K_QUOTE] \
[RALT K_EQUAL] [RALT K_3] [RALT K_W] [RALT K_Q] [K_J]
store(diacritic_out) U+17CB U+17D0 U+17CC U+17CF U+17CD U+17C8 \
U+17CE U+17D1 U+17DD U+17DC U+17D2
c Consonant shifter--Muusikatoan(U+17C9) and Triisap(U+17CA)
store(c_shifter_key) [SHIFT K_QUOTE] [K_SLASH]
store(c_shifter) U+17C9 U+17CA
c punctuations
store(punct_key) [K_PERIOD] [SHIFT K_PERIOD] [RALT K_COLON] [SHIFT K_2] [RALT K_L] [RALT K_6] [RALT K_7] [RALT K_M]
store(punct_out) U+17D4 U+17D5 U+17D6 U+17D7 U+17D8 U+17D9 U+17DA U+17D3
store(latin_punct_key) [K_BKQUOTE] [SHIFT K_BKQUOTE] [SHIFT K_9] [SHIFT K_0] [SHIFT K_1] [SHIFT K_3] \
[SHIFT K_5] [SHIFT K_EQUAL] [SHIFT K_SLASH] [RALT K_9] [RALT K_0] [RALT K_BKSLASH] \
[RALT K_2] [RALT K_8] [RALT K_COMMA] [RALT K_D] [RALT K_PERIOD] [RALT K_SLASH] \
[RALT K_Y] [RALT K_U] [RALT K_BKQUOTE] [RALT K_1] [RALT K_A] [RALT K_S] \
[RALT K_F] [RALT K_G] [RALT K_HYPHEN] [RALT K_H] [RALT K_J] [RALT K_N] \
[RALT K_Z] [RALT K_X] [RALT K_C] [RALT K_V]
store(latin_punct_out) U+00AB U+00BB U+0028 U+0029 U+0021 U+0022 \
U+0025 U+003D U+003F U+007B U+007D U+005C \
U+0040 U+002A U+002C U+00D7 U+002E U+002F \
U+005B U+005D U+200D U+200C U+002B U+002D \
U+00F7 U+003A U+2248 U+2018 U+2019 U+003B \
U+003C U+003E U+0023 U+0026
c white space, ZWSP, ZWJ
store(spaces_key) [K_SPACE] [SHIFT K_SPACE] [RALT K_SPACE]
store(spaces_out) U+200B U+0020 U+00A0
c Currency: Riel, Dollar and Euro
store(currency_key) [SHIFT K_4] [RALT K_4] [RALT K_5]
store(currency_out) U+17DB U+0024 U+20AC
c Khmer digits 0 through 9
store(digit_key) [K_0] [K_1] [K_2] [K_3] [K_4] [K_5] [K_6] [K_7] [K_8] [K_9]
store(digit_out) U+17E0 U+17E1 U+17E2 U+17E3 U+17E4 U+17E5 U+17E6 U+17E7 U+17E8 U+17E9
c Khmer Lek Attak (a.k.a. divination lore)
store(lek_attak_key) [SHIFT RALT K_0] [SHIFT RALT K_1] [SHIFT RALT K_2] [SHIFT RALT K_3] [SHIFT RALT K_4] \
[SHIFT RALT K_5] [SHIFT RALT K_6] [SHIFT RALT K_7] [SHIFT RALT K_8] [SHIFT RALT K_9]
store(lek_attak_out) U+17F0 U+17F1 U+17F2 U+17F3 U+17F4 \
U+17F5 U+17F6 U+17F7 U+17F8 U+17F9
c Lunar date format (Khmer style)
store(lunar_date_key) [SHIFT RALT K_A] [SHIFT RALT K_B] [SHIFT RALT K_C] [SHIFT RALT K_D] [SHIFT RALT K_E] \
[SHIFT RALT K_F] [SHIFT RALT K_G] [SHIFT RALT K_H] [SHIFT RALT K_I] [SHIFT RALT K_J] \
[SHIFT RALT K_K] [SHIFT RALT K_L] [SHIFT RALT K_M] [SHIFT RALT K_N] [SHIFT RALT K_O] \
[SHIFT RALT K_P] [SHIFT RALT K_Q] [SHIFT RALT K_R] [SHIFT RALT K_S] [SHIFT RALT K_T] \
[SHIFT RALT K_U] [SHIFT RALT K_V] [SHIFT RALT K_W] [SHIFT RALT K_X] [SHIFT RALT K_Y] \
[SHIFT RALT K_Z] [SHIFT RALT K_COLON] [SHIFT RALT K_COMMA] [SHIFT RALT K_PERIOD] [SHIFT RALT K_LBRKT] \
[SHIFT RALT K_RBRKT] [SHIFT RALT K_QUOTE]
store(lunar_date_out) U+19EC U+19FB U+19F9 U+19EE U+19E2 \
U+19EF U+19F0 U+19F1 U+19E7 U+19F2 \
U+19F3 U+19F4 U+19FD U+19FC U+19E8 \
U+19E9 U+19E0 U+19E3 U+19ED U+19E4 \
U+19E6 U+19FA U+19E1 U+19F8 U+19E5 \
U+19F7 U+19F5 U+19FE U+19FF U+19EA \
U+19EB U+19F6
c Subscripts​ for mobile/table layouts
store(input_subcons) \
[T_17D2_1780] [T_17D2_1781] [T_17D2_1782] [T_17D2_1783] [T_17D2_1784] [T_17D2_1785] [T_17D2_1786] [T_17D2_1787] \
[T_17D2_1788] [T_17D2_1789] [T_17D2_178A] [T_17D2_178B] [T_17D2_178C] [T_17D2_178D] [T_17D2_178E] [T_17D2_178F] \
[T_17D2_1790] [T_17D2_1791] [T_17D2_1792] [T_17D2_1793] [T_17D2_1794] [T_17D2_1795] [T_17D2_1796] [T_17D2_1797] \
[T_17D2_1798] [T_17D2_1799] [T_17D2_179A] [T_17D2_179B] [T_17D2_179C] [T_17D2_179D] [T_17D2_179E] [T_17D2_179F] \
[T_17D2_17A0] [T_17D2_17A1] [T_17D2_17A2]
store(subcons) \
U+1780 U+1781 U+1782 U+1783 U+1784 U+1785 U+1786 U+1787 \
U+1788 U+1789 U+178A U+178B U+178C U+178D U+178E U+178F \
U+1790 U+1791 U+1792 U+1793 U+1794 U+1795 U+1796 U+1797 \
U+1798 U+1799 U+179A U+179B U+179C U+179D U+179E U+179F \
U+17A0 U+17A1 U+17A2
c Arabic digits 0 through 9
store(arabic_digit_key) [U_0030] [U_0031] [U_0032] [U_0033] [U_0034] [U_0035] [U_0036] [U_0037] [U_0038] [U_0039]
store(arabic_digit_out) U+0030 U+0031 U+0032 U+0033 U+0034 U+0035 U+0036 U+0037 U+0038 U+0039
c These stores are for consonant (cluster) and consonant shifter configuration
store(v_above) U+17B7 U+17B8 U+17B9 U+17BA \
U+17BE U+17D0 c These vowels are rendered above the base consonant.
store(shiftable_c_1st) U+179F U+17A0 U+17A2 c These consonants shift to the 2nd series when adding U+17CA to them.
store(shiftable_BA) U+1794 c BA is unique because it can be used with both U+17CA (shift to 2nd) and and U+179C (shift to PA, still in 1st series).
store(shiftable_c_2nd) U+1784 U+1789 U+1798 U+1799 \
U+179A U+179C U+1793 U+179B c These consonants shift to the 1st series when adding U+17C9 to them.
store(shiftable_c_2nd_with_BA) outs(shiftable_c_2nd) outs(shiftable_BA)
store(c_2nd_combo_LO) U+1799 U+1798 U+1784 U+1794 \
U+179C c These can be combined with U+179B to make a 2nd series consonant cluster.
store(c_2nd_combo_MO) U+1799 U+179B U+1784 U+179A c These can be combined with U+1798 to make a 2nd series consonant cluster.
store(c_1st_combo_LO) U+1794 U+17A0 U+17A2 c they can be combined with U+179B to make a 1st series consonant cluster.
store(c_1st_combo_MO) U+17A0 U+179F U+17A2 c they can be combined with U+1798 to make a 1st series consonant cluster.
store(c_combo_SA) U+1794 U+1799 U+179B U+1798 \
U+1793 U+1789 U+1784 U+179A \
U+179C U+17A2 c they can be combined with U+179F to make a 1st series consonant cluster.
store(c_combo_QA) U+1786 U+1788 U+1794 U+1795 \
U+178F U+1791 c they can be combined with U+17A2 to make a 1st series consonant cluster.
store(c_combo_HA) U+179C U+17A3 c they can be combined with U+17A0 to make a 1st series consonant cluster.
c ==============================================BASIC RULES & CONSTRAINTS/ROTATIONS==============================================
group(main) using keys
c Basic rules with RALT key
+ any(c_key) > index(c_out,1)
+ any(v_key) > index(v_out,1)
+ any(ind_v_key) > index(ind_v_out,1)
+ any(diacritic_key) > index(diacritic_out,1)
+ any(c_shifter_key) > index(c_shifter,1)
+ any(punct_key) > index(punct_out,1)
+ any(latin_punct_key) > index(latin_punct_out,1)
+ any(currency_key) > index(currency_out,1)
+ any(digit_key) > index(digit_out,1)
+ any(lek_attak_key) > index(lek_attak_out,1)
+ any(lunar_date_key) > index(lunar_date_out,1)
+ any(spaces_key) > index(spaces_out,1)
c two-unicode-code-point vowels
+ [SHIFT K_A] > U+17B6 U+17C6
+ [SHIFT K_V] > U+17C1 U+17C7
+ [SHIFT K_COLON] > U+17C4 U+17C7
+ [K_COMMA] > U+17BB U+17C6
+ [SHIFT K_COMMA] > U+17BB U+17C7
c Constraints
any(v_gen) + [K_QUOTE] > context beep c no Bantoc after any genuine vowel
any(v_pseudo) + [K_QUOTE] > context beep c no Bantoc after any pseudo vowel
any(c_shifter) + [K_QUOTE] > context beep c no Bantoc after Triisap/Muusikatoan
U+17D2 any(c_out) + [K_QUOTE] > context beep c no Bantoc on a subscript
any(c_shifter) + any(c_shifter_key) > context beep c no two Triisap/Muusikatoan
c Rotation
U+17C7 + [SHIFT K_H] > U+17C8 c U+17C7 is changed to U+17C8 when pressing [SHIFT K_H].
U+17C8 + [SHIFT K_H] > U+17C7 c U+17C8 is changed to U+17C7 when K_H is pressed.
c any(v_gen) U+17C7 + [SHIFT K_H] > context beep c Don't rotate between U+17C7 and U+17C8.
c Press Space twice to get a White Space
U+200B + [K_SPACE] > U+0020
c One backspace deletes a two-part vowels (i.e. psuedoVowel + U+17C7)
any(v_combo_N) U+17C6 + [K_BKSP] > nul
any(v_combo_R) U+17C7 + [K_BKSP] > nul
c When pressing [K_U] after ាំ and ុំ, the U+17BB is converted to a respective consonant shifter placing before them (ាំ and ុំ).
any(shiftable_c_1st) U+17B6 U+17C6 + [K_U] > context(1) U+17CA context(2) context(3) c fix ស ាំ ុ > ស៊ាំ
any(shiftable_c_2nd_with_BA) U+17B6 U+17C6 + [K_U] > context(1) U+17C9 context(2) context(3) c fix ម ាំ ុ > ម៉ាំ
c Consonant shifter constraints (beep when the consonant shifter is used in inappropriate environment)
any(shiftable_c_1st) + [SHIFT K_QUOTE] > context U+17C9 beep
any(shiftable_c_2nd_with_BA) + [K_SLASH] > context U+17CA beep
c 1st series consonant clusters
any(c_combo_QA) U+17D2 U+17A2 + [SHIFT K_QUOTE] > context U+17C9 beep
U+179B U+17D2 any(c_1st_combo_LO) + [SHIFT K_QUOTE] > context U+17C9 beep
U+1798 U+17D2 any(c_1st_combo_MO) + [SHIFT K_QUOTE] > context U+17C9 beep
U+179F U+17D2 any(c_combo_SA) + [SHIFT K_QUOTE] > context U+17C9 beep
any(c_combo_HA) U+17D2 U+17A0 + [SHIFT K_QUOTE] > context U+17C9 beep
U+17A2 U+17D2 U+1784 + [SHIFT K_QUOTE] > context U+17C9 beep
U+17A2 U+17D2 U+179C + [SHIFT K_QUOTE] > context U+17C9 beep
c 2nd series consonant clusters
U+179B U+17D2 any(c_2nd_combo_LO) + [K_SLASH] > context U+17CA beep
U+1798 U+17D2 any(c_2nd_combo_MO) + [K_SLASH] > context U+17CA beep
c ==============================================MOBILE/TABLET TOUCH LAYOUT==============================================
+ any(input_subcons) > U+17D2 index(subcons, 1)
c delete a subscript (a subscript sign + a consonant) upon a backspace
platform('touch') U+17D2 any(c_out) + [K_BKSP] > nul c working OK
+ [K_NPSTAR] > U+002A
+ [SHIFT K_NPSTAR] > U+002A
+ [K_NPPLUS] > U+002B
+ [SHIFT K_NPPLUS] > U+002B
+ [K_NPMINUS] > U+002D
+ [SHIFT K_NPMINUS] > U+002D
+ [K_NPDOT] > U+002E
+ [SHIFT K_NPDOT] > U+002E
+ [K_NPSLASH] > U+002F
+ [SHIFT K_NPSLASH] > U+002F
c ==============================================NORMALIZATION==============================================
match > use(normalise)
group(normalise)
c Illegitimate vowel combinations should be transformed to the legitimate ones (Case #6 and #7).
U+17C1 U+17B6 > U+17C4 c when type េ and ា, tranform them to ោ
U+17B6 U+17C1 > U+17C4 c when type ា and េ, tranform them to ោ
U+17C1 U+17B8 > U+17BE c when type េ and ី , tranform them to ើ
U+17B8 U+17C1 > U+17BE c when type ី and េ, tranform them to ើ
c Illegitimate vowel combinations should be transformed to the legitimate ones (Case #5).
U+17C6 U+17BB > U+17BB U+17C6 c when type ំ and ុ , tranform them to ុំ
U+17C6 U+17B6 > U+17B6 U+17C6 c when type ំ and ា, tranform them to ាំ
c Rotate vowels
any(v_gen) any(v_gen) > context(2) c the last vowel stroke replaces the previous one.
c Rotation of two-part vowels
any(v_gen) any(v_pseudo) any(v_gen) > context(3) c a two-part vowel rotates to a genuine vowel.
any(v_gen) any(v_pseudo) any(v_pseudo) > context(3) c a two-part vowel rotates to a pseudo vowel.
any(v_gen) any(v_gen) any(v_pseudo) > context(2) context(3) c a genuine vowel rotates to a two-part vowel.
any(v_pseudo) any(v_gen) any(v_pseudo) > context(2) context(3) c a pseudo vowel roates to a two-part vowel.
any(v_gen) any(v_pseudo) any(v_gen) any(v_pseudo) > context(3) context(4) c a two-part vowel rotates to another two-part vowel.
any(v_pseudo) any(v_pseudo) > context(2) c a pseudo vowel rotates to another pseudo vowel.
c any(v_pseudo) any(v_gen) > context(2) c a pseudo vowel roates to a genuine vowel.
c Delete extraneous coeng markers
U+17D2 U+17D2 > context(1) c no two coengs are allowed.
U+17D2 any(v_any) > context(2) c The coeng is deleted when a vowel is put after it.
U+17D2 any(v_gen) any(v_pseudo) > context(2) context(3) c The coeng is deleted when a pseudo vowel is put after it.
c Case #1: when a subscript is typed after a vowel, the subscript should be automatically moved to before the vowel.
any(v_any) U+17D2 any(subcons) > context(2) context(3) context(1) c a vowel has to come after a subscript.
any(v_combo_N) U+17C6 U+17D2 any(subcons) > context(3) context(4) context(1) context(2) c these two-part vowels have to come after a subscript.
any(v_combo_R) U+17C7 U+17D2 any(subcons) > context(3) context(4) context(1) context(2) c these two-part vowels have to come after a subscript.
c a subscript placed after a shifter and a vowel (a genuine/pseudo vowel or a two-part vowel) has to move to before them.
any(c_shifter) any(v_any) U+17D2 any(subcons) > context(3) context(4) context(1) context(2) c after a genuine/pseudo vowel
any(c_shifter) any(v_combo_N) U+17C6 U+17D2 any(subcons) > context(4) context(5) context(1) context(2) context(3) c after a two-part vowel
any(c_shifter) any(v_combo_R) U+17C7 U+17D2 any(subcons) > context(4) context(5) context(1) context(2) context(3) c after a two-part vowel
c Case #2: when [D2+9A] is typed before a subscript, it should be moved to after it.
U+17D2 U+178A U+17D2 U+179A > context(1) U+178F context(3) context(4) c change subscript ្ដ to ្ត after ្រ
U+17D2 U+179A U+17D2 U+178A > context(3) U+178F context(1) context(2) c ្ដ placed after ្រ has to be changed to ្ត and placed before ្រ
U+17D2 U+178A any(v_any) U+17D2 U+179A > context(1) U+178F context(4) context(5) context(3) c fixed 6e
U+17D2 U+178A any(v_combo_N) U+17C6 U+17D2 U+179A > context(1) U+178F context(5) context(6) context(3) context(4)
U+17D2 U+178A any(v_combo_R) U+17C7 U+17D2 U+179A > context(1) U+178F context(5) context(6) context(3) context(4)
U+17D2 U+179A any(v_any) U+17D2 U+178A > context(1) U+178F context(4) context(2) context(3) c fixed 6g
U+17D2 U+179A any(v_combo_N) U+17C6 U+17D2 U+178A > context(1) U+178F context(5) context(2) context(3) context(4)
U+17D2 U+179A any(v_combo_R) U+17C7 U+17D2 U+178A > context(1) U+178F context(5) context(2) context(3) context(4)
U+17D2 U+179A any(c_shifter) U+17D2 any(subcons) > context(4) context(5) context(1) context(2) context(3) c when a shifter intervenes, move it to the end.
U+17D2 U+179A U+17D2 any(subcons) > context(3) context(4) context(1) context(2) c move any subscript placed after [D2+DA] to before them
U+17D2 U+179A any(v_any) U+17D2 any(subcons) > context(4) context(5) context(1) context(2) context(3) c when a vowel intervenes, move it to the end
U+17D2 U+179A any(v_combo_N) U+17C6 U+17D2 any(subcons) > context(5) context(6) context(1) context(2) context(3) context(4) c when a two-part vowel intervenes, move it to the end too.
U+17D2 U+179A any(v_combo_R) U+17C7 U+17D2 any(subcons) > context(5) context(6) context(1) context(2) context(3) context(4) c (same as stated right above)
c when a subscript is placed after a consonant shifter, they should be reversed. (Case #3)
any(c_shifter) U+17D2 any(subcons) > context(2) context(3) context(1)
c Reordering - Triisap and Muusikatoan store before a vowel and a vowelcomboR and pseudo vowel.
any(v_any) any(c_shifter) > context(2) context(1) c place either a enguine or pseudo vowel after a consonant shifter
any(v_gen) any(v_pseudo) any(c_shifter) > context(3) context(1) context(2) c place a two-part vowel after a consonant shifter(i.e. មោះ៉ )
any(c_shifter) any(v_any) any(c_shifter) > context(3) context(2) c when either a genuine or pseudo vowel is in between two consonant shifters, replace the one on the left with the one on the right.
any(c_shifter) any(v_gen) any(v_pseudo) any(c_shifter) > context(2) context(3) context(4) c when a two-part vowel is in between two consonants shifter, replace the one on the left with the one the right.
c +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
c + when [BB] is place either before or after an above vowel (ិ ី ឹ ឺ ើ and ាំ) and a Samyok Sannya (Case #4) +
c + the [BB] should transform to a respective consonant shifter placed before the vowel. +
c + the consonant shifter is a Trisap if the base consonant is ស ហ អ +
c + the consonant shifter is a Muusikatoan if the base consonant is ង ញ ម យ រ វ ប and ន ល +
c +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
c consonant clusters --1st > 2nd series (with U+17BB)
any(c_combo_QA) U+17D2 U+17A2 U+17BB any(v_above) > context(1) context(2) context(3) U+17CA context(5)
any(c_combo_QA) U+17D2 U+17A2 U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
any(c_combo_QA) U+17D2 U+17A2 any(v_above) U+17BB > context(1) context(2) context(3) U+17CA context(4)
any(c_combo_QA) U+17D2 U+17A2 U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(6) context(7)
U+179B U+17D2 any(c_1st_combo_LO) U+17BB any(v_above) > context(1) context(2) context(3) U+17CA context(5)
U+179B U+17D2 any(c_1st_combo_LO) U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
U+179B U+17D2 any(c_1st_combo_LO) any(v_above) U+17BB > context(1) context(2) context(3) U+17CA context(4)
U+179B U+17D2 any(c_1st_combo_LO) U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(6) context(7)
U+1798 U+17D2 any(c_1st_combo_MO) U+17BB any(v_above) > context(1) context(2) context(3) U+17CA context(5)
U+1798 U+17D2 any(c_1st_combo_MO) U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
U+1798 U+17D2 any(c_1st_combo_MO) any(v_above) U+17BB > context(1) context(2) context(3) U+17CA context(4)
U+1798 U+17D2 any(c_1st_combo_MO) U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(6) context(7)
U+179F U+17D2 U+1794 U+17BB any(v_above) > context(1) context(2) context(3) U+17C9 context(5) c prevent ស្បុិ from transforming to ស្ប៊ិ​​​, but produce ស្ប៉ិ instead because ប never have Triisap shifted down.
U+179F U+17D2 U+1794 U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17C9 context(5) context(6)
U+179F U+17D2 U+1794 any(v_above) U+17BB > context(1) context(2) context(3) U+17C9 context(4)
U+179F U+17D2 U+1794 U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17C9 context(6) context(7)
U+179F U+17D2 any(c_combo_SA) U+17BB any(v_above) > context(1) context(2) context(3) U+17CA context(5)
U+179F U+17D2 any(c_combo_SA) U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
U+179F U+17D2 any(c_combo_SA) any(v_above) U+17BB > context(1) context(2) context(3) U+17CA context(4)
U+179F U+17D2 any(c_combo_SA) U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(6) context(7)
any(c_combo_HA) U+17D2 U+17A0 U+17BB any(v_above) > context(1) context(2) context(3) U+17CA context(5)
any(c_combo_HA) U+17D2 U+17A0 U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
any(c_combo_HA) U+17D2 U+17A0 any(v_above) U+17BB > context(1) context(2) context(3) U+17CA context(4)
any(c_combo_HA) U+17D2 U+17A0 U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(6) context(7)
U+17A2 U+17D2 U+1784 U+17BB any(v_above) > context(1) context(2) context(3) U+17CA context(5)
U+17A2 U+17D2 U+1784 U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
U+17A2 U+17D2 U+1784 any(v_above) U+17BB > context(1) context(2) context(3) U+17CA context(4)
U+17A2 U+17D2 U+1784 U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(6) context(7)
U+17A2 U+17D2 U+179C U+17BB any(v_above) > context(1) context(2) context(3) U+17CA context(5)
U+17A2 U+17D2 U+179C U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
U+17A2 U+17D2 U+179C any(v_above) U+17BB > context(1) context(2) context(3) U+17CA context(4)
U+17A2 U+17D2 U+179C U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(6) context(7)
U+17A0 U+17D2 U+1794 U+17BB any(v_above) > context(1) context(2) context(3) U+17C9 context(5) c change U+17BB preceded by HA and coeng BA to U+17C9 before the above vowel
U+17A0 U+17D2 U+1794 U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17C9 context(3) context(6) c change U+17BB preceded by HA and coeng BA to U+17C9 before ាំ
U+17A0 U+17D2 U+1794 any(v_above) U+17BB > context(1) context(2) context(3) U+17C9 context(4) c change U+17BB preceded by HA and coeng BA to U+17C9 before the above vowel
U+17A0 U+17D2 U+1794 U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17C9 context(6) context(7) c change U+17BB preceded by HA and coeng BA to U+17C9 before U+17B6 U+17C6
U+17A0 U+17D2 any(shiftable_c_2nd_with_BA) U+17BB any(v_above) > context(1) context(2) context(3) U+17CA context(5) c change U+17BB preceded by HA and coeng shiftable_c_2nd to U+17CA before the above vowel
U+17A0 U+17D2 any(shiftable_c_2nd_with_BA) U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(3) context(6) c change U+17BB preceded by HA and coeng shiftable_c_2nd to U+17CA before ាំ
U+17A0 U+17D2 any(shiftable_c_2nd_with_BA) any(v_above) U+17BB > context(1) context(2) context(3) U+17CA context(4) c change U+17BB preceded by HA and coeng shiftable_c_2nd to U+17CA before the above vowel
U+17A0 U+17D2 any(shiftable_c_2nd_with_BA) U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(6) context(7) c change U+17BB preceded by HA and coeng shiftable_c_2nd to U+17CA before U+17B6 U+17C6
c consonant clusters --2nd > 1st series (with U+17BB)
U+179B U+17D2 any(c_2nd_combo_LO) U+17BB any(v_above) > context(1) context(2) context(3) U+17C9 context(5)
U+179B U+17D2 any(c_2nd_combo_LO) U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17C9 context(5) context(6)
U+179B U+17D2 any(c_2nd_combo_LO) any(v_above) U+17BB > context(1) context(2) context(3) U+17C9 context(4)
U+179B U+17D2 any(c_2nd_combo_LO) U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17C9 context(6) context(7)
U+1798 U+17D2 any(c_2nd_combo_MO) U+17BB any(v_above) > context(1) context(2) context(3) U+17C9 context(5)
U+1798 U+17D2 any(c_2nd_combo_MO) U+17BB U+17B6 U+17C6 > context(1) context(2) context(3) U+17C9 context(5) context(6)
U+1798 U+17D2 any(c_2nd_combo_MO) any(v_above) U+17BB > context(1) context(2) context(3) U+17C9 context(4)
U+1798 U+17D2 any(c_2nd_combo_MO) U+17BB U+17C6 U+17B6 U+17C6 > context(1) context(2) context(3) U+17C9 context(6) context(7)
c single consonants --1st > 2nd series (with U+17BB)
any(shiftable_c_1st) U+17BB any(v_above) > context(1) U+17CA context(3) c change U+17BB to U+17CA before the above vowel
any(shiftable_c_1st) U+17BB U+17B6 U+17C6 > context(1) U+17CA context(3) context(4) c change U+17BB to U+17CA before ាំ
any(shiftable_c_1st) any(v_above) U+17BB > context(1) U+17CA context(2) c change U+17BB to U+17CA and place it before the above vowel
any(shiftable_c_1st) U+17BB U+17C6 U+17B6 U+17C6 > context(1) U+17CA context(4) context(5) c change U+17BB U+17C6 to U+17CA
c single consonants --2nd > 1st series (with U+17BB)
any(shiftable_c_2nd_with_BA) U+17BB any(v_above) > context(1) U+17C9 context(3) c change U+17BB to U+17C9 before the above vowel
any(shiftable_c_2nd_with_BA) U+17BB U+17B6 U+17C6 > context(1) U+17C9 context(3) context(4) c change U+17BB to U+17C9 before ាំ
any(shiftable_c_2nd_with_BA) any(v_above) U+17BB > context(1) U+17C9 context(2) c change U+17BB to U+17C9 before the above vowel
any(shiftable_c_2nd_with_BA) U+17BB U+17C6 U+17B6 U+17C6 > context(1) U+17C9 context(4) context(5) c change U+17BB U+17C6 to U+17C9
c prevent incorrect use of consonant shifter with 2nd series consonant clusters (without U+17BB)
U+179B U+17D2 any(c_2nd_combo_LO) U+17CA any(v_above) > context(1) context(2) context(3) U+17C9 context(5)
U+1798 U+17D2 any(c_2nd_combo_MO) U+17CA any(v_above) > context(1) context(2) context(3) U+17C9 context(5)
U+179B U+17D2 any(c_2nd_combo_LO) U+17CA any(v_gen) any(v_pseudo) > context(1) context(2) context(3) U+17C9 context(5) context(6)
U+1798 U+17D2 any(c_2nd_combo_MO) U+17CA any(v_gen) any(v_pseudo) > context(1) context(2) context(3) U+17C9 context(5) context(6)
c single consonants --2nd > 1st series (without U+17BB)
any(shiftable_c_2nd) U+17CA any(v_above) > context(1) U+17C9 context(3) c change U+17CA to U+17C9 before the above vowel
any(shiftable_c_2nd) U+17CA any(v_gen) any(v_pseudo) > context(1) U+17C9 context(3) context(4) c change U+17CA to U+17C9 before ាំ
U+17D2 any(shiftable_c_2nd) U+17CA any(v_above) > context c stop ស្រ៊ី from becoming ស្រ៉ី
U+17D2 any(shiftable_c_2nd) U+17CA any(v_gen) any(v_pseudo) > context
c SPECIAL CASE -- the following clusters may accept either of the two consonant shifters.
U+1794 U+17D2 U+1799 any(c_shifter) > context
U+179F U+17D2 U+1794 any(c_shifter) > context
U+1786 U+17D2 U+1794 any(c_shifter) > context
U+1794 U+17D2 U+1799 any(c_shifter) > context
U+179F U+17D2 U+1794 any(c_shifter) > context
U+1786 U+17D2 U+1794 any(c_shifter) > context
c single --1st > 2nd series (without U+17BB)
any(shiftable_c_1st) U+17C9 any(v_above) > context(1) U+17CA context(3) c change U+17C9 to U+17CA before the above vowel
any(shiftable_c_1st) U+17C9 any(v_gen) any(v_pseudo) > context(1) U+17CA context(3) context(4) c change U+17C9 to U+17CA before ាំ
c consonant clusters --1st > 2nd series (without U+17BB)
any(c_combo_QA) U+17D2 U+17A2 U+17C9 any(v_above) > context(1) context(2) context(3) U+17CA context(5)
U+179B U+17D2 any(c_1st_combo_LO) U+17C9 any(v_above) > context(1) context(2) context(3) U+17CA context(5)
U+1798 U+17D2 any(c_1st_combo_MO) U+17C9 any(v_above) > context(1) context(2) context(3) U+17CA context(5)
U+179F U+17D2 U+1794 U+17C9 any(v_above) > context(1) context(2) context(3) U+17C9 context(5) c prevent ស្ប៉ី from transforming to ស្ប៊ី
U+179F U+17D2 any(c_combo_SA) U+17C9 any(v_above) > context(1) context(2) context(3) U+17CA context(5)
any(c_combo_HA) U+17D2 U+17A0 U+17C9 any(v_above) > context(1) context(2) context(3) U+17CA context(5)
U+17A2 U+17D2 U+1784 U+17C9 any(v_above) > context(1) context(2) context(3) U+17CA context(5)
U+17A2 U+17D2 U+179C U+17C9 any(v_above) > context(1) context(2) context(3) U+17CA context(5)
any(c_combo_QA) U+17D2 U+17A2 U+17C9 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
U+179B U+17D2 any(c_1st_combo_LO) U+17C9 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
U+1798 U+17D2 any(c_1st_combo_MO) U+17C9 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
U+179F U+17D2 U+1794 U+17C9 U+17B6 U+17C6 > context(1) context(2) context(3) U+17C9 context(5) context(6) c prevent ស្ប៉ាំ from transforming to ស្ប៊ាំ
U+179F U+17D2 any(c_combo_SA) U+17C9 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
any(c_combo_HA) U+17D2 U+17A0 U+17C9 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
U+17A2 U+17D2 U+1784 U+17C9 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
U+17A2 U+17D2 U+179C U+17C9 U+17B6 U+17C6 > context(1) context(2) context(3) U+17CA context(5) context(6)
c U+17B6 U+17BB and U+17C6 in that order preceeded by any first series consonant (cluster) are transformed to U+17CA U+17B6 and U+17C6
any(shiftable_c_1st) U+17B6 U+17BB U+17C6 > context(1) U+17CA context(2) context(4)
any(shiftable_c_1st) U+17BB U+17C6 U+17B6 > context(1) U+17CA context(4) context(3)
any(c_combo_QA) U+17D2 U+17A2 U+17B6 U+17BB U+17C6 > context(1) context(2) context(3) U+17CA context(4) context(6)
any(c_combo_QA) U+17D2 U+17A2 U+17BB U+17C6 U+17B6 > context(1) context(2) context(3) U+17CA context(6) context(5)
U+179B U+17D2 any(c_1st_combo_LO) U+17B6 U+17BB U+17C6 > context(1) context(2) context(3) U+17CA context(4) context(6)
U+179B U+17D2 any(c_1st_combo_LO) U+17BB U+17C6 U+17B6 > context(1) context(2) context(3) U+17CA context(6) context(5)
U+1798 U+17D2 any(c_1st_combo_MO) U+17B6 U+17BB U+17C6 > context(1) context(2) context(3) U+17CA context(4) context(6)
U+1798 U+17D2 any(c_1st_combo_MO) U+17BB U+17C6 U+17B6 > context(1) context(2) context(3) U+17CA context(6) context(5)
U+179F U+17D2 any(c_combo_SA) U+17B6 U+17BB U+17C6 > context(1) context(2) context(3) U+17CA context(4) context(6)
U+179F U+17D2 any(c_combo_SA) U+17BB U+17C6 U+17B6 > context(1) context(2) context(3) U+17CA context(6) context(5)
any(c_combo_HA) U+17D2 U+17A0 U+17B6 U+17BB U+17C6 > context(1) context(2) context(3) U+17CA context(4) context(6)
any(c_combo_HA) U+17D2 U+17A0 U+17BB U+17C6 U+17B6 > context(1) context(2) context(3) U+17CA context(6) context(5)
U+17A2 U+17D2 U+1784 U+17B6 U+17BB U+17C6 > context(1) context(2) context(3) U+17CA context(4) context(6)
U+17A2 U+17D2 U+1784 U+17BB U+17C6 U+17B6 > context(1) context(2) context(3) U+17CA context(6) context(5)
U+17A2 U+17D2 U+179C U+17B6 U+17BB U+17C6 > context(1) context(2) context(3) U+17CA context(4) context(6)
U+17A2 U+17D2 U+179C U+17BB U+17C6 U+17B6 > context(1) context(2) context(3) U+17CA context(4) context(6)
c U+17B6 U+17BB and U+17C6 in that order preceeded by any second series consonant (cluster) are transformed to U+17C9 U+17B6 and U+17C6
any(shiftable_c_2nd_with_BA) U+17B6 U+17BB U+17C6 > context(1) U+17C9 context(2) context(4)
any(shiftable_c_2nd_with_BA) U+17BB U+17C6 U+17B6 > context(1) U+17C9 context(4) context(3)
U+179B U+17D2 any(c_2nd_combo_LO) U+17B6 U+17BB U+17C6 > context(1) context(2) context(3) U+17C9 context(4) context(6)
U+179B U+17D2 any(c_2nd_combo_LO) U+17BB U+17C6 U+17B6 > context(1) context(2) context(3) U+17C9 context(6) context(5)
U+1798 U+17D2 any(c_2nd_combo_MO) U+17B6 U+17BB U+17C6 > context(1) context(2) context(3) U+17C9 context(4) context(6)
U+1798 U+17D2 any(c_2nd_combo_MO) U+17BB U+17C6 U+17B6 > context(1) context(2) context(3) U+17C9 context(6) context(5)
c េុី/ុេី/៉េី > ៊ើ
any(shiftable_c_1st) U+17C1 U+17BB U+17B8 > context(1) U+17CA U+17BE c cancel out by vowel rotation
any(shiftable_c_1st) U+17BB U+17C1 U+17B8 > context(1) U+17CA U+17BE c cancel out by vowel rotation
any(shiftable_c_1st) U+17C9 U+17C1 U+17B8 > context(1) U+17CA U+17BE
any(c_combo_QA) U+17D2 U+17A2 U+17C1 U+17BB U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
any(c_combo_QA) U+17D2 U+17A2 U+17BB U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
any(c_combo_QA) U+17D2 U+17A2 U+17C9 U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE
U+179B U+17D2 any(c_1st_combo_LO) U+17C1 U+17BB U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
U+179B U+17D2 any(c_1st_combo_LO) U+17BB U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
U+179B U+17D2 any(c_1st_combo_LO) U+17C9 U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE
U+1798 U+17D2 any(c_1st_combo_MO) U+17C1 U+17BB U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
U+1798 U+17D2 any(c_1st_combo_MO) U+17BB U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
U+1798 U+17D2 any(c_1st_combo_MO) U+17C9 U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE
U+179F U+17D2 any(c_combo_SA) U+17C1 U+17BB U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
U+179F U+17D2 any(c_combo_SA) U+17BB U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
U+179F U+17D2 any(c_combo_SA) U+17C9 U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE
any(c_combo_HA) U+17D2 U+17A0 U+17C1 U+17BB U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
any(c_combo_HA) U+17D2 U+17A0 U+17BB U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
any(c_combo_HA) U+17D2 U+17A0 U+17C9 U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE
U+17A2 U+17D2 U+1784 U+17C1 U+17BB U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
U+17A2 U+17D2 U+1784 U+17BB U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
U+17A2 U+17D2 U+1784 U+17C9 U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE
U+17A2 U+17D2 U+179C U+17C1 U+17BB U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
U+17A2 U+17D2 U+179C U+17BB U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE c cancel out by vowel rotation
U+17A2 U+17D2 U+179C U+17C9 U+17C1 U+17B8 > context(1) context(2) context(3) U+17CA U+17BE
c េុី/ុេី/៊េី > ៉ើ
any(shiftable_c_2nd) U+17C1 U+17BB U+17B8 > context(1) U+17C9 U+17BE c cancel out by vowel rotation
any(shiftable_c_2nd) U+17BB U+17C1 U+17B8 > context(1) U+17C9 U+17BE c cancel out by vowel rotation
any(shiftable_c_2nd) U+17CA U+17C1 U+17B8 > context(1) U+17C9 U+17BE
U+179B U+17D2 any(c_2nd_combo_LO) U+17C1 U+17BB U+17B8 > context(1) context(2) context(3) U+17C9 U+17BE c cancel out by vowel rotation
U+179B U+17D2 any(c_2nd_combo_LO) U+17BB U+17C1 U+17B8 > context(1) context(2) context(3) U+17C9 U+17BE c cancel out by vowel rotation
U+179B U+17D2 any(c_2nd_combo_LO) U+17CA U+17C1 U+17B8 > context(1) context(2) context(3) U+17C9 U+17BE
U+1798 U+17D2 any(c_2nd_combo_MO) U+17C1 U+17BB U+17B8 > context(1) context(2) context(3) U+17C9 U+17BE c cancel out by vowel rotation
U+1798 U+17D2 any(c_2nd_combo_MO) U+17BB U+17C1 U+17B8 > context(1) context(2) context(3) U+17C9 U+17BE c cancel out by vowel rotation
U+1798 U+17D2 any(c_2nd_combo_MO) U+17CA U+17C1 U+17B8 > context(1) context(2) context(3) U+17C9 U+17BE
c place Robat after a base before any vowel
any(c_out) any(v_gen) U+17CC > context(1) context(3) context(2)
c Use appropriate identical subscript in two separate contexts. (Case #8)
U+178E U+17D2 U+178F > context(1) context(2) U+178A
U+1793 U+17D2 U+178A > context(1) context(2) U+178F
c BONUSES​(note that these combinations are invalid and they can't be found in the CN dictionary 1967 nor RH 1997, but users think they are)
U+1791 U+17D2 U+1794 > U+17A1 c ទ្ប transforms to ឡ
U+1794 U+17D2 U+1789 > U+17AB c ប្ញ transforms to ឫ
U+17AB U+17BB > U+17AC c ឫុ transforms to ឬ
U+17AD U+17B6 > U+1789 c ឭា transforms to ញ
U+17AE U+17B6 > U+1789 c ឮា transforms to ញ
U+1796 U+17D2 U+1789 > U+17AD c ព្ញ transforms to ឭ
U+17AD U+17BB > U+17AE c ឭុ transforms to ឮ
U+1796 U+17D2 U+178B > U+17B0 c ព្ឋ transforms to ឰ
U+17A7 U+17B7 > U+17B1 c ឧិ transforms to ឱ
U+17A7 U+17CC > U+17B1 c ឧ៌ transforms to ឱ
U+17A7 U+17CD > U+17B1 c ឧ៍ transforms to ឱ
c U+17D4 U+179B U+17D4 > U+17D8 c ។ល។ transforms to U+17D8
U+178A U+17D2 U+1792 > U+178A U+17D2 U+178B c ដ្ធ > ដ្ឋ
U+1791 U+17D2 U+178B > U+1791 U+17D2 U+1792 c ទ្ឋ > ទ្ធ
c additional rules
U+1796 U+1793 U+17D2 U+178B > context(1) context(2) context(3) U+1792 c ពន្ឋ > ពន្ធ
U+1796 U+17D0 U+1793 U+17D2 U+178B > context(1) context(2) context(3) context(4) U+1792 c ព័ន្ឋ > ព័ន្ធ
U+17AA U+17D2 U+1799 > U+17B1 context(2) context(3) c ឪ្យ > ឱ្យ
U+17B3 U+17D2 U+1799 > U+17B1 context(2) context(3) c ឳ្យ > ឱ្យ
U+1789 U+17D2 U+179C > U+1796 context(2) context(3) U+17B6 c ញ្វ (as in សញ្វវុធ) > ព្វា as in សញ្វវុធ
U+17D2 U+1799 U+17C1 U+17BA > U+17BF c េ្យឺ > ឿ
U+17D2 U+1799 U+17C1 U+17B9 > U+17BF c េ្យឹ > ឿ
U+17D2 U+1799 U+17C1 U+17B8 > U+17BF c េ្យី > ឿ
U+17D2 U+1799 U+17D2 any(c_out) U+17C1 U+17BA > context(3) context(4) U+17BF c គ្រេ្យឺង > គ្រឿង
U+17D2 U+1799 U+17D2 any(c_out) U+17C1 U+17B9 > context(3) context(4) U+17BF c គ្រេ្យឹង > គ្រឿង
U+17D2 U+1799 U+17D2 any(c_out) U+17C1 U+17B8 > context(3) context(4) U+17BF c គ្រេ្យីង > គ្រឿង
c In everyday practice, both 'regular space' and 'no-break space' are not used before punctuation, i.e. ។ ! etc.
c An annoying issue of having line broken up at the end of the line dropping the punctuation (។, ! and also ៗ) to the next line.
c It may be helpful to include a rule where a 'Zero Width No-Break Space' should be output together with those punct.
c ref: https://www.unicode.org/L2/L2020/20008-core-text.pdf
c ref: https://www.unicode.org/versions/Unicode12.0.0/ch23.pdf#G12985
c ref: https://www.compart.com/en/unicode/U+FEFF
c EOF

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<Package>
<System>
<KeymanDeveloperVersion>15.0.266.0</KeymanDeveloperVersion>
<FileVersion>7.0</FileVersion>
</System>
<Options>
<ExecuteProgram></ExecuteProgram>
<ReadMeFile>readme.htm</ReadMeFile>
<GraphicFile>splash.gif</GraphicFile>
<MSIFileName></MSIFileName>
<MSIOptions></MSIOptions>
<FollowKeyboardVersion/>
</Options>
<StartMenu>
<Folder></Folder>
<Items/>
</StartMenu>
<Info>
<Name URL="">Khmer Angkor</Name>
<Copyright URL="">© 2015-2022 SIL International</Copyright>
<Author URL="mailto:makara_sok@sil.org">Makara Sok</Author>
<Version URL=""></Version>
<WebSite URL="https://keyman.com/keyboards/khmer_angkor">https://keyman.com/keyboards/khmer_angkor</WebSite>
<Description>Khmer Angkor</Description>
</Info>
<Files>
<File>
<Name>..\build\khmer_angkor.js</Name>
<Description>File khmer_angkor.js</Description>
<CopyLocation>0</CopyLocation>
<FileType>.js</FileType>
</File>
<File>
<Name>..\build\khmer_angkor.kvk</Name>
<Description>File khmer_angkor.kvk</Description>
<CopyLocation>0</CopyLocation>
<FileType>.kvk</FileType>
</File>
<File>
<Name>..\build\khmer_angkor.kmx</Name>
<Description>Keyboard Khmer Angkor</Description>
<CopyLocation>0</CopyLocation>
<FileType>.kmx</FileType>
</File>
</Files>
<Keyboards>
<Keyboard>
<Name>Khmer Angkor</Name>
<ID>khmer_angkor</ID>
<Version>1.3</Version>
<Languages>
<Language ID="km">Central Khmer (Khmer, Cambodia)</Language>
</Languages>
</Keyboard>
</Keyboards>
<Strings/>
</Package>

View file

@ -0,0 +1,206 @@
<?xml version="1.0" encoding="utf-8"?>
<visualkeyboard>
<header>
<version>10.0</version>
<kbdname>khmer_angkor</kbdname>
<flags>
<usealtgr/>
</flags>
</header>
<encoding name="unicode" fontname="Khmer Busra Kbd" fontsize="16">
<layer shift="RA">
<key vkey="K_B">ឞ</key>
<key vkey="K_K">ឝ</key>
<key vkey="K_QUOTE">ៈ</key>
<key vkey="K_RBRKT">ឳ</key>
<key vkey="K_T">ឨ</key>
<key vkey="K_LBRKT">ឩ</key>
<key vkey="K_P">ឰ</key>
<key vkey="K_R">ឫ</key>
<key vkey="K_I">ឦ</key>
<key vkey="K_O">ឱ</key>
<key vkey="K_E">ឯ</key>
<key vkey="K_3"></key>
<key vkey="K_W"></key>
<key vkey="K_Q">ៜ</key>
<key vkey="K_EQUAL"></key>
<key vkey="K_COLON">៖</key>
<key vkey="K_6">៙</key>
<key vkey="K_7">៚</key>
<key vkey="K_M"></key>
<key vkey="K_L">៘</key>
<key vkey="K_1">‌</key>
<key vkey="K_BKQUOTE">‍</key>
<key vkey="K_U">]</key>
<key vkey="K_Y">[</key>
<key vkey="K_SLASH">/</key>
<key vkey="K_PERIOD">.</key>
<key vkey="K_H">‘</key>
<key vkey="K_A">+</key>
<key vkey="K_V">&amp;</key>
<key vkey="K_J">’</key>
<key vkey="K_8">*</key>
<key vkey="K_2">@</key>
<key vkey="K_BKSLASH">\</key>
<key vkey="K_0">}</key>
<key vkey="K_9">{</key>
<key vkey="K_S">-</key>
<key vkey="K_F">÷</key>
<key vkey="K_G">:</key>
<key vkey="K_COMMA">,</key>
<key vkey="K_HYPHEN">≈</key>
<key vkey="K_N">;</key>
<key vkey="K_Z">&lt;</key>
<key vkey="K_C">#</key>
<key vkey="K_X">&gt;</key>
<key vkey="K_D">×</key>
<key vkey="K_4">$</key>
<key vkey="K_5">€</key>
<key vkey="K_SPACE"> </key>
</layer>
<layer shift="SRA">
<key vkey="K_8">៸</key>
<key vkey="K_0">៰</key>
<key vkey="K_1">៱</key>
<key vkey="K_2">៲</key>
<key vkey="K_3">៳</key>
<key vkey="K_4">៴</key>
<key vkey="K_5">៵</key>
<key vkey="K_6">៶</key>
<key vkey="K_7">៷</key>
<key vkey="K_9">៹</key>
<key vkey="K_PERIOD">᧿</key>
<key vkey="K_COMMA">᧾</key>
<key vkey="K_LBRKT">᧪</key>
<key vkey="K_RBRKT">᧫</key>
<key vkey="K_QUOTE">᧶</key>
<key vkey="K_COLON">᧵</key>
<key vkey="K_A">᧬</key>
<key vkey="K_Z">᧷</key>
<key vkey="K_Y">᧥</key>
<key vkey="K_X">᧸</key>
<key vkey="K_W">᧡</key>
<key vkey="K_B">᧻</key>
<key vkey="K_C">᧹</key>
<key vkey="K_D">᧮</key>
<key vkey="K_E">᧢</key>
<key vkey="K_F">᧯</key>
<key vkey="K_G">᧰</key>
<key vkey="K_U">᧦</key>
<key vkey="K_H">᧱</key>
<key vkey="K_T">᧤</key>
<key vkey="K_S">᧭</key>
<key vkey="K_R">᧣</key>
<key vkey="K_I">᧧</key>
<key vkey="K_Q">᧠</key>
<key vkey="K_V">᧺</key>
<key vkey="K_J">᧲</key>
<key vkey="K_K">᧳</key>
<key vkey="K_L">᧴</key>
<key vkey="K_P">᧩</key>
<key vkey="K_O">᧨</key>
<key vkey="K_N">᧼</key>
<key vkey="K_M">᧽</key>
</layer>
<layer shift="">
<key vkey="K_SPACE">​</key>
<key vkey="K_QUOTE"></key>
<key vkey="K_COMMA"></key>
<key vkey="K_HYPHEN">ឥ</key>
<key vkey="K_PERIOD">។</key>
<key vkey="K_SLASH"></key>
<key vkey="K_0">០</key>
<key vkey="K_1">១</key>
<key vkey="K_2">២</key>
<key vkey="K_3">៣</key>
<key vkey="K_4">៤</key>
<key vkey="K_5">៥</key>
<key vkey="K_6">៦</key>
<key vkey="K_7">៧</key>
<key vkey="K_8">៨</key>
<key vkey="K_9">៩</key>
<key vkey="K_COLON"></key>
<key vkey="K_EQUAL">ឲ</key>
<key vkey="K_LBRKT"></key>
<key vkey="K_BKSLASH">ឮ</key>
<key vkey="K_RBRKT">ឪ</key>
<key vkey="K_BKQUOTE">«</key>
<key vkey="K_A"></key>
<key vkey="K_B">ប</key>
<key vkey="K_C">ច</key>
<key vkey="K_D">ដ</key>
<key vkey="K_E"></key>
<key vkey="K_F">ថ</key>
<key vkey="K_G">ង</key>
<key vkey="K_H">ហ</key>
<key vkey="K_I"></key>
<key vkey="K_J"></key>
<key vkey="K_K">ក</key>
<key vkey="K_L">ល</key>
<key vkey="K_M">ម</key>
<key vkey="K_N">ន</key>
<key vkey="K_O"></key>
<key vkey="K_P">ផ</key>
<key vkey="K_Q">ឆ</key>
<key vkey="K_R">រ</key>
<key vkey="K_S">ស</key>
<key vkey="K_T">ត</key>
<key vkey="K_U"></key>
<key vkey="K_V">វ</key>
<key vkey="K_W"></key>
<key vkey="K_X">ខ</key>
<key vkey="K_Y">យ</key>
<key vkey="K_Z">ឋ</key>
</layer>
<layer shift="S">
<key vkey="K_SPACE"></key>
<key vkey="K_1">!</key>
<key vkey="K_QUOTE"></key>
<key vkey="K_3">"</key>
<key vkey="K_4">៛</key>
<key vkey="K_5">%</key>
<key vkey="K_7"></key>
<key vkey="K_9">(</key>
<key vkey="K_0">)</key>
<key vkey="K_8"></key>
<key vkey="K_EQUAL">=</key>
<key vkey="K_COLON"></key>
<key vkey="K_PERIOD">៕</key>
<key vkey="K_SLASH">?</key>
<key vkey="K_2">ៗ</key>
<key vkey="K_A"></key>
<key vkey="K_B">ព</key>
<key vkey="K_C">ជ</key>
<key vkey="K_D">ឌ</key>
<key vkey="K_E"></key>
<key vkey="K_F">ធ</key>
<key vkey="K_G">អ</key>
<key vkey="K_H">ះ</key>
<key vkey="K_I"></key>
<key vkey="K_J">ញ</key>
<key vkey="K_K">គ</key>
<key vkey="K_L">ឡ</key>
<key vkey="K_M"></key>
<key vkey="K_N">ណ</key>
<key vkey="K_O"></key>
<key vkey="K_P">ភ</key>
<key vkey="K_Q">ឈ</key>
<key vkey="K_R">ឬ</key>
<key vkey="K_S"></key>
<key vkey="K_T">ទ</key>
<key vkey="K_U"></key>
<key vkey="K_V"></key>
<key vkey="K_W"></key>
<key vkey="K_X">ឃ</key>
<key vkey="K_Y"></key>
<key vkey="K_Z">ឍ</key>
<key vkey="K_6"></key>
<key vkey="K_HYPHEN"></key>
<key vkey="K_LBRKT"></key>
<key vkey="K_BKSLASH">ឭ</key>
<key vkey="K_RBRKT">ឧ</key>
<key vkey="K_BKQUOTE">»</key>
<key vkey="K_COMMA"></key>
</layer>
</encoding>
</visualkeyboard>

View file

@ -0,0 +1,3 @@
This placeholder file represents keymandesktop.msi, the Windows Installer package for Keyman for Windows.
(Not including the real file to reduce file size in repo, as it is not necessary in order to complete the unit test)

View file

@ -0,0 +1,3 @@
This placeholder file represents license.txt.
(Not including the real file to reduce file size in repo, as it is not necessary in order to complete the unit test)

View file

@ -0,0 +1,3 @@
This placeholder file represents setup-redist.exe, the sfx loader.
(Not including the real file to reduce file size in repo, as it is not necessary in order to complete the unit test)

View file

@ -162,7 +162,7 @@ gtestcompilertest = executable('gtest-compiler-tests', 'gtest-compiler.tests.cpp
include_directories: inc,
name_suffix: name_suffix,
link_args: links + tests_links,
objects: lib.extract_all_objects(),
objects: lib.extract_all_objects(recursive: false),
dependencies: [ icuuc_dep, gtest_dep, gmock_dep ],
)
@ -173,7 +173,7 @@ gtest_km_u16_test = executable('gtest-km_u16-tests', 'gtest-km_u16.tests.cpp',
include_directories: inc,
name_suffix: name_suffix,
link_args: links + tests_links,
objects: lib.extract_all_objects(),
objects: lib.extract_all_objects(recursive: false),
dependencies: [ icuuc_dep, gtest_dep, gmock_dep ],
)

View file

@ -2,3 +2,4 @@
c = ['$EMSCRIPTEN_BASE/emcc']
cpp = ['$EMSCRIPTEN_BASE/em++']
ar = ['$EMSCRIPTEN_BASE/emar']
strip = ['$EMSCRIPTEN_BASE/emstrip']

View file

@ -2,3 +2,4 @@
c = ['$EMSCRIPTEN_BASE/emcc']
cpp = ['$EMSCRIPTEN_BASE/em++']
ar = ['$EMSCRIPTEN_BASE/emar']
strip = ['$EMSCRIPTEN_BASE/emstrip']

View file

@ -2,3 +2,4 @@
c = ['python.exe', '$EMSCRIPTEN_BASE/emcc.py']
cpp = ['python.exe', '$EMSCRIPTEN_BASE/em++.py']
ar = ['python.exe', '$EMSCRIPTEN_BASE/emar.py']
strip = ['python.exe', '$EMSCRIPTEN_BASE/emstrip.py']

View file

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

View file

@ -113,7 +113,17 @@ function installer_server() {
# monorepo context
cat package.json | "$JQ" \
'. | del(.devDependencies."@keymanapp/resources-gosh") | del(.devDependencies."@keymanapp/keyman-version") | del(.dependencies."@keymanapp/developer-utils")' \
> "$PRODBUILDTEMP/package0.json"
# #12950: Merge dependencies from @keymanapp/developer-utils and Server; this
# is a bit messy but will mean we don't lose the needed dependencies.
"$JQ" -s '.[0] + { dependencies: (.[1].dependencies * .[0].dependencies) }' "$PRODBUILDTEMP/package0.json" \
"$KEYMAN_ROOT/developer/src/common/web/utils/package.json" > "$PRODBUILDTEMP/package1.json"
cat $PRODBUILDTEMP/package1.json | "$JQ" \
'. | del(.dependencies."@keymanapp/common-types")' \
> "$PRODBUILDTEMP/package.json"
rm "$PRODBUILDTEMP/package0.json"
rm "$PRODBUILDTEMP/package1.json"
pushd "$PRODBUILDTEMP"
npm install --omit=dev --omit=optional

View file

@ -369,6 +369,8 @@ type
FLayoutSetup: Boolean;
FCheckboxGridHelper: TCheckboxGridHelper;
FDisplayMapFont: TFont;
function GetCurrentRule: TKeyboardParser_LayoutRule;
procedure EnableControls;
@ -475,6 +477,7 @@ type
procedure OrderDetailsPanels;
function ValidateFileHasNoUnicodeCharacters(sw: WideString): Boolean;
procedure UpdateQRCode;
procedure SetDisplayMapFont(const Value: TFont);
protected
function GetHelpTopic: string; override;
@ -495,6 +498,8 @@ type
function ShouldRememberFocus(Control: TWinControl): Boolean; override; // I4679
property DisplayMapFont: TFont read FDisplayMapFont write SetDisplayMapFont;
public
procedure StartDebugging(FStartTest: Boolean = False);
procedure StopDebugging;
@ -630,6 +635,8 @@ procedure TfrmKeymanWizard.FormCreate(Sender: TObject);
begin
FLoading := True;
FDisplayMapFont := TFont.Create;
//SetWindowTheme(pages.Handle, 'Explorer', '');
cmdAddToProject.Caption := '&Add to Project';
@ -681,6 +688,7 @@ begin
FreeAndNil(FKeyboardParser); // I4557
FreeAndNil(FDebugForm);
FreeAndNil(FCheckboxGridHelper);
FreeAndNil(FDisplayMapFont);
UninitSystemKeyboard;
end;
@ -1825,6 +1833,15 @@ begin
Delete(Result.Size, Length(Result.Size)-1, 2);
end;
end;
kfontDisplayMap:
begin
Result.Enabled := FKeyboardParser.GetSystemStoreValue(ssDisplayMap) <> '';
if Result.Enabled then
begin
Result.Name := DisplayMapFont.Name;
Result.Size := IntToStr(TFontUtils.FontSizeInPoints(DisplayMapFont.Name, DisplayMapFont.Size)); // I4872
end;
end;
end;
end;
@ -2535,6 +2552,8 @@ begin
FFont.Free;
end;
end;
SetFontFromString(DisplayMapFont, ProjectFile.IDEState['DisplayMapFont']);
end;
procedure TfrmKeymanWizard.SaveSettings(SaveProject: Boolean);
@ -2548,6 +2567,7 @@ begin
else ProjectFile.IDEState['DebugDefaultFont'] := '0'; // I4702
ProjectFile.IDEState['DebugFont'] := FontAsString(FDebugForm.memo.Font); // I4702
ProjectFile.IDEState['DisplayMapFont'] := FontAsString(DisplayMapFont);
inherited;
end;
@ -2781,6 +2801,13 @@ begin
TUtilExecute.URL(lbDebugHosts.Items[lbDebugHosts.ItemIndex]);
end;
procedure TfrmKeymanWizard.SetDisplayMapFont(const Value: TFont);
begin
FDisplayMapFont.Assign(Value);
if ProjectFile <> nil then
ProjectFile.IDEState['DisplayMapFont'] := FontAsString(DisplayMapFont);
end;
procedure TfrmKeymanWizard.SetFontInfo(Index: TKeyboardFont; const Value: TKeyboardFontInfo); // I4057
var
f: TFont;
@ -2814,6 +2841,11 @@ begin
CharFont.Name := Value.Name;
CharFont.Size := StrToIntDef(Value.Size, 12);
end;
kfontDisplayMap:
begin
DisplayMapFont.Name := Value.Name;
DisplayMapFont.Size := StrToIntDef(Value.Size, 12);
end;
kfontOSK:
begin
if FKeyboardParser.Features.ContainsKey(kfOSK) then // I4258

View file

@ -2,11 +2,11 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
BorderIcons = [biSystemMenu]
BorderStyle = bsDialog
Caption = 'Keyboard Fonts'
ClientHeight = 291
ClientHeight = 322
ClientWidth = 512
Position = poScreenCenter
ExplicitWidth = 518
ExplicitHeight = 320
ExplicitHeight = 351
PixelsPerInch = 96
TextHeight = 13
object lblDevEnv: TLabel
@ -147,6 +147,20 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
Height = 13
Caption = 'em'
end
object lblDisplayMap: TLabel
Left = 32
Top = 248
Width = 109
Height = 13
Caption = '&displayMap target font'
end
object Label4: TLabel
Left = 463
Top = 248
Width = 14
Height = 13
Caption = 'em'
end
object fcbCode: TscFontComboBox
Left = 176
Top = 41
@ -260,23 +274,23 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
end
object cmdOK: TButton
Left = 162
Top = 248
Top = 280
Width = 91
Height = 25
Caption = 'OK'
Default = True
TabOrder = 12
TabOrder = 14
OnClick = cmdOKClick
end
object cmdCancel: TButton
Left = 259
Top = 248
Top = 280
Width = 90
Height = 25
Cancel = True
Caption = 'Cancel'
ModalResult = 2
TabOrder = 13
TabOrder = 15
end
object fcbTouchLayoutPhone: TscFontComboBox
Left = 176
@ -347,4 +361,34 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
ShowPreviewFontName = False
ShowPreviewInList = False
end
object editDisplayMapSize: TEdit
Left = 391
Top = 245
Width = 66
Height = 21
TabOrder = 13
end
object fcbDisplayMap: TscFontComboBox
Left = 176
Top = 245
Width = 209
Height = 24
DropDownCount = 12
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -13
Font.Name = 'Tahoma'
Font.Style = []
ItemHeight = 18
ParentFont = False
TabOrder = 12
MoveUsedToTop = False
ColorUsed = clWindow
PreviewWidth = 240
PreviewHeight = 60
FontTypes = [ftTrueTypeAnsi, ftTrueTypeSymbol]
ShowPreview = False
ShowPreviewFontName = False
ShowPreviewInList = False
end
end

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