Merge pull request #13090 from keymanapp/chore/merge-master-into-kmc-convert

chore: merge master into kmc-convert 😎
This commit is contained in:
Marc Durdin 2025-01-31 08:44:05 +07:00 • committed by GitHub
commit 75de2c27e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
238 changed files with 5465 additions and 699 deletions

View file

@ -1,5 +1,98 @@
# Keyman Version History
## 18.0.180 alpha 2025-01-29
* chore(common): Update crowdin strings for Portuguese (#13056)
* chore(common): Update crowdin strings for Spanish (Latin America) (#13044)
* chore(linux): Update debian changelog (#13063)
* chore(linux): improve compatibility with Gentoo Linux (#13070)
## 18.0.179 alpha 2025-01-28
* fix(android): improve resource-update tool handling of host Activity's closure (#13004)
* fix(windows): bool return value of KeymanIsTextSelected (#12841)
* chore(mac): add breadcrumbs for Sentry (#12939)
* fix(mac): show and hide OSK consistently for all keyboards (#12988)
* fix(linux): remove unnecessary files from source tarball (#13053)
* fix(windows): handle keyboard package not downloaded (#12948)
## 18.0.178 alpha 2025-01-27
* fix(web): fix handling of data: url fonts, fonts with single quotes in filename (#13032)
* fix(android): disable early keyboard-loading when not feasible (#13001)
* fix(web): add null guard for focusing next element via keystroke (#13013)
* chore: adjust one bookmark link in windows doc (#13050)
* chore: adjust bookmark links in developer docs (#13049)
## 18.0.177 alpha 2025-01-24
* fix(android): restores DOMRect polyfill for old Chrome support (#13012)
* fix(developer): produce correct index parameter value for context in kmw compiler (#13003)
* chore: add platform to what's new titles (#12989)
* chore: add platforms to welcome titles (#12990)
* chore: add platforms to version history titles (#13016)
* chore: add platforms to troubleshooting titles and windows to help homepage (#13019)
* chore: add platforms to system requirements and download-and-install docs (#13024)
* chore: add mac platform to titles (#13027)
* chore: add linux platform to titles (#13028)
* chore: add ios platform to titles (#13031)
* fix(developer): escape font facename in touch layout editor (#13020)
* fix: use correct path for resources/build/version (#13029)
* fix(developer): don't list .js keyboards for multi-keyboard packages in kmc-keyboard-info (#13034)
* fix(web): do not simplify nul-prefixed contexts when rule-matching (#12998)
* feat(android): add breadcrumbing during package install about package file, source (#13021)
* fix(linux): correct permissions and paths in Linux docker image (#13008)
## 18.0.176 alpha 2025-01-23
* chore(developer): stub out icon support in kmc-generate (#12971)
* chore(developer): remove '--rename' option from kmc-copy (#12973)
* feat(developer): add 'verbose' and 'debug' log levels to kmc (#12976)
* chore(developer): validate output folder name in kmc-copy (#12979)
* chore(developer): report on filename collisions in kmc-copy (#12982)
* feat(developer): support copying disorganized projects in kmc-copy (#12984)
* chore(linux): docker build: update package index before installing packages in Dockerfiles (#12992)
* feat(web): allow main manual-test page to load from KMP (#12977)
* fix(android): Remove access to MEDIA storage permissions (#12999)
## 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)
@ -953,7 +1046,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)
@ -1149,6 +1242,31 @@
* chore(common): move to 18.0 alpha (#10713)
* chore: move to 18.0 alpha
## 17.0.334 stable 2025-01-27
* chore(linux): update copyright year (#12919)
* chore(linux): Update debian changelog (#12916)
* fix(android): Remove access to MEDIA storage permissions (#13005)
* fix(android): restores DOMRect polyfill for old Chrome support (#13014)
* fix(web): add null guard for focusing next element via keystroke (#13035)
* fix: use correct path for resources/build/version (#13030)
* fix(web): fix handling of fonts with single quotes in filename (#13041)
* fix(android): disable early keyboard-loading when not feasible (#13039)
* fix(web): do not simplify nul-prefixed contexts when rule-matching (#13040)
* feat(android): add breadcrumbing during package install about package file, source (#13038)
* fix(web): prevent multitap warning that occurs multitapping longpressable modifier keys (#12974)
## 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.170
18.0.181

View file

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

View file

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

View file

@ -23,6 +23,7 @@ import androidx.appcompat.app.AppCompatActivity;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.KMManager;
import com.keyman.engine.util.KMLog;
import com.keyman.engine.util.KMPLink;
import com.keyman.engine.util.KMString;
import com.keyman.engine.util.WebViewUtils;
@ -105,6 +106,7 @@ public class KMPBrowserActivity extends BaseActivity {
// Create intent with keyboard download link for KMAPro main activity to handle
Intent intent = new Intent(context, MainActivity.class);
intent.setData(downloadURI);
KMLog.LogBreadcrumb(TAG, "Successful keyboard search now triggering package install", true);
startActivity(intent);
// Finish activity

View file

@ -262,6 +262,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
if (loadingIntentUri != null) {
String scheme = loadingIntentUri.getScheme().toLowerCase();
final String link = loadingIntentUri.toString();
switch (scheme) {
// content:// Android DownloadManager
// file:// Chrome downloads and Filebrowsers
@ -269,6 +270,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
case "file":
requestPermissionIntentUri = loadingIntentUri;
if (CheckPermissions.isPermissionOK(this)) {
KMLog.LogBreadcrumb(TAG, "Installing local KMP: " + link, true);
useLocalKMP(context, loadingIntentUri);
} else {
CheckPermissions.requestPermission(this, context);
@ -277,14 +279,16 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
case "http" :
case "https" :
// Might need to modify link like KMPBrowserActivity
String link = loadingIntentUri.toString();
if (KMPLink.isKeymanInstallLink(link)) {
loadingIntentUri = KMPLink.getKeyboardDownloadLink(link);
}
// Is usually triggered by KMPBrowserActivity
KMLog.LogBreadcrumb(TAG, "Installing via HTTPS intent: " + link, true);
downloadKMP(loadingIntentUri, KmpInstallMode.Full);
break;
case "keyman" :
// TODO: Only accept download links from Keyman browser activities when universal links work
KMLog.LogBreadcrumb(TAG, "Installing via keyman-link intent: " + link, true);
if (KMPLink.isKeymanDownloadLink(loadingIntentUri.toString())) {
// Convert opaque URI to hierarchical URI so the query parameters can be parsed
@ -296,7 +300,6 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
loadingIntentUri = Uri.parse(builder.build().toString());
downloadKMP(loadingIntentUri, KmpInstallMode.Full);
} else if (KMPLink.isLegacyKeymanDownloadLink(loadingIntentUri.toString())) {
link = loadingIntentUri.toString();
loadingIntentUri = KMPLink.getLegacyKeyboardDownloadLink(link);
downloadKMP(loadingIntentUri, KmpInstallMode.Full);
} else {

View file

@ -53,6 +53,7 @@
<string name="request_storage_permission" comment="Request storage permission to access keyboard packages"> Para instalar los paquetes del teclado, permite 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"> Falló la solicitud de permiso de almacenamiento. Seleccione 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 -->
@ -67,6 +68,8 @@
<!-- Context: Keyman Settings menu -->
<string name="adjust_keyboard_height" comment="Menu action to adjust keyboard height">Ajustar la altura del teclado</string>
<!-- Context: Keyman Settings menu -->
<string name="adjust_longpress_delay" comment="Menu action to adjust longpress delay duration">Adjust longpress delay</string>
<!-- Context: Keyman Settings menu -->
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">Leyenda de barra espaciadora</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_keyboard" comment="Spacebar caption option - keyboard">Teclado</string>
@ -86,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">A implementar</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, solo se muestra 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 fallas a través de la red</string>
@ -122,6 +125,14 @@
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Rota el dispositivo para ajustar el modo vertical y horizontal</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Restaurar valores predeterminados</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Delay Time: %1$.1f seconds</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_up" comment="Make longpress delay longer">Delay time longer</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_down" comment="Make longpress delay shorter">Delay time shorter</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_longpress_delay_slider" comment="Adjust longpress delay time">Longpress delay time slider</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

@ -68,6 +68,8 @@
<!-- Context: Keyman Settings menu -->
<string name="adjust_keyboard_height" comment="Menu action to adjust keyboard height">Ajustar altura do teclado</string>
<!-- Context: Keyman Settings menu -->
<string name="adjust_longpress_delay" comment="Menu action to adjust longpress delay duration">Ajustar atraso do toque longo</string>
<!-- Context: Keyman Settings menu -->
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">Barra de espaço</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_keyboard" comment="Spacebar caption option - keyboard">Teclado</string>
@ -87,11 +89,11 @@
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">Não mostrar legenda na barra de espaço</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
<string name="haptic_feedback" comment="haptic feedback on key press">Vibrar ao tocar</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner" comment="text suggestions banner">Sempre mostrar 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">A ser 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">Quando desativado, apenas mostrado quando o texto preditivo é ativado</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report" comment="permission for sending crash reports">Permitir o envio de relatórios de erro pela rede</string>
@ -123,6 +125,14 @@
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Gire o dispositivo para ajustar o retrato e a paisagem</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Restaurar padrões</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Tempo de atraso: %1$.1f segundos</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_up" comment="Make longpress delay longer">Tempo de atraso maior</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_down" comment="Make longpress delay shorter">Tempo de atraso menor</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_longpress_delay_slider" comment="Adjust longpress delay time">Atraso do tempo com toque longo</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_text" comment="Prompt to type in the browser search bar">Pesquise ou digite um endereço</string>
<!-- Context: Keyman Web Browser -->

View file

@ -37,7 +37,20 @@ function init() {
keyman.beepKeyboard = beepKeyboard;
// Readies the keyboard stub for instant loading during the init process.
KeymanWeb.registerStub(JSON.parse(jsInterface.initialKeyboard()));
try {
let starterStubStr = jsInterface.initialKeyboard();
if(starterStubStr) {
KeymanWeb.registerStub(JSON.parse(starterStubStr));
}
} catch (err) {
// If a Java exception triggers during this time... okay, we can't
// load a keyboard during startup. Rely on older loading techniques.
//
// Note: no information about the Java-side error is available here.
if(err.message.indexOf("Java exception") < 0) {
throw err;
}
}
keyman.init({
'embeddingApp':device,

View file

@ -64,7 +64,7 @@ public class DisplayLanguages {
new DisplayLanguageType("ann", "Obolo"),
new DisplayLanguageType("pl-PL", "Polski (Polish)"),
new DisplayLanguageType("el", "Polytonic Greek"),
new DisplayLanguageType("pt-PT", "Português do Portugal"),
new DisplayLanguageType("pt-PT", "Português de Portugal"),
new DisplayLanguageType("ff-ZA", "Pulaar-Fulfulde"), // or Fulah
new DisplayLanguageType("ru-RU", "Pyccĸий (Russian)"),
new DisplayLanguageType("shu-latn", "Shuwa (Latin)"),

View file

@ -75,7 +75,10 @@ public class KMKeyboardJSHandler {
Keyboard kbd = Keyboard.getDefaultKeyboard(context);
int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, -1);
if (index >= 0) {
kbd = KMManager.getKeyboardInfo(this.context, index);
// It is possible for this method to return `null` - especially
// if the KeyboardController instance hasn't yet been initialized.
Keyboard lastKbd = KMManager.getKeyboardInfo(this.context, index);
kbd = lastKbd != null ? lastKbd : kbd;
}
return kbd.toStub(context);
}

View file

@ -921,6 +921,9 @@ public final class KMManager {
copyAsset(context, KMFilename_KmwCss, "", true);
copyAsset(context, KMFilename_KmwGlobeHintCss, "", true);
copyAsset(context, KMFilename_Osk_Ttf_Font, "", true);
// Needed until our minimum version of Chrome is 61.0+.
copyAsset(context, KMFilename_JSPolyfill2, "", true);
// Copy default keyboard font
copyAsset(context, KMDefault_KeyboardFont, "", true);
@ -2267,6 +2270,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

@ -520,10 +520,6 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl
@Override
public void onPackageInstalled(List<Map<String, String>> keyboardsInstalled) {
if (!isContextAvailable()) {
KMLog.LogError(TAG, "onPackageInstalled where context is null.");
}
if (! openUpdates.isEmpty() && keyboardsInstalled != null) {
for (Map<String, String> k : keyboardsInstalled) {
String _langid = k.get(KMManager.KMKey_LanguageID);
@ -537,10 +533,6 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl
@Override
public void onLexicalModelInstalled(List<Map<String, String>> lexicalModelsInstalled) {
if (!isContextAvailable()) {
KMLog.LogError(TAG, "onLexicalModelInstalled where context is null.");
}
if (! openUpdates.isEmpty()) {
for(Map<String,String> _lm:lexicalModelsInstalled) {
String _langid = _lm.get(KMManager.KMKey_LanguageID);
@ -566,8 +558,9 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl
// TODO: make it smoother. Documented as #11097.
KMManager.clearKeyboardCache();
Context appContext = currentContext.getApplicationContext();
if (failedUpdateCount > 0) {
BaseActivity.makeToast(currentContext, R.string.update_failed, Toast.LENGTH_SHORT);
BaseActivity.makeToast(appContext, R.string.update_failed, Toast.LENGTH_SHORT);
lastUpdateCheck = Calendar.getInstance();
updateFailed = true;
checkingUpdates = false;

View file

@ -447,6 +447,9 @@ public class PackageProcessor {
public String getPackageTarget(File kmpPath) {
try {
File tempPath = unzipKMP(kmpPath);
String kmpFilename = kmpPath.getName();
KMLog.LogBreadcrumb(TAG, "Determining package type for " + kmpFilename, false);
String target = getPackageTarget(loadPackageInfo(tempPath));
FileUtils.deleteDirectory(tempPath);
return target;

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; 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">Error al descargar</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 correctamente!</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 borrado</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

@ -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 correções</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 previsões</string>
<!-- Context: Language Settings menu strings - radio buttons -->
<string name="suggestions_radio_0" comment="Predictions and corrections disabled">Disable suggestions</string>
<string name="suggestions_radio_1" comment="Suggestions Enabled: Predictions with no corrections">Predictions only</string>
<string name="suggestions_radio_2" comment="Suggestions Enabled: Predictions with corrections">Predictions with corrections</string>
<string name="suggestions_radio_3" comment="Suggestions Enabled: Predictions with auto-corrections">Predictions with auto-corrections</string>
<!-- Context: Language Settings menu strings -->
<string name="model_label">Dicionário</string>
<plurals name="model_count">

View file

@ -1,5 +1,5 @@
---
title: What's New
title: What's New in Keyman Engine 18.0 for Android
---
## Keyman Engine for Android Breaking Changes: ##

View file

@ -1,5 +1,5 @@
---
title: Version History
title: Version History of Keyman for Android
---
A frequent updated changelog for Keyman is kept at [help.keyman.com/version-history](https://help.keyman.com/version-history/) on the Keyman Help website.

View file

@ -1,5 +1,5 @@
---
title: Welcome to Keyman
title: Welcome to Keyman for Android
---
Thank you for installing Keyman for Android. Whether you're a new or returning user, we believe you will appreciate the significant enhancements in this latest version of Keyman for Android.

View file

@ -1,5 +1,5 @@
---
title: What's New
title: What's New in Keyman 18.0 for Android
---
Here are some of the new features we have added to Keyman 18.0 for Android:

View file

@ -1,5 +1,5 @@
---
title: How To - Download and Install a Keyman Keyboard
title: How To - Download and Install a Keyman Keyboard on Android
---
## Adding New Keyboards

View file

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

View file

@ -1,5 +1,5 @@
---
title: Troubleshooting
title: Keyman for Android Troubleshooting
---
* [How To - Getting Keyboard Help](keyboard-help)

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

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

@ -23,16 +23,16 @@ the specification itself.
There are three ways - all of them optional - to extend and customize
the word-breaking rules themselves:
* If you need to prevent splits in very specific scenarios and/or add splits in other specific scenarios, you may specify [context-based rules](#rules) to obtain the desired behavior.
* If you need to prevent splits in very specific scenarios and/or add splits in other specific scenarios, you may specify [context-based rules](#toc-custom-word-breaking-rules) to obtain the desired behavior.
* If certain characters are not handled appropriately for their role in
your language, you may [map characters](#map) to different
your language, you may [map characters](#toc-character-property-remapping) to different
word-breaking character classes - including custom ones. This will
override the default property they are assigned by the default
implementation, with the new property applying for all word-breaking
rules.
* If the default word-breaking classes from the specification are
too general for certain aspects of your language, it is possible to
[define custom character classes](#define) for use in custom
[define custom character classes](#toc-defining-and-using-new-word-breaking-properties) for use in custom
rules.
## A first example
@ -183,7 +183,7 @@ the potential boundary.
### Word-breaking property names
The names used in each array must be defined in one of the following places:
* https://unicode.org/reports/tr29/#Table_Word_Break_Property_Values
* `customProperties` - your [declaration of any custom property types](#define)
* `customProperties` - your [declaration of any custom property types](#toc-defining-and-using-new-word-breaking-properties)
* One of the special property types `"Other"`, `"sot"`, or `"eot"`:
* `Other`: a character without an associated word-breaking property value
* `sot`: "start of text" - a marker indicating the beginning of the string being word-broken
@ -277,7 +277,7 @@ As noted at the top of the file:
### Redefining character properties
Of note from [our first example](#example):
Of note from [our first example](#toc-a-first-example):
```typescript
/*** Character class overrides for specific characters ***/
@ -324,7 +324,7 @@ words (and/or names) in some languages. Default word-breaking behavior will spl
hyphenated words and names apart, but by changing the property of hyphens, it is
possible to disable this behavior.
Noting [rule WB6](#WB6) and WB7, the `MidLetter` class is designed to prevent
Noting [rule WB6](#toc-custom-word-breaking-rules) and WB7, the `MidLetter` class is designed to prevent
word-breaks from occurring when its characters lie directly between letters -
hence the property name. Assigning hyphens to this class can provide the
desired behavior.
@ -364,7 +364,7 @@ the second example above. (After all, `can'` could be the end of a quoted
phrase in English - `'sure you can'` - in which case we might want the split
to occur.)
Revisiting [an earlier example](#example) and simplifying a little bit:
Revisiting [an earlier example](#toc-redefining-character-properties) and simplifying a little bit:
```typescript
/*** Definition of extra word-breaking rules ***/
@ -440,7 +440,7 @@ included a couple of extra rules:
}
```
By replicating [WB6](#WB6) and WB7's structure and allowing `Hyphen` to match in the same
By replicating [WB6](#toc-custom-word-breaking-rules) and WB7's structure and allowing `Hyphen` to match in the same
position as `MidLetter` in the original rules, we can prevent word-breaking splits
after additional text has been typed after a `Hyphen`-property character. This does not
_replace_ the behavior of WB6 and WB7 - it merely _extends_ it to include the new property.

View file

@ -13,9 +13,9 @@ However, in languages written in other scripts — especially East Asian
scripts like Chinese, Japanese, Khmer, Lao, and Thai — there are no obvious break in between words. For these languages, there must be special rules for determining when words start and stop. This is what a _word breaker function_ is responsible for. It is a little bit of code that looks at some text to determine where the words are.
You can customize the word breaker in three ways:
- If your language uses its writing system in an unconventional way (e.g., use spaces to separate words in Thai, Lao, Burmese, or Khmer), you can [override the script's default behaviour](#overrides)
- If the default word breaker creates **too many splits**, you can [choose which strings join words together](#join).
- If the default word breaker creates **not enough splits**, you must [create your own word breaker function](#custom).
- If your language uses its writing system in an unconventional way (e.g., use spaces to separate words in Thai, Lao, Burmese, or Khmer), you can [override the script's default behaviour](#toc-overriding-script-defaults)
- If the default word breaker creates **too many splits**, you can [choose which strings join words together](#toc-customize-joining-rules).
- If the default word breaker creates **not enough splits**, you must [create your own word breaker function](#toc-writing-a-custom-word-breaker-function).
- Alternatively, you may choose to [customize and extend the wordbreaker's behavior](./unicode-breaker-extension) by adding extra rules and changing how it treats specific characters.
## Overriding script defaults

View file

@ -38,25 +38,25 @@ The kmp.json file is a base object described as
: `Object`
[`System`](#obj-system) object.
[`System`](#toc-the-system-object) object.
`options`
: `Object`
An [`Options`](#obj-options) object.
An [`Options`](#toc-the-options-object) object.
`startMenu`
: `Object`
[`Start Menu`](#obj-startMenu) object.
[`Start Menu`](#toc-the-start-menu-object) object.
`info`
: `Object`
[`Info`](#obj-info) object.
[`Info`](#toc-the-info-object) object.
`files`
@ -69,13 +69,13 @@ The kmp.json file is a base object described as
: `Object`
Array of [`Keyboard`](#obj-keyboard) objects.
Array of [`Keyboard`](#toc-the-keyboard-object) objects.
`lexicalModels`
: `Object`
Array of [`LexicalModel`](#obj-lexicalModel) objects.
Array of [`LexicalModel`](#toc-the-lexicalmodel-object) objects.
### The System object
@ -139,7 +139,7 @@ The `StartMenu` object is used by Keyman Desktop to install windows
: `Array`
An array of [Item](#obj-item) objects
An array of [Item](#toc-the-item-object) objects
### The Item Object
@ -229,7 +229,7 @@ The `Keyboard` object describes an individual keyboard in the Keyman package. A
: `Array`
An array of [`Language`](#obj-language) objects linked to the keyboard.
An array of [`Language`](#toc-the-language-object) objects linked to the keyboard.
`displayFont`
@ -247,7 +247,7 @@ The `Keyboard` object describes an individual keyboard in the Keyman package. A
: `Array`
An array of [`Example`](#obj-example) objects linked to the keyboard.
An array of [`Example`](#toc-the-example-object) objects linked to the keyboard.
### The Language object
@ -342,4 +342,4 @@ The `LexicalModel` object describes an individual model in the Keyman package. A
: `Array`
An array of [`Language`](#obj-language) objects linked to the model.
An array of [`Language`](#toc-the-language-object) objects linked to the model.

View file

@ -16,7 +16,7 @@ Details:
Spreadsheet programs such as Microsoft Excel and Google Sheets can
export into TSV format. TSVs can also be programmatically generated
from other data sources. For advanced users, see [File
Format](#file-format) for more details.
Format](#toc-file-format) for more details.
Distributed with lexical model:
: No. This is a development file and should not be distributed.

View file

@ -367,10 +367,13 @@ and the files in that folder will follow the
: The name of keyboard author (default: blank)
<!-- TODO-GENERATE: uncomment once supported
`-i, --icon`
: Include a generated icon. The icon will be a 16x16 pixel box with the first
letters of the first language tag (default: true, include an icon)
-->
`-d, --description <description>`

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

@ -28,11 +28,13 @@
namespace CompilerErrorSeverity {
// We use a namespace to stop the enum names leaking out into global namespace scope
enum {
Info = 0x000000, // Informational, not necessarily a problem
Hint = 0x100000, // Something the user might want to be aware of
Warn = 0x200000, // Warning: Not great, but we can keep going.
Error = 0x300000, // Severe error where we can't continue
Fatal = 0x400000, // OOM or should-not-happen internal problem
Debug = 0x000000, // log everything including internal debug
Verbose = 0x100000, // log everything, except debug
Info = 0x200000, // Informational, not necessarily a problem
Hint = 0x300000, // Something the user might want to be aware of
Warn = 0x400000, // Warning: Not great, but we can keep going.
Error = 0x500000, // Severe error where we can't continue
Fatal = 0x600000, // OOM or should-not-happen internal problem
};
};

View file

@ -2,7 +2,8 @@ import * as fs from 'fs';
import * as path from 'path';
import { CompilerEvent, CompilerCallbacks, CompilerPathCallbacks, CompilerFileSystemCallbacks,
CompilerError, CompilerNetAsyncCallbacks, DefaultCompilerFileSystemAsyncCallbacks,
CompilerFileSystemAsyncCallbacks } from '@keymanapp/developer-utils';
CompilerFileSystemAsyncCallbacks,
CompilerErrorSeverity} from '@keymanapp/developer-utils';
import { fileURLToPath } from 'url';
const { TEST_SAVE_FIXTURES } = process.env;
@ -27,6 +28,10 @@ export class TestCompilerCallbacks implements CompilerCallbacks {
this.messages = [];
}
filteredMessages(severity: CompilerErrorSeverity = CompilerErrorSeverity.Info) {
return this.messages.filter(m => CompilerError.severity(m.code) >= severity);
}
printMessages() {
if(this.messages.length) {
process.stdout.write(CompilerError.formatEvent(this.messages));

View file

@ -31,7 +31,7 @@ export function verifyCompilerMessagesObject(source: Record<string,any>, namespa
// Verify each object member matches the pattern we expect
if(typeof m[key] == 'function') {
const o = /^(Info|Hint|Warn|Error|Fatal)_([A-Za-z0-9_]+)$/.exec(key);
const o = /^(Debug|Verbose|Info|Hint|Warn|Error|Fatal)_([A-Za-z0-9_]+)$/.exec(key);
expect(o).to.be.instanceOf(Array, `Expected member ${key} to be a valid message function name`);
const c = o[1].toUpperCase() + '_' + o[2];
@ -41,7 +41,7 @@ export function verifyCompilerMessagesObject(source: Record<string,any>, namespa
expect(v.code).to.equal(m[c], `Function ${key} returns the wrong code`);
}
else if(typeof m[key] == 'number') {
const o = /^(INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key);
const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key);
expect(o).to.be.instanceOf(Array);
const f = toTitleCase(o[1]) + '_' + o[2];
@ -53,7 +53,7 @@ export function verifyCompilerMessagesObject(source: Record<string,any>, namespa
// Verify severify masks
if(typeof m[key] == 'number') {
const o = /^(INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key);
const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key);
expect(o).to.be.instanceOf(Array);
const mask = CompilerError.formatSeverity(m[key]).toUpperCase();

View file

@ -21,14 +21,18 @@ export interface CompilerEvent {
};
export enum CompilerErrorSeverity {
Info = 0x000000, // Informational, not necessarily a problem
Hint = 0x100000, // Something the user might want to be aware of
Warn = 0x200000, // Warning: Not great, but we can keep going.
Error = 0x300000, // Severe error where we can't continue
Fatal = 0x400000, // OOM or should-not-happen internal problem
Debug = 0x000000, // log everything including internal debug
Verbose = 0x100000, // log everything, except debug
Info = 0x200000, // Informational, not necessarily a problem
Hint = 0x300000, // Something the user might want to be aware of
Warn = 0x400000, // Warning: Not great, but we can keep going.
Error = 0x500000, // Severe error where we can't continue
Fatal = 0x600000, // OOM or should-not-happen internal problem
};
export const CompilerErrorSeverityValues = [
CompilerErrorSeverity.Debug,
CompilerErrorSeverity.Verbose,
CompilerErrorSeverity.Info,
CompilerErrorSeverity.Hint,
CompilerErrorSeverity.Warn,
@ -48,6 +52,8 @@ export enum CompilerErrorMask {
};
const errorSeverityName = {
[CompilerErrorSeverity.Debug]: 'debug',
[CompilerErrorSeverity.Verbose]: 'verbose',
[CompilerErrorSeverity.Info]: 'info',
[CompilerErrorSeverity.Hint]: 'hint',
[CompilerErrorSeverity.Warn]: 'warn',
@ -419,8 +425,9 @@ export const ALL_COMPILER_LOG_LEVELS = [
'error', /// Only errors emitted
'warn', /// Errors + warnings
'hint', /// Errors + warnings + hints
'info', /// All messages: errors + warnings + hints + info
'debug' /// All messages: errors + warnings + hints + info, plus debug logs
'info', /// All normal messages: errors + warnings + hints + info
'verbose', /// All messages + verbose logging
'debug', /// All messages + verbose + internal debug
] as const;
type CompilerLogLevelTuple = typeof ALL_COMPILER_LOG_LEVELS;
@ -432,7 +439,8 @@ export const compilerLogLevelToSeverity: {[index in CompilerLogLevel]: number} =
'warn': CompilerErrorSeverity.Warn,
'hint': CompilerErrorSeverity.Hint,
'info': CompilerErrorSeverity.Info,
'debug': CompilerErrorSeverity.Info
'verbose': CompilerErrorSeverity.Verbose,
'debug': CompilerErrorSeverity.Debug,
};
export const ALL_COMPILER_LOG_FORMATS = [

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

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

@ -39,6 +39,6 @@ builder_run_action api api-extractor run --local --verbose
# note: `export TEST_SAVE_ARTIFACTS=1` to save a copy of artifacts to temp path
# note: `export TEST_SAVE_FIXTURES=1` to get a copy of cloud-based fixtures saved to online/
# TODO: -skip-full
builder_run_action test builder_do_typescript_tests 70
builder_run_action test builder_do_typescript_tests 75
builder_run_action publish builder_publish_npm

View file

@ -4,7 +4,7 @@
* Copy a keyboard or lexical model project
*/
import { CloudUrls, GitHubUrls, CompilerCallbacks, CompilerLogLevel, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter } from "@keymanapp/developer-utils";
import { CloudUrls, GitHubUrls, CompilerCallbacks, CompilerLogLevel, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter, ValidIds } from "@keymanapp/developer-utils";
import { KeymanFileTypes } from "@keymanapp/common-types";
import { CopierMessages } from "./copier-messages.js";
@ -29,11 +29,6 @@ export interface CopierOptions /* not inheriting from CompilerBaseOptions */ {
* output path where project folder will be created
*/
outPath: string;
/*
* TODO-COPY rename: rename existing files rather than copy
rename: boolean;
*/
/**
* dryRun: show what would happen
*/
@ -125,7 +120,6 @@ export class KeymanProjectCopier implements KeymanCompiler {
this.asyncCallbacks = new CopierAsyncCallbacks(this.callbacks, this.githubRef);
// TODO-COPY: validate outputId is valid for the project type?
this.outputId = this.callbacks.path.basename(this.outPath);
this.sourceId = this.callbacks.path.basename(projectSource, KeymanFileTypes.Source.Project);
@ -136,6 +130,20 @@ export class KeymanProjectCopier implements KeymanCompiler {
return null;
}
if(project.isKeyboardProject()) {
// LDML and .kmn keyboards have the same id validity requirements, so
// we'll just test against .kmn
if(!ValidIds.isValidKeymanKeyboardId(this.outputId)) {
this.callbacks.reportMessage(CopierMessages.Error_InvalidKeyboardId({id: this.outputId}));
return null;
}
} else if(project.isLexicalModelProject()) {
if(!ValidIds.isValidLexicalModelId(this.outputId)) {
this.callbacks.reportMessage(CopierMessages.Error_InvalidLexicalModelId({id: this.outputId}));
return null;
}
}
const result: CopierResult = { artifacts: {
'kmc-copy:outputPath': {
data: null,
@ -302,7 +310,6 @@ export class KeymanProjectCopier implements KeymanCompiler {
};
private async copyFolder(inputPath: string, outputPath: string, ignorePatterns: (string | RegExp)[], result: CopierResult) {
// TODO-COPY: watch out for file collisions when copying project files -- after renames
const files = await this.asyncCallbacks.fsAsync.readdir(inputPath);
for(const {filename,type} of files) {
const fullPath = this.normalizePath(this.callbacks.path.join(inputPath, filename));
@ -312,9 +319,17 @@ export class KeymanProjectCopier implements KeymanCompiler {
if(type == 'dir') {
await this.copyFolder(fullPath, this.callbacks.path.join(outputPath, filename), ignorePatterns, result);
} else if(!result.artifacts[fullPath]) {
const filename = this.generateNewFilename(fullPath, outputPath);
for(const artifact of Object.keys(result.artifacts)) {
if(result.artifacts[artifact].filename == filename) {
this.callbacks.reportMessage(CopierMessages.Warn_FilenameCollides({filename}));
}
}
result.artifacts[fullPath] = {
data: await this.asyncCallbacks.fsAsync.readFile(fullPath),
filename: this.generateNewFilename(fullPath, outputPath)
filename
}
}
}
@ -365,7 +380,7 @@ export class KeymanProjectCopier implements KeymanCompiler {
return true;
}
private async copyGenericFile(_project: KeymanDeveloperProject, filename: string, outputPath: string, _source: string, result: CopierResult): Promise<boolean> {
private async copyGenericFile(project: KeymanDeveloperProject, filename: string, outputPath: string, _source: string, result: CopierResult): Promise<boolean> {
if(result.artifacts[filename]) {
return true;
}
@ -388,6 +403,10 @@ export class KeymanProjectCopier implements KeymanCompiler {
return false;
}
if(KeymanFileTypes.binaryTypeFromFilename(filename) != null) {
outputPath = this.callbacks.path.join(this.outPath, 'build');
}
result.artifacts[filename] = {
data,
filename: this.generateNewFilename(filename, outputPath)
@ -578,6 +597,7 @@ export class KeymanProjectCopier implements KeymanCompiler {
*/
private generateNewFilename(filename: string, outputPath: string): string {
const {ext, base} = this.splitFilename(filename);
const newFilename = base == this.sourceId
? this.callbacks.path.join(outputPath, this.outputId + ext)
: this.callbacks.path.join(outputPath, base + ext);

View file

@ -63,9 +63,10 @@ export class KeymanCloudSource {
}
public async downloadFolderFromGitHub(ref: GitHubRef): Promise<{filename:string,type:'dir'|'file'}[]> {
// this.callbacks.reportMessage(CopierMessages.Info_DownloadingFile({ref})); // TODO-COPY: verbose mode support
const url = `https://api.github.com/repos/${ref.owner}/${ref.repo}/contents/${ref.path}?ref=${ref.branch}`;
this.callbacks.reportMessage(CopierMessages.Verbose_DownloadingFolder({path:ref.path, url}));
let folder: any;
try {
folder = await this.callbacks.net.fetchJSON(url);
@ -94,6 +95,7 @@ export class KeymanCloudSource {
}
const url = `https://raw.githubusercontent.com/${ref.owner}/${ref.repo}/refs/heads/${ref.branch}${ref.path}`;
this.callbacks.reportMessage(CopierMessages.Verbose_DownloadingFile({filename:this.callbacks.path.basename(ref.path), url}));
try {
return await this.callbacks.net.fetchBlob(url);

View file

@ -7,6 +7,7 @@
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/developer-utils";
const Namespace = CompilerErrorNamespace.Copier;
const SevVerbose = CompilerErrorSeverity.Verbose | Namespace;
const SevInfo = CompilerErrorSeverity.Info | Namespace;
// const SevHint = CompilerErrorSeverity.Hint | Namespace;
const SevWarn = CompilerErrorSeverity.Warn | Namespace;
@ -198,4 +199,36 @@ export class CopierMessages {
the provided error details for more details.`
);
static VERBOSE_DownloadingFile = SevVerbose | 0x001C;
static Verbose_DownloadingFile = (o:{filename: string, url: string}) => m(
this.VERBOSE_DownloadingFile,
`Downloading '${def(o.filename)}' from '${def(o.url)}'`,
);
static VERBOSE_DownloadingFolder = SevVerbose | 0x001D;
static Verbose_DownloadingFolder = (o:{path: string, url: string}) => m(
this.VERBOSE_DownloadingFolder,
`Downloading folder '${def(o.path)}' from '${def(o.url)}'`,
);
// See also generator-messages.ERROR_InvalidKeymanKeyboardId
static ERROR_InvalidKeyboardId = SevError | 0x001E;
static Error_InvalidKeyboardId = (o:{id: string}) => m(
this.ERROR_InvalidKeyboardId,
`The specified keyboard id '${def(o.id)}' contains characters that are not permitted for a keyboard id or filename.`,
);
// See also generator-messages.ERROR_InvalidLexicalModelId
static ERROR_InvalidLexicalModelId = SevError | 0x001F;
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'.`,
);
static WARN_FilenameCollides = SevWarn | 0x0020;
static Warn_FilenameCollides = (o:{filename:string}) => m(
this.WARN_FilenameCollides,
`The output file '${def(o.filename)}' has two different possible source files.`,
);
};

View file

@ -0,0 +1,54 @@
/*
* Keyman is copyright (C) SIL International. MIT License.
*/
import 'mocha';
// import * as path from 'path';
// import * as fs from 'fs';
// import { fileURLToPath } from 'url';
import { assert } from 'chai';
import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
import { CompilerErrorNamespace } from '@keymanapp/developer-utils';
import { CopierMessages } from '../src/copier-messages.js';
import { KeymanProjectCopier } from '../src/KeymanProjectCopier.js';
import { makePathToFixture } from './helpers/index.js';
describe('CopierMessages', function () {
const callbacks = new TestCompilerCallbacks(makePathToFixture('online'));
this.beforeEach(function() {
callbacks.clear();
});
this.afterEach(function() {
if(this.currentTest?.isFailed()) {
callbacks.printMessages();
}
});
it('should have a valid CopierMessages object', function() {
return verifyCompilerMessagesObject(CopierMessages, CompilerErrorNamespace.Copier);
});
it('should generate ERROR_InvalidKeyboardId if the output keyboard id is invalid', async function() {
const copier = new KeymanProjectCopier();
assert.isTrue(await copier.init(callbacks, {dryRun: true, outPath: '0invalid'}))
assert.isNull(await copier.run('cloud:jawa'));
assert.isTrue(callbacks.hasMessage(CopierMessages.ERROR_InvalidKeyboardId));
});
it('should generate ERROR_InvalidLexicalModelId if the output lexical model id is invalid', async function() {
const copier = new KeymanProjectCopier();
assert.isTrue(await copier.init(callbacks, {dryRun: true, outPath: 'nrc.str-x-sencoten.sencoten'}))
assert.isNull(await copier.run('cloud:nrc.str.sencoten'));
assert.isTrue(callbacks.hasMessage(CopierMessages.ERROR_InvalidLexicalModelId));
});
it('should generate WARN_FilenameCollision if a copied file will collide', async function() {
const copier = new KeymanProjectCopier();
assert.isTrue(await copier.init(callbacks, {dryRun: true, outPath: 'collided'}))
const result = await copier.run(makePathToFixture('projects/collision/collision.kpj'));
assert.isNotNull(result);
assert.isTrue(callbacks.hasMessage(CopierMessages.WARN_FilenameCollides));
});
});

View file

@ -306,7 +306,6 @@ describe('KeymanProjectCopier', function() {
assert.isTrue(await copier.init(callbacks, {
dryRun: false,
outPath,
// TODO-COPY rename: false,
}));
const result = await copier.run(fullSource);
@ -316,7 +315,7 @@ describe('KeymanProjectCopier', function() {
//
assert.isOk(result);
assert.isEmpty(callbacks.messages);
assert.isEmpty(callbacks.filteredMessages());
if(TEST_SAVE_ARTIFACTS) {
assert.isTrue(await copier.write(result.artifacts));
@ -370,7 +369,6 @@ describe('KeymanProjectCopier', function() {
assert.isTrue(await copier.init(callbacks, {
dryRun: false,
outPath,
// TODO-COPY rename: false,
}));
// armenian_mnemonic selected because (a) small, and (b) has v2.0 project, so
@ -379,7 +377,7 @@ describe('KeymanProjectCopier', function() {
// We should have no messages and a successful result
assert.isOk(result);
assert.isEmpty(callbacks.messages);
assert.isEmpty(callbacks.filteredMessages());
// TODO-COPY: verify outcome using pattern above
@ -400,14 +398,13 @@ describe('KeymanProjectCopier', function() {
assert.isTrue(await copier.init(callbacks, {
dryRun: false,
outPath,
// TODO-COPY rename: false,
}));
const result = await copier.run('cloud:'+id);
// We should have no messages and a successful result
assert.isOk(result);
assert.isEmpty(callbacks.messages);
assert.isEmpty(callbacks.filteredMessages());
// TODO-COPY: verify outcome using pattern above
@ -492,8 +489,41 @@ describe('KeymanProjectCopier', function() {
});
}));
it('should copy a disorganized project into current structure', async function() {
const copier = new KeymanProjectCopier();
assert.isTrue(await copier.init(callbacks, {
dryRun: false,
outPath: 'organized',
}));
const result = await copier.run(makePathToFixture('projects','disorganized','alephwithbeth.kpj'));
assert.sameMembers(Object.keys(result.artifacts).map(k => result.artifacts[k].filename), [
'organized', // kmc-copy:outputPath
'organized/organized.kpj',
'organized/HISTORY.md',
'organized/LICENSE.md',
'organized/README.md',
// primary source files get moved to 'source/'
'organized/source/organized.kps',
'organized/source/organized.kmn',
'organized/source/organized.ico',
'organized/source/organized.kvks',
// files in 'other/' get moved to 'source/other/'
'organized/source/other/organized.keyman-touch-layout',
'organized/source/other/readme.htm',
'organized/source/other/welcome/welcome.htm',
'organized/source/other/welcome/KeyboardLayout.png',
'organized/source/other/welcome/KeyboardLayoutAlt.png',
'organized/source/other/welcome/KeyboardLayoutShift.png',
// build artifact files get moved to build/
'organized/build/organized.js',
'organized/build/organized.kvk',
'organized/build/organized.kmx',
// 'organized/build/organized.kmp', // TODO-COPY: do we need to copy this file as well?
]);
});
// TODO-COPY: additional tests
it.skip('should copy a disorganized project into current structure', async function() {});
it.skip('should copy a standalone .kmn into a new project', async function() {});
it.skip('should copy a standalone .kmn and .kps into a new project', async function() {});
});

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<KeymanDeveloperProject>
<Options>
<ProjectType>keyboard</ProjectType>
<Version>2.0</Version>
</Options>
</KeymanDeveloperProject>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,7 @@
store(&TARGETS) 'any'
store(&NAME) 'Collision'
store(&BITMAP) 'collision.ico'
begin Unicode > use(main)
group(main) using keys

View file

@ -0,0 +1,35 @@
alephwithbeth Change History
====================
1.0.6 (2024-06-11)
------------------
* Remove source files from package
1.0.5 (2023-02-28)
------------------
* replaced hyphen with maqef
* added a touch-optimized keyboard layout for phone
1.0.4 (2023-02-13)
------------------
* Removed unnecessary version numbers.
* added description
1.0.3 (2023-02-11)
------------------
* Removed the faulty auto correct feature.
* Captialized all words of the title.
* Added title to .kmn
1.0.2 (2023-02-09)
------------------
* Renamed keyboard to "Aleph with Beth"
1.0.1 (2023-02-08)
----------------
* Updated author and website (in kps)
* Added copyrights where needed.
1.0 (2023-01-28)
----------------
* Created by Cameron Briggs

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2023-2024 Cameron Briggs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,25 @@
alephwithbeth keyboard
==============
Description
-----------
A Hebrew keyboard designed to intuitively match the standard QWERTY layout.
Copyright
---------
See [LICENSE.md](LICENSE.md)
Supported Platforms
-------------------
* Windows
* macOS
* Linux
* Web
* iPhone
* iPad
* Android phone
* Android tablet
* Mobile devices
* Desktop devices
* Tablet devices

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,215 @@
store(&TARGETS) 'any'
store(&NAME) 'Aleph With Beth'
store(&BITMAP) 'alephwithbeth.ico'
store(&VISUALKEYBOARD) 'alephwithbeth.kvks'
store(letters-with-final-forms) "כמנפצ"
store(sofit-forms-of-letters) "ךםןףץ"
store(presentation-dageshed-letters-with-final-forms) "כּפּ"
store(presentation-dageshed-sofit-forms-of-letters) "ךּףּ"
store(dagesh) 'ּ'
c store(vowels) "ְֱֲֳִֵֶַׇׇָֹֻׅ"
store(normal-dageshed-letters-with-final-forms) "מּנּצּ"
store(normal-undageshed-sofit-forms-of-letters) "םןץ"
store(punctuation) U+0591 .. U+05AF U+05BD .. U+05BF U+05C0 U+05C4
store(whitespace) [K_SPACE] [K_ENTER] [K_TAB]
store(&KMW_RTL) '1'
store(&LAYOUTFILE) 'other\alephwithbeth.keyman-touch-layout'
store(&COPYRIGHT) '© Cameron Briggs'
store(&KEYBOARDVERSION) '1.0.6'
store(&KMW_HELPTEXT) 'Aleph With Beth'
begin Unicode > use(main)
group(main) using keys
+ [SHIFT K_I] > nul
+ [ALT K_BKQUOTE] > nul
+ [ALT K_B] > nul
+ [ALT K_V] > nul
+ [ALT K_C] > nul
+ [ALT K_Z] > nul
+ [ALT K_L] > nul
+ [ALT K_J] > nul
+ [ALT K_F] > nul
+ [ALT K_D] > nul
+ [ALT K_S] > nul
+ [ALT K_U] > nul
+ [ALT K_Y] > nul
+ [ALT K_T] > nul
+ [ALT K_R] > nul
+ [ALT K_W] > nul
+ [ALT K_Q] > nul
+ [SHIFT K_BKSLASH] > '|'
+ [SHIFT K_EQUAL] > '+'
+ [SHIFT K_HYPHEN] > '_'
+ [SHIFT K_0] > ')'
+ [SHIFT K_9] > '('
+ [SHIFT K_SLASH] > '?'
+ [SHIFT K_QUOTE] > '"'
+ [SHIFT K_BKQUOTE] > nul
+ [K_EQUAL] > '='
+ [K_HYPHEN] > '־'
+ [K_0] > '0'
+ [K_9] > '9'
+ [K_8] > '8'
+ [K_7] > '7'
+ [K_6] > '6'
+ [K_5] > '5'
+ [K_4] > '4'
+ [K_3] > '3'
+ [K_2] > '2'
+ [K_1] > '1'
+ [K_PERIOD] > '.'
+ [K_SLASH] > '/'
+ [K_QUOTE] > U+0027
+ [SHIFT K_COLON] > '״'
+ [ALT K_A] > 'ֲ'
+ [SHIFT K_RBRKT] > '}'
+ [SHIFT K_LBRKT] > '{'
+ [K_BKSLASH] > '\'
+ [K_RBRKT] > ']'
+ [K_LBRKT] > '['
+ [SHIFT K_R] > 'רּ'
+ [SHIFT CTRL K_SPACE] > ' '
+ [SHIFT K_SPACE] > ' '
+ [K_SPACE] > ' '
+ [ALT K_SPACE] > ' '
+ [SHIFT K_8] > '֝'
+ [SHIFT K_7] > '֬'
+ [SHIFT K_6] > '֔'
+ [SHIFT K_5] > '֞'
+ [SHIFT K_4] > '֜'
+ [SHIFT K_3] > '֨'
+ [SHIFT K_2] > '֘'
+ [SHIFT K_1] > '!'
+ [ALT K_SLASH] > '״'
+ [ALT K_PERIOD] > '̈'
+ [ALT K_COMMA] > '֫'
+ [ALT K_QUOTE] > 'ׄ'
+ [ALT K_COLON] > '׃'
+ [ALT K_H] > '̊'
+ [ALT K_G] > '•'
+ [ALT K_BKSLASH] > '֫'
+ [ALT K_RBRKT] > '֒'
+ [ALT K_LBRKT] > '֔'
+ [ALT K_I] > 'ֽ'
+ [ALT K_O] > 'ֳ'
+ [ALT K_E] > 'ֱ'
+ [ALT K_EQUAL] > '◌'
+ [ALT K_HYPHEN] > 'ֿ'
+ [ALT K_0] > '֯'
+ [ALT K_9] > '֓'
+ [ALT K_8] > '֕'
+ [ALT K_7] > '֡'
+ [ALT K_6] > '֟'
+ [ALT K_5] > '֩'
+ [ALT K_4] > '֠'
+ [ALT K_3] > '֙'
+ [ALT K_2] > '֮'
+ [ALT K_1] > '֗'
+ [ALT K_N] > 'ן'
+ [ALT K_P] > 'ף'
+ [ALT K_K] > 'ך'
+ [ALT K_M] > 'ם'
+ [ALT K_X] > 'ץ'
+ [SHIFT K_PERIOD] > 'ּ'
+ [SHIFT K_COMMA] > nul
+ [SHIFT K_M] > 'מּ'
+ [SHIFT K_N] > 'נּ'
+ [SHIFT K_B] > 'בּ'
+ [SHIFT K_V] > 'שּׁ'
+ [SHIFT K_C] > 'שּׂ'
+ [SHIFT K_X] > 'צּ'
+ [SHIFT K_Z] > 'זּ'
+ [SHIFT K_L] > 'לּ'
+ [SHIFT K_K] > 'כּ'
+ [SHIFT K_J] > 'וַיּ'
+ [SHIFT K_H] > 'הּ'
+ [SHIFT K_G] > 'גּ'
+ [SHIFT K_F] > 'טּ'
+ [SHIFT K_D] > 'דּ'
+ [SHIFT K_S] > 'סּ'
+ [SHIFT K_A] > 'ָ'
+ [SHIFT K_P] > 'פּ'
+ [SHIFT K_O] > 'וֹ'
+ [SHIFT K_U] > 'ֻ'
+ [SHIFT K_Y] > 'יּ'
+ [SHIFT K_T] > 'תּ'
+ [SHIFT K_E] > 'ֵ'
+ [SHIFT K_W] > 'וּ'
+ [SHIFT K_Q] > 'קּ'
+ [K_BKQUOTE] > '֑'
+ [K_COMMA] > 'א'
+ [K_M] > 'מ'
+ [K_N] > 'נ'
+ [K_B] > 'ב'
+ [K_V] > 'שׁ'
+ [K_C] > 'שׂ'
+ [K_X] > 'צ'
+ [K_Z] > 'ז'
+ [K_COLON] > 'ְ'
+ [K_L] > 'ל'
+ [K_K] > 'כ'
+ [K_J] > 'ח'
+ [K_H] > 'ה'
+ [K_G] > 'ג'
+ [K_F] > 'ט'
+ [K_D] > 'ד'
+ [K_S] > 'ס'
+ [K_A] > 'ַ'
+ [K_P] > 'פ'
+ [K_O] > 'ֹ'
+ [K_I] > 'ִ'
+ [K_U] > 'ע'
+ [K_Y] > 'י'
+ [K_T] > 'ת'
+ [K_R] > 'ר'
+ [K_E] > 'ֶ'
+ [K_W] > 'ו'
+ [K_Q] > 'ק'
c Transform final letters
any(letters-with-final-forms) + any(whitespace) > index(sofit-forms-of-letters,1)
c Transform final letters with a dagesh
any(letters-with-final-forms) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh
any(presentation-dageshed-letters-with-final-forms) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1)
any(normal-dageshed-letters-with-final-forms) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh
c Transform final letters with a vowel
c any(letters-with-final-forms) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2)
c Transform final letters with a vowel and a dagesh
c any(letters-with-final-forms) any(vowels) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) $dagesh
c any(letters-with-final-forms) $dagesh any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(vowels,3)
c any(presentation-dageshed-letters-with-final-forms) any(vowels) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(vowels,2)
c any(normal-dageshed-letters-with-final-forms) any(vowels) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(vowels,2)
c Transform final letters with punctuation
any(letters-with-final-forms) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2)
c Transform final letters with punctuation and a dagesh
any(letters-with-final-forms) any(punctuation) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) $dagesh
any(letters-with-final-forms) $dagesh any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(punctuation,3)
any(presentation-dageshed-letters-with-final-forms) any(punctuation) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(punctuation,2)
any(normal-dageshed-letters-with-final-forms) any(punctuation) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(punctuation,2)
c Transfrom final letters with a punctuation and a vowel
c any(letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3)
c any(letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3)
c Transform final letters with punctuation, a vowel, and a dagesh (phew!)
c any(letters-with-final-forms) any(punctuation) any(vowels) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3) $dagesh
c any(letters-with-final-forms) any(punctuation) $dagesh any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) $dagesh index(vowels,4)
c any(letters-with-final-forms) any(vowels) any(punctuation) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3) $dagesh
c any(letters-with-final-forms) any(vowels) $dagesh any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) $dagesh index(punctuation,4)
c any(letters-with-final-forms) $dagesh any(punctuation) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(punctuation,3) index(vowels,4)
c any(letters-with-final-forms) $dagesh any(vowels) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(vowels,3) index(punctuation,4)
c any(presentation-dageshed-letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3)
c any(presentation-dageshed-letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3)
c any(normal-dageshed-letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(punctuation,2) index(vowels,3)
c any(normal-dageshed-letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(vowels,2) index(punctuation,3)

View file

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<KeymanDeveloperProject>
<Options>
<BuildPath>$PROJECTPATH</BuildPath>
<CompilerWarningsAsErrors>True</CompilerWarningsAsErrors>
<CheckFilenameConventions>True</CheckFilenameConventions>
</Options>
<Files>
<File>
<ID>id_ede98e4633e239f933cbfd1f4e1b766c</ID>
<Filename>HISTORY.md</Filename>
<Filepath>HISTORY.md</Filepath>
<FileVersion></FileVersion>
<FileType>.md</FileType>
</File>
<File>
<ID>id_53e892b8b41cc4caece1cfd5ef21d6e7</ID>
<Filename>LICENSE.md</Filename>
<Filepath>LICENSE.md</Filepath>
<FileVersion></FileVersion>
<FileType>.md</FileType>
</File>
<File>
<ID>id_0730bb7c2e8f9ea2438b52e419dd86c9</ID>
<Filename>README.md</Filename>
<Filepath>README.md</Filepath>
<FileVersion></FileVersion>
<FileType>.md</FileType>
</File>
<File>
<ID>id_195752eb7ec06cfb1f90ed508d2c5b54</ID>
<Filename>alephwithbeth.kps</Filename>
<Filepath>alephwithbeth.kps</Filepath>
<FileVersion></FileVersion>
<FileType>.kps</FileType>
<Details>
<Name>Aleph With Beth</Name>
<Copyright>© Cameron Briggs</Copyright>
</Details>
</File>
<File>
<ID>id_e72bb7d9956f6973ce3de0362e47b0d3</ID>
<Filename>alephwithbeth.kmn</Filename>
<Filepath>alephwithbeth.kmn</Filepath>
<FileVersion>1.0.6</FileVersion>
<FileType>.kmn</FileType>
<Details>
<Name>Aleph With Beth</Name>
<Copyright>© Cameron Briggs</Copyright>
</Details>
</File>
<File>
<ID>id_dc799c273ed24ff1ca06a2429e5af8b4</ID>
<Filename>alephwithbeth.ico</Filename>
<Filepath>alephwithbeth.ico</Filepath>
<FileVersion></FileVersion>
<FileType>.ico</FileType>
<ParentFileID>id_e72bb7d9956f6973ce3de0362e47b0d3</ParentFileID>
</File>
<File>
<ID>id_8da344c4cea6f467013357fe099006f5</ID>
<Filename>readme.htm</Filename>
<Filepath>other\readme.htm</Filepath>
<FileVersion></FileVersion>
<FileType>.htm</FileType>
<ParentFileID>id_195752eb7ec06cfb1f90ed508d2c5b54</ParentFileID>
</File>
<File>
<ID>id_724e5b4c63f10bc0abf7077f7c3172fc</ID>
<Filename>welcome.htm</Filename>
<Filepath>other\welcome\welcome.htm</Filepath>
<FileVersion></FileVersion>
<FileType>.htm</FileType>
<ParentFileID>id_195752eb7ec06cfb1f90ed508d2c5b54</ParentFileID>
</File>
<File>
<ID>id_101067b24e270db6fdd8b52ac6c77297</ID>
<Filename>alephwithbeth.js</Filename>
<Filepath>alephwithbeth.js</Filepath>
<FileVersion></FileVersion>
<FileType>.js</FileType>
<ParentFileID>id_195752eb7ec06cfb1f90ed508d2c5b54</ParentFileID>
</File>
<File>
<ID>id_cf57b9fb0e29a5e6b4ff4b9a55fc7ef3</ID>
<Filename>alephwithbeth.kvk</Filename>
<Filepath>alephwithbeth.kvk</Filepath>
<FileVersion></FileVersion>
<FileType>.kvk</FileType>
<ParentFileID>id_195752eb7ec06cfb1f90ed508d2c5b54</ParentFileID>
</File>
<File>
<ID>id_dfa1b7bb444da6106e7ffcce1b6ae59d</ID>
<Filename>alephwithbeth.kmx</Filename>
<Filepath>alephwithbeth.kmx</Filepath>
<FileVersion></FileVersion>
<FileType>.kmx</FileType>
<ParentFileID>id_195752eb7ec06cfb1f90ed508d2c5b54</ParentFileID>
</File>
<File>
<ID>id_92d25e21c1bb82cab07440032ea15ca1</ID>
<Filename>KeyboardLayout.png</Filename>
<Filepath>other\welcome\KeyboardLayout.png</Filepath>
<FileVersion></FileVersion>
<FileType>.png</FileType>
<ParentFileID>id_195752eb7ec06cfb1f90ed508d2c5b54</ParentFileID>
</File>
<File>
<ID>id_a751525f488287b36c872b48e3c385b8</ID>
<Filename>KeyboardLayoutAlt.png</Filename>
<Filepath>other\welcome\KeyboardLayoutAlt.png</Filepath>
<FileVersion></FileVersion>
<FileType>.png</FileType>
<ParentFileID>id_195752eb7ec06cfb1f90ed508d2c5b54</ParentFileID>
</File>
<File>
<ID>id_a260e1bca7e99f6955c706b3697f842c</ID>
<Filename>KeyboardLayoutShift.png</Filename>
<Filepath>other\welcome\KeyboardLayoutShift.png</Filepath>
<FileVersion></FileVersion>
<FileType>.png</FileType>
<ParentFileID>id_195752eb7ec06cfb1f90ed508d2c5b54</ParentFileID>
</File>
<File>
<ID>id_c296b5df3dc126e219e0173fcbc1baec</ID>
<Filename>LICENSE.md</Filename>
<Filepath>LICENSE.md</Filepath>
<FileVersion></FileVersion>
<FileType>.md</FileType>
<ParentFileID>id_195752eb7ec06cfb1f90ed508d2c5b54</ParentFileID>
</File>
</Files>
</KeymanDeveloperProject>

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<Package>
<System>
<KeymanDeveloperVersion>17.0.325.0</KeymanDeveloperVersion>
<FileVersion>7.0</FileVersion>
</System>
<Options>
<ExecuteProgram></ExecuteProgram>
<ReadMeFile>other\readme.htm</ReadMeFile>
<LicenseFile>LICENSE.md</LicenseFile>
<WelcomeFile>other\welcome\welcome.htm</WelcomeFile>
<MSIFileName></MSIFileName>
<MSIOptions></MSIOptions>
<FollowKeyboardVersion/>
</Options>
<StartMenu>
<Folder></Folder>
<Items/>
</StartMenu>
<Info>
<Version URL=""></Version>
<Name URL="">Aleph With Beth</Name>
<Copyright URL="">© Cameron Briggs</Copyright>
<Author URL="">Cameron Briggs</Author>
<WebSite URL="https://freehebrew.online">https://freehebrew.online</WebSite>
<Description URL="">alephwithbeth keyboard</Description>
</Info>
<Files>
<File>
<Name>other\readme.htm</Name>
<Description>File readme.htm</Description>
<CopyLocation>0</CopyLocation>
<FileType>.htm</FileType>
</File>
<File>
<Name>other\welcome\welcome.htm</Name>
<Description>File welcome.htm</Description>
<CopyLocation>0</CopyLocation>
<FileType>.htm</FileType>
</File>
<File>
<Name>alephwithbeth.js</Name>
<Description>File alephwithbeth.js</Description>
<CopyLocation>0</CopyLocation>
<FileType>.js</FileType>
</File>
<File>
<Name>alephwithbeth.kvk</Name>
<Description>File alephwithbeth.kvk</Description>
<CopyLocation>0</CopyLocation>
<FileType>.kvk</FileType>
</File>
<File>
<Name>alephwithbeth.kmx</Name>
<Description>Keyboard alephwithbeth</Description>
<CopyLocation>0</CopyLocation>
<FileType>.kmx</FileType>
</File>
<File>
<Name>other\welcome\KeyboardLayout.png</Name>
<Description>File KeyboardLayout.png</Description>
<CopyLocation>0</CopyLocation>
<FileType>.png</FileType>
</File>
<File>
<Name>other\welcome\KeyboardLayoutAlt.png</Name>
<Description>File KeyboardLayoutAlt.png</Description>
<CopyLocation>0</CopyLocation>
<FileType>.png</FileType>
</File>
<File>
<Name>other\welcome\KeyboardLayoutShift.png</Name>
<Description>File KeyboardLayoutShift.png</Description>
<CopyLocation>0</CopyLocation>
<FileType>.png</FileType>
</File>
<File>
<Name>LICENSE.md</Name>
<Description></Description>
<CopyLocation>0</CopyLocation>
<FileType>.md</FileType>
</File>
</Files>
<Keyboards>
<Keyboard>
<Name>Aleph With Beth</Name>
<ID>alephwithbeth</ID>
<Version>1.0.4</Version>
<Languages>
<Language ID="he">Hebrew</Language>
</Languages>
</Keyboard>
</Keyboards>
<Strings/>
</Package>

View file

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<visualkeyboard>
<header>
<version>10.0</version>
<kbdname>alephwithbeth</kbdname>
<flags/>
</header>
<encoding name="unicode" fontname="Arial" fontsize="-12">
<layer shift="S">
<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_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>
</layer>
<layer shift="">
<key vkey="K_QUOTE">'</key>
<key vkey="K_COMMA">א</key>
<key vkey="K_PERIOD">.</key>
<key vkey="K_SLASH">/</key>
<key vkey="K_1">1</key>
<key vkey="K_2">2</key>
<key vkey="K_3">3</key>
<key vkey="K_4">4</key>
<key vkey="K_5">5</key>
<key vkey="K_6">6</key>
<key vkey="K_7">7</key>
<key vkey="K_8">8</key>
<key vkey="K_9">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>
<key vkey="K_HYPHEN">־</key>
<key vkey="K_0">0</key>
</layer>
<layer shift="A">
<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_A">ֲ</key>
<key vkey="K_E">ֱ</key>
<key vkey="K_G">•</key>
<key vkey="K_H">̊</key>
<key vkey="K_I">ֽ</key>
<key vkey="K_K">ך</key>
<key vkey="K_M">ם</key>
<key vkey="K_N">ן</key>
<key vkey="K_O">ֳ</key>
<key vkey="K_P">ף</key>
<key vkey="K_X">ץ</key>
</layer>
</encoding>
</visualkeyboard>

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View file

@ -0,0 +1,22 @@
<?php
$pagename = 'Aleph with Beth Keyboard Help';
$pagetitle = $pagename;
$pagestyle = <<<END
.center { display: block; margin-left: auto; margin-right: auto; width: 75%; }
END;
require_once('header.php');
?>
<body>
<h1>Start Using Aleph with Beth</h1>
<h1>Desktop Layout</h1>
<h2>Base layout</h2>
<img src="KeyboardLayout.png" alt="Keyboard layout" class="center">
<h2>Layout when shift is pressed</h2>
<img src="KeyboardLayoutShift.png" alt="Keyboard layout (when shift key is pressed)" class="center">
<h2>Layout when alt is pressed</h2>
<img src="KeyboardLayoutAlt.png" alt="Keyboard layout (when alt key is pressed)" class="center">

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>alephwithbeth</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>Aleph With Beth</h1>
<p>A Hebrew keyboard designed to intuitively match the standard QWERTY layout.</p>
<p>© Cameron Briggs</p>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Start Using alephwithbeth</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; }
.center { display: block; margin-left: auto; margin-right: auto; width: 75%; }
</style>
</head>
<body>
<h1>Start Using Aleph With Beth</h1>
<h1>Keyboard Layouts</h1>
<h2>Base layout</h2>
<img src="KeyboardLayout.png" alt="Keyboard layout" class="center">
<h2>Layout when shift is pressed</h2>
<img src="KeyboardLayoutShift.png" alt="Keyboard layout (when shift key is pressed)" class="center">
<h2>Layout when alt is pressed</h2>
<img src="KeyboardLayoutAlt.png" alt="Keyboard layout (when alt key is pressed)" class="center">
</body>
</html>

View file

@ -1,51 +0,0 @@
/*
* Keyman is copyright (C) SIL International. MIT License.
*/
import 'mocha';
// import * as path from 'path';
// import * as fs from 'fs';
// import { fileURLToPath } from 'url';
// import { assert } from 'chai';
import { CopierMessages } from '../src/copier-messages.js';
import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
import { CompilerErrorNamespace } from '@keymanapp/developer-utils';
describe('CopierMessages', function () {
it('should have a valid CopierMessages object', function() {
return verifyCompilerMessagesObject(CopierMessages, CompilerErrorNamespace.Copier);
});
// it('should generate ERROR_OutputPathAlreadyExists if output path already exists', async function () {
// const ag = new AbstractGenerator();
// const callbacks = new TestCompilerCallbacks();
// const options: GeneratorOptions = {
// id: 'ERROR_OutputPathAlreadyExists',
// outPath: path.dirname(fileURLToPath(import.meta.url)),
// };
// assert(await ag.init(callbacks, options));
// const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'ERROR_OutputPathAlreadyExists');
// if(!fs.existsSync(dir))
// fs.mkdirSync(dir);
// assert.isFalse(await ag.write({}));
// assert.isTrue(callbacks.hasMessage(CopierMessages.ERROR_OutputPathAlreadyExists),
// `messageId ERROR_OutputPathAlreadyExists not generated, instead got: `+JSON.stringify(callbacks.messages,null,2));
// fs.rmdirSync(dir);
// });
// it('should generate ERROR_CannotWriteOutputFile if it cannot create a folder, e.g. invalid filename', async function () {
// const ag = new AbstractGenerator();
// const callbacks = new TestCompilerCallbacks();
// const options: GeneratorOptions = {
// id: 'ERROR_CannotWriteOutputFile',
// outPath: path.dirname(fileURLToPath(import.meta.url)),
// };
// assert(await ag.init(callbacks, options));
// assert.isFalse(await ag.write({
// '.': {filename: '.', data: new Uint8Array([1,2,3])}
// }));
// assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_CannotWriteOutputFile),
// `messageId ERROR_CannotWriteOutputFile not generated, instead got: `+JSON.stringify(callbacks.messages,null,2));
// });
});

View file

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

View file

@ -53,11 +53,14 @@ export interface GeneratorOptions /* not inheriting from CompilerBaseOptions */
* name of the author of the keyboard
*/
author?: string;
/**
// TODO-GENERATE: icon support
/* *
* set to true to generate an icon for a Keyman keyboard with the first
* characters of the first specified BCP 47 tag
*/
icon?: boolean;
// icon?: boolean;
/**
* description of the keyboard, Markdown permissible
*/

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,67 @@ 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.`
);
// See also copier-messages.ERROR_InvalidKeyboardId
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.`
);
// See also copier-messages.ERROR_InvalidLexicalModelId
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();
@ -63,9 +72,10 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom
KeymanKeyboardGenerator.SPath_Source+this.options.id+KeymanFileTypes.Source.TouchLayout;
}
if(this.hasIcon()) {
this.includedPrefixes.push('Icon');
}
// TODO-GENERATE: icon support
// if(this.hasIcon()) {
// this.includedPrefixes.push('Icon');
// }
if(this.hasKMX()) {
this.includedPrefixes.push('KMX');
@ -82,9 +92,9 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom
// Special case for creating icon, run after successful creation of other
// project bits and pieces
if(this.hasIcon()) {
this.writeIcon(artifacts);
}
// if(this.hasIcon()) {
// this.writeIcon(artifacts);
// }
return {artifacts};
}
@ -98,13 +108,13 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom
private readonly hasKMX = () => this.targetIncludes(KeymanTargets.KMXKeymanTargets);
private readonly hasTouchLayout = () => this.targetIncludes(KeymanTargets.TouchKeymanTargets);
// TODO-GENERATE, once writeIcon is implemented:
// TODO-GENERATE: icon support
// hasIcon = () => this.options.icon && this.targetIncludes(KeymanTargets.KMXKeymanTargets);
private readonly hasIcon = () => false;
// private readonly hasIcon = () => false;
private writeIcon(artifacts: GeneratorArtifacts) {
// TODO-GENERATE: this will require some effort
// proposal: generate 16x16 icon with 2-3 letters. Following TKeyboardIconGenerator.GenerateIcon
// research for .ico writer in node
}
// TODO-GENERATE: icon support
// private writeIcon(artifacts: GeneratorArtifacts) {
// proposal: generate 16x16 icon with 2-3 letters. Following TKeyboardIconGenerator.GenerateIcon
// research for .ico writer in node
// }
}

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

@ -6,7 +6,8 @@ import { KeymanTargets } from "@keymanapp/common-types";
import { GeneratorOptions } from "../src/abstract-generator.js";
export const options: GeneratorOptions = {
icon: false,
// TODO-GENERATE: icon support
// icon: false,
id: 'sample',
languageTags: ['en'],
name: 'Sample Project',

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" });

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

@ -669,7 +669,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp
switch(recContext.Code) {
case KMX.KMXFile.CODE_ANY:
Index = AdjustIndex(fkp.dpContext, ContextIndex) + 1; // I3910 // I4611
Index = AdjustIndex(fkp.dpContext, ContextIndex); // I3910 // I4611
Result += nlt + `k.KIO(${len},this.s${JavaScript_Name(recContext.Any.StoreIndex, recContext.Any.Store.dpName)},${Index},t);`; // I4611
break;
case KMX.KMXFile.CODE_DEADKEY:
@ -681,7 +681,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp
// Note that this is checked in compiler.cpp as well, so this error can probably never occur
throw new Error('Unexpected: notany() encountered with invalid version');
}
Result += nlt + `k.KCXO(${len},t,${AdjustIndex(fkp.dpContext, xstrlen(fkp.dpContext))},${AdjustIndex(fkp.dpContext, ContextIndex)+1});`;
Result += nlt + `k.KCXO(${len},t,${AdjustIndex(fkp.dpContext, xstrlen(fkp.dpContext))},${AdjustIndex(fkp.dpContext, ContextIndex)});`;
break;
case KMX.KMXFile.CODE_IFOPT:
case KMX.KMXFile.CODE_IFSYSTEMSTORE:
@ -759,7 +759,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp
case KMX.KMXFile.CODE_CONTEXT:
if (x > 0 || len == -1) {
let xContext = 0;
let n = 0;
let n = 1;
while(xContext < fkp.dpContext.length) { // I4611
if(!isGroupReadOnly(fk, fgp)) {
Result += ContextChar(n, fkp.dpContext, xContext);
@ -767,7 +767,6 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp
n++;
xContext = incxstr(fkp.dpContext, xContext);
}
//Result := Result + Format('k.KO(%d,t,k.KC(%d,%d,t));', [len, xstrlen_printing(fkp.dpContext), xstrlen_printing(fkp.dpContext)]);
}
// else, we don't need to output anything - just don't delete the context
len = -1;
@ -779,7 +778,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp
}
if(!isGroupReadOnly(fk, fgp)) {
Result += ContextChar(rec.ContextEx.Index, fkp.dpContext, xContext); // I4611
Result += ContextChar(rec.ContextEx.Index + 1, fkp.dpContext, xContext); // I4611
}
len = -1;
break;

View file

@ -0,0 +1,29 @@
c test_index_12980 generated from template at 2025-01-22 13:25:35
c Validates index() offsets when used with if() - see #12980
store(&VERSION) '14.0'
store(&NAME) 'test_index_12980'
store(&TARGETS) 'web'
begin Unicode > use(main)
group(main) using keys
store(a) '1'
store(b) '2'
store(c) '3'
store(d) '4'
store(e) '5'
store(f) '6'
store(g) '7'
store(h) '8'
if(&platform = 'web') any(a) + 'a' > context 'A'
if(&platform = 'web') any(b) + 'b' > context(2) 'B'
if(&platform = 'web') notany(c) + 'c' > context 'C'
if(&platform = 'web') notany(d) + 'd' > context(2) 'D'
nul any(e) + 'e' > context 'E'
nul any(f) + 'f' > context(2) 'F'
nul notany(g) + 'g' > context 'G'
nul notany(h) + 'h' > context(2) 'H'

View file

@ -218,6 +218,30 @@ describe('KeymanWeb Compiler', function() {
assert.lengthOf(callbacks.messages, 2);
});
it('should generate correct index offsets (#12980) for context and context(n) when building a KMW keyboard', async function() {
const filenames = generateTestFilenames('test_index_12980');
let result = await kmnCompiler.run(filenames.source, null);
assert.isNotNull(result);
const data = new TextDecoder('utf-8').decode(result.artifacts.js.data);
function doMatchKIO(s: string) {
const regex = new RegExp('k\\.KIO\\(-1,this\\.'+s+',(.+?),t\\);');
const m = regex.exec(data);
assert.isNotNull(m, `Could not find match for ${s}`);
assert.equal(m[1], '1', `Expected '${s}' usage to have an index parameter == 1`);
}
doMatchKIO('s_a_6');
doMatchKIO('s_b_7');
// matches the 4 notany() rules
const matches = [...data.matchAll(/k\.KCXO\(-1,t,1,1\);/g)];
assert.lengthOf(matches, 4);
doMatchKIO('s_e_10');
doMatchKIO('s_f_11');
});
});
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;
}
}

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