diff --git a/.clang-format b/.clang-format index 76e36e1d44d..81ca2c82a37 100644 --- a/.clang-format +++ b/.clang-format @@ -19,7 +19,7 @@ AllowShortLambdasOnASingleLine: All AllowShortIfStatementsOnASingleLine: Never AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None -AlwaysBreakAfterReturnType: AllDefinitions +AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: true AlwaysBreakTemplateDeclarations: MultiLine BinPackArguments: true @@ -58,7 +58,7 @@ ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true DeriveLineEnding: true -DerivePointerAlignment: true +DerivePointerAlignment: false DisableFormat: false ExperimentalAutoDetectBinPacking: false FixNamespaceComments: true diff --git a/.github/labeler.yml b/.github/labeler.yml index 5250e321935..19c858cc81d 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -20,10 +20,8 @@ common/: - common/** - resources/** -common/models/: common/models/** -common/models/types/: common/models/types/** -common/models/templates/: common/models/templates/** -common/models/wordbreakers/: common/models/wordbreakers/** +common/models/templates/: web/src/engine/predictive-text/templates/** +common/models/wordbreakers/: web/src/engine/predictive-text/wordbreakers/** common/resources/: resources/** common/web/: common/web/** diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index 29f069efd50..9c374f5d050 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -116,7 +116,7 @@ jobs: strategy: fail-fast: true matrix: - dist: [focal, jammy, mantic, noble] + dist: [focal, jammy, noble] steps: - name: Checkout diff --git a/.gitignore b/.gitignore index 8534d172a9c..772784fdb8c 100644 --- a/.gitignore +++ b/.gitignore @@ -183,4 +183,3 @@ lcov.info /keyman*.buildinfo /keyman*.changes /keyman*.tar.?z - diff --git a/HISTORY.md b/HISTORY.md index 21cceb07eb7..9bc4268dc30 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,514 @@ # Keyman Version History +## 18.0.117 alpha 2024-09-25 + +* docs(common): Document how to skip generating CDN on websites (#12446) +* fix(android): Remove toggle for "Always Show Banner" (#12430) +* fix(android): Hide suggestion banner on password fields (#12442) +* fix(developer): prevent invalid string ids (#12465) + +## 18.0.116 alpha 2024-09-20 + +* change(mac): remove verbose logging option (#12431) +* chore(common): Allow to build offline (#12439) + +## 18.0.115 alpha 2024-09-19 + +* chore(common): detect ssh remotes in git hooks (#12437) +* fix(common): add proper configure output for hextobin (#12440) +* fix(core): add missing dependency for core (#12438) +* chore(developer): remove .js output from LDML compiler (#12432) + +## 18.0.114 alpha 2024-09-17 + +* fix(developer): rewrite ldml visual keyboard compiler (#12402) +* fix(developer): check vars string usage before definition (#12404) +* change(mac): remove 'Always show OSK' option (#12355) + +## 18.0.113 alpha 2024-09-16 + +* test(developer): kmcmplib compiler unit tests 3 (#11990) + +## 18.0.112 alpha 2024-09-14 + +* chore(deps): bump express from 4.19.2 to 4.20.0 (#12396) + +## 18.0.111 alpha 2024-09-13 + +* chore(common): Update crowdin strings for Italian (#12408) +* fix(common): correct offsets in KMX+ spec (#12350) +* fix(android): Add gating to setLongpressDelay() (#12410) + +## 18.0.110 alpha 2024-09-12 + +* chore(common): Update to Unicode 16.0 (#12393) +* refactor(web): move `common/web/es-bundling` → `web/src/tools/es-bundling` (#12389) +* refactor(web): move `common/web/eslint` → `common/tools/eslint` (#12390) +* refactor(web): move sentry-manager → `web/src/engine/sentry-manager` (#12397) +* refactor(web): merge `device-detect` with `web/src/engine/main` (#12399) +* chore(web): allow to run unit tests in vscode test explorer (#12400) +* fix(developer): index() requires comma between parameters in kmcmplib compiler (#12328) + +## 18.0.109 alpha 2024-09-11 + +* chore(common): Update history with 17.0.329 stable (#12394) +* refactor(web): move `model/templates` to `web/src/engine/predictive/text` (#12382) +* refactor(web): move `common/models` to `web/src/engine/predictive-text` (#12383) +* refactor(web): move `common/web/utils` to `web/src/engine/common/web-utils/` (#12384) + +## 18.0.108 alpha 2024-09-10 + +* docs(android): Update help docs (#12367) +* fix(developer): fix building with Ubuntu 24.04 (#12379) +* refactor(android): Move build-publish.sh to builder script (#12351) +* fix(android): Separate `publishSentry` Gradle task to publish symbols to Sentry (#12358) +* refactor(web): move `model/types` to `web/types` (#12370) + +## 18.0.107 alpha 2024-09-09 + +* fix(android): Update Text Size menu icons for RTL support (#12290) + +## 18.0.106 alpha 2024-09-06 + +* feat(windows): add right modifier included in hotkey optional functionality (#12259) +* fix(android): Skip language counts for lexical-model packages (#12361) +* docs(web): fix link to documentation page (#12369) + +## 18.0.105 alpha 2024-09-05 + +* chore(common): Fix missing entries in HISTORY.md (#12352) +* docs(android): Add in-app help for adjusting longpress delay (#12359) +* fix(mac): avoid crash on startup with macOS 10.15 (Catalina) (#12354) +* chore(oem/fv): Update to fv_all 13.0 (#12362) +* feat(windows): Add two new strings for SIL Global name instead of SIL International (#12327) + +## 18.0.104 alpha 2024-09-03 + +* fix(mac): display package info after keyboard installation (#12326) + +## 18.0.103 alpha 2024-09-02 + +* feat(windows): Remove hotkey related feature flags (#12252) +* feat(windows): update SIL logo for Windows UI (#12250) + +## 18.0.102 alpha 2024-08-30 + +* docs(web): add documentation comments for touch layout interfaces (#12314) +* change(mac): store data in Library directory instead of Documents (#12106) +* change(mac): store partial path in UserDefaults (#12144) + +## 18.0.101 alpha 2024-08-29 + +* refactor(web): move `gesture-recognizer` → `gesture-processor` (#12194) +* docs(core): fix a typo in the KMX+ doc (#12302) +* chore(web): remove obsolete comment (#12304) +* chore(android,ios): Update FirstVoices keyboards to fv_all.kmp 12.15 (#12300) +* fix(developer): make LDML import path consistent for all bundlings of kmc (#12280) +* fix(core): properly support 'other' modifier state with `uint32_t` type (#12281) +* chore(windows): fix typo in environment.inc.sh (#12286) +* fix(android): Check in material-stepper as internal Maven dependency (#12267) +* docs(core): improve formatting of KMX+ doc (#12303) +* fix(android): Prioritize certain actions over multi-line for ENTER key (#12315) +* fix(linux): add `keymanFacename` to .ldml file (#12277) +* chore(common): Update crowdin strings for Czech (#12316) +* fix(web): prevent unintuitive space-output blocking for mid-context suggestions (#12313) + +## 18.0.100 alpha 2024-08-28 + +* fix(windows): check IM window will be in a visible location (#11967) + +## 18.0.99 alpha 2024-08-27 + +* feat(web): import the generator for the pred-text wordbreaker's Unicode-property data-table (#10690) +* feat(web): optimize the wordbreaker data table for filesize and ease of first-load parsing (#10692) +* fix(web): fixes wordbreaker test import path (#12297) +* feat(web): enable utf8 charset encoding for the build artifacts (#12115) + +## 18.0.98 alpha 2024-08-26 + +* change(ios): defer registration of fonts past initialization (#12190) +* refactor(ios): optimize font registration (#12210) +* test(mac): add unit tests to validate first calls to compliance check (#11724) +* fix(mac): limit short bundle version string to x.y.z format in info.plist (#12233) +* chore(developer): extend timeouts for lm compiler tests to 5 secs (#12273) +* fix(developer): find last matching key in LDML key bag when building KVK (#12278) +* chore(android): Cleanup stray debug statements in console (#12287) +* fix(developer): ensure call() detects invalid store in kmcmplib compiler (#12263) + +## 18.0.97 alpha 2024-08-24 + +* refactor(linux): cleanup API of kvk2ldml.py (#12276) +* chore(common): adjust build settings for windows clean builds (#12264) +* chore(windows): remove remaining unused Makefiles (#12274) +* docs(developer): update build documentation to refer to build.sh (#12272) + +## 18.0.96 alpha 2024-08-23 + +* fix(android): Fix navigation arrows in Info Activity for RTL (#12244) +* fix(web): fix documentation-keyboard spacebar-text scaling (#12232) +* fix(android): Use increment and decrement arrows on longpress delay menu (#12242) +* fix(android): Add RTL assets for adjusting keyboard height menu (#12261) +* chore(common): use `npm install` in emsdk update (#12269) +* docs: refresh windows.md (#12248) + +## 18.0.95 alpha 2024-08-22 + +* chore(common): allow build agents to automatically select emsdk version, and enable support for 3.1.60+ (#12243) + +## 18.0.94 alpha 2024-08-21 + +* fix(core): look for `emcc` instead of `emcc.py` (#12235) +* fix(web): improve tokenization output when wordbreaker breaks spec for span properties in output (#12229) +* chore(android): Use RTL-aware alignment and padding for layouts (#12225) +* fix(web): disable fat-finger data use when mayCorrect = false (#12220) +* fix(android): Auto-mirror back and forward arrows for RTL support (#12227) +* feat(android): Add localization for Arabic (#12228) +* fix(android): Auto-mirror increment and decrement arrows for RTL support (#12230) + +## 18.0.93 alpha 2024-08-20 + +* refactor(web): remove engine/interfaces dependency on engine/js-processor (#12188) +* fix(web): fix malformed reversion display strings (#12201) +* feat(android): Add menu to specify long-press delay (#12170) +* feat(android): Pass longpress delay to KeymanWeb (#12185) +* fix(core): set mac build version for meson cli build to 10.13 (#12223) + +## 18.0.92 alpha 2024-08-19 + +* chore(deps-dev): bump @75lb/deep-merge from 1.1.1 to 1.1.2 (#12118) +* chore(deps): bump semver from 7.5.4 to 7.6.0 (#12119) +* fix(windows): "Keyman" is not localized in UI strings (#12162) +* feat(android): Enhance how ENTER key is handled in apps (#12125) +* refactor(web): move `lm-worker` → `worker-thread` (#12150) +* fix(developer): remove redundant check in LdmlKeyboardCompiler.validate() (#11858) + +## 18.0.91 alpha 2024-08-16 + +* refactor(web): move `predictive-text` → `worker-main` (#12146) +* fix(web): restore flick functionality (#12187) +* refactor(web): move `lm-message-types` → `predictive-text/types` (#12149) +* fix(developer): enforce presence of kps Info.Description field in info compilers (#12204) +* fix(developer): enforce presence of Version field when FollowKeyboardVersion is not set, in package compiler (#12205) + +## 18.0.90 alpha 2024-08-15 + +* refactor(web): move parts of `keyboard-processor` → `js-processor` (#12111) +* fix(web): allow `lm-worker` to build on Linux (#12181) +* refactor(web): move remaining parts of `keyboard-processor` → `keyboard` (#12131) +* docs: update .kmx documentation around bitmaps, modifier state (#12183) +* refactor(web): rename `package-cache` → `keyboard-storage` (#12135) + +## 18.0.89 alpha 2024-08-14 + +* feat(web): test skipped prediction round handling (#12169) +* fix(web): support live configuration of longpress delay (#12175) +* feat(web): add osk.gestureParams for better gesture-config persistence (#12176) +* refactor(core): move utfcodec to common (#12171) +* refactor: move kmx_u16 to common and rename to km_u16 (#12177) +* refactor(developer): use npm or local source for server addons instead of github references (#12090) +* fix(developer): fix crash with Windows Clipboard by ignoring zero scan code in debugger (#12166) +* chore(developer): update SIL logo (#12168) + +## 18.0.88 alpha 2024-08-13 + +* docs: add .kmx specification (#12163) + +## 18.0.87 alpha 2024-08-12 + +* chore(web): drop flaky auto-test component (#12155) +* fix(common): show description even if child projects specifies path (#12145) + +## 18.0.86 alpha 2024-08-09 + +* chore(android): Update splash screen with SIL Tai Heritage Pro logo (#12127) +* chore(common): Update localization for Greek Polytonic (#12112) +* chore(common): add support for build target platform exclusions (#12113) +* feat(common): add top-level build.sh (#12114) +* chore(developer): start building Developer on linux,mac (#12117) +* refactor(web): Move `common/web/recorder` → `web/src/tools/testing/recorder-core` (#12092) + +## 18.0.85 alpha 2024-08-08 + +* fix(developer): update date for last git commit date fixture (#12122) +* refactor(android/engine): Parse keyboards.json for FirstVoices app (#11943) + +## 18.0.84 alpha 2024-08-07 + +* refactor(common): move kpj-related files into developer-utils (#11531) +* refactor(common): move kps-file.ts to @keymanapp/developer-utils (#11763) +* refactor(common): move kvks-file to @keymanapp/developer-utils (#11764) +* refactor(common): move .keyman-touch-layout reader/writer to @keymanapp/developer-utils (#11765) +* refactor(common): move LDML keyboard .xml reader/writer and kmx-plus builder to @keymanapp/developer-utils (#12081) +* refactor(common): move compiler-interfaces to @keymanapp/developer-utils (#12088) +* refactor(common): move xml2js and related deps to @keymanapp/developer-utils (#12101) + +## 18.0.83 alpha 2024-08-06 + +* chore(common): Update history from 17.0.328 (#12093) +* change(common/models): change model tokenization to also tokenize whitespace (#11975) +* feat(web): support transform tokenization when given a root context (#11998) +* change(web): track whitespace-aware tokenization for context + correction-search caching (#11979) +* change(web): leverage tokenization to preserve punctuation and whitespace when predicting (#11997) +* fix(web): patch up worker build to provide artifacts for its tests (#12082) +* chore(web): move `web/src/engine/paths/` → `web/src/engine/interfaces/` (#12064) +* chore(web): move `common/web/input-processor/` → `web/src/engine/main/` (#12066) +* refactor(web): refactor and harmonize constants (#12072) +* chore(common): add data versions to minimum-versions.inc.sh (#12103) +* fix(web): fix lm-worker test broken by botched merge conflict resolution (#12104) + +## 18.0.82 alpha 2024-08-05 + +* chore(windows): remove QIT_VSHIFTDOWN QIT _VSHIFTUP (#11973) +* chore(developer): add language/reference (#11799) + +## 18.0.81 alpha 2024-08-03 + +* chore(developer): Revert "chore(developer): remove redundant references from tsconfig.json" (#12076) +* chore(common): use nvm in builds to select a node version automatically (#12069) +* refactor(developer): move functions and variables into CompilerErrors.cpp (#12030) +* refactor(developer): improve the callbacks for kmcmplib (#12031) +* refactor(developer): align kmcmplib error codes and names with kmc-kmn (#12044) +* refactor(developer): move error reporting to error site in kmcmplib (part 1) (#12045) +* refactor(developer): move error reporting inside `GetCompileTargetsFromTargetsStore` and `ProcessGroupFinish` (#12048) +* refactor(developer): move kmcmplib message construction to kmc-kmn (#12059) +* refactor(developer): rename compiler error reporting functions in kmcmplib (#12060) +* chore(developer): support filename field in kmcmplib compiler messages (#12061) +* refactor(developer): replace `VERIFY_KEYBOARD_VERSION()` calls with `VerifyKeyboardVersion()` (#12063) + +## 18.0.80 alpha 2024-07-31 + +* chore(developer): remove redundant references from tsconfig.json (#12037) +* fix(web): add nullish test in setOsk (#12039) +* fix(web): unrevert #11258, leaving OSK hidden before instructed to display (#12049) +* test(developer): check correct use of u16chr when second parameter could be null (#11894) +* change(web): remove support for es5 (#11881) + +## 18.0.79 alpha 2024-07-30 + +* change(mac): add custom tags in sentry to better identify errors (#11947) +* feat(web): add unit tests for case-detection & handling (#11950) +* refactor(web): spin off method for correction-search probability-thresholding check (#11952) + +## 18.0.78 alpha 2024-07-29 + +* chore(common): Update history from 17.0.327 and add missing descriptions (#12021) +* change(web): revert #11174, which loads keyboards before initializing the OSK (#12015) +* feat(web): add unit testing for finalization of generated suggestions (#11946) +* feat(web): add unit tests for prediction lookup component (#11949) + +## 18.0.77 alpha 2024-07-27 + +* refactor(windows): clean up logging (#11921) +* chore(developer): rename to analyzer-messages.ts (#12017) +* fix(developer): remove `paths` from tsconfig.json (#12028) +* chore(developer): api doc refresh (#12029) + +## 18.0.76 alpha 2024-07-26 + +* change(linux): improve changelog PRs after upload to debian (#12024) +* test(developer): kmcmplib compiler unit tests 2 (#11663) +* fix(linux): set local directory if not specified (#12032) + +## 18.0.75 alpha 2024-07-25 + +* chore(windows): remove the posting WM_KEYUP/DOWN events to IM (#12002) +* feat(web): check for low-probability exact + exact-key correction matches (#11876) +* refactor(web): extract suggestion-finalization block into its own function (#11899) +* chore(developer): remove `CompilerMessages` stub and use `KmnCompilerMessages` (#11986) +* chore(developer): rename kmc-ldml `CompilerMessages`, `LdmlKeyboardCompilerMessages` to `LdmlCompilerMessages` (#11988) +* chore(developer): rename kmc-package `CompilerMessages` to `PackageCompilerMessages` (#11989) +* fix(developer): handle errors parsing .kps file when loading project (#12008) +* chore(common): updated stats script to support end date/sprint (#12009) +* fix(windows): align engine.sln platforms and configurations (#12011) +* refactor(web): spin off deduplication, suggestion-similarity sections (#11900) +* refactor(web): extract the correct-and-raw-predict blocks into their own method (#11888) +* refactor(web): convert internal prediction methods to stateless format (#11940) +* feat(web): add unit tests for predict auto-selection method (#11941) +* feat(web): extend unit-test oriented dummy model (#11948) +* feat(web): add unit tests for suggestion-similarity detection (#11944) +* feat(web): add unit testing for suggestion deduplication (#11945) +* feat(developer): add hint when index() store is longer than any() store (#12000) +* chore(android,ios): Add ojibwa ifinal/rdot keyboards to FirstVoices (#11889) + +## 18.0.74 alpha 2024-07-24 + +* fix(developer): correct handling of trailing spaces by GetDelimitedString() in kmcmplib compiler (#11938) +* chore(linux): remove Ubuntu Mantic, add Oracular (#12003) + +## 18.0.73 alpha 2024-07-23 + +* fix(windows): add text selected bool emit backspace key when text selected in TSF (#11884) +* fix(developer): prevent buffer overrun in `u16tok` (#11910) +* fix(developer): prevent invalid values in targets store (#11918) +* feat(developer): automatically detect version for `U_xxxx_yyyy` ids (#11957) +* feat(developer): handle automatic versioning of chiral modifiers (#11965) +* test(developer): Add tests for automatic versioning of notany() with context() (#11980) +* feat(developer): handle automatic versioning of special key caps on normal keys (#11981) +* feat(developer): automatically upgrade version when gestures are found in the touch layout (#11982) +* refactor(developer): rename `verifyMinimumKeymanVersion` (#11983) +* feat(developer): add searching for message identifiers to `kmc message` (#11984) +* fix(developer): kmc-keyboard-info: use default version 1.0 if version information missing (#11985) +* fix(web): present a "keep" option when a context-altering suggestion is auto-selected (#11969) +* fix(web): prevents auto-accept immediately after reversion (#11970) + +## 18.0.72 alpha 2024-07-22 + +* fix(web): remedy unit-test stability issues (#11933) +* refactor(web): fix TypeScript errors and warnings (#11911) + +## 18.0.71 alpha 2024-07-18 + +* chore(windows): add comments for _WIN64 tests (#11929) +* chore(common): Update Crowdin strings for Portuguese (#11974) + +## 18.0.70 alpha 2024-07-08 + +* feat(web): provide lexicon probabilities directly on the search path (#11868) +* feat(common/models): support direct-child access for Trie node iteration (#11869) +* change(common/models/templates): rework Trie predict method to utilize traversals (#11870) +* change(web): track the base correction for generated predictions (#11875) +* feat(web): add and enable auto-correction (#11866) + +## 18.0.69 alpha 2024-07-05 + +* fix(core): allow to successfully build on Ubuntu 24.04 (#11926) +* chore(windows): correct output file for 64-bit build of keyman32 in build.sh (#11930) +* chore(android,ios): Add Crowdin localization for Polytonic Greek (#11877) + +## 18.0.68 alpha 2024-07-04 + +* refactor(windows): merge keyman64 build into keyman32 (#11906) +* refactor(windows): remove wm_keyman_keydown and wm_keyman_keyup (#11920) + +## 18.0.67 alpha 2024-07-03 + +* refactor(common/models): move TS priority-queue implementation to web-utils (#11867) + +## 18.0.66 alpha 2024-07-02 + +* fix(developer): handle second parameter of index correctly in kmcmplib compiler (#11815) + +## 18.0.65 alpha 2024-07-01 + +* fix(developer): prevent non-BMP characters in key part of rule (#11806) +* chore(linux): remove unused building with pbuilder (#11862) + +## 18.0.64 alpha 2024-06-28 + +* fix(web): use fat-finger data with simple keypresses (#11854) + +## 18.0.63 alpha 2024-06-26 + +* feat(linux): implement Linux side of SimulateAltGr option (#11852) + +## 18.0.62 alpha 2024-06-25 + +* chore(common): update C/C++ formatting options (#11836) +* chore(linux): use shared meson config (#11863) +* fix(linux): ignore exceptions trying to install cache (#11861) + +## 18.0.61 alpha 2024-06-24 + +* feat(web): optimization via lazy preprocessing of keyboard touch-layout info (#11265) +* fix(android): clear globe highlight when displaying keyboard picker (#11826) +* refactor(linux): add KeymanOption class for options (#11850) +* refactor(linux): rename methods that deal with keyboard options (#11851) + +## 18.0.60 alpha 2024-06-21 + +* chore(web): define common timeout variable for automated testing (#11839) +* feat(developer): warn on empty keycaps (#11810) +* change(web): optimization for keyboard-layout preprocessing (#11263) +* feat(web): optimization via lazy construction of OSK layers (#11264) +* fix(developer): layr: fix modifier err message on layer w/o id (#11843) + +## 18.0.59 alpha 2024-06-19 + +* chore(deps-dev): bump braces from 3.0.2 to 3.0.3 (#11756) +* chore(developer): clarify project upgrade messages about file locations (#11819) +* chore(deps): bump ws from 8.16.0 to 8.17.1 (#11822) +* chore(common): update base-package node-engine setting (#11798) +* change(web): change after-word whitespace check to be more selective (#11800) +* chore: Revert "chore(common): update base-package node-engine setting" (#11829) +* change(web): drop correction batching (#11768) +* chore(web): move correction-search execution timer to its own file (#11757) +* refactor(web): overhaul predictive-text engine's timer to better detect paused time (#11758) +* feat(web): improve predictive-text responsiveness when typing rapidly (#11784) +* feat(linux): re-create missing files at run-time (#11789) + +## 18.0.58 alpha 2024-06-18 + +* test(core): Add a minimal test that exercises the core API (#11781) +* fix(developer): check HISTORY.md to get last modified date for keyboard_info and model_info (#11805) +* docs(developer): extra context help for keyboard-editor (does not exist in Keyman Developer 17.0) (#11771) + +## 18.0.57 alpha 2024-06-17 + +* fix(web): fix id of longpress keys with modifier set in touch layout (#11783) +* fix(web): prevent desktop OSK crash when addKeyboards is called before engine init (#11786) +* fix(core): serialize tests for core/wasm on mac agents (#11795) +* fix(developer): refactor kmcmplib compiler messages to use map (#11738) +* fix(developer): make native compilation of kmcmplib under Linux possible (#11779) +* feat(core): devolve regex to javascript (#11777) +* feat(core): remove ICU from core under wasm (#11778) +* fix(linux): restart ibus after manual integration test run (#11775) + +## 18.0.56 alpha 2024-06-14 + +* feat(core): devolve normalization to js (#11541) +* fix(developer): show message if no more platforms to add to touch layout editor (#11759) +* docs(developer): context help in package-editor and put the existing context help in their own tab comments (#11760) +* docs(developer): context help in keyboard-editor section (#11754) +* docs(developer): context help in new-project section (#11767) +* docs(developer): context help for new-project-parameters in keyman developer (#11769) +* docs(developer): context help for Select BCP 47 tag in Keyman Developer (#11770) +* change(common): update esbuild to 0.18.9 (#11693) +* change(web): more prep for better async prediction handling (#10347) +* fix(web): set new-context rules' device to match that of the active OSK (#11743) +* chore(linux): Update debian changelog (#11671) +* fix(web): add limited Array.from polyfill for lm-worker use (#11732) + +## 18.0.55 alpha 2024-06-13 + +* fix(developer): handle missing OSK when importing a Windows keyboard into a touch-only project (#11720) +* fix(developer): verify email addresses in .kps and .keyboard_info (#11735) +* change(web): prep for better asynchronous prediction handling (#10343) + +## 18.0.54 alpha 2024-06-12 + +* fix(common): remove subpackage entries for older TS version (#11745) +* chore(common): end use of ts-node (#11746) +* feat(web): add bulk_render variant that loads and renders keyboards from local KMP (#10432) + +## 18.0.53 alpha 2024-06-10 + +* fix(android): check current orientation when redisplaying system keyboard (#11604) +* fix(android): fix keyboard size after rotation and restore via onSizeChanged, after layout (#11722) +* fix(developer): fix kmcmplib unit-test include paths (#11749) + +## 18.0.52 alpha 2024-06-08 + +* fix(developer): prevent two touch layout editors opening for the same file (#11717) +* chore(common): cleanup meson deprecations and warnings (#11523) +* feat(developer): support language reference in context help (#11737) +* test(developer): kmcmplib compiler unit tests (#11378) + +## 18.0.51 alpha 2024-06-07 + +* fix(web): fix osk touch-focus tracking (#11705) +* fix(web): defer keyboard activation requests made during engine initialization (#11713) +* chore(developer): add context/character-map (#11656) +* chore(developer): add context/wordlist-editor (#11658) +* chore(developer): add context/new-model-project-parameters (#11677) +* fix(common): remove allowJs from web's tsconfig.base.json (#11718) +* change(web): precompile all TS-based tests (#11723) +* chore(developer): add extra logging for assertion failure when pressing backspace in debugger (#11707) +* chore: add cherry-pick information in commit messages (#11708) +* fix(developer): handle encoding errors when loading wordlists (#11711) +* chore(ios): remove dead Swift-side keyboard gesture code (#11672) +* fix(mac): change build configuration to prevent cycle error in Xcode 15 (#11730) +* refactor(web): Replace deprecated substr with substring (#11637) + ## 18.0.50 alpha 2024-06-06 * chore(common): adds retry mechanism for build script npm ci calls (#11451) @@ -80,7 +589,7 @@ * fix(web): explicitly terminate banner gesture-handling when banner is swapped (#11599) * chore(web): removes unused locals, imports, and private fields (#11460) * fix(web): use correct parameter name in button UI OSK `hide` event (#11600) -* (#11444) +* chore(mac): rework of main build script (#11444) * fix(ios): do not write to shared storage from system keyboard (#11613) * fix(developer): handle invalid default project path in options (#11555) * fix(developer): handle missing data in .kps `` (#11563) @@ -165,7 +674,7 @@ ## 18.0.35 alpha 2024-05-14 -* (#11340) +* chore(core): update core to C++17 (#11340) ## 18.0.34 alpha 2024-05-13 @@ -318,6 +827,68 @@ * chore(common): move to 18.0 alpha (#10713) * chore: move to 18.0 alpha +## 17.0.329 stable 2024-09-09 + +* chore(android,ios): Add ojibwa ifinal/rdot keyboards to FirstVoices (#12020) +* change(web): revert #11174, which loads keyboards before initializing the OSK (#12040) +* fix(web): unrevert #11258, leaving OSK hidden before instructed to display (#12058) +* chore(common): use `nvm` to select version of node for builds (#12074) +* fix(developer): ignore scan code if zero in debugger (#12182) +* fix(developer): enforce presence of Version field when FollowKeyboardVersion is not set, in package compiler (#12206) +* fix(developer): enforce presence of kps Info.Description field in info compilers (#12207) +* fix(web): disable fat-finger data use when mayCorrect = false (#12226) +* chore(common): allow build agents to automatically select emsdk version, and enable support for 3.1.60+ (#12245) +* fix(web): fix documentation-keyboard spacebar-text scaling (#12240) +* fix(core): set mac build version for meson cli build to 10.13 (#12246) +* change(ios): defer registration of fonts past initialization (#12241) +* chore(android,ios): Update FirstVoices keyboards to 12.15 (#12301) +* fix(core): properly support 'other' modifier state with `uint32_t` type (#12285) +* fix(developer): find last matching key in LDML key bag when building KVK (#12284) +* fix(android): check in material-stepper as internal Maven dependency (#12324) +* fix(linux): add `keymanFacename` to .ldml file (#12283) +* chore(oem/fv): Update to fv_all 13.0 (#12363) +* fix(mac): avoid crash on startup with macOS 10.15 (Catalina) (#12364) +* fix(android): skip language counts for lexical-model packages (#12368) + +## 17.0.328 stable 2024-07-27 + +* fix(web): add nullish test in setOsk (#12041) + +## 17.0.327 stable 2024-07-25 + +* fix(android): include DOMRect polyfill for older ES6-supporting devices (#11654) +* fix(web): Don't apply suggestion unless fully configured (#11636) +* fix(mac): handle command keys without crashing (#11675) +* fix(web): get row-height for flick constraints after performing layout (#11692) +* fix(android): handle IllegalArgumentException when initializing CloudDownloadMgr, add logging to check for unhandled side-effects (#11628) +* fix(developer): handle editor initializing after debugger when setting execution point (#11588) +* fix(developer): treat js files with unrecognized encodings as non-keyboard files (#11699) +* fix(developer): disable example edit controls if no examples in Package Editor (#11703) +* chore(developer): add extra logging for assertion failure when pressing backspace in debugger (#11709) +* fix(developer): handle encoding errors when loading wordlists (#11712) +* fix(mac): change build configuration to prevent cycle error in Xcode 15 (#11731) +* fix(developer): handle missing OSK when importing a Windows keyboard into a touch-only project (#11721) +* fix(developer): prevent two touch layout editors opening for the same file (#11727) +* fix(android): check current orientation, fix keyboard size after system keyboard rotations and resumes (#11747) +* chore(linux): Update debian changelog (#11670) +* feat(developer): support language reference in context help (#11741) +* fix(developer): show message if no more platforms to add to touch layout editor (#11766) +* fix(web): add limited Array.from polyfill for lm-worker use (#11733) +* fix(web): set new-context rules' device to match that of the active OSK (#11744) +* fix(web): prevent desktop OSK crash when addKeyboards is called before engine init (#11787) +* fix(windows): add -k parameter for keyboards build.sh (#11811) +* fix(core): serialize tests for core/wasm on mac agents (#11809) +* chore(developer): clarify project upgrade messages about file locations (#11820) +* fix(developer): check HISTORY.md to get last modified date for keyboard_info and model_info (#11808) +* fix(web): fix id of longpress keys with modifier set in touch layout (#11797) +* change(web): change after-word whitespace check to be more selective (#11824) +* fix(android): clear globe highlight when displaying keyboard picker (#11827) +* fix(web): use fat-finger data with simple keypresses (#11871) +* fix(developer): prevent non-BMP characters in key part of rule (#11807) +* fix(linux): ignore exceptions trying to install cache (#11885) +* chore(common): Update Crowdin strings for Portuguese (#11976) +* chore(linux): remove Ubuntu 23.10 Mantic (#12004) + ## 17.0.326 stable 2024-06-02 * cherrypick(android/engine): Handle globe key on lock screen (#11468) @@ -394,8 +965,8 @@ ## 17.0.317 beta 2024-05-01 -* (#11322) -* (#11321) +* chore(web): remove old reference-doc from alpha that has completed its purpose (#11322) +* fix(web): gesture-model initial-state, callback failure handling (#11321) * fix(linux): Fix icon for .kmp files (#11295) ## 17.0.316 beta 2024-04-30 diff --git a/LICENSE.md b/LICENSE.md index e031c6cc139..5b203762357 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # License -Copyright (c) 2017-2022 SIL International. All rights reserved. +Copyright (c) 2017-2024 SIL Global. All rights reserved. Licensed under the MIT License. @@ -10,7 +10,7 @@ Licensed under the MIT License. The MIT License -Copyright (c) 2017-2022 SIL International +Copyright (c) 2017-2024 SIL Global Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index acd890cc452..caab39c2f9d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # License -Copyright (c) SIL International. +Copyright (c) SIL Global. Keyman is an open source project distributed under the [MIT license](LICENSE.md). diff --git a/VERSION.md b/VERSION.md index dd6ab3716f8..e272e231ff7 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.51 \ No newline at end of file +18.0.118 \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore index 4f0c3bb5018..a5720bccc73 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -37,8 +37,6 @@ KMEA/**/assets/keymanandroid.js KMEA/**/assets/keyman.js.map KMEA/**/assets/keymanweb-webview.js KMEA/**/assets/keymanweb-webview.js.map -KMEA/**/assets/keymanweb-webview.es5.js -KMEA/**/assets/keymanweb-webview.es5.js.map KMEA/**/assets/map-polyfill.js KMEA/**/assets/sentry.min.js KMEA/**/assets/keyman-sentry.js diff --git a/android/KMAPro/build.gradle b/android/KMAPro/build.gradle index b1f2d240a72..5614fcae6a6 100644 --- a/android/KMAPro/build.gradle +++ b/android/KMAPro/build.gradle @@ -2,8 +2,10 @@ buildscript { repositories { google() - jcenter() mavenCentral() + flatDir { + dirs "kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/" + } } dependencies { classpath 'com.android.tools.build:gradle:7.4.2' @@ -11,16 +13,16 @@ buildscript { classpath 'io.sentry:sentry-android-gradle-plugin:4.6.0' classpath 'name.remal:gradle-plugins:1.5.0' - // From jcenter() which could be sunset in future + // From jcenter() which was deprecated August 2024 // https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/ - classpath 'com.stepstone.stepper:material-stepper:4.3.1' + classpath 'com.stepstone.stepper:material-stepper:4.3.1@aar' } } allprojects { repositories { + maven { url uri("${projectDir}/libs") } google() - jcenter() mavenCentral() maven { url "https://jitpack.io" } } diff --git a/android/KMAPro/build.sh b/android/KMAPro/build.sh index e9a51062106..1eafe1e6671 100755 --- a/android/KMAPro/build.sh +++ b/android/KMAPro/build.sh @@ -10,6 +10,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "$KEYMAN_ROOT/resources/build/build-help.inc.sh" . "$KEYMAN_ROOT/resources/build/build-download-resources.sh" +. "$KEYMAN_ROOT/android/KMAPro/build-play-store-notes.inc.sh" + # ################################ Main script ################################ # Definition of global compile constants @@ -25,6 +27,7 @@ builder_describe "Builds Keyman for Android app." \ "configure" \ "build" \ "test Runs lint and unit tests." \ + "publish Publishes symbols to Sentry and the APK to the Play Store." \ "--ci Don't start the Gradle daemon. For CI" \ "--upload-sentry Upload to sentry" @@ -109,3 +112,13 @@ if builder_start_action test; then builder_finish_action success test fi + +if builder_start_action publish; then + # Copy Release Notes + generateReleaseNotes + + # Publish symbols and Keyman for Android to Play Store + ./gradlew $DAEMON_FLAG publishSentry publishReleaseApk + + builder_finish_action success publish +fi diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index 7eec37bcefa..11ec4efd2db 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -105,12 +105,17 @@ android { } // how to configure the sentry android gradle plugin -sentry { - // Disables or enables the automatic configuration of Native symbols - uploadNativeSymbols = true - - // Does or doesn't include the source code of native code for Sentry - includeNativeSources = true +task publishSentry { + doLast { + println 'Publishing Keyman symbols to Sentry' + sentry { + // Disables or enables the automatic configuration of Native symbols + uploadNativeSymbols = true + + // Does or doesn't include the source code of native code for Sentry + includeNativeSources = true + } + } } String env_keys_json_file = System.getenv("keys_json_file") @@ -145,7 +150,6 @@ repositories { dirs 'libs' } google() - jcenter() } dependencies { diff --git a/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-javadoc.jar b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-javadoc.jar new file mode 100644 index 00000000000..895d772c397 Binary files /dev/null and b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-javadoc.jar differ diff --git a/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-sources.jar b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-sources.jar new file mode 100644 index 00000000000..a3c41483871 Binary files /dev/null and b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-sources.jar differ diff --git a/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.aar b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.aar new file mode 100644 index 00000000000..9bc9a3f05cd Binary files /dev/null and b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.aar differ diff --git a/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.pom b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.pom new file mode 100644 index 00000000000..5bd1fc5d399 --- /dev/null +++ b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.pom @@ -0,0 +1,90 @@ + + + 4.0.0 + com.stepstone.stepper + material-stepper + 4.3.1 + aar + Android Material Stepper + This library allows to use Material steppers inside Android applications. + https://github.com/stepstone-tech/android-material-stepper + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + zawadz88 + Piotr Zawadzki + piotr.zawadzki@stepstone.com + + + + https://github.com/stepstone-tech/android-material-stepper.git + https://github.com/stepstone-tech/android-material-stepper.git + https://github.com/stepstone-tech/android-material-stepper + + + + com.android.support + appcompat-v7 + 25.4.0 + compile + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.7.21 + test + + + com.squareup.assertj + assertj-android + 1.1.1 + test + + + org.robolectric + robolectric + 3.3.1 + test + + + httpclient + org.apache.httpcomponents + + + commons-logging + commons-logging + + + + + org.jetbrains.kotlin + kotlin-stdlib-jre7 + 1.1.4-3 + test + + + org.jetbrains.kotlin + kotlin-reflect + 1.1.4-3 + test + + + com.nhaarman + mockito-kotlin + 1.4.0 + test + + + diff --git a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml index 7196abdb229..3ba7eaf9fca 100644 --- a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml +++ b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml @@ -297,6 +297,11 @@ android:configChanges="orientation" android:label="@string/app_name" android:theme="@style/AppTheme.Base" /> + diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index cf32ba0c000..4fb1c567922 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -4,6 +4,7 @@ package com.keyman.android; +import com.tavultesoft.kmapro.AdjustLongpressDelayActivity; import com.tavultesoft.kmapro.BuildConfig; import com.tavultesoft.kmapro.DefaultLanguageResource; import com.tavultesoft.kmapro.KeymanSettingsActivity; @@ -147,6 +148,16 @@ public void onStartInput(EditorInfo attribute, boolean restarting) { KMManager.onStartInput(attribute, restarting); KMManager.resetContext(KeyboardType.KEYBOARD_TYPE_SYSTEM); + // This method (likely) includes the IME equivalent to `onResume` for `Activity`-based classes, + // making it an important time to detect orientation changes. + Context appContext = getApplicationContext(); + int newOrientation = KMManager.getOrientation(appContext); + if(newOrientation != lastOrientation) { + lastOrientation = newOrientation; + Configuration newConfig = this.getResources().getConfiguration(); + KMManager.onConfigurationChanged(newConfig); + } + // Temporarily disable predictions on certain fields (e.g. hidden password field or numeric) int inputType = attribute.inputType; KMManager.setMayPredictOverride(inputType); @@ -154,7 +165,6 @@ public void onStartInput(EditorInfo attribute, boolean restarting) { KMManager.setBannerOptions(false); } else if (KMManager.isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)){ // Check if predictions needs to be re-enabled per Settings preference - Context appContext = getApplicationContext(); Keyboard kbInfo = KMManager.getCurrentKeyboardInfo(appContext); if (kbInfo != null) { String langId = kbInfo.getLanguageID(); @@ -166,12 +176,15 @@ public void onStartInput(EditorInfo attribute, boolean restarting) { } } + // Determine special handling for ENTER key + KMManager.setEnterMode(attribute.imeOptions, inputType); + InputConnection ic = getCurrentInputConnection(); if (ic != null) { ExtractedText icText = ic.getExtractedText(new ExtractedTextRequest(), 0); /* We do sometimes receive null `icText.text`, even though - getExtractedText() docs does not list this as a possible + getExtractedText() docs does not list this as a possible return value, so we test for that as well (#11479) */ if (icText != null && icText.text != null) { @@ -199,15 +212,6 @@ public void onUpdateExtractingVisibility(EditorInfo ei) { super.onUpdateExtractingVisibility(ei); } - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - if (newConfig.orientation != lastOrientation) { - lastOrientation = newConfig.orientation; - KMManager.onConfigurationChanged(newConfig); - } - } - @Override public void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly) { super.onConfigureWindow(win, isFullscreen, isCandidatesOnly); @@ -244,6 +248,8 @@ public void onKeyboardLoaded(KeyboardType keyboardType) { if (exText != null) exText = null; } + // Initialize keyboard options + KMManager.sendOptionsToKeyboard(); } @Override diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustLongpressDelayActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustLongpressDelayActivity.java new file mode 100644 index 00000000000..e79cb917ff3 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustLongpressDelayActivity.java @@ -0,0 +1,147 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * (Optional description of this file) + */ +package com.tavultesoft.kmapro; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; + +import com.keyman.engine.BaseActivity; +import com.keyman.engine.KMManager; + +/** + * Settings menu for adjusting the longpress delay time. The value for the current longpress delay time + * is saved in shared preferences as an integer (milliseconds). + */ +public class AdjustLongpressDelayActivity extends BaseActivity { + private static final String TAG = "AdjustLongpressDelay"; + public static final String adjustLongpressDelayKey = "AdjustLongpressDelay"; + + // Keeps track of the adjusted longpress delay time for saving. + // Internally use milliseconds, but GUI displays seconds + private static int currentDelayTimeMS = KMManager.KMDefault_LongpressDelay; // ms + private static int delayTimeIncrement = 200; // ms + + /** + * Convert currentDelayTimeMS to progress + * @return int + */ + private int delayTimeToProgress() { + return (currentDelayTimeMS / delayTimeIncrement) - 1; + } + + /** + * Convert progress to currentDelayTimeMS + * @param progress + * @return int (milliseconds) + */ + private int progressToDelayTime(int progress) { + return (progress + 1) * delayTimeIncrement + 100; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Context context = this; + + setContentView(R.layout.activity_adjust_longpress_delay); + Toolbar toolbar = (Toolbar) findViewById(R.id.titlebar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(null); + actionBar.setDisplayUseLogoEnabled(false); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setDisplayShowTitleEnabled(false); + actionBar.setDisplayShowCustomEnabled(true); + actionBar.setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.keyman_blue))); + } + + TextView adjustLongpressDelayActivityTitle = (TextView) findViewById(R.id.bar_title); + + String titleStr = getString(R.string.adjust_longpress_delay); + adjustLongpressDelayActivityTitle.setTextColor(ContextCompat.getColor(this, R.color.ms_white)); + adjustLongpressDelayActivityTitle.setText(titleStr); + + currentDelayTimeMS = KMManager.getLongpressDelay(); + + TextView adjustLongpressDelayText = (TextView) findViewById(R.id.delayTimeText); + String longpressDelayTextSeconds = String.format(getString(R.string.longpress_delay_time), (float)(currentDelayTimeMS/1000.0)); + adjustLongpressDelayText.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + adjustLongpressDelayText.setText(longpressDelayTextSeconds); + + final SeekBar seekBar = (SeekBar) findViewById(R.id.seekBar); + seekBar.setProgress(delayTimeToProgress()); + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Do nothing + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Do nothing + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + // Update the text field. + // The keyboard options will be saved and sent to KeymanWeb when exiting the menu + currentDelayTimeMS = progressToDelayTime(progress); + String longpressDelayTextSeconds = String.format(getString(R.string.longpress_delay_time), (float)(currentDelayTimeMS/1000.0)); + adjustLongpressDelayText.setText(longpressDelayTextSeconds); + } + }); + + findViewById(R.id.delayTimeDownButton).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (currentDelayTimeMS > KMManager.KMMinimum_LongpressDelay) { + currentDelayTimeMS -= delayTimeIncrement; + seekBar.setProgress(delayTimeToProgress()); + } + } + }); + + findViewById(R.id.delayTimeUpButton).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (currentDelayTimeMS < KMManager.KMMaximum_LongpressDelay) { + currentDelayTimeMS += delayTimeIncrement; + seekBar.setProgress(delayTimeToProgress()); + } + } + }); + } + + @Override + public void onBackPressed() { + // Store the longpress delay as a reference + // and then update KeymanWeb with the longpress delay + KMManager.setLongpressDelay(currentDelayTimeMS); + KMManager.sendOptionsToKeyboard(); + + super.onBackPressed(); + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; + } + +} diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeyboardSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeyboardSettingsActivity.java index 0989bf0d08b..689548a56ec 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeyboardSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeyboardSettingsActivity.java @@ -105,7 +105,7 @@ public void onCreate(Bundle savedInstanceState) { hashMap = new HashMap<>(); final String customHelpLink = kbd.getHelpLink(); // Check if app declared FileProvider - icon = String.valueOf(R.drawable.ic_arrow_forward); + icon = String.valueOf(R.drawable.ic_action_forward); // Don't show help link arrow if File Provider unavailable, or custom help doesn't exist if ( (customHelpLink != null && !FileProviderUtils.exists(context)) || (customHelpLink == null && !packageID.equals(KMManager.KMDefault_UndefinedPackageID)) ) { diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java index 86494e929b0..7e6dcbe5b15 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java @@ -10,7 +10,6 @@ public class KeymanSettingsActivity extends BaseActivity { protected static final String installedLanguagesKey = "InstalledLanguages"; protected static final String installKeyboardOrDictionaryKey = "InstallKeyboardOrDictionary"; - protected static final String showBannerKey = "ShowBanner"; protected static final String sendCrashReport = "SendCrashReport"; public static final String spacebarTextKey = "SpacebarText"; public static final String hapticFeedbackKey = "HapticFeedback"; diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java index 72c54aa0d02..59ac59e2f86 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java @@ -28,7 +28,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { private static Context context; private Preference languagesPreference, installKeyboardOrDictionary, displayLanguagePreference, - adjustKeyboardHeight; + adjustKeyboardHeight, adjustLongpressDelay; private ListPreference spacebarTextPreference; private CheckBoxPreference setSystemKeyboardPreference; private CheckBoxPreference setDefaultKeyboardPreference; @@ -99,6 +99,7 @@ public boolean onPreferenceClick(Preference preference) { }); setDefaultKeyboardPreference.setOnPreferenceChangeListener(checkBlocker); + adjustKeyboardHeight = new Preference(context); adjustKeyboardHeight.setKey(AdjustKeyboardHeightActivity.adjustKeyboardHeightKey); adjustKeyboardHeight.setTitle(getString(R.string.adjust_keyboard_height)); @@ -106,6 +107,13 @@ public boolean onPreferenceClick(Preference preference) { Intent adjustKeyboardHeightIntent = new Intent(context, AdjustKeyboardHeightActivity.class); adjustKeyboardHeight.setIntent(adjustKeyboardHeightIntent); + adjustLongpressDelay = new Preference(context); + adjustLongpressDelay.setKey(AdjustLongpressDelayActivity.adjustLongpressDelayKey); + adjustLongpressDelay.setTitle(getString(R.string.adjust_longpress_delay)); + adjustLongpressDelay.setWidgetLayoutResource(R.layout.preference_duration_icon_layout); + Intent adjustLongpressDelayIntent = new Intent(context, AdjustLongpressDelayActivity.class); + adjustLongpressDelay.setIntent(adjustLongpressDelayIntent); + /* Spacebar Caption Preference */ spacebarTextPreference = new ListPreference(context); @@ -170,12 +178,6 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { editor.putBoolean(KeymanSettingsActivity.showBannerKey, isChecked); as part of the default onClick() used by SwitchPreference. */ - SwitchPreference bannerPreference = new SwitchPreference(context); - bannerPreference.setKey(KeymanSettingsActivity.showBannerKey); - bannerPreference.setTitle(getString(R.string.show_banner)); - bannerPreference.setSummaryOn(getString(R.string.show_banner_on)); - bannerPreference.setSummaryOff(getString(R.string.show_banner_off)); - SwitchPreference getStartedPreference = new SwitchPreference(context); getStartedPreference.setKey(GetStartedActivity.showGetStartedKey); getStartedPreference.setTitle(String.format(getString(R.string.show_get_started), getString(R.string.get_started))); @@ -188,8 +190,6 @@ as part of the default onClick() used by SwitchPreference. sendCrashReportPreference.setSummaryOff(getString(R.string.show_send_crash_report_off)); sendCrashReportPreference.setDefaultValue(true); - - screen.addPreference(languagesPreference); screen.addPreference(installKeyboardOrDictionary); screen.addPreference(displayLanguagePreference); @@ -197,10 +197,10 @@ as part of the default onClick() used by SwitchPreference. screen.addPreference(setDefaultKeyboardPreference); screen.addPreference(adjustKeyboardHeight); + screen.addPreference(adjustLongpressDelay); screen.addPreference(spacebarTextPreference); screen.addPreference(hapticFeedbackPreference); - screen.addPreference(bannerPreference); screen.addPreference(getStartedPreference); screen.addPreference(sendCrashReportPreference); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java index faa3e0efbf1..75116bad8cc 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java @@ -174,7 +174,7 @@ public void onCreate(Bundle savedInstanceState) { updateActiveLexicalModel(); ImageView imageView = (ImageView) layout.findViewById(R.id.image1); - imageView.setImageResource(R.drawable.ic_arrow_forward); + imageView.setImageResource(R.drawable.ic_action_forward); layout.setEnabled(true); layout.setOnClickListener(new View.OnClickListener() { @Override @@ -198,7 +198,7 @@ public void onClick(View v) { * textView = (TextView) layout.findViewById(R.id.text1); * textView.setText(getString(R.string.manage_dictionary)); * imageView = (ImageView) layout.findViewById(R.id.image1); - * imageView.setImageResource(R.drawable.ic_arrow_forward); + * imageView.setImageResource(R.drawable.ic_action_forward); */ listView.setAdapter(adapter); @@ -346,7 +346,7 @@ public View getView(int position, View convertView, ViewGroup parent) { } holder.text.setText(kbd.getResourceName()); - holder.img.setImageResource(R.drawable.ic_arrow_forward); + holder.img.setImageResource(R.drawable.ic_action_forward); return convertView; } diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java index 7b621da4b7a..6e21d04f7bf 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java @@ -203,7 +203,7 @@ public View getView(int position, View convertView, ViewGroup parent) { } holder.textLang.setText(data.name); - holder.img.setImageResource(R.drawable.ic_arrow_forward); + holder.img.setImageResource(R.drawable.ic_action_forward); holder.textCount.setText(getContext().getResources().getQuantityString(R.plurals.keyboard_count, data.keyboards.size(), data.keyboards.size())); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index 1e5231f5215..2ae689364c3 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -472,7 +472,8 @@ public boolean onKeyUp(int keycode, KeyEvent e) { @Override public void onKeyboardLoaded(KeyboardType keyboardType) { - // Do nothing + // Initialize keyboard options + KMManager.sendOptionsToKeyboard(); } @Override @@ -646,7 +647,7 @@ private void showTextSizeDialog() { final View textSizeController = inflater.inflate(R.layout.text_size_controller, null); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(MainActivity.this); dialogBuilder.setIcon(R.drawable.ic_light_action_textsize); - dialogBuilder.setTitle(String.format(getString(R.string.text_size), textSize)); + dialogBuilder.setTitle(getTextSizeString()); dialogBuilder.setView(textSizeController); dialogBuilder.setPositiveButton(getString(R.string.label_ok), new DialogInterface.OnClickListener() { @Override @@ -677,7 +678,7 @@ public void onStartTrackingTouch(SeekBar seekBar) { public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { textSize = progress + minTextSize; textView.setTextSize((float) textSize); - dialog.setTitle(String.format(getString(R.string.text_size), textSize)); + dialog.setTitle(getTextSizeString()); } }); @@ -702,6 +703,16 @@ public void onClick(View v) { }); } + /** + * Combine a localized string for "Text Size" plus Arabic numerals + * @return String + */ + private String getTextSizeString() { + // Instead of formatting the number, will truncate formatting and concat the actual textSize + String label = getString(R.string.text_size).replace("%1$d", ""); + return label + KMString.format(" %d", textSize); + } + private void showClearTextDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(MainActivity.this); dialogBuilder.setIcon(R.drawable.ic_light_action_trash); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java index 3806d5f4b87..161d9663089 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java @@ -112,7 +112,8 @@ public void onCreate(Bundle savedInstanceState) { // Number of languages associated with the first keyboard in a keyboard package. // lexical-model packages will be 0 - final int languageCount = kmpProcessor.getLanguageCount(pkgInfo, PackageProcessor.PP_KEYBOARDS_KEY, 0); + final int languageCount = (keyboardCount > 0) ? + kmpProcessor.getLanguageCount(pkgInfo, PackageProcessor.PP_KEYBOARDS_KEY, 0) : 0; // Sanity check for keyboard packages if (pkgTarget.equals(PackageProcessor.PP_TARGET_KEYBOARDS)) { diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java index 46bc68ca1a5..9b053321824 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java @@ -81,7 +81,7 @@ public void onCreate(Bundle savedInstanceState) { HashMap hashMap = new HashMap<>(); hashMap.put(titleKey, keyboardName); hashMap.put(subtitleKey, pkgID); - String icon = String.valueOf(R.drawable.ic_arrow_forward); + String icon = String.valueOf(R.drawable.ic_action_forward); hashMap.put(iconKey, icon); list.add(hashMap); } diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/ic_light_action_textsize.png b/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/ic_light_action_textsize.png index 3809ef23536..9c8e3354e6b 100644 Binary files a/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/ic_light_action_textsize.png and b/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/ic_light_action_textsize.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/sil_logo_blue_2017_1.9.png b/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/sil_logo_blue_2017_1.9.png deleted file mode 100644 index ddc29053471..00000000000 Binary files a/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/sil_logo_blue_2017_1.9.png and /dev/null differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/sil_logo_color_tai_heritage_pro.9.png b/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/sil_logo_color_tai_heritage_pro.9.png new file mode 100644 index 00000000000..8f25377aa82 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/sil_logo_color_tai_heritage_pro.9.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-hdpi/blank_osk.png b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-hdpi/blank_osk.png new file mode 100644 index 00000000000..c5d555cacae Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-hdpi/blank_osk.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-hdpi/ic_light_action_textsize.png b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-hdpi/ic_light_action_textsize.png new file mode 100644 index 00000000000..6764f528f29 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-hdpi/ic_light_action_textsize.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-land-hdpi/blank_osk.png b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-land-hdpi/blank_osk.png new file mode 100644 index 00000000000..3e41a359a03 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-land-hdpi/blank_osk.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-mdpi/ic_light_action_textsize.png b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-mdpi/ic_light_action_textsize.png new file mode 100644 index 00000000000..a31690d1271 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-mdpi/ic_light_action_textsize.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw600dp-land/blank_osk.png b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw600dp-land/blank_osk.png new file mode 100644 index 00000000000..282606a9d50 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw600dp-land/blank_osk.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw600dp/blank_osk.png b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw600dp/blank_osk.png new file mode 100644 index 00000000000..8246a641687 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw600dp/blank_osk.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw720dp-land/blank_osk.png b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw720dp-land/blank_osk.png new file mode 100644 index 00000000000..740c8559c99 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw720dp-land/blank_osk.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw720dp/blank_osk.png b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw720dp/blank_osk.png new file mode 100644 index 00000000000..0e3343bbd12 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-sw720dp/blank_osk.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-xhdpi/ic_light_action_textsize.png b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-xhdpi/ic_light_action_textsize.png new file mode 100644 index 00000000000..ca9829d1788 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-xhdpi/ic_light_action_textsize.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-xxhdpi/ic_light_action_textsize.png b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-xxhdpi/ic_light_action_textsize.png new file mode 100644 index 00000000000..d15adad0acd Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-ldrtl-xxhdpi/ic_light_action_textsize.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-mdpi/sil_logo_blue_2017_1.9.png b/android/KMAPro/kMAPro/src/main/res/drawable-mdpi/sil_logo_blue_2017_1.9.png deleted file mode 100644 index 0e720d69a11..00000000000 Binary files a/android/KMAPro/kMAPro/src/main/res/drawable-mdpi/sil_logo_blue_2017_1.9.png and /dev/null differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-mdpi/sil_logo_color_tai_heritage_pro.9.png b/android/KMAPro/kMAPro/src/main/res/drawable-mdpi/sil_logo_color_tai_heritage_pro.9.png new file mode 100644 index 00000000000..24cc6b9c923 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-mdpi/sil_logo_color_tai_heritage_pro.9.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-xhdpi/sil_logo_blue_2017_1.9.png b/android/KMAPro/kMAPro/src/main/res/drawable-xhdpi/sil_logo_blue_2017_1.9.png deleted file mode 100644 index 17f25f6cce6..00000000000 Binary files a/android/KMAPro/kMAPro/src/main/res/drawable-xhdpi/sil_logo_blue_2017_1.9.png and /dev/null differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-xhdpi/sil_logo_color_tai_heritage_pro.9.png b/android/KMAPro/kMAPro/src/main/res/drawable-xhdpi/sil_logo_color_tai_heritage_pro.9.png new file mode 100644 index 00000000000..308a7c6f9a8 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-xhdpi/sil_logo_color_tai_heritage_pro.9.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-xxhdpi/sil_logo_blue_2017_1.9.png b/android/KMAPro/kMAPro/src/main/res/drawable-xxhdpi/sil_logo_blue_2017_1.9.png deleted file mode 100644 index 014572d691c..00000000000 Binary files a/android/KMAPro/kMAPro/src/main/res/drawable-xxhdpi/sil_logo_blue_2017_1.9.png and /dev/null differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-xxhdpi/sil_logo_color_tai_heritage_pro.9.png b/android/KMAPro/kMAPro/src/main/res/drawable-xxhdpi/sil_logo_color_tai_heritage_pro.9.png new file mode 100644 index 00000000000..eee36efdecc Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-xxhdpi/sil_logo_color_tai_heritage_pro.9.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-xxxhdpi/sil_logo_blue_2017_1.9.png b/android/KMAPro/kMAPro/src/main/res/drawable-xxxhdpi/sil_logo_blue_2017_1.9.png deleted file mode 100644 index 04366bd151a..00000000000 Binary files a/android/KMAPro/kMAPro/src/main/res/drawable-xxxhdpi/sil_logo_blue_2017_1.9.png and /dev/null differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-xxxhdpi/sil_logo_color_tai_heritage_pro.9.png b/android/KMAPro/kMAPro/src/main/res/drawable-xxxhdpi/sil_logo_color_tai_heritage_pro.9.png new file mode 100644 index 00000000000..3dec6473b93 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-xxxhdpi/sil_logo_color_tai_heritage_pro.9.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/branded_logo.xml b/android/KMAPro/kMAPro/src/main/res/drawable/branded_logo.xml index cb6b2cbabfe..2f5d1eca2b9 100644 --- a/android/KMAPro/kMAPro/src/main/res/drawable/branded_logo.xml +++ b/android/KMAPro/kMAPro/src/main/res/drawable/branded_logo.xml @@ -15,7 +15,7 @@ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_back.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_back.xml deleted file mode 100644 index 165d535d3d0..00000000000 --- a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_back.xml +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_decrement.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_decrement.xml new file mode 100644 index 00000000000..c3085bec5e9 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_decrement.xml @@ -0,0 +1,5 @@ + + diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_increment.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_increment.xml new file mode 100644 index 00000000000..ea188f818f1 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_increment.xml @@ -0,0 +1,5 @@ + + diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_timelapse.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_timelapse.xml new file mode 100644 index 00000000000..80dc29e33f9 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_timelapse.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/textsize_decrease.xml b/android/KMAPro/kMAPro/src/main/res/drawable/textsize_decrease.xml new file mode 100644 index 00000000000..3e8ea253f78 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/textsize_decrease.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/textsize_increase.xml b/android/KMAPro/kMAPro/src/main/res/drawable/textsize_increase.xml new file mode 100644 index 00000000000..b51b51139b5 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/textsize_increase.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_longpress_delay.xml b/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_longpress_delay.xml new file mode 100644 index 00000000000..ef1ebd613b3 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_longpress_delay.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + diff --git a/android/KMAPro/kMAPro/src/main/res/layout/activity_info.xml b/android/KMAPro/kMAPro/src/main/res/layout/activity_info.xml index 59de5185356..314a5034129 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/activity_info.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/activity_info.xml @@ -1,12 +1,12 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="@color/ms_white" + tools:context=".InfoActivity" > - - + android:orientation="horizontal"> - + + + + + + android:src="@drawable/ic_action_back" /> + android:src="@drawable/ic_action_forward" /> + diff --git a/android/KMAPro/kMAPro/src/main/res/layout/preference_icon_layout.xml b/android/KMAPro/kMAPro/src/main/res/layout/preference_icon_layout.xml index 6aa84ce384b..7074c696de8 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/preference_icon_layout.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/preference_icon_layout.xml @@ -2,4 +2,4 @@ + android:src="@drawable/ic_action_forward" /> diff --git a/android/KMAPro/kMAPro/src/main/res/layout/text_size_controller.xml b/android/KMAPro/kMAPro/src/main/res/layout/text_size_controller.xml index 2245ef557a3..cfd36374deb 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/text_size_controller.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/text_size_controller.xml @@ -15,7 +15,7 @@ android:layout_marginStart="5dp" android:layout_marginEnd="5dp" android:contentDescription="@string/ic_text_size_down" - android:src="@drawable/ic_light_dialog_textsize_down" /> + android:src="@drawable/textsize_decrease" /> + android:src="@drawable/textsize_increase" /> diff --git a/android/KMAPro/kMAPro/src/main/res/layout/web_browser_bar_layout.xml b/android/KMAPro/kMAPro/src/main/res/layout/web_browser_bar_layout.xml index 2e9b059c55f..749412e0676 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/web_browser_bar_layout.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/web_browser_bar_layout.xml @@ -15,7 +15,6 @@ android:layout_height="match_parent" android:layout_margin="3dp" android:padding="4dp" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_above="@+id/progressBar" android:weightSum="2" @@ -76,7 +75,6 @@ style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal" android:layout_width="match_parent" android:layout_height="@dimen/keyman_bar_height" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignParentBottom="true" android:visibility="visible" diff --git a/android/KMAPro/kMAPro/src/main/res/values-ar-rSA/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-ar-rSA/strings.xml new file mode 100644 index 00000000000..6ceb657187b --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/values-ar-rSA/strings.xml @@ -0,0 +1,168 @@ + + + + + مشاركة + + متصفح ويب + + حجم النص + + المزيد + + محو النص + + معلومات + + الإعدادات + + تنزيل التحديثات + + إصدار: %1$s + + يتطلب Keyman إصدار 57 من Chrome أو أعلى. + + تحديث Chrome + + ابدأ الكتابة هنا… + + + + حجم النص: %1$d + + تكبير حجم النص + + تصغير حجم النص + + Text size slider + + \nسيتم محو كل النص\n + + إبدأ الآن + + اضف لوحة مفاتيح بلغتك + + تمكين Keyman لوحة المفاتيح الأساسية للنظام + + إعداد Keyman لوحة المفاتيح الإفتراضية + + المزيد من المعلومات + + أظهر \"%1$s\" عند البدء + + لتثبيت حزم لوحات المفاتيح، امنح Keyman الإذن لقراءة التخزين الخارجي. + + تم رفض أذونات التخزين. قد يفشل تثبيت حزمة لوحة المفاتيح + Storage permission request failed. Try Keyman Settings - Install from local file + + الإعدادات + + + اللغات المثبتة (%1$d) + اللغات المثبتة (%1$d) + اللغات المثبتة (%1$d) + اللغات المثبتة (%1$d) + اللغات المثبتة (%1$d) + اللغات المثبتة (%1$d) + + + تثبيت لوحة المفاتيح او القاموس + + لغة العرض + + تعديل ارتفاع لوحة المفاتيح + + نص مفتاح المسافة + + لوحة المفاتيح + + اللغة + + اللغة + لوحة المفاتيح + + فارغ + + أظهر اسم لوحة المفاتيح على مفتاح المسافة + + أظهر اسم اللغة على المسافة + + اللغة ولوحة المفاتيح على المسافة + + لا تظهر نص على المسافة + + الاهتزاز عند الكتابة + + إظهار اللافتة دائماً + + سيتم تنفيذها + + عند إيقاف التشغيل، يظهر فقط عند تفعيل النص التنبؤي + + السماح بإرسال تقارير توقف التشغيل المفاجئ عبر الشبك + + عند التشغيل، سيتم إرسال تقارير التوقف المفاجئ + + عند إيقاف التشغيل، لن يتم إرسال التقارير + + تثبيت من keyman.com + + التثبيت من ملف محلي + + التثبيت من جهاز آخر + + اضف لغات للوحة المفاتيح المثبتة + + (من حزمة لوحة المفاتيح) + + اختر حزمة لوحة مفاتيح + + اختر لغة %1$s + + تمت اضافة %1$s إلى %2$s + + كل اللغات مثبتة مسبقا + + اسحب لوحة المفاتيح لتغيير مدى ارتفاعها + + دور الجهاز لضبط الوضع العمودي والوضع الأفقي + + إعادة ضبط الافتراضيات + + ابحث أو اكتب عنوان URL + + علامات مرجعية + + لا تتوفر أي علامات مرجعية + + أضف علامة مرجعية + + العنوان + + عنوان Url + + فشل في تنزيل حزمة %1$s + + يتم الآن تحميل لوحة مفاتيح حزمة \n%1$s… + + فشل في الإستخراج + + تثبيت لوحة المفاتيح + + تثبيت القاموس + + ليس %1$s ملف حزمة صالحة لبرنامج Keyman.\n%2$s\" + + ليس في حزمة لوحة المفاتيح لوحات مفاتيح مكيّفة لشاشات اللمس + + لا يوجد نصوص تنبؤية للتنزيل + + لا لوحات مفاتيح و نصوص تنبؤية للتنزيل + + ليس لحزمة لوحة المفاتيح لغات ذات صلة لتثبيتها + + غير صالح\ناقص البيانات الوصفية في الحزمة + + تتطلب لوحة المفاتيح إصدارًا أحدث من Keyman + + غير قادر على تشغيل متصفح الويب + diff --git a/android/KMAPro/kMAPro/src/main/res/values-b+el/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-b+el/strings.xml new file mode 100644 index 00000000000..428bf5ad598 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/values-b+el/strings.xml @@ -0,0 +1,164 @@ + + + + + Μοιρασθῆτε + + Φυλλομετρητής + + Μέγεθος κειμένου + + Περισσότερα + + Διαγραφὴ κειμένου + + Πληροφορίες + + Ρυθμίσεις + + Εγκατάσταση Ενημερώσεων + + Ἔκδοση %1$s + + Τὸ Keyman ἀπαιτεῖ ἔκδοση Chrome 57 ἢ νεώτερη. + + Ἐνημερῶστε τὸ Chrome + + Ἀρχίστε νὰ γράφετε ἐδῶ… + + + + Μέγεθος κειμένου: %1$d + + Μεγαλῶστε τὴν γραμματοσειρά + + Μικρύνετε τὴν γραμματοσειρά + + Ρύθμιση μεγέθους κειμένου + + \nΤὸ κείμενο θὰ ἐκκαθαρισθεῖ πλήρως\n + + Μικρύνετε τὸ κείμενο + + Προσθέστε πληκτρολόγιο γιὰ τὴν γλῶσσα σας + + Ὁρίστε τὸ Κῆμαν ὡς παν-συστημικὸ πληκτρολόγιο + + Ὁρίστε τὸ Κῆμαν ὡς προεπιλεγμένο πληκτρολόγιο + + Περισσότερες πληροφορίες + + Κατὰ τὴν ἔναρξη νὰ προβάλλεται τὸ \"%1$s + + Γιὰ νὰ ἐγκαταστήσετε πακέτα πληκτρολογίων, ἐπιτρέψτε στὸ Κῆμαν νὰ διαβάζει ἐξωτερικὸ χῶρο ἀποθήκευσης. + + Ἀπερρίφθη αἴτημα χώρου ἀποθήκευσης. Πιθανὴ ἀποτυχία ἐγκατάστασης πακέτου πληκτρολογίου + Ἀπερρίφθη αἴτημα χώρου ἀποθηκεύσεως. Δοκιμάστε τὶς ρυθμίσεις Κῆμαν - Ἐγκατάσταση ἀπὸ τοπικὸ ἀρχεῖο + + Ρυθμίσεις + + + Ἐγκατεστημένες γλῶσσες (%1$d) + Ἐγκατεστημένες γλῶσσες (%1$d) + + + Ἐγκαταστῆστε πληκτρολόγιο ἢ λεξικό + + Γλῶσσα προβολῆς + + Μεταβολὴ ὕψους πληκτρολογίου + + Λεζάντα πλήκτρου διαστήματος + + Πληκτρολόγιο + + Γλῶσσα + + Γλῶσσα + Πληκτρολόγιο + + Κενό + + \'Ονομα πληκτρολογίου στὸ πλῆκτρο διαστήματος + + Ὄνομα γλώσσας στὸ πλῆκτρο διαστήματος + + Ὄνομα πληκτρολογίου καὶ γλώσσας στὸ πλῆκτρο διαστήματος + + Καμμία λεζάντα στὸ πλῆκτρο διαστήματος + + Δὸνηση κατὰ τὴν πληκτρολόγηση + + Νὰ ἐμφανίζεται πάντα banner + + Πρὸς ὑλοποίησιν + + OFF: ἐμφάνιση μόνον ὅταν ἔχει ἐνεργοποιηθεῖ ἡ πρόβλεψη κειμένου + + Ἐπιτρέψτε τὴν ἀποστολὴ ἀναφορῶν κατάρρευσης μέσῳ δικτύου + + ON: Ἀποστολὴ ἀναφορῶν κατάρρευσης + + OFF: Δὲν ἀποστέλλονται ἀναφορὲς κατάρρευσης + + Ἐγκατάσταση ἀπὸ τὸ keyman.com + + Ἐγκατάσταση ἀπὸ τοπικὸ ἀρχεῖο + + Ἐγκατάσταση ἀπὸ ἄλλη συσκευή + + Προσθέστε γλῶσσες σὲ ἐγκατεστημένο πληκτρολόγιο + + (ἀπὸ πακέτο πληκτρολογίου) + + Ἐπιλέξτε Πακέτο Πληκτρολογίου + + Ἐπιλέξτε γλῶσσες γιὰ τὸ %1$s + + Προσετέθη ἡ γλῶσσα %1$s στὸ %2$s + + Ὅλες οἱ γλῶσσες ἔχουν ἤδη ἐγκατασταθεῖ + + Σύρετε τὸ πληκτρολόγιο γιὰ νὰ ἀλλάξετε τὸ ὕψος του + + Περιστρέψτε τὴν συσκευὴ γιὰ προσαρμογὴ τοῦ ὕψους σὲ λειτουργία πορτραίτου καὶ τοπίου + + Ἐπαναφέρετε τὶς προεπιλεγμένες ρυθμίσεις + + Ἀναζητῆστε ἢ πληκτρολογῆστε URL + + Σελιδοδεῖκτες + + Δὲν ὑπάρχουν σελιδοδεῖκτες + + Προσθέστε σελιδοδείκτη + + Τίτλος + + URL + + Τὸ πακέτο %1$s ἀπέτυχε νὰ ἐγκατασταθεῖ + + Λήψη πακέτου πληκτρολογίου\n%1$s… + + Ἀποτυχία ἐξαγωγῆς + + Ἐγκαταστῆστε πληκτρολόγιο + + Ἐγκαταστῆστε Λεξικό + + Τὸ %1$s δὲν εἶναι ἔγκυρο ἀρχεῖο πακέτου Κῆμαν.\n%2$s\" + + Τὸ πακέτο πληκτρολογίου δὲν ἔχει βελτιστοποιημένα πληκτρολόγια ἀφῆς πρὸς ἐγκατάστασιν + + Δὲν ὑπάρχουν προγνώσεις κειμένων πρὸς ἐγκατάστασιν + + Δὲν ὑπάρχουν πληκτρολόγια ἢ προγνώσεις κειμένων πρὸς ἐγκατάστασιν + + Τὸ πακέτο πληκτρολογίου δὲν ἔχει σχετικὲς μὲ αὐτὸ γλῶσσες πρὸς ἐγκατάστασιν + + Ἄκυρα/ἐλλιπῆ μεταδεδομένα στὸ πακέτο + + Τὸ πληκτρολόγιο ἀπαιτεῖ νεώτερη ἔκδοση τοῦ Κῆμαν + + Ἀδυναμία ἐκκινήσεως φυλλομετρητῆ + diff --git a/android/KMAPro/kMAPro/src/main/res/values-cs-rCZ/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-cs-rCZ/strings.xml index bec7931da85..97da3115df7 100644 --- a/android/KMAPro/kMAPro/src/main/res/values-cs-rCZ/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values-cs-rCZ/strings.xml @@ -33,6 +33,8 @@ Velikost textu nahoru Velikost textu dolů + + Posuvník velikosti textu \nVeškerý text bude vymazán\n @@ -51,6 +53,7 @@ Pro instalaci balíčků klávesnice povolte klávesnici oprávnění ke čtení externího úložiště. Žádost o oprávnění k úložišti byla zamítnuta. Může selhat instalace balíčku klávesnice + Žádost o oprávnění k úložišti se nezdařila. Zkuste nastavení Keyman - Instalovat z místního souboru Nastavení @@ -67,6 +70,8 @@ Upravit výšku klávesnice + Upravit zpoždění dlouhého stisknutí + Titulek mezerníku Keyboard @@ -85,7 +90,7 @@ Nezobrazovat titulek v mezerníku - Vibrate when typing + Při psaní vibrovat Vždy zobrazovat banner @@ -122,6 +127,14 @@ Otočit zařízení pro nastavení na výšku a na šířku Obnovit výchozí nastavení + + Doba zpoždění: %1$.1f sekund + + Prodloužit dobu zpoždění + + Zkrátit dobu zpoždění + + Posuvník doby zpoždění dlouhého stisknutí Hledat nebo zadat URL @@ -157,7 +170,7 @@ Neplatná/chybějící metadata v balíčku - Keyboard requires a newer version of Keyman + Klávesnice vyžaduje novější verzi Keyman - Unable to launch web browser + Nelze spustit webový prohlížeč diff --git a/android/KMAPro/kMAPro/src/main/res/values-it-rIT/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-it-rIT/strings.xml index b3d15fc6317..a475ae37255 100644 --- a/android/KMAPro/kMAPro/src/main/res/values-it-rIT/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values-it-rIT/strings.xml @@ -1,5 +1,6 @@ + Condividi @@ -32,6 +33,8 @@ Dimensione testo in alto Dimensione del testo giù + + Cursore dimensione testo \nTutto il testo sarà cancellato\n @@ -50,6 +53,7 @@ Per installare pacchetti di tastiera, consentire a Keyman di leggere la memoria esterna. La richiesta di autorizzazione all\'archiviazione è stata negata. Potrebbe non essere stato possibile installare il pacchetto tastiera + Richiesta di autorizzazione archiviazione non riuscita. Prova Impostazioni Keyman - Installa da file locale Impostazioni @@ -64,6 +68,8 @@ Regola altezza tastiera + Adjust longpress delay + Didascalia barra spaziatrice Keyboard @@ -82,7 +88,7 @@ Non mostrare la didascalia sulla barra spaziatrice - Vibrate when typing + Vibra durante la digitazione Mostra sempre il banner @@ -119,6 +125,14 @@ Ruota il dispositivo per regolare verticale e orizzontale Ripristina impostazioni predefinite + + Delay Time: %1$.1f seconds + + Delay time longer + + Delay time shorter + + Longpress delay time slider Cerca o digita URL @@ -154,7 +168,7 @@ Metadati non validi/mancanti nel pacchetto - Keyboard requires a newer version of Keyman + La tastiera richiede una nuova versione di Keyman - Unable to launch web browser + Impossibile avviare il browser web diff --git a/android/KMAPro/kMAPro/src/main/res/values-pt-rPT/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-pt-rPT/strings.xml index 3e3580f9311..02f1d9291de 100644 --- a/android/KMAPro/kMAPro/src/main/res/values-pt-rPT/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values-pt-rPT/strings.xml @@ -1,5 +1,6 @@ + Compartilhar @@ -32,6 +33,8 @@ Tamanho do texto para cima Tamanho do texto para baixo + + Tamanho do texto deslizante \nTodo o texto será limpo\n @@ -50,6 +53,7 @@ Para instalar pacotes de teclado, permita que o Keyman leia o armazenamento externo. A solicitação de permissão de armazenamento foi negada. Pode falhar na instalação do pacote de teclado + Falha no pedido de permissão de armazenamento. Experimente instalar de um ficheiro local, nas configurações do Keyman Configurações @@ -82,7 +86,7 @@ Não mostrar legenda na barra de espaço - Vibrate when typing + Vibrar ao tocar Sempre mostrar banner @@ -154,7 +158,7 @@ Metadados inválidos/ausentes no pacote - Keyboard requires a newer version of Keyman + O teclado requer uma nova versão do Keyman - Unable to launch web browser + Não foi possível carregar o navegador diff --git a/android/KMAPro/kMAPro/src/main/res/values/dimens.xml b/android/KMAPro/kMAPro/src/main/res/values/dimens.xml index cfcc2f4917f..60e89def04b 100644 --- a/android/KMAPro/kMAPro/src/main/res/values/dimens.xml +++ b/android/KMAPro/kMAPro/src/main/res/values/dimens.xml @@ -23,4 +23,5 @@ 32dp 16dp 0dp + 24sp diff --git a/android/KMAPro/kMAPro/src/main/res/values/strings.xml b/android/KMAPro/kMAPro/src/main/res/values/strings.xml index 3cec4f2558c..01546f91ba5 100644 --- a/android/KMAPro/kMAPro/src/main/res/values/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values/strings.xml @@ -113,6 +113,9 @@ Adjust keyboard height + + Adjust longpress delay + Spacebar caption @@ -144,13 +147,13 @@ Vibrate when typing - + Always show banner - + To be implemented - + When off, only shown when predictive text is enabled @@ -198,6 +201,17 @@ Reset to Defaults + + Delay Time: %1$.1f seconds + + + Delay time longer + + + Delay time shorter + + + Longpress delay time slider Search or type URL diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 1582c970f80..5e5d7241235 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -37,11 +37,7 @@ function init() { keyman.beepKeyboard = beepKeyboard; // Readies the keyboard stub for instant loading during the init process. - try { - KeymanWeb.registerStub(JSON.parse(jsInterface.initialKeyboard())); - } catch(error) { - console.error(error); - } + KeymanWeb.registerStub(JSON.parse(jsInterface.initialKeyboard())); keyman.init({ 'embeddingApp':device, @@ -116,6 +112,17 @@ function notifyHost(event, params) { }, 10); } +// Update the KeymanWeb longpress delay +// delay is in milliseconds +function setLongpressDelay(delay) { + if (keyman.osk) { + keyman.osk.gestureParams.longpress.waitLength = delay; + console_debug('setLongpressDelay('+delay+')'); + } else { + window.console.log('setLongpressDelay error: keyman.osk undefined'); + } +} + // Update the KMW banner height // h is in dpi (different from iOS) function setBannerHeight(h) { @@ -145,7 +152,7 @@ function setOskHeight(h) { function setOskWidth(w) { if(w > 0) { - oskWidth = w; + oskWidth = w / window.devicePixelRatio; } } @@ -297,7 +304,7 @@ function updateKMSelectionRange(start, end) { console_debug('result:\n' + build_context_string(context)); } else { - console.debug('range unchanged'); + console_debug('range unchanged'); } } @@ -337,6 +344,14 @@ function menuKeyUp() { window.location.hash = hash; } +// The keyboard-picker displayed via Android longpress disrupts Web-side +// gesture-handling; this function helps force-clear the globe key's highlighting. +function clearGlobeHighlight() { + if(keyman.osk && keyman.osk.vkbd && keyman.osk.vkbd.currentLayer.globeKey) { + keyman.osk.vkbd.currentLayer.globeKey.highlight(false) + } +} + function hideKeyboard() { fragmentToggle = (fragmentToggle + 1) % 100; window.location.hash = 'hideKeyboard' + fragmentToggle; diff --git a/android/KMEA/app/src/main/assets/keyboard.es5.html b/android/KMEA/app/src/main/assets/keyboard.es5.html deleted file mode 100644 index 6384235456b..00000000000 --- a/android/KMEA/app/src/main/assets/keyboard.es5.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - Keyman - - - - - - - - - - - - - diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java b/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java index 27c462e95a0..4783e401b8e 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java @@ -40,6 +40,7 @@ public static final DisplayLanguageType[] getDisplayLanguages(Context context) { DisplayLanguageType[] languages = { new DisplayLanguageType(unspecifiedLocale, context.getString(R.string.default_locale)), new DisplayLanguageType("am-ET", "አማርኛ (Amharic)"), + new DisplayLanguageType("ar-SA", "(Arabic) العربية"), new DisplayLanguageType("az-AZ", "Azərbaycanca (Azəricə)"), new DisplayLanguageType("bwr-NG", "Bura-Pabir"), new DisplayLanguageType("cs-CZ", "čeština (Czech)"), @@ -62,6 +63,7 @@ public static final DisplayLanguageType[] getDisplayLanguages(Context context) { new DisplayLanguageType("nl-NL", "Nederlands (Dutch)"), 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("ff-ZA", "Pulaar-Fulfulde"), // or Fulah new DisplayLanguageType("ru-RU", "Pyccĸий (Russian)"), diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index d45937fe730..7d731f2b63d 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -195,13 +195,13 @@ protected boolean updateSelectionRange() { int selMin = icText.selectionStart, selMax = icText.selectionEnd; int textLength = rawText.length(); - + if (selMin < 0 || selMax < 0) { // There is no selection or cursor // Reference https://developer.android.com/reference/android/text/Selection#getSelectionEnd(java.lang.CharSequence) return false; } else if (selMin > textLength || selMax > textLength) { - // Selection is past end of existing text -- should not be possible but we + // Selection is past end of existing text -- should not be possible but we // are seeing it happen; #11506 return false; } @@ -231,7 +231,7 @@ protected boolean updateSelectionRange() { selMin -= pairsAtStart; selMax -= (pairsAtStart + pairsSelected); this.loadJavascript(KMString.format("updateKMSelectionRange(%d,%d)", selMin, selMax)); - + return true; } @@ -262,7 +262,7 @@ public void initKMKeyboard(final Context context) { // When `.isTestMode() == true`, the setWebContentsDebuggingEnabled method is not available // and thus will trigger unit-test failures. if (!KMManager.isTestMode() && ( - (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 || + (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 || KMManager.getTier(null) != KMManager.Tier.STABLE )) { // Enable debugging of WebView via adb. Not used during unit tests @@ -313,6 +313,12 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d public void onLongPress(MotionEvent event) { if (KMManager.getGlobeKeyState() == KMManager.GlobeKeyState.GLOBE_KEY_STATE_DOWN) { KMManager.setGlobeKeyState(KMManager.GlobeKeyState.GLOBE_KEY_STATE_LONGPRESS); + + // When we activate the keyboard picker, this will disrupt the JS-side's control + // flow for gesture-handling; we should pre-emptively clear the globe key, + // as Web will not receive a "globe key up" event. + loadJavascript("clearGlobeHighlight()"); + KMManager.handleGlobeKeyAction(context, true, keyboardType); return; /* For future implementation @@ -443,26 +449,42 @@ public void onDestroy() { dismissHelpBubble(); } + @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); RelativeLayout.LayoutParams params = KMManager.getKeyboardLayoutParams(); + // I suspect this is the part we should actually be calling directly... this.setLayoutParams(params); + this.invalidate(); + this.requestLayout(); + + this.dismissHelpBubble(); + + if(this.getShouldShowHelpBubble()) { + this.showHelpBubbleAfterDelay(2000); + } + } + @Override + public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); int bannerHeight = KMManager.getBannerHeight(context); int oskHeight = KMManager.getKeyboardHeight(context); + + if(bannerHeight + oskHeight != height) { + // We'll proceed, but cautiously and with logging. + KMLog.LogInfo(TAG, "Height mismatch: onSizeChanged = " + height + ", our version = " + (bannerHeight + oskHeight)); + } + if (this.htmlBannerString != null && !this.htmlBannerString.isEmpty()) { setHTMLBanner(this.htmlBannerString); } + loadJavascript(KMString.format("setBannerHeight(%d)", bannerHeight)); - loadJavascript(KMString.format("setOskWidth(%d)", newConfig.screenWidthDp)); + loadJavascript(KMString.format("setOskWidth(%d)", width)); + // Must be last - it's the one that triggers a Web-engine layout refresh. loadJavascript(KMString.format("setOskHeight(%d)", oskHeight)); - - this.dismissHelpBubble(); - - if(this.getShouldShowHelpBubble()) { - this.showHelpBubbleAfterDelay(2000); - } } public void dismissSuggestionMenuWindow() { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java index 926ef094f74..d403a29235b 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java @@ -20,6 +20,7 @@ import static android.content.Context.VIBRATOR_SERVICE; +import com.keyman.engine.KMManager.EnterModeType; import com.keyman.engine.KMManager.KeyboardType; import com.keyman.engine.data.Keyboard; import com.keyman.engine.util.CharSequenceUtil; @@ -153,7 +154,53 @@ public void run() { } if (s.length() > 0 && s.charAt(0) == '\n') { - keyDownUp(KeyEvent.KEYCODE_ENTER, 0); + if (k.keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM) { + // Special handling of ENTER key + switch (KMManager.enterMode) { + // Go action + case GO : + ic.performEditorAction(EditorInfo.IME_ACTION_GO); + break; + + // Search action + case SEARCH : + ic.performEditorAction(EditorInfo.IME_ACTION_SEARCH); + break; + + // Send action + case SEND : + ic.performEditorAction(EditorInfo.IME_ACTION_SEND); + break; + + // Next action + case NEXT : + ic.performEditorAction(EditorInfo.IME_ACTION_NEXT); + break; + + // Done action + case DONE : + ic.performEditorAction(EditorInfo.IME_ACTION_DONE); + break; + + // Previous action + case PREVIOUS : + ic.performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); + break; + + // Messaging apps + case NEWLINE : + // Send newline and advance cursor + ic.commitText("\n", 1); + break; + + // Default ENTER action + default: + keyDownUp(KeyEvent.KEYCODE_ENTER, 0); + } + } else { + // In-app keyboard uses default ENTER action + keyDownUp(KeyEvent.KEYCODE_ENTER, 0); + } ic.endBatchEdit(); return; } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java index b80f6766a33..1e28e4af0b3 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java @@ -164,7 +164,7 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { // appContext instead of context? SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); boolean modelPredictionPref = false; - if (KMManager.currentLexicalModel != null) { + if (!KMManager.getMayPredictOverride() && KMManager.currentLexicalModel != null) { modelPredictionPref = prefs.getBoolean(KMManager.getLanguagePredictionPreferenceKey(KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), true); } KMManager.setBannerOptions(modelPredictionPref); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index d1a40be69ae..592f4be2a15 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -25,7 +25,6 @@ import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.Point; import android.graphics.Typeface; import android.inputmethodservice.InputMethodService; @@ -180,6 +179,19 @@ public String toString() { } } + // Enum for how the System Keyboard ENTER key is handled for the EditorInfo action + // Reference: https://developer.android.com/reference/android/view/inputmethod/EditorInfo#summary + public enum EnterModeType { + GO, // Go action + SEARCH, // Search action + SEND, // Send action + NEXT, // Next action + DONE, // Done action + PREVIOUS, // Previous action + NEWLINE, // Send newline character + DEFAULT, // Default ENTER action + } + protected static InputMethodService IMService; private static boolean debugMode = false; @@ -218,6 +230,9 @@ public String toString() { // regardless what the Settings preference is. private static boolean mayPredictOverride = false; + // Determine how system keyboard handles ENTER key + public static EnterModeType enterMode = EnterModeType.DEFAULT; + // Boolean for whether a keyboard can send embedded KMW crash reports to Sentry // When maySendCrashReport is false, KMW will still attempt to send crash reports, but it // will be blocked. @@ -248,6 +263,8 @@ public String toString() { public static final String KMKey_KeyboardHeightPortrait = "keyboardHeightPortrait"; public static final String KMKey_KeyboardHeightLandscape = "keyboardHeightLandscape"; + public static final String KMKey_LongpressDelay = "longpressDelay"; + public static final String KMKey_CustomHelpLink = "CustomHelpLink"; public static final String KMKey_KMPLink = "kmp"; public static final String KMKey_UserKeyboardIndex = "UserKeyboardIndex"; @@ -293,11 +310,15 @@ public String toString() { public static final String KMDefault_DictionaryVersion = "0.1.4"; public static final String KMDefault_DictionaryKMP = KMDefault_DictionaryPackageID + FileUtils.MODELPACKAGE; + // Default KeymanWeb longpress delay constants in milliseconds + public static final int KMDefault_LongpressDelay = 500; + public static final int KMMinimum_LongpressDelay = 300; + public static final int KMMaximum_LongpressDelay = 1500; + + // Keyman files protected static final String KMFilename_KeyboardHtml = "keyboard.html"; - protected static final String KMFilename_KeyboardHtml_Legacy = "keyboard.es5.html"; protected static final String KMFilename_JSEngine = "keymanweb-webview.js"; - protected static final String KMFilename_JSLegacyEngine = "keymanweb-webview.es5.js"; protected static final String KMFilename_JSSentry = "sentry.min.js"; protected static final String KMFilename_JSSentryInit = "keyman-sentry.js"; protected static final String KMFilename_AndroidHost = "android-host.js"; @@ -858,25 +879,11 @@ public static boolean copyHTMLBannerAssets(Context context, String path) { private static void copyAssets(Context context) { AssetManager assetManager = context.getAssets(); - // Will build a temp WebView in order to check Chrome version internally. - boolean legacyMode = WebViewUtils.getEngineWebViewVersionStatus(context, null, null) != WebViewUtils.EngineWebViewVersionStatus.FULL; - try { // Copy KMW files - if(legacyMode) { - // Replaces the standard ES6-friendly version of the host page with a legacy one that - // includes polyfill requests and that links the legacy, ES5-compatible version of KMW. - copyAssetWithRename(context, KMFilename_KeyboardHtml_Legacy, KMFilename_KeyboardHtml, "", true); - - copyAsset(context, KMFilename_JSLegacyEngine, "", true); - } else { - copyAsset(context, KMFilename_KeyboardHtml, "", true); - - // For versions of Chrome with full ES6 support, we use the ES6 artifact. - copyAsset(context, KMFilename_JSEngine, "", true); - } + copyAsset(context, KMFilename_KeyboardHtml, "", true); - // Is still built targeting ES5. + copyAsset(context, KMFilename_JSEngine, "", true); copyAsset(context, KMFilename_JSSentry, "", true); copyAsset(context, KMFilename_JSSentryInit, "", true); copyAsset(context, KMFilename_AndroidHost, "", true); @@ -887,12 +894,6 @@ private static void copyAssets(Context context) { // Copy default keyboard font copyAsset(context, KMDefault_KeyboardFont, "", true); - if(legacyMode) { - copyAsset(context, KMFilename_JSPolyfill, "", true); - copyAsset(context, KMFilename_JSPolyfill2, "", true); - copyAsset(context, KMFilename_JSPolyfill3, "", true); - } - // Keyboard packages directory File packagesDir = new File(getPackagesDir()); if (!packagesDir.exists()) { @@ -1273,6 +1274,60 @@ public boolean accept(File pathname) { } } + /** + * Sets enterMode which specifies how the System keyboard ENTER key is handled + * + * @param imeOptions EditorInfo.imeOptions used to determine the action + * @param inputType InputType used to determine if the text field is multi-line + */ + public static void setEnterMode(int imeOptions, int inputType) { + EnterModeType value = EnterModeType.DEFAULT; + int imeActions = imeOptions & EditorInfo.IME_MASK_ACTION; + boolean isMultiLine = (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0; + + switch (imeActions) { + case EditorInfo.IME_ACTION_GO: + value = EnterModeType.GO; + break; + + case EditorInfo.IME_ACTION_SEARCH: + value = EnterModeType.SEARCH; + break; + + case EditorInfo.IME_ACTION_SEND: + value = isMultiLine ? + EnterModeType.NEWLINE :EnterModeType.SEND; + break; + + case EditorInfo.IME_ACTION_NEXT: + value = EnterModeType.NEXT; + break; + + case EditorInfo.IME_ACTION_DONE: + value = isMultiLine ? + EnterModeType.NEWLINE : EnterModeType.DONE; + break; + + case EditorInfo.IME_ACTION_PREVIOUS: + value = EnterModeType.PREVIOUS; + break; + + default: + value = isMultiLine ? + EnterModeType.NEWLINE : EnterModeType.DEFAULT; + } + + enterMode = value; + } + + /** + * Get the value of enterMode + * @return EnterModeType + */ + public static EnterModeType getEnterMode() { + return enterMode; + } + /** * Sets mayPredictOverride true if the InputType field is a hidden password text field * (either TYPE_TEXT_VARIATION_PASSWORD or TYPE_TEXT_VARIATION_WEB_PASSWORD @@ -1638,7 +1693,7 @@ public static boolean removeKeyboard(Context context, int position) { public static boolean isDefaultKey(String key) { return ( - key != null && + key != null && key.equals(KMString.format("%s_%s", KMDefault_LanguageID, KMDefault_KeyboardID))); } @@ -2032,6 +2087,53 @@ public static int getOrientation(Context context) { return Configuration.ORIENTATION_UNDEFINED; } + /** + * Get the longpress delay (in milliseconds) from stored preference. Defaults to 500ms + * @return int - longpress delay in milliseconds + */ + public static int getLongpressDelay() { + SharedPreferences prefs = appContext.getSharedPreferences( + appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + + return prefs.getInt(KMKey_LongpressDelay, KMDefault_LongpressDelay); + } + + /** + * Set the longpress delay (in milliseconds) as a stored preference. + * Valid range is 300 ms to 1500 ms. Returns true if the preference is successfully stored. + * @param longpressDelay - int longpress delay in milliseconds + * @return boolean + */ + public static boolean setLongpressDelay(int longpressDelay) { + if (longpressDelay < KMMinimum_LongpressDelay || longpressDelay > KMMaximum_LongpressDelay) { + return false; + } + + SharedPreferences prefs = appContext.getSharedPreferences( + appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(KMKey_LongpressDelay, longpressDelay); + editor.commit(); + + return true; + } + + /** + * Sends options to the KeymanWeb keyboard. + * 1. number of milliseconds to trigger a longpress gesture. + * This method requires a keyboard to be loaded for the value to take effect. + */ + public static void sendOptionsToKeyboard() { + int longpressDelay = getLongpressDelay(); + if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP)) { + InAppKeyboard.loadJavascript(KMString.format("setLongpressDelay(%d)", longpressDelay)); + } + + if (SystemKeyboard != null) { + SystemKeyboard.loadJavascript(KMString.format("setLongpressDelay(%d)", longpressDelay)); + } + } + public static int getBannerHeight(Context context) { int bannerHeight = 0; if (InAppKeyboard != null && InAppKeyboard.getBanner() != BannerType.BLANK) { @@ -2084,11 +2186,11 @@ public static Point getWindowSize(Context context) { wm.getDefaultDisplay().getSize(size); return size; } - + WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); return new Point( windowMetrics.getBounds().width(), - windowMetrics.getBounds().height()); + windowMetrics.getBounds().height()); } public static float getWindowDensity(Context context) { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardInfoActivity.java b/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardInfoActivity.java index c2daaea3030..1897920b731 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardInfoActivity.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardInfoActivity.java @@ -88,7 +88,7 @@ public void onCreate(Bundle savedInstanceState) { hashMap = new HashMap(); final String customHelpLink = kbd.getHelpLink(); // Check if app declared FileProvider - String icon = String.valueOf(R.drawable.ic_arrow_forward); + String icon = String.valueOf(R.drawable.ic_action_forward); // Don't show help link arrow if it's a local help file and File Provider unavailable, // or custom help doesn't exist if ( (customHelpLink != null && ! KMManager.isTestMode() && diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/ModelInfoActivity.java b/android/KMEA/app/src/main/java/com/keyman/engine/ModelInfoActivity.java index ef1b180c52f..ce81927699a 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/ModelInfoActivity.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/ModelInfoActivity.java @@ -81,7 +81,7 @@ public void onCreate(Bundle savedInstanceState) { final String customHelpLink = lm.getHelpLink(); // Check if app declared FileProvider // Currently, model help only available if custom link exists - icon = String.valueOf(R.drawable.ic_arrow_forward); + icon = String.valueOf(R.drawable.ic_action_forward); // Don't show help link arrow if both custom help and File Provider don't exist // TODO: Update this when model help available on help.keyman.com if ( (!customHelpLink.equals("") && !FileProviderUtils.exists(context)) || diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/ModelPickerActivity.java b/android/KMEA/app/src/main/java/com/keyman/engine/ModelPickerActivity.java index 8b74e261a37..4595aae1e70 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/ModelPickerActivity.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/ModelPickerActivity.java @@ -321,7 +321,7 @@ public View getView(int position, View convertView, ViewGroup parent) { // once it has its own backing Dataset instance. // Is this an installed model or not? - holder.imgDetails.setImageResource(R.drawable.ic_arrow_forward); + holder.imgDetails.setImageResource(R.drawable.ic_action_forward); if (KeyboardPickerActivity.containsLexicalModel(context, modelKey)) { holder.imgInstalled.setImageResource(R.drawable.ic_check); } else { diff --git a/android/KMEA/app/src/main/res/drawable-hdpi/ic_arrow_back.png b/android/KMEA/app/src/main/res/drawable-hdpi/ic_arrow_back.png new file mode 100644 index 00000000000..7922c650163 Binary files /dev/null and b/android/KMEA/app/src/main/res/drawable-hdpi/ic_arrow_back.png differ diff --git a/android/KMEA/app/src/main/res/drawable-mdpi/ic_arrow_back.png b/android/KMEA/app/src/main/res/drawable-mdpi/ic_arrow_back.png new file mode 100644 index 00000000000..0c1eb7a4174 Binary files /dev/null and b/android/KMEA/app/src/main/res/drawable-mdpi/ic_arrow_back.png differ diff --git a/android/KMEA/app/src/main/res/drawable-xhdpi/ic_arrow_back.png b/android/KMEA/app/src/main/res/drawable-xhdpi/ic_arrow_back.png new file mode 100644 index 00000000000..e8e7047ce78 Binary files /dev/null and b/android/KMEA/app/src/main/res/drawable-xhdpi/ic_arrow_back.png differ diff --git a/android/KMEA/app/src/main/res/drawable-xxhdpi/ic_arrow_back.png b/android/KMEA/app/src/main/res/drawable-xxhdpi/ic_arrow_back.png new file mode 100644 index 00000000000..831448cb178 Binary files /dev/null and b/android/KMEA/app/src/main/res/drawable-xxhdpi/ic_arrow_back.png differ diff --git a/android/KMEA/app/src/main/res/drawable-xxxhdpi/ic_arrow_back.png b/android/KMEA/app/src/main/res/drawable-xxxhdpi/ic_arrow_back.png new file mode 100644 index 00000000000..5794fbcaac0 Binary files /dev/null and b/android/KMEA/app/src/main/res/drawable-xxxhdpi/ic_arrow_back.png differ diff --git a/android/KMEA/app/src/main/res/drawable/ic_action_back.xml b/android/KMEA/app/src/main/res/drawable/ic_action_back.xml new file mode 100644 index 00000000000..cae0c5eed49 --- /dev/null +++ b/android/KMEA/app/src/main/res/drawable/ic_action_back.xml @@ -0,0 +1,5 @@ + + diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_forward.xml b/android/KMEA/app/src/main/res/drawable/ic_action_forward.xml similarity index 67% rename from android/KMAPro/kMAPro/src/main/res/drawable/ic_action_forward.xml rename to android/KMEA/app/src/main/res/drawable/ic_action_forward.xml index 15efecf5575..2ffd9c4540e 100644 --- a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_forward.xml +++ b/android/KMEA/app/src/main/res/drawable/ic_action_forward.xml @@ -1,4 +1,5 @@ + android:tint="@android:color/black" + android:autoMirrored="true"/> diff --git a/android/KMEA/app/src/main/res/layout/help_bubble_layout.xml b/android/KMEA/app/src/main/res/layout/help_bubble_layout.xml index 953c74e6924..16e2c3d47a0 100644 --- a/android/KMEA/app/src/main/res/layout/help_bubble_layout.xml +++ b/android/KMEA/app/src/main/res/layout/help_bubble_layout.xml @@ -15,8 +15,8 @@ android:layout_gravity="center" android:gravity="center" android:paddingBottom="10dp" - android:paddingLeft="2dp" - android:paddingRight="2dp" + android:paddingStart="2dp" + android:paddingEnd="2dp" android:text="@string/help_bubble_text" android:textColor="#404040" android:textSize="12sp" /> diff --git a/android/KMEA/app/src/main/res/layout/models_list_row_layout.xml b/android/KMEA/app/src/main/res/layout/models_list_row_layout.xml index ac07621cdd6..a2116bcb4c5 100644 --- a/android/KMEA/app/src/main/res/layout/models_list_row_layout.xml +++ b/android/KMEA/app/src/main/res/layout/models_list_row_layout.xml @@ -44,7 +44,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/transparent" - android:src="@drawable/ic_arrow_forward" + android:src="@drawable/ic_action_forward" android:layout_alignParentEnd="true" android:layout_centerVertical="true" android:layout_marginEnd="12dp" diff --git a/android/KMEA/app/src/main/res/values-ar-rSA/strings.xml b/android/KMEA/app/src/main/res/values-ar-rSA/strings.xml new file mode 100644 index 00000000000..cb2e9f0d512 --- /dev/null +++ b/android/KMEA/app/src/main/res/values-ar-rSA/strings.xml @@ -0,0 +1,188 @@ + + + + + + + لوحة مفاتيح + لوحات مفاتيح + لوحتا مفاتيح + لوحات مفاتيح + لوحة مفاتيح + لوحة مفاتيح + + + + طريقة إداخل أخرى + طريقة إدخال أخرى + طريقتا إدخال أخريتان + طرق إدخال + طريقة إدخال أخرى + طريقة إدخال أخرى + + + إضافة لوحة مفاتيح جديدة + + اللغات المثبتة + + إعدادات %1$s + + إضافة + + عودة + + إلغاء + + إغلاق + + إغلاق Keyman + + الأمام + + طريقة الإدخال التالية + + تحميل + + تثبيت + + لاحقًا + + التالي + + موافق + + تحديث + + لا يوجد اتصال بالإنترنت + + لا يمكن الاتصال بخادم Keyman! + + هل ترغب بإزالة لوحة المفاتيح هذه؟ + + هل ترغب بتحميل آخر إصدار من لوحة المفاتيح هذه؟ + + هل ترغب بتحديث لوحات المفاتيح والقواميس الآن؟ + + تحديثات الموارد + + تتوفر تحديثات موارد + + %1$s (تحديث متوفر) + + تحديثات متوفرة للوحة مفاتيح %1$s: %2$s + + تحديثات متوفرة لقاموس %1$s: %2$s + + إصدار لوحة المفاتيح + + رابط المساعدة + + إزالة تثبيت لوحة المفاتيح + + [new] %1$s + + امسح هذا الرمز لتحميل \nلوحة المفاتيح هذه على جهاز آخر + + أهلًا بك في %1$s + + مكتبة FileProvider مطلوبة لعرض ملفات المساعدة: %1$s + + خطأ كبير في لوحة مفاتيح مع %1$s:%2$s لأجل لغة %3$s. سيتم تحميل لوحة المفاتيح الأساسية. + + خطأ في لوحة مفاتيح %1$s:%2$s لأجل لغة %3$s. + + يجري التحقق من قاموس ذي صلة للتحميل + لا يمكن الاتصال بخادم Keyman للتحقق من تنزيل القاموس المرتبط + + هل ترغب بتحملي آخر إصدار من هذا القاموس؟ + + لا يوجد قاموس لتحميله + + كتالوغ المصدر غير متوفر + + بدء تحديث الكتالوغ في الخلفية + + الكتالوغ لا يزال يتم تحميله، يرجى المحاولة مجددًا بعد قليل! + + البحث عن مصدر + + بدء تحميل لوحة المفاتيح في الخلفية + + يجري بالفعل تحميل لوحة المفاتيح المختارة، يرجى المحاولة مجددًا بعد قليل! + + اكتمل تحميل لوحة المفاتيح! + + بدأ تحميل القاموس في الخلفية + + يجري بالفعل تحميل القاموس، يرجى المحاولة مجددًا بعد قليل! + + اكتمل تحميل القاموس. + + فشل التحميل + + فشل استرجاع الملف المحمل + + فشل الوصول إلى الخادم! + + "جميع الموارد محدّثة!" + + فشل تحديث واحد أو أكثر من المصادر! + + تم تحديث الموارد بنجاح! + + إصدار القاموس + + إزالة تثبيت القاموس + + هل ترغب بإزالة هذا القاموس؟ + + تم حذف القاموس + + تم تثبيت لوحة مفاتيح %1$s + + تم حذف لوحة المفاتيح + + تفعيل التصحيحات + + تفعيل التوقعات + + القاموس + + Dictionaries + Dictionary + Dictionaries + Dictionaries + Dictionaries + Dictionaries + + + البحث عن قاموس متوفر + تحقق من وجود قواميس على الإنترنت + + قاموس: %1$s + + قواميس %1$s + + + تم تثبيت القاموس + + + (%1$d اوحات) + (لوحة مفاتيح %1$d) + (لوحتا مفاتيح %1$d) + (%1$d لوحة مفاتيح) + (%1$d لوحة مفاتيح) + (%1$d لوحة مفاتيح) + + + اللغة الأصلية + + + + حذف + + + اضغط هنا لتغيير لوحة المفاتيح + + غير قادر على تشغيل متصفح الويب + diff --git a/android/KMEA/app/src/main/res/values-b+el/strings.xml b/android/KMEA/app/src/main/res/values-b+el/strings.xml new file mode 100644 index 00000000000..75c774ebd16 --- /dev/null +++ b/android/KMEA/app/src/main/res/values-b+el/strings.xml @@ -0,0 +1,172 @@ + + + + + + + Πληκτρολόγιο + Πληκτρολόγια + + + + Ἄλλη μέθοδος εἰσαγωγῆς + Ἄλλες Μέθοδοι Εἰσαγωγῆς + + + Προσθέστε νέο Πληκτρολόγιο + + Ἐγκατεστημένες γλῶσσες + + Ρυθμίσεις %1$s + + Προσθέστε + + Πίσω + + Ἀκυρῶστε + + Κλεῖστε + + Κλεῖστε τὸ Keyman + + Προχωρῆστε + + Ἑπόμενη Μέθοδος Εἰσαγωγῆς + + Λήψη + + Ἐγκαταστῆστε + + Ἀργότερα + + Ἑπόμενο + + OK + + Ἐνημερῶστε + + Δὲν ὑπάρχει σύνδεση μὲ τὸ Διαδίκτυο + + Ἀδυναμία συνδέσεως μὲ τὸν διακομιστὴ τοῦ Κῆμαν! + + Θέλετε νὰ διαγράψετε αὐτὸ τὸ πληκτρολόγιο; + + Θὰ θέλατε νὰ κατεβάσετε τὴν τελευταία ἔκδοση αὐτοῦ τοῦ πληκτρολογίου; + + Θὰ θέλατε νὰ ἐνημερώσετε τώρα πληκτρολόγια καὶ λεξικά; + + Ἐνημερώσεις + + Διαθέσιμες Ἐνημερώσεις + + %1$s (Διαθέσιμη Ἐνημέρωση) + + Διαθέσιμες ἐνημερώσεις γιὰ τὸ πληκτρολόγιο %1$s: %2$s + + Διαθέσιμες ἐνημερώσεις γιὰ τὸ λεξικὸ %1$s: %2$s + + Ἔκδοση πληκτρολογίου + + Βοήθεια + + Ἀπεγκαταστῆστε πληκτρολόγιο + + [νέο] %1$s + + Σαρῶστε αὐτὸν τὸν κωδικὸ γιὰ νὰ φορτώσετε\nαὐτὸ τὸ πληκτρολόγιο σὲ ἄλλη συσκευή + + Καλωσορίσατε στὸ %1$s + + Ἀπαιτεῖται βιβλιοθήκη FileProvider γιὰ νὰ δεῖτε ἀρχεῖο βοηθείας: %1$s + + Μοιραῖο σφάλμα πληκτρολογίου στὸ %1$s:%2$s γιὰ τὴν %3$s γλῶσσα. Φορτώνεται προεπιλεγμένο πληκτρολόγιο. + + Σφάλμα στὸ πληκτρολόγιο %1$s:%2$s γιὰ τὴν γλῶσσα \"%3$s\". + + Ἀναζήτηση σχετικοῦ λεξικοῦ πρὸς μεταφόρτωσιν + Ἀδυναμία συνδέσεως μὲ τὸν διακομιστὴ Keyman γιὰ τὴν εὕρεση σχετικοῦ λεξικοῦ πρὸς μεταφόρτωσιν + + Θὰ θέλατε νὰ κατεβάσετε τὴν τελευταία ἔκδοση αὐτοῦ τοῦ λεξικοῦ; + + Δὲν ὑπάρχει λεξικὸ πρὸς μεταφόρτωσιν + + Μὴ διαθέσιμος κατάλογος πόρων + + Ἔχει ξεκινήσει ἐνημέρωση καταλόγου στὸ παρασκήνιο + + Ἡ μεταφόρτωση τοῦ καταλόγου συνεχίζεται· παρακαλοῦμε ξαναδοκιμάστε σὲ λίγο! + + Ἀναζήτηση πόρου σὲ ἐξέλιξη + + Ἡ λήψη τοῦ πληκτρολογίου ἔχει ξεκινήσει στὸ παρασκήνιο + + Ἡ λήψη τοῦ ἐπιλεγέντος πληκτρολογίου βρίσκεται σὲ ἐξέλιξη· παρακαλοῦμε ξαναδοκιμάστε σὲ λίγο! + + Ἡ λήψη τοῦ πληκτρολογίου ὁλοκληρώθηκε! + + Ἡ λήψη τοῦ λεξικοῦ ἔχει ξεκινήσει στὸ παρασκήνιο + + Ἡ λήψη τοῦ ἐπιλεγέντος λεξικοῦ βρίσκεται σὲ ἐξέλιξη· παρακαλοῦμε ξαναδοκιμάστε σὲ λίγο! + + Ἡ λήψη τοῦ λεξικοῦ ὁλοκληρώθηκε. + + Ἡ λήψη ἀπέτυχε + + Ἀποτυχία ἀνακλήσεως ληφθέντος ἀρχείου + + Ἀποτυχία προσβάσεως στὸν διακομιστή! + + "Ὅλοι οἱ πόροι ἔχουν ἐνημερωθεῖ!" + + Ἕνας ἢ περισσότεροι πόροι ἀπέτυχαν νὰ ἐνημερωθοῦν! + + Ὅλες οἱ ἐνημερώσεις ὁλοκληρώθηκαν ἐπιτυχῶς! + + Ἔκδοση λεξικοῦ + + Ἀπεγκαταστῆστε λεξικό + + Θὰ θέλατε νὰ διαγράψετε αὐτὸ τὸ λεξικό; + + Τὸ λεξικὸ διεγράφη + + Τὸ πληκτρολόγιο %1$s ἐγκατεστάθη + + Τὸ πληκτρολόγιο διεγράφη + + Ἐνεργοποιῆστε τὶς διορθώσεις + + Ἐνεργοποιῆστε προβλέψεις κειμένου + + Λεξικό + + Λεξικό + Λεξικά + + + Ἀναζήτηση διαθεσίμου λεξικοῦ + Ἀναζήτηση λεξικῶν ὀνλάϊν + + Λεξικό: %1$s + + %1$s λεξικά + + + Τὸ λεξικὸ ἐγκατεστάθη + + + (%1$d πληκτρολόγιο) + (%1$d πληκτρολόγια) + + + Προεπιλεγμένη Γλῶσσα + + + + Διαγράψτε + + + Κτυπῆστε ἐδῶ γιὰ νὰ ἀλλάξετε πληκτρολόγιο + + Ἀδυναμία ἐκκινήσεως φυλλομετρητῆ + diff --git a/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml b/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml index 1bcc7926603..db45a6706e0 100644 --- a/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml +++ b/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml @@ -49,7 +49,7 @@ Aktualizovat - No internet connection + Žádné připojení k internetu Nelze se připojit k Keyman serveru! @@ -88,7 +88,7 @@ Chyba v klávesnici %1$s:%2$s pro jazyk %3$s. Kontroluji související slovník ke stažení - Cannot connect to Keyman server to check for associated dictionary to download + Nelze se připojit ke Keyman serveru a zkontrolovat, zda je možné stáhnout přidružený slovník Chcete stáhnout nejnovější verzi tohoto slovníku? @@ -111,7 +111,7 @@ Stahování slovníku začalo na pozadí Vybraný slovník se již stahuje; zkuste to prosím za chvíli znovu! - + Stahování slovníku je dokončeno. Stahování se nezdařilo @@ -123,7 +123,7 @@ "Všechny zdroje jsou aktuální!" Aktualizace jednoho nebo více zdrojů selhala! - + Zdroje byly úspěšně aktualizovány! Verze slovníku @@ -132,11 +132,11 @@ Chcete odstranit tento slovník? - Dictionary deleted + Slovník smazán Klávesnice %1$s nainstalována - Keyboard deleted + Klávesnice smazána Povolit opravy @@ -144,14 +144,14 @@ Slovník - Dictionary - Dictionaries - Dictionaries - Dictionaries + Slovník + Slovníky + Slovníků + Slovníků Zkontrolovat dostupný slovník - Check for dictionaries online + Kouknout se na slovníky online Slovník: %1$s @@ -176,5 +176,5 @@ Klepnutím sem změníte klávesnici - Unable to launch web browser + Nelze spustit webový prohlížeč diff --git a/android/KMEA/app/src/main/res/values-it-rIT/strings.xml b/android/KMEA/app/src/main/res/values-it-rIT/strings.xml index 2b9c950cc67..da1e3531a2b 100644 --- a/android/KMEA/app/src/main/res/values-it-rIT/strings.xml +++ b/android/KMEA/app/src/main/res/values-it-rIT/strings.xml @@ -45,7 +45,7 @@ Aggiorna - No internet connection + Nessuna connessione internet Impossibile connettersi al server Keyman! @@ -84,7 +84,7 @@ Errore nella tastiera %1$s:%2$s per %3$s lingua. Controllo del dizionario associato da scaricare - Cannot connect to Keyman server to check for associated dictionary to download + Non è possibile connettersi al server Keyman per verificare dizionari associati da scaricare Vuoi scaricare l\'ultima versione di questo dizionario? @@ -107,7 +107,7 @@ Scaricamento del dizionario avviato in background Il dizionario selezionato è già in download; riprova tra un momento! - + Il download del dizionario è terminato. Download non riuscito @@ -119,7 +119,7 @@ "Tutte le risorse sono aggiornate!" Una o più risorse non sono state aggiornate! - + Risorse aggiornate con successo! Versione dizionario @@ -128,11 +128,11 @@ Vuoi eliminare questo dizionario? - Dictionary deleted + Dizionario eliminato %1$s tastiera installata - Keyboard deleted + Tastiera eliminata Abilita correzioni @@ -140,12 +140,12 @@ Dizionario - Dictionary - Dictionaries + Dizionario + Dizionari Controlla il dizionario disponibile - Check for dictionaries online + Controlla i dizionari online Dizionario: %1$s @@ -168,5 +168,5 @@ Tocca qui per cambiare la tastiera - Unable to launch web browser + Impossibile avviare il browser web diff --git a/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml b/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml index f66d5fa4443..f58f48dd2dc 100644 --- a/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml +++ b/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml @@ -45,7 +45,7 @@ Atualizar - No internet connection + Sem ligação à Internet Não foi possível conectar ao servidor Keyman! @@ -84,7 +84,7 @@ Erro no teclado %1$s:%2$s para %3$s idioma. Verificando o download do dicionário associado - Cannot connect to Keyman server to check for associated dictionary to download + A ligação ao servidor do Keyman para verificar dicionários para descarregar falhou Gostaria de baixar a versão mais recente deste dicionário? @@ -107,7 +107,7 @@ Download do dicionário iniciado em segundo plano O dicionário selecionado já está baixando; por favor, tente novamente mais tarde! - + O download do dicionário terminou. Download falhou @@ -119,7 +119,7 @@ "Todos os recursos estão atualizados!" Um ou mais recursos falharam ao atualizar! - + Recursos atualizados com sucesso! Versão do dicionário @@ -128,11 +128,11 @@ Você gostaria de excluir este dicionário? - Dictionary deleted + Dicionário eliminado Teclado %1$s instalado - Keyboard deleted + Teclado eliminado Habilitar correções @@ -140,12 +140,12 @@ Dicionário - Dictionary - Dictionaries + Dicionário + Dicionários Verificar dicionário disponível - Check for dictionaries online + Verifique dicionários online Dicionário: %1$s @@ -168,5 +168,5 @@ Toque aqui para alterar o teclado - Unable to launch web browser + Não foi possível carregar o navegador diff --git a/android/KMEA/build.sh b/android/KMEA/build.sh index ccf7c92887b..29cb519cf56 100755 --- a/android/KMEA/build.sh +++ b/android/KMEA/build.sh @@ -23,7 +23,7 @@ JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\ builder_describe "Builds Keyman Engine for Android." \ "@/web/src/app/webview" \ - "@/common/web/sentry-manager" \ + "@/web/src/engine/sentry-manager" \ "clean" \ "configure" \ "build" \ @@ -77,8 +77,6 @@ if builder_start_action build:engine; then echo "Copying Keyman Web artifacts" cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js" "$ENGINE_ASSETS/keymanweb-webview.js" cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js.map" "$ENGINE_ASSETS/keymanweb-webview.js.map" - cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.es5.js" "$ENGINE_ASSETS/keymanweb-webview.es5.js" - cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.es5.js.map" "$ENGINE_ASSETS/keymanweb-webview.es5.js.map" cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/map-polyfill.js" "$ENGINE_ASSETS/map-polyfill.js" cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/ajax-loader.gif" cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/kmwosk.css" @@ -86,7 +84,7 @@ if builder_start_action build:engine; then cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/keymanweb-osk.ttf" "$ENGINE_ASSETS/keymanweb-osk.ttf" cp "$KEYMAN_ROOT/node_modules/@sentry/browser/build/bundle.min.js" "$ENGINE_ASSETS/sentry.min.js" - cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "$ENGINE_ASSETS/keyman-sentry.js" + cp "$KEYMAN_ROOT/web/src/engine/sentry-manager/build/lib/index.js" "$ENGINE_ASSETS/keyman-sentry.js" echo "Copying es6-shim polyfill" cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "$ENGINE_ASSETS/es6-shim.min.js" diff --git a/android/build-publish.sh b/android/build-publish.sh deleted file mode 100755 index 29c152c993d..00000000000 --- a/android/build-publish.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env bash -# CI script to publish specified app APKs to the Play Store. -# The APKs should already have been built from a separate script - -# set -x: Debugging use, print each statement -# set -x - -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "${THIS_SCRIPT%/*}/../resources/build/build-utils.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -. "$KEYMAN_ROOT/android/KMAPro/build-play-store-notes.inc.sh" - -echo Publishing APKs to Play Store - -# -# Prevents 'clear' on exit of mingw64 bash shell -# -SHLVL=0 - -display_usage ( ) { - echo "build-publish.sh [-no-daemon] [-kmapro] [-fv]" - echo - echo "Publish app to the Play Store" - echo " -no-daemon Don't start the Gradle daemon. Use for CI" - echo " -kmapro Keyman for Android" - echo " -fv First Voices" - exit 1 -} - -NO_DAEMON=false -DO_KMAPRO=false -DO_FV=false - -# Parse args -while [[ $# -gt 0 ]] ; do - key="$1" - case $key in - -no-daemon) - NO_DAEMON=true - ;; - -kmapro) - DO_KMAPRO=true - ;; - -fv) - DO_FV=true - ;; - -h|-\?) - display_usage - ;; - esac - shift # past argument -done - -# Override JAVA_HOME to OpenJDK 11 -set_java_home - -echo -echo "NO_DAEMON: $NO_DAEMON" -echo "DO_KMAPRO: $DO_KMAPRO" -echo "DO_FV: $DO_FV" -echo - -if [ "$NO_DAEMON" = true ]; then - DAEMON_FLAG=--no-daemon -else - DAEMON_FLAG= -fi - -BUILD_FLAGS="publishReleaseApk" -echo "BUILD_FLAGS $BUILD_FLAGS" - -# Publish Keyman for Android -if [ "$DO_KMAPRO" = true ]; then - # Copy Release Notes - generateReleaseNotes - cd "$KEYMAN_ROOT/android/KMAPro/" - ./gradlew $DAEMON_FLAG $BUILD_FLAGS -fi - -# Publish FV app -if [ "$DO_FV" = true ]; then - cd "$KEYMAN_ROOT/oem/firstvoices/android/" - ./gradlew $DAEMON_FLAG $BUILD_FLAGS -fi diff --git a/android/build.sh b/android/build.sh index 88048c7aa30..d876d882d77 100755 --- a/android/build.sh +++ b/android/build.sh @@ -18,7 +18,7 @@ builder_describe \ configure \ build \ test \ - "publish Publishes the APKs to the Play Store." \ + "publish Publishes symbols to Sentry and the APKs to the Play Store." \ --ci+ \ --upload-sentry+ \ ":engine=KMEA Keyman Engine for Android" \ diff --git a/android/help/about/privacy.md b/android/help/about/privacy.md index 6173d6c3c39..c3dbd000eee 100644 --- a/android/help/about/privacy.md +++ b/android/help/about/privacy.md @@ -19,7 +19,7 @@ Keyman does not use user accounts, so we are unable to identify which crash repo ### Developer Information -Keyman is developed and published by SIL International. +Keyman is developed and published by SIL Global. You can contact SIL or provide any feedback using the app store. diff --git a/android/help/about/whatsnew.md b/android/help/about/whatsnew.md index 96bd458a0c0..500d0a9593b 100644 --- a/android/help/about/whatsnew.md +++ b/android/help/about/whatsnew.md @@ -3,3 +3,7 @@ title: What's New --- Here are some of the new features we have added to Keyman 18.0 for Android: + +* New menu to adjust longpress delay time (#12170, #12185) +* Support localizations for right-to-left languages (#12215) +* Handle additional actions for ENTER key (#12125, #12315) diff --git a/android/help/android_images/ic_timelapse.png b/android/help/android_images/ic_timelapse.png new file mode 100644 index 00000000000..9f948825b2c Binary files /dev/null and b/android/help/android_images/ic_timelapse.png differ diff --git a/android/help/android_images/longpress-slider.png b/android/help/android_images/longpress-slider.png new file mode 100644 index 00000000000..f1ab61f7b3c Binary files /dev/null and b/android/help/android_images/longpress-slider.png differ diff --git a/android/help/android_images/settings-screen-ap.png b/android/help/android_images/settings-screen-ap.png index ec85e24e81e..f83da380406 100644 Binary files a/android/help/android_images/settings-screen-ap.png and b/android/help/android_images/settings-screen-ap.png differ diff --git a/android/help/basic/config/adjust-longpress-delay.md b/android/help/basic/config/adjust-longpress-delay.md new file mode 100644 index 00000000000..0f3e740bdfc --- /dev/null +++ b/android/help/basic/config/adjust-longpress-delay.md @@ -0,0 +1,13 @@ +--- +title: Adjust Longpress Delay - Keyman for Android Help +--- + +## Adjust Longpress Delay + +This menu is used to adjust the duration for triggering a longpress gesture. + +Drag the slider to set the delay between 0.3 seconds to 1.5 seconds. + +The default value is 0.5 seconds. + +![](../../android_images/longpress-slider.png) diff --git a/android/help/basic/config/index.md b/android/help/basic/config/index.md index 1c45d27508e..96a3fb5e35c 100644 --- a/android/help/basic/config/index.md +++ b/android/help/basic/config/index.md @@ -44,17 +44,17 @@ Click on this to bring up the Android system configuration for setting Keyman as Click on this to [adjust the keyboard height](adjust-keyboard-height). +## Adjust longpress delay +![](../../android_images/ic_timelapse.png) + +Click on this to [adjust the delay time](adjust-longpress-delay) for a longpress gesture. + ## Spacebar Caption Click on this to [change the displayed keyboard name](spacebar-caption) on the spacebar. ## Vibrate When Typing When enabled, the Keyman keyboard will provide haptic feedback (vibrate) when the user presses a key and generates output. The default preference is off. -## Always show banner -This toggle is reserved for future features. When off, the text suggestion banner is only displayed when -the dictionary is enabled. The language associated with the keyboard must match the language associated with -the dictionary. - ## Show "Get Started" on startup When enabled, the Keyman app will display the 'Get Started' screen on app startup. diff --git a/build.sh b/build.sh new file mode 100755 index 00000000000..a455d3307f6 --- /dev/null +++ b/build.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +builder_describe "Keyman -- all projects" \ + ":common" \ + ":core" \ + ":android" \ + ":ios" \ + ":linux" \ + ":mac" \ + ":web" \ + ":windows" \ + ":developer" \ + clean \ + configure \ + build \ + test \ + install \ + publish + +builder_describe_platform \ + :android android-studio \ + :ios mac \ + :linux linux \ + :mac mac \ + :windows win + +builder_parse "$@" + +#------------------------------------------------------------------------------------------------------------------- + +builder_run_child_actions clean configure build test publish install diff --git a/common/build.sh b/common/build.sh index 2d3c2ff8c40..5119bca8ac5 100755 --- a/common/build.sh +++ b/common/build.sh @@ -16,6 +16,11 @@ builder_describe "Keyman common and resources modules" \ build \ test +builder_describe_platform \ + :linux linux \ + :mac mac \ + :windows win + builder_parse "$@" #------------------------------------------------------------------------------------------------------------------- diff --git a/common/cpp/km_u16.cpp b/common/cpp/km_u16.cpp new file mode 100644 index 00000000000..78077202fc4 --- /dev/null +++ b/common/cpp/km_u16.cpp @@ -0,0 +1,430 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * std::u16string functions and string conversion utility functions + */ + +#include +#include +#include "utfcodec.hpp" +#include "km_u16.h" + +/** string <- wstring + * @brief Obtain a std::string from a std::wstring + * @param wstr the std::wstring to be converted + * @return a std::string + */ +std::string string_from_wstring(std::wstring const wstr) { + return convert((const std::wstring)wstr); +} + +/** wstring <- string + * @brief Obtain a std::wstring from a std::string + * @param str the std::string to be converted + * @return a std::wstring + */ +std::wstring wstring_from_string(std::string const str) { + return convert((const std::string)str); +} + +/** u16string <- string + * @brief Obtain a std::u16string from a std::string + * @param str the std::string to be converted + * @return a std::u16string + */ +std::u16string u16string_from_string(std::string const str) { + return convert((const std::string)str); +} + +/** string <- u16string + * @brief Obtain a std::string from a std::u16string + * @param str16 the std::u16string to be converted + * @return a std::string + */ +std::string string_from_u16string(std::u16string const str16) { + return convert((const std::u16string)str16); +} + +/** wstring <- u16string + * @brief Obtain a std::wstring from a std::u16string + * @param str16 the std::u16string to be converted + * @return a std::wstring + */ +std::wstring wstring_from_u16string(std::u16string const str16) { + return convert((const std::u16string)str16); +} + +/** u16string <- wstring + * @brief Obtain a std::u16string from a std::wstring + * @param wstr the std::wstring to be converted + * @return a std::u16string + */ +std::u16string u16string_from_wstring(std::wstring const wstr) { + return convert((const std::wstring)wstr); +} + +/** + * @brief Convert pointer to wchar_t to pointer to char16_t and copy sz elements into dst + * @param dst destination + * @param sz nr of characters to be copied + * @param fmt source to convert and copy + */ +void u16sprintf(KMX_WCHAR* dst, const size_t sz, const wchar_t* fmt, ...) { + wchar_t* wbuf = new wchar_t[sz]; + va_list args; + va_start(args, fmt); + vswprintf(wbuf, sz, fmt, args); + va_end(args); + + std::u16string u16str = u16string_from_wstring(wbuf); + u16ncpy(dst, u16str.c_str(), sz); + delete[] wbuf; +} + +/** + * @brief Convert u16string to long integer + * @param str u16string beginning with the representation of an integral number. + * @param endptr Reference to the next character in str + * @param base Numerical base (radix) that determines the valid characters and their interpretation + * @return a long + */ +long int u16tol(const KMX_WCHAR* str, KMX_WCHAR** endptr, int base) { + auto s = string_from_u16string(str); + char* t; + long int result = strtol(s.c_str(), &t, base); + if (endptr != nullptr) + *endptr = (KMX_WCHAR*)str + (t - s.c_str()); + return result; +} + +std::string toHex(int num1) { + if (num1 == 0) + return "0"; + int num = num1; + std::string s = ""; + while (num) { + int temp = num % 16; + if (temp <= 9) + s += (48 + temp); + else + s += (87 + temp); + num = num / 16; + } + std::reverse(s.begin(), s.end()); + return s; +} + +/** + * @brief Append max characters from u16string + * @param dst Pointer to the destination array + * @param src u16string to be appended + * @param max Maximum number of characters to be appended. + * @return Pointer to dst + */ +const KMX_WCHAR* u16ncat(KMX_WCHAR* dst, const KMX_WCHAR* src, size_t max) { + KMX_WCHAR* o = dst; + dst = (KMX_WCHAR*)u16chr(dst, 0); + // max -= (dst-o); + while (*src && max > 0) { + *dst++ = *src++; + max--; + } + if (max > 0) + *dst = 0; + return o; +} + +/** + * @brief Find last '/' or '\\' in an array of char16_t + * @param name Pointer to the source + * @return Pointer to the last slash/backslash + */ +const KMX_WCHAR* u16rchr_slash(KMX_WCHAR const* name) { + const KMX_WCHAR* cp = NULL; + cp = u16rchr(name, '\\'); + if (cp == NULL) + cp = u16rchr(name, '/'); + return cp; +} + +/** + * @brief Find last '/' or '\\' in an array of char + * @param name Pointer to the source + * @return Pointer to the last slash/backslash + */ +KMX_CHAR* strrchr_slash(KMX_CHAR* name) { + KMX_CHAR* cp = NULL; + cp = strrchr(name, '\\'); + if (cp == NULL) + cp = strrchr(name, '/'); + return cp; +} + +/** + * @brief Locate last occurrence of character in u16string + * @param p Pointer to the source + * @param ch The character to be found + * @return A pointer to the last occurrence of character in u16str + */ +const KMX_WCHAR* u16rchr(const KMX_WCHAR* p, KMX_WCHAR ch) { + const KMX_WCHAR* p_end = p + u16len(p) - 1; + + if (ch == '\0') + return p_end + 1; + while (p_end >= p) { + if (*p_end == ch) + return p_end; + p_end--; + } + return NULL; +} + +/** + * @brief Locate first occurrence of character in u16string + * @param p Pointer to the source + * @param ch The character to be found + * @return A pointer to the first occurrence of character in u16str + */ +const KMX_WCHAR* u16chr(const KMX_WCHAR* p, KMX_WCHAR ch) { + while (*p) { + if (*p == ch) + return p; + p++; + } + return ch == 0 ? p : NULL; +} + +/** + * @brief Copy the u16string pointed to by scr into the array pointed to by dst + * @param dst Pointer to the destination + * @param src Pointer to the source to be copied + * @return Pointer to dst + */ +const KMX_WCHAR* u16cpy(KMX_WCHAR* dst, const KMX_WCHAR* src) { + KMX_WCHAR* o = dst; + while (*src) { + *dst++ = *src++; + } + *dst = 0; + return o; +} + +/** + * @brief Copy max characters of the u16string pointed to by src into the array pointed by dst + * @param dst Pointer to the destination + * @param src Pointer to the source to be copied + * @param max Maximum number of characters to be copied + * @return Pointer to dst + */ +const KMX_WCHAR* u16ncpy(KMX_WCHAR* dst, const KMX_WCHAR* src, size_t max) { + KMX_WCHAR* o = dst; + while (*src && max > 0) { + *dst++ = *src++; + max--; + } + if (max > 0) { + *dst = 0; + } + return o; +} + +/** + * @brief Return the length of the u16string str + * @param p Pointer to the source + * @return The length of u16string + */ +size_t u16len(const KMX_WCHAR* p) { + int i = 0; + while (*p) { + p++; + i++; + } + return i; +} + +/** + * @brief Compare two u16strings + * @param p Pointer one u16string + * @param q Pointer another u16string + * @return 0 if strings are equal + * ! = 0 if unequal + */ +int u16cmp(const KMX_WCHAR* p, const KMX_WCHAR* q) { + while (*p && *q) { + if (*p != *q) + return *p - *q; + p++; + q++; + } + return *p - *q; +} + +/** + * @brief Case insensitive comparison of up to count characters in two strings + * @param p Pointer one u16string + * @param q Pointer another u16string + * @param count Maximum number of characters to compare + * @return 0 if strings are equal + * ! = 0 if unequal + */ +int u16nicmp(const KMX_WCHAR* p, const KMX_WCHAR* q, size_t count) { + while (*p && *q && count) { + if (toupper(*p) != toupper(*q)) + return *p - *q; + p++; + q++; + count--; + } + if (count) + return *p - *q; + return 0; +} + +/** + * @brief Case insensitive comparison of two strings + * @param p Pointer one u16string + * @param q Pointer another u16string + * @return 0 if strings are equal + * ! = 0 if unequal + */ +int u16icmp(const KMX_WCHAR* p, const KMX_WCHAR* q) { + while (*p && *q) { + if (toupper(*p) != toupper(*q)) + return *p - *q; + p++; + q++; + } + return *p - *q; +} + +/** + * @brief Comparison of up to count characters in two strings + * @param p Pointer one u16string + * @param q Pointer another u16string + * @param count Maximum number of characters to compare + * @return 0 if strings are equal + * ! = 0 if unequal + */ +int u16ncmp(const KMX_WCHAR* p, const KMX_WCHAR* q, size_t count) { + while (*p && *q && count) { + if (*p != *q) + return *p - *q; + p++; + q++; + count--; + } + if (count) + return *p - *q; + return 0; +} + +/** + * @brief Split u16string into tokens + * @param p Pointer to u16string to parse. + * @param ch the delimiter character + * @param ctx the remaining string after the first delimiter + * @return Pointer to the first token in p + */ +KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR ch, KMX_WCHAR** ctx) { + if (!p) { + p = *ctx; + if (!p) + return NULL; + } + + KMX_WCHAR* q = p; + while (*q && *q != ch) { + q++; + } + if (*q) { + *q = 0; + q++; + while (*q == ch) + q++; + *ctx = q; + } else { + *ctx = NULL; + } + return *p ? p : NULL; +} + +/** + * @brief Split u16string into tokens + * @param p Pointer to u16string to parse. + * @param delimiters an array of delimiter characters + * @param ctx the remaining string after the first delimiter + * @return Pointer to the first token in p + */ +KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR* delimiters, KMX_WCHAR** ctx) { + if (!p) { + p = *ctx; + if (!p) + return NULL; + } + + KMX_WCHAR* q = p; + while (*q && !u16chr(delimiters, *q)) { + q++; + } + if (*q) { + *q = 0; + q++; + while (*q && u16chr(delimiters, *q)) + q++; + *ctx = q; + } else { + *ctx = NULL; + } + return *p ? p : NULL; +} + +/** + * @brief Convert a u16string to a double + * @param str Pointer to u16string + * @return double value equivalent to the string + */ +double u16tof(KMX_WCHAR* str16) { + char* pEnd; + std::string str = string_from_u16string(str16); + return strtof(str.c_str(), &pEnd); +} + +/** + * @brief Trim whitespace from the start (left) of a string + * @param p Pointer to u16string + * @return Pointer to the string modified to remove leading whitespace + */ +KMX_WCHAR* u16ltrim(KMX_WCHAR* p) { + if (p && (u16len(p) > 0)) { + PKMX_WCHAR q = p; + while(iswspace(*q)) q++; + u16cpy(p, q); + } + return p; +} + +/** + * @brief Trim whitespace from the end (right) of a string + * @param p Pointer to u16string + * @return Pointer to the string modified to remove trailing whitespace + */ +KMX_WCHAR* u16rtrim(KMX_WCHAR *p) { + if (p && (u16len(p) > 0)) { + PKMX_WCHAR q = p + u16len(p) - 1; + if (iswspace(*q)) { + while (iswspace(*q) && q > p) q--; + if (!iswspace(*q)) q++; + *q = '\0'; // delete first following whitespace + } + } + return p; +} + +/** + * @brief Trim whitespace from both the start and end of a string + * @param p Pointer to u16string + * @return Pointer to the string modified to remove leading and trailing whitespace + */ +KMX_WCHAR* u16trim(KMX_WCHAR *p) { + return u16rtrim(u16ltrim(p)); +} \ No newline at end of file diff --git a/core/src/utfcodec.cpp b/common/cpp/utfcodec.cpp similarity index 100% rename from core/src/utfcodec.cpp rename to common/cpp/utfcodec.cpp diff --git a/common/include/km_u16.h b/common/include/km_u16.h new file mode 100644 index 00000000000..91df6b0eb92 --- /dev/null +++ b/common/include/km_u16.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include + +/** @brief Obtain a std::string from a std::wstring */ +std::string string_from_wstring(std::wstring const wstr); + +/** @brief Obtain a std::wstring from a std::string */ +std::wstring wstring_from_string(std::string const str); + +/** @brief Obtain a std::u16string from a std::string */ +std::u16string u16string_from_string(std::string const str); + +/** @brief Obtain a std::string from a std::u16string */ +std::string string_from_u16string(std::u16string const str16); + +/** @brief Obtain a std::wstring from a std::u16string */ +std::wstring wstring_from_u16string(std::u16string const str16); + +/** @brief Obtain a std::u16string from a std::wstring */ +std::u16string u16string_from_wstring(std::wstring const wstr); + +/** @brief Convert pointer to wchar_t to pointer to char16_t and copy sz elements into dst */ +void u16sprintf(KMX_WCHAR* dst, const size_t sz, const wchar_t* fmt, ...); + +/** @brief Return the length of the u16string str */ +size_t u16len(const KMX_WCHAR* p); + +/** @brief Compare two u16strings */ +int u16cmp(const KMX_WCHAR* p, const KMX_WCHAR* q); + +/** @brief Case insensitive comparison of two strings */ +int u16icmp(const KMX_WCHAR* p, const KMX_WCHAR* q); + +/** @brief Comparison of up to count characters in two strings */ +int u16ncmp(const KMX_WCHAR* p, const KMX_WCHAR* q, size_t count); + +/** @brief Case insensitive comparison of up to count characters in two strings */ +int u16nicmp(const KMX_WCHAR* p, const KMX_WCHAR* q, size_t count); + +/** @brief Copy max characters of the u16string pointed to by src into the array pointed to by dst */ +const KMX_WCHAR* u16ncpy(KMX_WCHAR* dst, const KMX_WCHAR* src, size_t max); + +/** @brief Copy the u16string pointed to by src into the array pointed to by dst */ +const KMX_WCHAR* u16cpy(KMX_WCHAR* dst, const KMX_WCHAR* src); + +/** @brief Locate last occurrence of character in u16string */ +const KMX_WCHAR* u16rchr(const KMX_WCHAR* p, KMX_WCHAR ch); + +/** @brief Locate first occurrence of character in u16string */ +const KMX_WCHAR* u16chr(const KMX_WCHAR* p, KMX_WCHAR ch); + +/** @brief Append max characters from u16string */ +const KMX_WCHAR* u16ncat(KMX_WCHAR* dst, const KMX_WCHAR* src, size_t max); + +/** @brief Split u16string into tokens */ +KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR ch, KMX_WCHAR** ctx); + +/** @brief Split u16string into tokens */ +KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR* delimiters, KMX_WCHAR** ctx); + +/** @brief Convert a u16string to a double */ +long int u16tol(const KMX_WCHAR* str, KMX_WCHAR** endptr, int base); + +/** @brief Convert a u16string to a double */ +double u16tof(KMX_WCHAR* str); + +/** @brief find last '/' or '\\' in an array of char */ +KMX_CHAR* strrchr_slash(KMX_CHAR* Name); + +/** @brief find last '/' or '\\' in an array of char16_t */ +const KMX_WCHAR* u16rchr_slash(KMX_WCHAR const* Name); + +std::string toHex(int num1); + +/** @brief Trim whitespace from the start (left) of a string */ +KMX_WCHAR* u16ltrim(KMX_WCHAR* p); + +/** @brief Trim whitespace from the end (right) of a string */ +KMX_WCHAR* u16rtrim(KMX_WCHAR *p); + +/** @brief Trim whitespace from both the start and end of a string */ +KMX_WCHAR* u16trim(KMX_WCHAR *p); diff --git a/core/src/utfcodec.hpp b/common/include/utfcodec.hpp similarity index 100% rename from core/src/utfcodec.hpp rename to common/include/utfcodec.hpp diff --git a/common/models/templates/src/tokenization.ts b/common/models/templates/src/tokenization.ts deleted file mode 100644 index 2aa5c392e6d..00000000000 --- a/common/models/templates/src/tokenization.ts +++ /dev/null @@ -1,127 +0,0 @@ -// While we _could_ define this within @keymanapp/models-wordbreakers instead, it's probably -// better to leave that package as _just_ the wordbreakers. - -export interface Tokenization { - /** - * An array of tokens to the left of the caret. If the caret is in the middle of a token, - * only the part to the left of the caret is included. - */ - left: USVString[], - - /** - * An array of tokens to the right of the caret. If the caret is in the middle of a token, - * only the part to the right of the caret is included. - */ - right: USVString[], - - /** - * A flag indicating whether or not the caret's position in the context caused a token - * to be split. If `true`, the last entry of `left` is from the same token as the first - * entry of `right`. - */ - caretSplitsToken: boolean -} - -export function tokenize(wordBreaker: WordBreakingFunction, context?: Partial): Tokenization { - context = context || { - left: undefined, - startOfBuffer: undefined, - endOfBuffer: undefined - }; - - let leftSpans = wordBreaker(context.left || '') || []; - let rightSpans = wordBreaker(context.right || '') || []; - - let leftTail: Span; - if(leftSpans.length > 0) { - leftTail = leftSpans[leftSpans.length - 1]; - } - - // Handle any desired special handling for directly-pre-caret scenarios - where for this - // _specific_ context, we should not make a token division where one normally would exist otherwise. - // - // One notable example: word-final apostrophe is tokenized separate from preceding text, but - // word-internal apostrophe is treated as part of the same word (i.e, English contractions). - // But, if the user is editing text and the caret is directly after a caret, there's a notable - // chance they may in the middle of typing a contraction. Refer to - // https://github.com/keymanapp/keyman/issues/6572. - if(leftSpans.length > 1) { - const leftTailBase = leftSpans[leftSpans.length - 2]; - - // If the final two pre-caret spans are adjacent - without intervening whitespace... - if(leftTailBase.end == leftTail!.start) { - // Ideal: if(leftTailBase is standard-char-class && leftTail is single-quote-class) - // But we don't have character class access here; it's all wordbreaker-function internal. - // Upon inspection of the wordbreaker data definitions... the single-quote-class is ONLY "'". - // So... we'll just be lazy for now and append the `'`. - if(leftTail!.text == "'") { - let mergedSpan: Span = { - text: leftTailBase.text + leftTail!.text, - start: leftTailBase.start, - end: leftTail!.end, - length: leftTailBase.length + leftTail!.length - }; - - leftSpans.pop(); // leftTail - leftSpans.pop(); // leftTailBase - leftSpans.push(mergedSpan); - leftTail = mergedSpan; // don't forget to update the `leftTail` Span! - } - } - } - - // With any 'detokenization' cases already handled, we may now begin to build the return object. - let tokenization: Tokenization = { - left: leftSpans.map(span => span.text), - right: rightSpans.map(span => span.text), - - // A default initialization of the value. - caretSplitsToken: false - }; - - // Now the hard part - determining whether or not the caret caused a token split. - if(leftSpans.length > 0 && rightSpans.length > 0) { - let rightHead = rightSpans[0]; - - // If tokenization includes all characters on each side of the caret, - // we have a good candidate for a caret-splitting scenario. - let leftSuffixWordbreak = leftTail!.end != context.left!.length; - let rightPrefixWordbreak = rightHead.start != 0; - - if(leftSuffixWordbreak || rightPrefixWordbreak) { - // Bypass the final test, as we already know the caret didn't split a token. - // (The tokenization process already removed characters between the two.) - return tokenization; - } - - // Worth note - some languages don't use wordbreaking characters. So, a final check: - // - // Does the wordbreaker split a merge of the 'two center' tokens? - // If not, then the caret is responsible for the split. - if(wordBreaker(leftTail!.text + rightHead.text).length == 1) { - tokenization.caretSplitsToken = true; - } - } - - return tokenization; -} - -/** - * Get the last word of the phrase before the caret or nothing. - * @param fullLeftContext the entire left context of the string. - */ -export function getLastPreCaretToken(wordBreaker: WordBreakingFunction, context: Context): string { - let tokenization = tokenize(wordBreaker, context); - if (tokenization.left.length > 0) { - return tokenization.left.pop() as string; - } - - return ''; -} - -// While it is currently identical to getLastWord, this may change in the future. -// It's best not to write ourselves into a corner on this one, as disambiguating later -// would likely be pretty painful. -export function wordbreak(wordBreaker: WordBreakingFunction, context: Context): USVString { - return getLastPreCaretToken(wordBreaker, context); -} \ No newline at end of file diff --git a/common/models/tsconfig.kmw-worker-base.json b/common/models/tsconfig.kmw-worker-base.json deleted file mode 100644 index 0b581af1be3..00000000000 --- a/common/models/tsconfig.kmw-worker-base.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../web/tsconfig.base.json", - - "compilerOptions": { - // To help better support legacy Android devices - "downlevelIteration": true, - // Facilitates & simplifies stitching together the worker sourcemaps during the polyfill-concatenation step. - "inlineSourceMap": true, - // May not be set at the same time as the prior setting. - "sourceMap": false - } -} diff --git a/common/models/types/.build-builder b/common/models/types/.build-builder deleted file mode 100644 index 6db097fddbb..00000000000 --- a/common/models/types/.build-builder +++ /dev/null @@ -1 +0,0 @@ -The presence of this file tells CI to use the new builder_ style parameters for build.sh. \ No newline at end of file diff --git a/common/models/types/.gitignore b/common/models/types/.gitignore deleted file mode 100644 index e113ad0abd6..00000000000 --- a/common/models/types/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Specifically here: -test.js - -node_modules/ -build/ diff --git a/common/models/types/README.md b/common/models/types/README.md deleted file mode 100644 index 3d7dbf7d595..00000000000 --- a/common/models/types/README.md +++ /dev/null @@ -1,68 +0,0 @@ -`@keymanapp/models-types` -========================= - -Declares TypeScript types and interfaces required to define **Lexical -Models**, **Word breaking functions**, and all things in between. - -Install -------- - -> (see below if you are working in the keymanapp/keyman repo!) - - npm install --save-dev @keymanapp/models-types - -Usage ------ - -Say you are creating a **custom lexical model**. Then import this module -in your lexical model file! - -```Typescript -// my-nifty-model.ts - -import "@keymanapp/models-types"; - -export class MyNiftyClass implements LexicalModel { - configure(capabilities: Capabilities): Configuration { - // TODO: implement me! - throw new Error("Method not implemented."); - } - predict(transform: Transform, context: Context): ProbabilityMass[] { - // TODO: implement me! - throw new Error("Method not implemented."); - } - wordbreak(context: Context): string { - // TODO: implement me! - throw new Error("Method not implemented."); - } - punctuation?: LexicalModelPunctuation; -} -``` - -If your are creating a **custom word breaker**, you would do the same: - -```typescript -import "@keymanapp/models-types"; - -export let myShinyWordBreaker: WordBreakingFunction; -myShinyWordBreaker = function (phrase: string): Span[] { - // TODO: implement me! - throw new Error("Function not implemented") -} -``` - -Look at [index.d.ts](./index.d.ts) for documentation on each and every -type! - - -Usage within keymanapp/keyman repo ----------------------------------- - -To use it in other subprojects within keymanapp/keyman, ensure that this package -is linked using `"*"`. Add this dependency to your subproject like so: - -```json - "dependencies": { - "@keymanapp/models-types": "*", - } -``` diff --git a/common/models/types/build.sh b/common/models/types/build.sh deleted file mode 100755 index 25bf4515c43..00000000000 --- a/common/models/types/build.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# -# Packages the @keymanapp/models-types package. -# -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" -. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" - -builder_describe "Keyman model types package" \ - "clean" \ - "configure" \ - "build" \ - "test" \ - "publish publish to npm" \ - "--npm-publish+ For publish, do a npm publish, not npm pack (only for CI)" \ - "--dry-run,-n don't actually publish, just dry run" - -builder_describe_outputs \ - configure /node_modules \ - build /common/models/types/build/common/models/types/tsconfig.tsbuildinfo - -builder_parse "$@" - -#------------------------------------------------------------------------------------------------------------------- - -builder_run_action clean rm -rf ./build/ -builder_run_action configure verify_npm_setup -builder_run_action build tsc --build -builder_run_action test tsc test.ts -lib es6 -builder_run_action publish builder_publish_npm diff --git a/common/models/types/package.json b/common/models/types/package.json deleted file mode 100644 index c3005a8eb5a..00000000000 --- a/common/models/types/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@keymanapp/models-types", - "description": "Type definitions in used in the modeling (lexical model/predictive text) component of Keyman.", - "types": "./index.d.ts", - "scripts": { - "test": "tsc test.ts -lib es6" - }, - "repository": { - "type": "git", - "url": "https://github.com/keymanapp/keyman" - }, - "keywords": [ - "lmlayer", - "keyman", - "predictive", - "text", - "types", - "typing", - "typescript" - ], - "author": "Marc Durdin (https://github.com/mcdurdin)", - "contributors": [ - "Eddie Antonio Santos (https://github.com/eddieantonio)", - "Joshua Horton" - ], - "license": "MIT", - "bugs": { - "url": "https://github.com/keymanapp/keyman/issues" - }, - "homepage": "https://github.com/keymanapp/keyman/tree/master/common/models/types#readme", - "devDependencies": { - "typescript": "^4.9.5" - }, - "files": [ - "README.md", - "index.d.ts" - ] -} diff --git a/common/models/types/tsconfig.json b/common/models/types/tsconfig.json deleted file mode 100644 index ddd1c5b196a..00000000000 --- a/common/models/types/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.kmw-worker-base.json", - "compilerOptions": { - "declaration": true, - "outDir": "build/", - }, - "include": ["./*.ts"], - "exclude": ["test.ts"] -} diff --git a/common/models/wordbreakers/.gitignore b/common/models/wordbreakers/.gitignore deleted file mode 100644 index d16386367f7..00000000000 --- a/common/models/wordbreakers/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ \ No newline at end of file diff --git a/common/models/wordbreakers/src/default/data.ts b/common/models/wordbreakers/src/default/data.ts deleted file mode 100644 index abd128f020e..00000000000 --- a/common/models/wordbreakers/src/default/data.ts +++ /dev/null @@ -1,1802 +0,0 @@ -// Automatically generated file. DO NOT MODIFY. - -/** - * Valid values for a word break property. - */ -export const enum WordBreakProperty { // Scary bit: this does not exist as an object at run-time! - Other, - LF, - Newline, - CR, - WSegSpace, - Double_Quote, - Single_Quote, - MidNum, - MidNumLet, - Numeric, - MidLetter, - ALetter, - ExtendNumLet, - Format, - Extend, - Hebrew_Letter, - ZWJ, - Katakana, - Regional_Indicator, - sot, - eot -}; - -// Not currently built by the auto-generator tool, but it easily could be. -// If and when we import the data.ts rebuilder, we can add this in. -export const propertyMap = [ - "Other", - "LF", - "Newline", - "CR", - "WSegSpace", - "Double_Quote", - "Single_Quote", - "MidNum", - "MidNumLet", - "Numeric", - "MidLetter", - "ALetter", - "ExtendNumLet", - "Format", - "Extend", - "Hebrew_Letter", - "ZWJ", - "Katakana", - "Regional_Indicator", - "sot", - "eot" -]; - -/** - * Constants for indexing values in WORD_BREAK_PROPERTY. - */ -export const enum I { - Start = 0, - Value = 1 -} - -export const WORD_BREAK_PROPERTY: [number, WordBreakProperty][] = [ - [/*start*/ 0x0, WordBreakProperty.Other], - [/*start*/ 0xA, WordBreakProperty.LF], - [/*start*/ 0xB, WordBreakProperty.Newline], - [/*start*/ 0xD, WordBreakProperty.CR], - [/*start*/ 0xE, WordBreakProperty.Other], - [/*start*/ 0x20, WordBreakProperty.WSegSpace], - [/*start*/ 0x21, WordBreakProperty.Other], - [/*start*/ 0x22, WordBreakProperty.Double_Quote], - [/*start*/ 0x23, WordBreakProperty.Other], - [/*start*/ 0x27, WordBreakProperty.Single_Quote], - [/*start*/ 0x28, WordBreakProperty.Other], - [/*start*/ 0x2C, WordBreakProperty.MidNum], - [/*start*/ 0x2D, WordBreakProperty.Other], - [/*start*/ 0x2E, WordBreakProperty.MidNumLet], - [/*start*/ 0x2F, WordBreakProperty.Other], - [/*start*/ 0x30, WordBreakProperty.Numeric], - [/*start*/ 0x3A, WordBreakProperty.MidLetter], - [/*start*/ 0x3B, WordBreakProperty.MidNum], - [/*start*/ 0x3C, WordBreakProperty.Other], - [/*start*/ 0x41, WordBreakProperty.ALetter], - [/*start*/ 0x5B, WordBreakProperty.Other], - [/*start*/ 0x5F, WordBreakProperty.ExtendNumLet], - [/*start*/ 0x60, WordBreakProperty.Other], - [/*start*/ 0x61, WordBreakProperty.ALetter], - [/*start*/ 0x7B, WordBreakProperty.Other], - [/*start*/ 0x85, WordBreakProperty.Newline], - [/*start*/ 0x86, WordBreakProperty.Other], - [/*start*/ 0xAA, WordBreakProperty.ALetter], - [/*start*/ 0xAB, WordBreakProperty.Other], - [/*start*/ 0xAD, WordBreakProperty.Format], - [/*start*/ 0xAE, WordBreakProperty.Other], - [/*start*/ 0xB5, WordBreakProperty.ALetter], - [/*start*/ 0xB6, WordBreakProperty.Other], - [/*start*/ 0xB7, WordBreakProperty.MidLetter], - [/*start*/ 0xB8, WordBreakProperty.Other], - [/*start*/ 0xBA, WordBreakProperty.ALetter], - [/*start*/ 0xBB, WordBreakProperty.Other], - [/*start*/ 0xC0, WordBreakProperty.ALetter], - [/*start*/ 0xD7, WordBreakProperty.Other], - [/*start*/ 0xD8, WordBreakProperty.ALetter], - [/*start*/ 0xF7, WordBreakProperty.Other], - [/*start*/ 0xF8, WordBreakProperty.ALetter], - [/*start*/ 0x2D8, WordBreakProperty.Other], - [/*start*/ 0x2DE, WordBreakProperty.ALetter], - [/*start*/ 0x300, WordBreakProperty.Extend], - [/*start*/ 0x370, WordBreakProperty.ALetter], - [/*start*/ 0x375, WordBreakProperty.Other], - [/*start*/ 0x376, WordBreakProperty.ALetter], - [/*start*/ 0x378, WordBreakProperty.Other], - [/*start*/ 0x37A, WordBreakProperty.ALetter], - [/*start*/ 0x37E, WordBreakProperty.MidNum], - [/*start*/ 0x37F, WordBreakProperty.ALetter], - [/*start*/ 0x380, WordBreakProperty.Other], - [/*start*/ 0x386, WordBreakProperty.ALetter], - [/*start*/ 0x387, WordBreakProperty.MidLetter], - [/*start*/ 0x388, WordBreakProperty.ALetter], - [/*start*/ 0x38B, WordBreakProperty.Other], - [/*start*/ 0x38C, WordBreakProperty.ALetter], - [/*start*/ 0x38D, WordBreakProperty.Other], - [/*start*/ 0x38E, WordBreakProperty.ALetter], - [/*start*/ 0x3A2, WordBreakProperty.Other], - [/*start*/ 0x3A3, WordBreakProperty.ALetter], - [/*start*/ 0x3F6, WordBreakProperty.Other], - [/*start*/ 0x3F7, WordBreakProperty.ALetter], - [/*start*/ 0x482, WordBreakProperty.Other], - [/*start*/ 0x483, WordBreakProperty.Extend], - [/*start*/ 0x48A, WordBreakProperty.ALetter], - [/*start*/ 0x530, WordBreakProperty.Other], - [/*start*/ 0x531, WordBreakProperty.ALetter], - [/*start*/ 0x557, WordBreakProperty.Other], - [/*start*/ 0x559, WordBreakProperty.ALetter], - [/*start*/ 0x55D, WordBreakProperty.Other], - [/*start*/ 0x55E, WordBreakProperty.ALetter], - [/*start*/ 0x55F, WordBreakProperty.MidLetter], - [/*start*/ 0x560, WordBreakProperty.ALetter], - [/*start*/ 0x589, WordBreakProperty.MidNum], - [/*start*/ 0x58A, WordBreakProperty.ALetter], - [/*start*/ 0x58B, WordBreakProperty.Other], - [/*start*/ 0x591, WordBreakProperty.Extend], - [/*start*/ 0x5BE, WordBreakProperty.Other], - [/*start*/ 0x5BF, WordBreakProperty.Extend], - [/*start*/ 0x5C0, WordBreakProperty.Other], - [/*start*/ 0x5C1, WordBreakProperty.Extend], - [/*start*/ 0x5C3, WordBreakProperty.Other], - [/*start*/ 0x5C4, WordBreakProperty.Extend], - [/*start*/ 0x5C6, WordBreakProperty.Other], - [/*start*/ 0x5C7, WordBreakProperty.Extend], - [/*start*/ 0x5C8, WordBreakProperty.Other], - [/*start*/ 0x5D0, WordBreakProperty.Hebrew_Letter], - [/*start*/ 0x5EB, WordBreakProperty.Other], - [/*start*/ 0x5EF, WordBreakProperty.Hebrew_Letter], - [/*start*/ 0x5F3, WordBreakProperty.ALetter], - [/*start*/ 0x5F4, WordBreakProperty.MidLetter], - [/*start*/ 0x5F5, WordBreakProperty.Other], - [/*start*/ 0x600, WordBreakProperty.Format], - [/*start*/ 0x606, WordBreakProperty.Other], - [/*start*/ 0x60C, WordBreakProperty.MidNum], - [/*start*/ 0x60E, WordBreakProperty.Other], - [/*start*/ 0x610, WordBreakProperty.Extend], - [/*start*/ 0x61B, WordBreakProperty.Other], - [/*start*/ 0x61C, WordBreakProperty.Format], - [/*start*/ 0x61D, WordBreakProperty.Other], - [/*start*/ 0x620, WordBreakProperty.ALetter], - [/*start*/ 0x64B, WordBreakProperty.Extend], - [/*start*/ 0x660, WordBreakProperty.Numeric], - [/*start*/ 0x66A, WordBreakProperty.Other], - [/*start*/ 0x66B, WordBreakProperty.Numeric], - [/*start*/ 0x66C, WordBreakProperty.MidNum], - [/*start*/ 0x66D, WordBreakProperty.Other], - [/*start*/ 0x66E, WordBreakProperty.ALetter], - [/*start*/ 0x670, WordBreakProperty.Extend], - [/*start*/ 0x671, WordBreakProperty.ALetter], - [/*start*/ 0x6D4, WordBreakProperty.Other], - [/*start*/ 0x6D5, WordBreakProperty.ALetter], - [/*start*/ 0x6D6, WordBreakProperty.Extend], - [/*start*/ 0x6DD, WordBreakProperty.Format], - [/*start*/ 0x6DE, WordBreakProperty.Other], - [/*start*/ 0x6DF, WordBreakProperty.Extend], - [/*start*/ 0x6E5, WordBreakProperty.ALetter], - [/*start*/ 0x6E7, WordBreakProperty.Extend], - [/*start*/ 0x6E9, WordBreakProperty.Other], - [/*start*/ 0x6EA, WordBreakProperty.Extend], - [/*start*/ 0x6EE, WordBreakProperty.ALetter], - [/*start*/ 0x6F0, WordBreakProperty.Numeric], - [/*start*/ 0x6FA, WordBreakProperty.ALetter], - [/*start*/ 0x6FD, WordBreakProperty.Other], - [/*start*/ 0x6FF, WordBreakProperty.ALetter], - [/*start*/ 0x700, WordBreakProperty.Other], - [/*start*/ 0x70F, WordBreakProperty.Format], - [/*start*/ 0x710, WordBreakProperty.ALetter], - [/*start*/ 0x711, WordBreakProperty.Extend], - [/*start*/ 0x712, WordBreakProperty.ALetter], - [/*start*/ 0x730, WordBreakProperty.Extend], - [/*start*/ 0x74B, WordBreakProperty.Other], - [/*start*/ 0x74D, WordBreakProperty.ALetter], - [/*start*/ 0x7A6, WordBreakProperty.Extend], - [/*start*/ 0x7B1, WordBreakProperty.ALetter], - [/*start*/ 0x7B2, WordBreakProperty.Other], - [/*start*/ 0x7C0, WordBreakProperty.Numeric], - [/*start*/ 0x7CA, WordBreakProperty.ALetter], - [/*start*/ 0x7EB, WordBreakProperty.Extend], - [/*start*/ 0x7F4, WordBreakProperty.ALetter], - [/*start*/ 0x7F6, WordBreakProperty.Other], - [/*start*/ 0x7F8, WordBreakProperty.MidNum], - [/*start*/ 0x7F9, WordBreakProperty.Other], - [/*start*/ 0x7FA, WordBreakProperty.ALetter], - [/*start*/ 0x7FB, WordBreakProperty.Other], - [/*start*/ 0x7FD, WordBreakProperty.Extend], - [/*start*/ 0x7FE, WordBreakProperty.Other], - [/*start*/ 0x800, WordBreakProperty.ALetter], - [/*start*/ 0x816, WordBreakProperty.Extend], - [/*start*/ 0x81A, WordBreakProperty.ALetter], - [/*start*/ 0x81B, WordBreakProperty.Extend], - [/*start*/ 0x824, WordBreakProperty.ALetter], - [/*start*/ 0x825, WordBreakProperty.Extend], - [/*start*/ 0x828, WordBreakProperty.ALetter], - [/*start*/ 0x829, WordBreakProperty.Extend], - [/*start*/ 0x82E, WordBreakProperty.Other], - [/*start*/ 0x840, WordBreakProperty.ALetter], - [/*start*/ 0x859, WordBreakProperty.Extend], - [/*start*/ 0x85C, WordBreakProperty.Other], - [/*start*/ 0x860, WordBreakProperty.ALetter], - [/*start*/ 0x86B, WordBreakProperty.Other], - [/*start*/ 0x8A0, WordBreakProperty.ALetter], - [/*start*/ 0x8B5, WordBreakProperty.Other], - [/*start*/ 0x8B6, WordBreakProperty.ALetter], - [/*start*/ 0x8C8, WordBreakProperty.Other], - [/*start*/ 0x8D3, WordBreakProperty.Extend], - [/*start*/ 0x8E2, WordBreakProperty.Format], - [/*start*/ 0x8E3, WordBreakProperty.Extend], - [/*start*/ 0x904, WordBreakProperty.ALetter], - [/*start*/ 0x93A, WordBreakProperty.Extend], - [/*start*/ 0x93D, WordBreakProperty.ALetter], - [/*start*/ 0x93E, WordBreakProperty.Extend], - [/*start*/ 0x950, WordBreakProperty.ALetter], - [/*start*/ 0x951, WordBreakProperty.Extend], - [/*start*/ 0x958, WordBreakProperty.ALetter], - [/*start*/ 0x962, WordBreakProperty.Extend], - [/*start*/ 0x964, WordBreakProperty.Other], - [/*start*/ 0x966, WordBreakProperty.Numeric], - [/*start*/ 0x970, WordBreakProperty.Other], - [/*start*/ 0x971, WordBreakProperty.ALetter], - [/*start*/ 0x981, WordBreakProperty.Extend], - [/*start*/ 0x984, WordBreakProperty.Other], - [/*start*/ 0x985, WordBreakProperty.ALetter], - [/*start*/ 0x98D, WordBreakProperty.Other], - [/*start*/ 0x98F, WordBreakProperty.ALetter], - [/*start*/ 0x991, WordBreakProperty.Other], - [/*start*/ 0x993, WordBreakProperty.ALetter], - [/*start*/ 0x9A9, WordBreakProperty.Other], - [/*start*/ 0x9AA, WordBreakProperty.ALetter], - [/*start*/ 0x9B1, WordBreakProperty.Other], - [/*start*/ 0x9B2, WordBreakProperty.ALetter], - [/*start*/ 0x9B3, WordBreakProperty.Other], - [/*start*/ 0x9B6, WordBreakProperty.ALetter], - [/*start*/ 0x9BA, WordBreakProperty.Other], - [/*start*/ 0x9BC, WordBreakProperty.Extend], - [/*start*/ 0x9BD, WordBreakProperty.ALetter], - [/*start*/ 0x9BE, WordBreakProperty.Extend], - [/*start*/ 0x9C5, WordBreakProperty.Other], - [/*start*/ 0x9C7, WordBreakProperty.Extend], - [/*start*/ 0x9C9, WordBreakProperty.Other], - [/*start*/ 0x9CB, WordBreakProperty.Extend], - [/*start*/ 0x9CE, WordBreakProperty.ALetter], - [/*start*/ 0x9CF, WordBreakProperty.Other], - [/*start*/ 0x9D7, WordBreakProperty.Extend], - [/*start*/ 0x9D8, WordBreakProperty.Other], - [/*start*/ 0x9DC, WordBreakProperty.ALetter], - [/*start*/ 0x9DE, WordBreakProperty.Other], - [/*start*/ 0x9DF, WordBreakProperty.ALetter], - [/*start*/ 0x9E2, WordBreakProperty.Extend], - [/*start*/ 0x9E4, WordBreakProperty.Other], - [/*start*/ 0x9E6, WordBreakProperty.Numeric], - [/*start*/ 0x9F0, WordBreakProperty.ALetter], - [/*start*/ 0x9F2, WordBreakProperty.Other], - [/*start*/ 0x9FC, WordBreakProperty.ALetter], - [/*start*/ 0x9FD, WordBreakProperty.Other], - [/*start*/ 0x9FE, WordBreakProperty.Extend], - [/*start*/ 0x9FF, WordBreakProperty.Other], - [/*start*/ 0xA01, WordBreakProperty.Extend], - [/*start*/ 0xA04, WordBreakProperty.Other], - [/*start*/ 0xA05, WordBreakProperty.ALetter], - [/*start*/ 0xA0B, WordBreakProperty.Other], - [/*start*/ 0xA0F, WordBreakProperty.ALetter], - [/*start*/ 0xA11, WordBreakProperty.Other], - [/*start*/ 0xA13, WordBreakProperty.ALetter], - [/*start*/ 0xA29, WordBreakProperty.Other], - [/*start*/ 0xA2A, WordBreakProperty.ALetter], - [/*start*/ 0xA31, WordBreakProperty.Other], - [/*start*/ 0xA32, WordBreakProperty.ALetter], - [/*start*/ 0xA34, WordBreakProperty.Other], - [/*start*/ 0xA35, WordBreakProperty.ALetter], - [/*start*/ 0xA37, WordBreakProperty.Other], - [/*start*/ 0xA38, WordBreakProperty.ALetter], - [/*start*/ 0xA3A, WordBreakProperty.Other], - [/*start*/ 0xA3C, WordBreakProperty.Extend], - [/*start*/ 0xA3D, WordBreakProperty.Other], - [/*start*/ 0xA3E, WordBreakProperty.Extend], - [/*start*/ 0xA43, WordBreakProperty.Other], - [/*start*/ 0xA47, WordBreakProperty.Extend], - [/*start*/ 0xA49, WordBreakProperty.Other], - [/*start*/ 0xA4B, WordBreakProperty.Extend], - [/*start*/ 0xA4E, WordBreakProperty.Other], - [/*start*/ 0xA51, WordBreakProperty.Extend], - [/*start*/ 0xA52, WordBreakProperty.Other], - [/*start*/ 0xA59, WordBreakProperty.ALetter], - [/*start*/ 0xA5D, WordBreakProperty.Other], - [/*start*/ 0xA5E, WordBreakProperty.ALetter], - [/*start*/ 0xA5F, WordBreakProperty.Other], - [/*start*/ 0xA66, WordBreakProperty.Numeric], - [/*start*/ 0xA70, WordBreakProperty.Extend], - [/*start*/ 0xA72, WordBreakProperty.ALetter], - [/*start*/ 0xA75, WordBreakProperty.Extend], - [/*start*/ 0xA76, WordBreakProperty.Other], - [/*start*/ 0xA81, WordBreakProperty.Extend], - [/*start*/ 0xA84, WordBreakProperty.Other], - [/*start*/ 0xA85, WordBreakProperty.ALetter], - [/*start*/ 0xA8E, WordBreakProperty.Other], - [/*start*/ 0xA8F, WordBreakProperty.ALetter], - [/*start*/ 0xA92, WordBreakProperty.Other], - [/*start*/ 0xA93, WordBreakProperty.ALetter], - [/*start*/ 0xAA9, WordBreakProperty.Other], - [/*start*/ 0xAAA, WordBreakProperty.ALetter], - [/*start*/ 0xAB1, WordBreakProperty.Other], - [/*start*/ 0xAB2, WordBreakProperty.ALetter], - [/*start*/ 0xAB4, WordBreakProperty.Other], - [/*start*/ 0xAB5, WordBreakProperty.ALetter], - [/*start*/ 0xABA, WordBreakProperty.Other], - [/*start*/ 0xABC, WordBreakProperty.Extend], - [/*start*/ 0xABD, WordBreakProperty.ALetter], - [/*start*/ 0xABE, WordBreakProperty.Extend], - [/*start*/ 0xAC6, WordBreakProperty.Other], - [/*start*/ 0xAC7, WordBreakProperty.Extend], - [/*start*/ 0xACA, WordBreakProperty.Other], - [/*start*/ 0xACB, WordBreakProperty.Extend], - [/*start*/ 0xACE, WordBreakProperty.Other], - [/*start*/ 0xAD0, WordBreakProperty.ALetter], - [/*start*/ 0xAD1, WordBreakProperty.Other], - [/*start*/ 0xAE0, WordBreakProperty.ALetter], - [/*start*/ 0xAE2, WordBreakProperty.Extend], - [/*start*/ 0xAE4, WordBreakProperty.Other], - [/*start*/ 0xAE6, WordBreakProperty.Numeric], - [/*start*/ 0xAF0, WordBreakProperty.Other], - [/*start*/ 0xAF9, WordBreakProperty.ALetter], - [/*start*/ 0xAFA, WordBreakProperty.Extend], - [/*start*/ 0xB00, WordBreakProperty.Other], - [/*start*/ 0xB01, WordBreakProperty.Extend], - [/*start*/ 0xB04, WordBreakProperty.Other], - [/*start*/ 0xB05, WordBreakProperty.ALetter], - [/*start*/ 0xB0D, WordBreakProperty.Other], - [/*start*/ 0xB0F, WordBreakProperty.ALetter], - [/*start*/ 0xB11, WordBreakProperty.Other], - [/*start*/ 0xB13, WordBreakProperty.ALetter], - [/*start*/ 0xB29, WordBreakProperty.Other], - [/*start*/ 0xB2A, WordBreakProperty.ALetter], - [/*start*/ 0xB31, WordBreakProperty.Other], - [/*start*/ 0xB32, WordBreakProperty.ALetter], - [/*start*/ 0xB34, WordBreakProperty.Other], - [/*start*/ 0xB35, WordBreakProperty.ALetter], - [/*start*/ 0xB3A, WordBreakProperty.Other], - [/*start*/ 0xB3C, WordBreakProperty.Extend], - [/*start*/ 0xB3D, WordBreakProperty.ALetter], - [/*start*/ 0xB3E, WordBreakProperty.Extend], - [/*start*/ 0xB45, WordBreakProperty.Other], - [/*start*/ 0xB47, WordBreakProperty.Extend], - [/*start*/ 0xB49, WordBreakProperty.Other], - [/*start*/ 0xB4B, WordBreakProperty.Extend], - [/*start*/ 0xB4E, WordBreakProperty.Other], - [/*start*/ 0xB55, WordBreakProperty.Extend], - [/*start*/ 0xB58, WordBreakProperty.Other], - [/*start*/ 0xB5C, WordBreakProperty.ALetter], - [/*start*/ 0xB5E, WordBreakProperty.Other], - [/*start*/ 0xB5F, WordBreakProperty.ALetter], - [/*start*/ 0xB62, WordBreakProperty.Extend], - [/*start*/ 0xB64, WordBreakProperty.Other], - [/*start*/ 0xB66, WordBreakProperty.Numeric], - [/*start*/ 0xB70, WordBreakProperty.Other], - [/*start*/ 0xB71, WordBreakProperty.ALetter], - [/*start*/ 0xB72, WordBreakProperty.Other], - [/*start*/ 0xB82, WordBreakProperty.Extend], - [/*start*/ 0xB83, WordBreakProperty.ALetter], - [/*start*/ 0xB84, WordBreakProperty.Other], - [/*start*/ 0xB85, WordBreakProperty.ALetter], - [/*start*/ 0xB8B, WordBreakProperty.Other], - [/*start*/ 0xB8E, WordBreakProperty.ALetter], - [/*start*/ 0xB91, WordBreakProperty.Other], - [/*start*/ 0xB92, WordBreakProperty.ALetter], - [/*start*/ 0xB96, WordBreakProperty.Other], - [/*start*/ 0xB99, WordBreakProperty.ALetter], - [/*start*/ 0xB9B, WordBreakProperty.Other], - [/*start*/ 0xB9C, WordBreakProperty.ALetter], - [/*start*/ 0xB9D, WordBreakProperty.Other], - [/*start*/ 0xB9E, WordBreakProperty.ALetter], - [/*start*/ 0xBA0, WordBreakProperty.Other], - [/*start*/ 0xBA3, WordBreakProperty.ALetter], - [/*start*/ 0xBA5, WordBreakProperty.Other], - [/*start*/ 0xBA8, WordBreakProperty.ALetter], - [/*start*/ 0xBAB, WordBreakProperty.Other], - [/*start*/ 0xBAE, WordBreakProperty.ALetter], - [/*start*/ 0xBBA, WordBreakProperty.Other], - [/*start*/ 0xBBE, WordBreakProperty.Extend], - [/*start*/ 0xBC3, WordBreakProperty.Other], - [/*start*/ 0xBC6, WordBreakProperty.Extend], - [/*start*/ 0xBC9, WordBreakProperty.Other], - [/*start*/ 0xBCA, WordBreakProperty.Extend], - [/*start*/ 0xBCE, WordBreakProperty.Other], - [/*start*/ 0xBD0, WordBreakProperty.ALetter], - [/*start*/ 0xBD1, WordBreakProperty.Other], - [/*start*/ 0xBD7, WordBreakProperty.Extend], - [/*start*/ 0xBD8, WordBreakProperty.Other], - [/*start*/ 0xBE6, WordBreakProperty.Numeric], - [/*start*/ 0xBF0, WordBreakProperty.Other], - [/*start*/ 0xC00, WordBreakProperty.Extend], - [/*start*/ 0xC05, WordBreakProperty.ALetter], - [/*start*/ 0xC0D, WordBreakProperty.Other], - [/*start*/ 0xC0E, WordBreakProperty.ALetter], - [/*start*/ 0xC11, WordBreakProperty.Other], - [/*start*/ 0xC12, WordBreakProperty.ALetter], - [/*start*/ 0xC29, WordBreakProperty.Other], - [/*start*/ 0xC2A, WordBreakProperty.ALetter], - [/*start*/ 0xC3A, WordBreakProperty.Other], - [/*start*/ 0xC3D, WordBreakProperty.ALetter], - [/*start*/ 0xC3E, WordBreakProperty.Extend], - [/*start*/ 0xC45, WordBreakProperty.Other], - [/*start*/ 0xC46, WordBreakProperty.Extend], - [/*start*/ 0xC49, WordBreakProperty.Other], - [/*start*/ 0xC4A, WordBreakProperty.Extend], - [/*start*/ 0xC4E, WordBreakProperty.Other], - [/*start*/ 0xC55, WordBreakProperty.Extend], - [/*start*/ 0xC57, WordBreakProperty.Other], - [/*start*/ 0xC58, WordBreakProperty.ALetter], - [/*start*/ 0xC5B, WordBreakProperty.Other], - [/*start*/ 0xC60, WordBreakProperty.ALetter], - [/*start*/ 0xC62, WordBreakProperty.Extend], - [/*start*/ 0xC64, WordBreakProperty.Other], - [/*start*/ 0xC66, WordBreakProperty.Numeric], - [/*start*/ 0xC70, WordBreakProperty.Other], - [/*start*/ 0xC80, WordBreakProperty.ALetter], - [/*start*/ 0xC81, WordBreakProperty.Extend], - [/*start*/ 0xC84, WordBreakProperty.Other], - [/*start*/ 0xC85, WordBreakProperty.ALetter], - [/*start*/ 0xC8D, WordBreakProperty.Other], - [/*start*/ 0xC8E, WordBreakProperty.ALetter], - [/*start*/ 0xC91, WordBreakProperty.Other], - [/*start*/ 0xC92, WordBreakProperty.ALetter], - [/*start*/ 0xCA9, WordBreakProperty.Other], - [/*start*/ 0xCAA, WordBreakProperty.ALetter], - [/*start*/ 0xCB4, WordBreakProperty.Other], - [/*start*/ 0xCB5, WordBreakProperty.ALetter], - [/*start*/ 0xCBA, WordBreakProperty.Other], - [/*start*/ 0xCBC, WordBreakProperty.Extend], - [/*start*/ 0xCBD, WordBreakProperty.ALetter], - [/*start*/ 0xCBE, WordBreakProperty.Extend], - [/*start*/ 0xCC5, WordBreakProperty.Other], - [/*start*/ 0xCC6, WordBreakProperty.Extend], - [/*start*/ 0xCC9, WordBreakProperty.Other], - [/*start*/ 0xCCA, WordBreakProperty.Extend], - [/*start*/ 0xCCE, WordBreakProperty.Other], - [/*start*/ 0xCD5, WordBreakProperty.Extend], - [/*start*/ 0xCD7, WordBreakProperty.Other], - [/*start*/ 0xCDE, WordBreakProperty.ALetter], - [/*start*/ 0xCDF, WordBreakProperty.Other], - [/*start*/ 0xCE0, WordBreakProperty.ALetter], - [/*start*/ 0xCE2, WordBreakProperty.Extend], - [/*start*/ 0xCE4, WordBreakProperty.Other], - [/*start*/ 0xCE6, WordBreakProperty.Numeric], - [/*start*/ 0xCF0, WordBreakProperty.Other], - [/*start*/ 0xCF1, WordBreakProperty.ALetter], - [/*start*/ 0xCF3, WordBreakProperty.Other], - [/*start*/ 0xD00, WordBreakProperty.Extend], - [/*start*/ 0xD04, WordBreakProperty.ALetter], - [/*start*/ 0xD0D, WordBreakProperty.Other], - [/*start*/ 0xD0E, WordBreakProperty.ALetter], - [/*start*/ 0xD11, WordBreakProperty.Other], - [/*start*/ 0xD12, WordBreakProperty.ALetter], - [/*start*/ 0xD3B, WordBreakProperty.Extend], - [/*start*/ 0xD3D, WordBreakProperty.ALetter], - [/*start*/ 0xD3E, WordBreakProperty.Extend], - [/*start*/ 0xD45, WordBreakProperty.Other], - [/*start*/ 0xD46, WordBreakProperty.Extend], - [/*start*/ 0xD49, WordBreakProperty.Other], - [/*start*/ 0xD4A, WordBreakProperty.Extend], - [/*start*/ 0xD4E, WordBreakProperty.ALetter], - [/*start*/ 0xD4F, WordBreakProperty.Other], - [/*start*/ 0xD54, WordBreakProperty.ALetter], - [/*start*/ 0xD57, WordBreakProperty.Extend], - [/*start*/ 0xD58, WordBreakProperty.Other], - [/*start*/ 0xD5F, WordBreakProperty.ALetter], - [/*start*/ 0xD62, WordBreakProperty.Extend], - [/*start*/ 0xD64, WordBreakProperty.Other], - [/*start*/ 0xD66, WordBreakProperty.Numeric], - [/*start*/ 0xD70, WordBreakProperty.Other], - [/*start*/ 0xD7A, WordBreakProperty.ALetter], - [/*start*/ 0xD80, WordBreakProperty.Other], - [/*start*/ 0xD81, WordBreakProperty.Extend], - [/*start*/ 0xD84, WordBreakProperty.Other], - [/*start*/ 0xD85, WordBreakProperty.ALetter], - [/*start*/ 0xD97, WordBreakProperty.Other], - [/*start*/ 0xD9A, WordBreakProperty.ALetter], - [/*start*/ 0xDB2, WordBreakProperty.Other], - [/*start*/ 0xDB3, WordBreakProperty.ALetter], - [/*start*/ 0xDBC, WordBreakProperty.Other], - [/*start*/ 0xDBD, WordBreakProperty.ALetter], - [/*start*/ 0xDBE, WordBreakProperty.Other], - [/*start*/ 0xDC0, WordBreakProperty.ALetter], - [/*start*/ 0xDC7, WordBreakProperty.Other], - [/*start*/ 0xDCA, WordBreakProperty.Extend], - [/*start*/ 0xDCB, WordBreakProperty.Other], - [/*start*/ 0xDCF, WordBreakProperty.Extend], - [/*start*/ 0xDD5, WordBreakProperty.Other], - [/*start*/ 0xDD6, WordBreakProperty.Extend], - [/*start*/ 0xDD7, WordBreakProperty.Other], - [/*start*/ 0xDD8, WordBreakProperty.Extend], - [/*start*/ 0xDE0, WordBreakProperty.Other], - [/*start*/ 0xDE6, WordBreakProperty.Numeric], - [/*start*/ 0xDF0, WordBreakProperty.Other], - [/*start*/ 0xDF2, WordBreakProperty.Extend], - [/*start*/ 0xDF4, WordBreakProperty.Other], - [/*start*/ 0xE31, WordBreakProperty.Extend], - [/*start*/ 0xE32, WordBreakProperty.Other], - [/*start*/ 0xE34, WordBreakProperty.Extend], - [/*start*/ 0xE3B, WordBreakProperty.Other], - [/*start*/ 0xE47, WordBreakProperty.Extend], - [/*start*/ 0xE4F, WordBreakProperty.Other], - [/*start*/ 0xE50, WordBreakProperty.Numeric], - [/*start*/ 0xE5A, WordBreakProperty.Other], - [/*start*/ 0xEB1, WordBreakProperty.Extend], - [/*start*/ 0xEB2, WordBreakProperty.Other], - [/*start*/ 0xEB4, WordBreakProperty.Extend], - [/*start*/ 0xEBD, WordBreakProperty.Other], - [/*start*/ 0xEC8, WordBreakProperty.Extend], - [/*start*/ 0xECE, WordBreakProperty.Other], - [/*start*/ 0xED0, WordBreakProperty.Numeric], - [/*start*/ 0xEDA, WordBreakProperty.Other], - [/*start*/ 0xF00, WordBreakProperty.ALetter], - [/*start*/ 0xF01, WordBreakProperty.Other], - [/*start*/ 0xF18, WordBreakProperty.Extend], - [/*start*/ 0xF1A, WordBreakProperty.Other], - [/*start*/ 0xF20, WordBreakProperty.Numeric], - [/*start*/ 0xF2A, WordBreakProperty.Other], - [/*start*/ 0xF35, WordBreakProperty.Extend], - [/*start*/ 0xF36, WordBreakProperty.Other], - [/*start*/ 0xF37, WordBreakProperty.Extend], - [/*start*/ 0xF38, WordBreakProperty.Other], - [/*start*/ 0xF39, WordBreakProperty.Extend], - [/*start*/ 0xF3A, WordBreakProperty.Other], - [/*start*/ 0xF3E, WordBreakProperty.Extend], - [/*start*/ 0xF40, WordBreakProperty.ALetter], - [/*start*/ 0xF48, WordBreakProperty.Other], - [/*start*/ 0xF49, WordBreakProperty.ALetter], - [/*start*/ 0xF6D, WordBreakProperty.Other], - [/*start*/ 0xF71, WordBreakProperty.Extend], - [/*start*/ 0xF85, WordBreakProperty.Other], - [/*start*/ 0xF86, WordBreakProperty.Extend], - [/*start*/ 0xF88, WordBreakProperty.ALetter], - [/*start*/ 0xF8D, WordBreakProperty.Extend], - [/*start*/ 0xF98, WordBreakProperty.Other], - [/*start*/ 0xF99, WordBreakProperty.Extend], - [/*start*/ 0xFBD, WordBreakProperty.Other], - [/*start*/ 0xFC6, WordBreakProperty.Extend], - [/*start*/ 0xFC7, WordBreakProperty.Other], - [/*start*/ 0x102B, WordBreakProperty.Extend], - [/*start*/ 0x103F, WordBreakProperty.Other], - [/*start*/ 0x1040, WordBreakProperty.Numeric], - [/*start*/ 0x104A, WordBreakProperty.Other], - [/*start*/ 0x1056, WordBreakProperty.Extend], - [/*start*/ 0x105A, WordBreakProperty.Other], - [/*start*/ 0x105E, WordBreakProperty.Extend], - [/*start*/ 0x1061, WordBreakProperty.Other], - [/*start*/ 0x1062, WordBreakProperty.Extend], - [/*start*/ 0x1065, WordBreakProperty.Other], - [/*start*/ 0x1067, WordBreakProperty.Extend], - [/*start*/ 0x106E, WordBreakProperty.Other], - [/*start*/ 0x1071, WordBreakProperty.Extend], - [/*start*/ 0x1075, WordBreakProperty.Other], - [/*start*/ 0x1082, WordBreakProperty.Extend], - [/*start*/ 0x108E, WordBreakProperty.Other], - [/*start*/ 0x108F, WordBreakProperty.Extend], - [/*start*/ 0x1090, WordBreakProperty.Numeric], - [/*start*/ 0x109A, WordBreakProperty.Extend], - [/*start*/ 0x109E, WordBreakProperty.Other], - [/*start*/ 0x10A0, WordBreakProperty.ALetter], - [/*start*/ 0x10C6, WordBreakProperty.Other], - [/*start*/ 0x10C7, WordBreakProperty.ALetter], - [/*start*/ 0x10C8, WordBreakProperty.Other], - [/*start*/ 0x10CD, WordBreakProperty.ALetter], - [/*start*/ 0x10CE, WordBreakProperty.Other], - [/*start*/ 0x10D0, WordBreakProperty.ALetter], - [/*start*/ 0x10FB, WordBreakProperty.Other], - [/*start*/ 0x10FC, WordBreakProperty.ALetter], - [/*start*/ 0x1249, WordBreakProperty.Other], - [/*start*/ 0x124A, WordBreakProperty.ALetter], - [/*start*/ 0x124E, WordBreakProperty.Other], - [/*start*/ 0x1250, WordBreakProperty.ALetter], - [/*start*/ 0x1257, WordBreakProperty.Other], - [/*start*/ 0x1258, WordBreakProperty.ALetter], - [/*start*/ 0x1259, WordBreakProperty.Other], - [/*start*/ 0x125A, WordBreakProperty.ALetter], - [/*start*/ 0x125E, WordBreakProperty.Other], - [/*start*/ 0x1260, WordBreakProperty.ALetter], - [/*start*/ 0x1289, WordBreakProperty.Other], - [/*start*/ 0x128A, WordBreakProperty.ALetter], - [/*start*/ 0x128E, WordBreakProperty.Other], - [/*start*/ 0x1290, WordBreakProperty.ALetter], - [/*start*/ 0x12B1, WordBreakProperty.Other], - [/*start*/ 0x12B2, WordBreakProperty.ALetter], - [/*start*/ 0x12B6, WordBreakProperty.Other], - [/*start*/ 0x12B8, WordBreakProperty.ALetter], - [/*start*/ 0x12BF, WordBreakProperty.Other], - [/*start*/ 0x12C0, WordBreakProperty.ALetter], - [/*start*/ 0x12C1, WordBreakProperty.Other], - [/*start*/ 0x12C2, WordBreakProperty.ALetter], - [/*start*/ 0x12C6, WordBreakProperty.Other], - [/*start*/ 0x12C8, WordBreakProperty.ALetter], - [/*start*/ 0x12D7, WordBreakProperty.Other], - [/*start*/ 0x12D8, WordBreakProperty.ALetter], - [/*start*/ 0x1311, WordBreakProperty.Other], - [/*start*/ 0x1312, WordBreakProperty.ALetter], - [/*start*/ 0x1316, WordBreakProperty.Other], - [/*start*/ 0x1318, WordBreakProperty.ALetter], - [/*start*/ 0x135B, WordBreakProperty.Other], - [/*start*/ 0x135D, WordBreakProperty.Extend], - [/*start*/ 0x1360, WordBreakProperty.Other], - [/*start*/ 0x1380, WordBreakProperty.ALetter], - [/*start*/ 0x1390, WordBreakProperty.Other], - [/*start*/ 0x13A0, WordBreakProperty.ALetter], - [/*start*/ 0x13F6, WordBreakProperty.Other], - [/*start*/ 0x13F8, WordBreakProperty.ALetter], - [/*start*/ 0x13FE, WordBreakProperty.Other], - [/*start*/ 0x1401, WordBreakProperty.ALetter], - [/*start*/ 0x166D, WordBreakProperty.Other], - [/*start*/ 0x166F, WordBreakProperty.ALetter], - [/*start*/ 0x1680, WordBreakProperty.WSegSpace], - [/*start*/ 0x1681, WordBreakProperty.ALetter], - [/*start*/ 0x169B, WordBreakProperty.Other], - [/*start*/ 0x16A0, WordBreakProperty.ALetter], - [/*start*/ 0x16EB, WordBreakProperty.Other], - [/*start*/ 0x16EE, WordBreakProperty.ALetter], - [/*start*/ 0x16F9, WordBreakProperty.Other], - [/*start*/ 0x1700, WordBreakProperty.ALetter], - [/*start*/ 0x170D, WordBreakProperty.Other], - [/*start*/ 0x170E, WordBreakProperty.ALetter], - [/*start*/ 0x1712, WordBreakProperty.Extend], - [/*start*/ 0x1715, WordBreakProperty.Other], - [/*start*/ 0x1720, WordBreakProperty.ALetter], - [/*start*/ 0x1732, WordBreakProperty.Extend], - [/*start*/ 0x1735, WordBreakProperty.Other], - [/*start*/ 0x1740, WordBreakProperty.ALetter], - [/*start*/ 0x1752, WordBreakProperty.Extend], - [/*start*/ 0x1754, WordBreakProperty.Other], - [/*start*/ 0x1760, WordBreakProperty.ALetter], - [/*start*/ 0x176D, WordBreakProperty.Other], - [/*start*/ 0x176E, WordBreakProperty.ALetter], - [/*start*/ 0x1771, WordBreakProperty.Other], - [/*start*/ 0x1772, WordBreakProperty.Extend], - [/*start*/ 0x1774, WordBreakProperty.Other], - [/*start*/ 0x17B4, WordBreakProperty.Extend], - [/*start*/ 0x17D4, WordBreakProperty.Other], - [/*start*/ 0x17DD, WordBreakProperty.Extend], - [/*start*/ 0x17DE, WordBreakProperty.Other], - [/*start*/ 0x17E0, WordBreakProperty.Numeric], - [/*start*/ 0x17EA, WordBreakProperty.Other], - [/*start*/ 0x180B, WordBreakProperty.Extend], - [/*start*/ 0x180E, WordBreakProperty.Format], - [/*start*/ 0x180F, WordBreakProperty.Other], - [/*start*/ 0x1810, WordBreakProperty.Numeric], - [/*start*/ 0x181A, WordBreakProperty.Other], - [/*start*/ 0x1820, WordBreakProperty.ALetter], - [/*start*/ 0x1879, WordBreakProperty.Other], - [/*start*/ 0x1880, WordBreakProperty.ALetter], - [/*start*/ 0x1885, WordBreakProperty.Extend], - [/*start*/ 0x1887, WordBreakProperty.ALetter], - [/*start*/ 0x18A9, WordBreakProperty.Extend], - [/*start*/ 0x18AA, WordBreakProperty.ALetter], - [/*start*/ 0x18AB, WordBreakProperty.Other], - [/*start*/ 0x18B0, WordBreakProperty.ALetter], - [/*start*/ 0x18F6, WordBreakProperty.Other], - [/*start*/ 0x1900, WordBreakProperty.ALetter], - [/*start*/ 0x191F, WordBreakProperty.Other], - [/*start*/ 0x1920, WordBreakProperty.Extend], - [/*start*/ 0x192C, WordBreakProperty.Other], - [/*start*/ 0x1930, WordBreakProperty.Extend], - [/*start*/ 0x193C, WordBreakProperty.Other], - [/*start*/ 0x1946, WordBreakProperty.Numeric], - [/*start*/ 0x1950, WordBreakProperty.Other], - [/*start*/ 0x19D0, WordBreakProperty.Numeric], - [/*start*/ 0x19DA, WordBreakProperty.Other], - [/*start*/ 0x1A00, WordBreakProperty.ALetter], - [/*start*/ 0x1A17, WordBreakProperty.Extend], - [/*start*/ 0x1A1C, WordBreakProperty.Other], - [/*start*/ 0x1A55, WordBreakProperty.Extend], - [/*start*/ 0x1A5F, WordBreakProperty.Other], - [/*start*/ 0x1A60, WordBreakProperty.Extend], - [/*start*/ 0x1A7D, WordBreakProperty.Other], - [/*start*/ 0x1A7F, WordBreakProperty.Extend], - [/*start*/ 0x1A80, WordBreakProperty.Numeric], - [/*start*/ 0x1A8A, WordBreakProperty.Other], - [/*start*/ 0x1A90, WordBreakProperty.Numeric], - [/*start*/ 0x1A9A, WordBreakProperty.Other], - [/*start*/ 0x1AB0, WordBreakProperty.Extend], - [/*start*/ 0x1AC1, WordBreakProperty.Other], - [/*start*/ 0x1B00, WordBreakProperty.Extend], - [/*start*/ 0x1B05, WordBreakProperty.ALetter], - [/*start*/ 0x1B34, WordBreakProperty.Extend], - [/*start*/ 0x1B45, WordBreakProperty.ALetter], - [/*start*/ 0x1B4C, WordBreakProperty.Other], - [/*start*/ 0x1B50, WordBreakProperty.Numeric], - [/*start*/ 0x1B5A, WordBreakProperty.Other], - [/*start*/ 0x1B6B, WordBreakProperty.Extend], - [/*start*/ 0x1B74, WordBreakProperty.Other], - [/*start*/ 0x1B80, WordBreakProperty.Extend], - [/*start*/ 0x1B83, WordBreakProperty.ALetter], - [/*start*/ 0x1BA1, WordBreakProperty.Extend], - [/*start*/ 0x1BAE, WordBreakProperty.ALetter], - [/*start*/ 0x1BB0, WordBreakProperty.Numeric], - [/*start*/ 0x1BBA, WordBreakProperty.ALetter], - [/*start*/ 0x1BE6, WordBreakProperty.Extend], - [/*start*/ 0x1BF4, WordBreakProperty.Other], - [/*start*/ 0x1C00, WordBreakProperty.ALetter], - [/*start*/ 0x1C24, WordBreakProperty.Extend], - [/*start*/ 0x1C38, WordBreakProperty.Other], - [/*start*/ 0x1C40, WordBreakProperty.Numeric], - [/*start*/ 0x1C4A, WordBreakProperty.Other], - [/*start*/ 0x1C4D, WordBreakProperty.ALetter], - [/*start*/ 0x1C50, WordBreakProperty.Numeric], - [/*start*/ 0x1C5A, WordBreakProperty.ALetter], - [/*start*/ 0x1C7E, WordBreakProperty.Other], - [/*start*/ 0x1C80, WordBreakProperty.ALetter], - [/*start*/ 0x1C89, WordBreakProperty.Other], - [/*start*/ 0x1C90, WordBreakProperty.ALetter], - [/*start*/ 0x1CBB, WordBreakProperty.Other], - [/*start*/ 0x1CBD, WordBreakProperty.ALetter], - [/*start*/ 0x1CC0, WordBreakProperty.Other], - [/*start*/ 0x1CD0, WordBreakProperty.Extend], - [/*start*/ 0x1CD3, WordBreakProperty.Other], - [/*start*/ 0x1CD4, WordBreakProperty.Extend], - [/*start*/ 0x1CE9, WordBreakProperty.ALetter], - [/*start*/ 0x1CED, WordBreakProperty.Extend], - [/*start*/ 0x1CEE, WordBreakProperty.ALetter], - [/*start*/ 0x1CF4, WordBreakProperty.Extend], - [/*start*/ 0x1CF5, WordBreakProperty.ALetter], - [/*start*/ 0x1CF7, WordBreakProperty.Extend], - [/*start*/ 0x1CFA, WordBreakProperty.ALetter], - [/*start*/ 0x1CFB, WordBreakProperty.Other], - [/*start*/ 0x1D00, WordBreakProperty.ALetter], - [/*start*/ 0x1DC0, WordBreakProperty.Extend], - [/*start*/ 0x1DFA, WordBreakProperty.Other], - [/*start*/ 0x1DFB, WordBreakProperty.Extend], - [/*start*/ 0x1E00, WordBreakProperty.ALetter], - [/*start*/ 0x1F16, WordBreakProperty.Other], - [/*start*/ 0x1F18, WordBreakProperty.ALetter], - [/*start*/ 0x1F1E, WordBreakProperty.Other], - [/*start*/ 0x1F20, WordBreakProperty.ALetter], - [/*start*/ 0x1F46, WordBreakProperty.Other], - [/*start*/ 0x1F48, WordBreakProperty.ALetter], - [/*start*/ 0x1F4E, WordBreakProperty.Other], - [/*start*/ 0x1F50, WordBreakProperty.ALetter], - [/*start*/ 0x1F58, WordBreakProperty.Other], - [/*start*/ 0x1F59, WordBreakProperty.ALetter], - [/*start*/ 0x1F5A, WordBreakProperty.Other], - [/*start*/ 0x1F5B, WordBreakProperty.ALetter], - [/*start*/ 0x1F5C, WordBreakProperty.Other], - [/*start*/ 0x1F5D, WordBreakProperty.ALetter], - [/*start*/ 0x1F5E, WordBreakProperty.Other], - [/*start*/ 0x1F5F, WordBreakProperty.ALetter], - [/*start*/ 0x1F7E, WordBreakProperty.Other], - [/*start*/ 0x1F80, WordBreakProperty.ALetter], - [/*start*/ 0x1FB5, WordBreakProperty.Other], - [/*start*/ 0x1FB6, WordBreakProperty.ALetter], - [/*start*/ 0x1FBD, WordBreakProperty.Other], - [/*start*/ 0x1FBE, WordBreakProperty.ALetter], - [/*start*/ 0x1FBF, WordBreakProperty.Other], - [/*start*/ 0x1FC2, WordBreakProperty.ALetter], - [/*start*/ 0x1FC5, WordBreakProperty.Other], - [/*start*/ 0x1FC6, WordBreakProperty.ALetter], - [/*start*/ 0x1FCD, WordBreakProperty.Other], - [/*start*/ 0x1FD0, WordBreakProperty.ALetter], - [/*start*/ 0x1FD4, WordBreakProperty.Other], - [/*start*/ 0x1FD6, WordBreakProperty.ALetter], - [/*start*/ 0x1FDC, WordBreakProperty.Other], - [/*start*/ 0x1FE0, WordBreakProperty.ALetter], - [/*start*/ 0x1FED, WordBreakProperty.Other], - [/*start*/ 0x1FF2, WordBreakProperty.ALetter], - [/*start*/ 0x1FF5, WordBreakProperty.Other], - [/*start*/ 0x1FF6, WordBreakProperty.ALetter], - [/*start*/ 0x1FFD, WordBreakProperty.Other], - [/*start*/ 0x2000, WordBreakProperty.WSegSpace], - [/*start*/ 0x2007, WordBreakProperty.Other], - [/*start*/ 0x2008, WordBreakProperty.WSegSpace], - [/*start*/ 0x200B, WordBreakProperty.Other], - [/*start*/ 0x200C, WordBreakProperty.Extend], - [/*start*/ 0x200D, WordBreakProperty.ZWJ], - [/*start*/ 0x200E, WordBreakProperty.Format], - [/*start*/ 0x2010, WordBreakProperty.Other], - [/*start*/ 0x2018, WordBreakProperty.MidNumLet], - [/*start*/ 0x201A, WordBreakProperty.Other], - [/*start*/ 0x2024, WordBreakProperty.MidNumLet], - [/*start*/ 0x2025, WordBreakProperty.Other], - [/*start*/ 0x2027, WordBreakProperty.MidLetter], - [/*start*/ 0x2028, WordBreakProperty.Newline], - [/*start*/ 0x202A, WordBreakProperty.Format], - [/*start*/ 0x202F, WordBreakProperty.ExtendNumLet], - [/*start*/ 0x2030, WordBreakProperty.Other], - [/*start*/ 0x203F, WordBreakProperty.ExtendNumLet], - [/*start*/ 0x2041, WordBreakProperty.Other], - [/*start*/ 0x2044, WordBreakProperty.MidNum], - [/*start*/ 0x2045, WordBreakProperty.Other], - [/*start*/ 0x2054, WordBreakProperty.ExtendNumLet], - [/*start*/ 0x2055, WordBreakProperty.Other], - [/*start*/ 0x205F, WordBreakProperty.WSegSpace], - [/*start*/ 0x2060, WordBreakProperty.Format], - [/*start*/ 0x2065, WordBreakProperty.Other], - [/*start*/ 0x2066, WordBreakProperty.Format], - [/*start*/ 0x2070, WordBreakProperty.Other], - [/*start*/ 0x2071, WordBreakProperty.ALetter], - [/*start*/ 0x2072, WordBreakProperty.Other], - [/*start*/ 0x207F, WordBreakProperty.ALetter], - [/*start*/ 0x2080, WordBreakProperty.Other], - [/*start*/ 0x2090, WordBreakProperty.ALetter], - [/*start*/ 0x209D, WordBreakProperty.Other], - [/*start*/ 0x20D0, WordBreakProperty.Extend], - [/*start*/ 0x20F1, WordBreakProperty.Other], - [/*start*/ 0x2102, WordBreakProperty.ALetter], - [/*start*/ 0x2103, WordBreakProperty.Other], - [/*start*/ 0x2107, WordBreakProperty.ALetter], - [/*start*/ 0x2108, WordBreakProperty.Other], - [/*start*/ 0x210A, WordBreakProperty.ALetter], - [/*start*/ 0x2114, WordBreakProperty.Other], - [/*start*/ 0x2115, WordBreakProperty.ALetter], - [/*start*/ 0x2116, WordBreakProperty.Other], - [/*start*/ 0x2119, WordBreakProperty.ALetter], - [/*start*/ 0x211E, WordBreakProperty.Other], - [/*start*/ 0x2124, WordBreakProperty.ALetter], - [/*start*/ 0x2125, WordBreakProperty.Other], - [/*start*/ 0x2126, WordBreakProperty.ALetter], - [/*start*/ 0x2127, WordBreakProperty.Other], - [/*start*/ 0x2128, WordBreakProperty.ALetter], - [/*start*/ 0x2129, WordBreakProperty.Other], - [/*start*/ 0x212A, WordBreakProperty.ALetter], - [/*start*/ 0x212E, WordBreakProperty.Other], - [/*start*/ 0x212F, WordBreakProperty.ALetter], - [/*start*/ 0x213A, WordBreakProperty.Other], - [/*start*/ 0x213C, WordBreakProperty.ALetter], - [/*start*/ 0x2140, WordBreakProperty.Other], - [/*start*/ 0x2145, WordBreakProperty.ALetter], - [/*start*/ 0x214A, WordBreakProperty.Other], - [/*start*/ 0x214E, WordBreakProperty.ALetter], - [/*start*/ 0x214F, WordBreakProperty.Other], - [/*start*/ 0x2160, WordBreakProperty.ALetter], - [/*start*/ 0x2189, WordBreakProperty.Other], - [/*start*/ 0x24B6, WordBreakProperty.ALetter], - [/*start*/ 0x24EA, WordBreakProperty.Other], - [/*start*/ 0x2C00, WordBreakProperty.ALetter], - [/*start*/ 0x2C2F, WordBreakProperty.Other], - [/*start*/ 0x2C30, WordBreakProperty.ALetter], - [/*start*/ 0x2C5F, WordBreakProperty.Other], - [/*start*/ 0x2C60, WordBreakProperty.ALetter], - [/*start*/ 0x2CE5, WordBreakProperty.Other], - [/*start*/ 0x2CEB, WordBreakProperty.ALetter], - [/*start*/ 0x2CEF, WordBreakProperty.Extend], - [/*start*/ 0x2CF2, WordBreakProperty.ALetter], - [/*start*/ 0x2CF4, WordBreakProperty.Other], - [/*start*/ 0x2D00, WordBreakProperty.ALetter], - [/*start*/ 0x2D26, WordBreakProperty.Other], - [/*start*/ 0x2D27, WordBreakProperty.ALetter], - [/*start*/ 0x2D28, WordBreakProperty.Other], - [/*start*/ 0x2D2D, WordBreakProperty.ALetter], - [/*start*/ 0x2D2E, WordBreakProperty.Other], - [/*start*/ 0x2D30, WordBreakProperty.ALetter], - [/*start*/ 0x2D68, WordBreakProperty.Other], - [/*start*/ 0x2D6F, WordBreakProperty.ALetter], - [/*start*/ 0x2D70, WordBreakProperty.Other], - [/*start*/ 0x2D7F, WordBreakProperty.Extend], - [/*start*/ 0x2D80, WordBreakProperty.ALetter], - [/*start*/ 0x2D97, WordBreakProperty.Other], - [/*start*/ 0x2DA0, WordBreakProperty.ALetter], - [/*start*/ 0x2DA7, WordBreakProperty.Other], - [/*start*/ 0x2DA8, WordBreakProperty.ALetter], - [/*start*/ 0x2DAF, WordBreakProperty.Other], - [/*start*/ 0x2DB0, WordBreakProperty.ALetter], - [/*start*/ 0x2DB7, WordBreakProperty.Other], - [/*start*/ 0x2DB8, WordBreakProperty.ALetter], - [/*start*/ 0x2DBF, WordBreakProperty.Other], - [/*start*/ 0x2DC0, WordBreakProperty.ALetter], - [/*start*/ 0x2DC7, WordBreakProperty.Other], - [/*start*/ 0x2DC8, WordBreakProperty.ALetter], - [/*start*/ 0x2DCF, WordBreakProperty.Other], - [/*start*/ 0x2DD0, WordBreakProperty.ALetter], - [/*start*/ 0x2DD7, WordBreakProperty.Other], - [/*start*/ 0x2DD8, WordBreakProperty.ALetter], - [/*start*/ 0x2DDF, WordBreakProperty.Other], - [/*start*/ 0x2DE0, WordBreakProperty.Extend], - [/*start*/ 0x2E00, WordBreakProperty.Other], - [/*start*/ 0x2E2F, WordBreakProperty.ALetter], - [/*start*/ 0x2E30, WordBreakProperty.Other], - [/*start*/ 0x3000, WordBreakProperty.WSegSpace], - [/*start*/ 0x3001, WordBreakProperty.Other], - [/*start*/ 0x3005, WordBreakProperty.ALetter], - [/*start*/ 0x3006, WordBreakProperty.Other], - [/*start*/ 0x302A, WordBreakProperty.Extend], - [/*start*/ 0x3030, WordBreakProperty.Other], - [/*start*/ 0x3031, WordBreakProperty.Katakana], - [/*start*/ 0x3036, WordBreakProperty.Other], - [/*start*/ 0x303B, WordBreakProperty.ALetter], - [/*start*/ 0x303D, WordBreakProperty.Other], - [/*start*/ 0x3099, WordBreakProperty.Extend], - [/*start*/ 0x309B, WordBreakProperty.Katakana], - [/*start*/ 0x309D, WordBreakProperty.Other], - [/*start*/ 0x30A0, WordBreakProperty.Katakana], - [/*start*/ 0x30FB, WordBreakProperty.Other], - [/*start*/ 0x30FC, WordBreakProperty.Katakana], - [/*start*/ 0x3100, WordBreakProperty.Other], - [/*start*/ 0x3105, WordBreakProperty.ALetter], - [/*start*/ 0x3130, WordBreakProperty.Other], - [/*start*/ 0x3131, WordBreakProperty.ALetter], - [/*start*/ 0x318F, WordBreakProperty.Other], - [/*start*/ 0x31A0, WordBreakProperty.ALetter], - [/*start*/ 0x31C0, WordBreakProperty.Other], - [/*start*/ 0x31F0, WordBreakProperty.Katakana], - [/*start*/ 0x3200, WordBreakProperty.Other], - [/*start*/ 0x32D0, WordBreakProperty.Katakana], - [/*start*/ 0x32FF, WordBreakProperty.Other], - [/*start*/ 0x3300, WordBreakProperty.Katakana], - [/*start*/ 0x3358, WordBreakProperty.Other], - [/*start*/ 0xA000, WordBreakProperty.ALetter], - [/*start*/ 0xA48D, WordBreakProperty.Other], - [/*start*/ 0xA4D0, WordBreakProperty.ALetter], - [/*start*/ 0xA4FE, WordBreakProperty.Other], - [/*start*/ 0xA500, WordBreakProperty.ALetter], - [/*start*/ 0xA60D, WordBreakProperty.Other], - [/*start*/ 0xA610, WordBreakProperty.ALetter], - [/*start*/ 0xA620, WordBreakProperty.Numeric], - [/*start*/ 0xA62A, WordBreakProperty.ALetter], - [/*start*/ 0xA62C, WordBreakProperty.Other], - [/*start*/ 0xA640, WordBreakProperty.ALetter], - [/*start*/ 0xA66F, WordBreakProperty.Extend], - [/*start*/ 0xA673, WordBreakProperty.Other], - [/*start*/ 0xA674, WordBreakProperty.Extend], - [/*start*/ 0xA67E, WordBreakProperty.Other], - [/*start*/ 0xA67F, WordBreakProperty.ALetter], - [/*start*/ 0xA69E, WordBreakProperty.Extend], - [/*start*/ 0xA6A0, WordBreakProperty.ALetter], - [/*start*/ 0xA6F0, WordBreakProperty.Extend], - [/*start*/ 0xA6F2, WordBreakProperty.Other], - [/*start*/ 0xA708, WordBreakProperty.ALetter], - [/*start*/ 0xA7C0, WordBreakProperty.Other], - [/*start*/ 0xA7C2, WordBreakProperty.ALetter], - [/*start*/ 0xA7CB, WordBreakProperty.Other], - [/*start*/ 0xA7F5, WordBreakProperty.ALetter], - [/*start*/ 0xA802, WordBreakProperty.Extend], - [/*start*/ 0xA803, WordBreakProperty.ALetter], - [/*start*/ 0xA806, WordBreakProperty.Extend], - [/*start*/ 0xA807, WordBreakProperty.ALetter], - [/*start*/ 0xA80B, WordBreakProperty.Extend], - [/*start*/ 0xA80C, WordBreakProperty.ALetter], - [/*start*/ 0xA823, WordBreakProperty.Extend], - [/*start*/ 0xA828, WordBreakProperty.Other], - [/*start*/ 0xA82C, WordBreakProperty.Extend], - [/*start*/ 0xA82D, WordBreakProperty.Other], - [/*start*/ 0xA840, WordBreakProperty.ALetter], - [/*start*/ 0xA874, WordBreakProperty.Other], - [/*start*/ 0xA880, WordBreakProperty.Extend], - [/*start*/ 0xA882, WordBreakProperty.ALetter], - [/*start*/ 0xA8B4, WordBreakProperty.Extend], - [/*start*/ 0xA8C6, WordBreakProperty.Other], - [/*start*/ 0xA8D0, WordBreakProperty.Numeric], - [/*start*/ 0xA8DA, WordBreakProperty.Other], - [/*start*/ 0xA8E0, WordBreakProperty.Extend], - [/*start*/ 0xA8F2, WordBreakProperty.ALetter], - [/*start*/ 0xA8F8, WordBreakProperty.Other], - [/*start*/ 0xA8FB, WordBreakProperty.ALetter], - [/*start*/ 0xA8FC, WordBreakProperty.Other], - [/*start*/ 0xA8FD, WordBreakProperty.ALetter], - [/*start*/ 0xA8FF, WordBreakProperty.Extend], - [/*start*/ 0xA900, WordBreakProperty.Numeric], - [/*start*/ 0xA90A, WordBreakProperty.ALetter], - [/*start*/ 0xA926, WordBreakProperty.Extend], - [/*start*/ 0xA92E, WordBreakProperty.Other], - [/*start*/ 0xA930, WordBreakProperty.ALetter], - [/*start*/ 0xA947, WordBreakProperty.Extend], - [/*start*/ 0xA954, WordBreakProperty.Other], - [/*start*/ 0xA960, WordBreakProperty.ALetter], - [/*start*/ 0xA97D, WordBreakProperty.Other], - [/*start*/ 0xA980, WordBreakProperty.Extend], - [/*start*/ 0xA984, WordBreakProperty.ALetter], - [/*start*/ 0xA9B3, WordBreakProperty.Extend], - [/*start*/ 0xA9C1, WordBreakProperty.Other], - [/*start*/ 0xA9CF, WordBreakProperty.ALetter], - [/*start*/ 0xA9D0, WordBreakProperty.Numeric], - [/*start*/ 0xA9DA, WordBreakProperty.Other], - [/*start*/ 0xA9E5, WordBreakProperty.Extend], - [/*start*/ 0xA9E6, WordBreakProperty.Other], - [/*start*/ 0xA9F0, WordBreakProperty.Numeric], - [/*start*/ 0xA9FA, WordBreakProperty.Other], - [/*start*/ 0xAA00, WordBreakProperty.ALetter], - [/*start*/ 0xAA29, WordBreakProperty.Extend], - [/*start*/ 0xAA37, WordBreakProperty.Other], - [/*start*/ 0xAA40, WordBreakProperty.ALetter], - [/*start*/ 0xAA43, WordBreakProperty.Extend], - [/*start*/ 0xAA44, WordBreakProperty.ALetter], - [/*start*/ 0xAA4C, WordBreakProperty.Extend], - [/*start*/ 0xAA4E, WordBreakProperty.Other], - [/*start*/ 0xAA50, WordBreakProperty.Numeric], - [/*start*/ 0xAA5A, WordBreakProperty.Other], - [/*start*/ 0xAA7B, WordBreakProperty.Extend], - [/*start*/ 0xAA7E, WordBreakProperty.Other], - [/*start*/ 0xAAB0, WordBreakProperty.Extend], - [/*start*/ 0xAAB1, WordBreakProperty.Other], - [/*start*/ 0xAAB2, WordBreakProperty.Extend], - [/*start*/ 0xAAB5, WordBreakProperty.Other], - [/*start*/ 0xAAB7, WordBreakProperty.Extend], - [/*start*/ 0xAAB9, WordBreakProperty.Other], - [/*start*/ 0xAABE, WordBreakProperty.Extend], - [/*start*/ 0xAAC0, WordBreakProperty.Other], - [/*start*/ 0xAAC1, WordBreakProperty.Extend], - [/*start*/ 0xAAC2, WordBreakProperty.Other], - [/*start*/ 0xAAE0, WordBreakProperty.ALetter], - [/*start*/ 0xAAEB, WordBreakProperty.Extend], - [/*start*/ 0xAAF0, WordBreakProperty.Other], - [/*start*/ 0xAAF2, WordBreakProperty.ALetter], - [/*start*/ 0xAAF5, WordBreakProperty.Extend], - [/*start*/ 0xAAF7, WordBreakProperty.Other], - [/*start*/ 0xAB01, WordBreakProperty.ALetter], - [/*start*/ 0xAB07, WordBreakProperty.Other], - [/*start*/ 0xAB09, WordBreakProperty.ALetter], - [/*start*/ 0xAB0F, WordBreakProperty.Other], - [/*start*/ 0xAB11, WordBreakProperty.ALetter], - [/*start*/ 0xAB17, WordBreakProperty.Other], - [/*start*/ 0xAB20, WordBreakProperty.ALetter], - [/*start*/ 0xAB27, WordBreakProperty.Other], - [/*start*/ 0xAB28, WordBreakProperty.ALetter], - [/*start*/ 0xAB2F, WordBreakProperty.Other], - [/*start*/ 0xAB30, WordBreakProperty.ALetter], - [/*start*/ 0xAB6A, WordBreakProperty.Other], - [/*start*/ 0xAB70, WordBreakProperty.ALetter], - [/*start*/ 0xABE3, WordBreakProperty.Extend], - [/*start*/ 0xABEB, WordBreakProperty.Other], - [/*start*/ 0xABEC, WordBreakProperty.Extend], - [/*start*/ 0xABEE, WordBreakProperty.Other], - [/*start*/ 0xABF0, WordBreakProperty.Numeric], - [/*start*/ 0xABFA, WordBreakProperty.Other], - [/*start*/ 0xAC00, WordBreakProperty.ALetter], - [/*start*/ 0xD7A4, WordBreakProperty.Other], - [/*start*/ 0xD7B0, WordBreakProperty.ALetter], - [/*start*/ 0xD7C7, WordBreakProperty.Other], - [/*start*/ 0xD7CB, WordBreakProperty.ALetter], - [/*start*/ 0xD7FC, WordBreakProperty.Other], - [/*start*/ 0xFB00, WordBreakProperty.ALetter], - [/*start*/ 0xFB07, WordBreakProperty.Other], - [/*start*/ 0xFB13, WordBreakProperty.ALetter], - [/*start*/ 0xFB18, WordBreakProperty.Other], - [/*start*/ 0xFB1D, WordBreakProperty.Hebrew_Letter], - [/*start*/ 0xFB1E, WordBreakProperty.Extend], - [/*start*/ 0xFB1F, WordBreakProperty.Hebrew_Letter], - [/*start*/ 0xFB29, WordBreakProperty.Other], - [/*start*/ 0xFB2A, WordBreakProperty.Hebrew_Letter], - [/*start*/ 0xFB37, WordBreakProperty.Other], - [/*start*/ 0xFB38, WordBreakProperty.Hebrew_Letter], - [/*start*/ 0xFB3D, WordBreakProperty.Other], - [/*start*/ 0xFB3E, WordBreakProperty.Hebrew_Letter], - [/*start*/ 0xFB3F, WordBreakProperty.Other], - [/*start*/ 0xFB40, WordBreakProperty.Hebrew_Letter], - [/*start*/ 0xFB42, WordBreakProperty.Other], - [/*start*/ 0xFB43, WordBreakProperty.Hebrew_Letter], - [/*start*/ 0xFB45, WordBreakProperty.Other], - [/*start*/ 0xFB46, WordBreakProperty.Hebrew_Letter], - [/*start*/ 0xFB50, WordBreakProperty.ALetter], - [/*start*/ 0xFBB2, WordBreakProperty.Other], - [/*start*/ 0xFBD3, WordBreakProperty.ALetter], - [/*start*/ 0xFD3E, WordBreakProperty.Other], - [/*start*/ 0xFD50, WordBreakProperty.ALetter], - [/*start*/ 0xFD90, WordBreakProperty.Other], - [/*start*/ 0xFD92, WordBreakProperty.ALetter], - [/*start*/ 0xFDC8, WordBreakProperty.Other], - [/*start*/ 0xFDF0, WordBreakProperty.ALetter], - [/*start*/ 0xFDFC, WordBreakProperty.Other], - [/*start*/ 0xFE00, WordBreakProperty.Extend], - [/*start*/ 0xFE10, WordBreakProperty.MidNum], - [/*start*/ 0xFE11, WordBreakProperty.Other], - [/*start*/ 0xFE13, WordBreakProperty.MidLetter], - [/*start*/ 0xFE14, WordBreakProperty.MidNum], - [/*start*/ 0xFE15, WordBreakProperty.Other], - [/*start*/ 0xFE20, WordBreakProperty.Extend], - [/*start*/ 0xFE30, WordBreakProperty.Other], - [/*start*/ 0xFE33, WordBreakProperty.ExtendNumLet], - [/*start*/ 0xFE35, WordBreakProperty.Other], - [/*start*/ 0xFE4D, WordBreakProperty.ExtendNumLet], - [/*start*/ 0xFE50, WordBreakProperty.MidNum], - [/*start*/ 0xFE51, WordBreakProperty.Other], - [/*start*/ 0xFE52, WordBreakProperty.MidNumLet], - [/*start*/ 0xFE53, WordBreakProperty.Other], - [/*start*/ 0xFE54, WordBreakProperty.MidNum], - [/*start*/ 0xFE55, WordBreakProperty.MidLetter], - [/*start*/ 0xFE56, WordBreakProperty.Other], - [/*start*/ 0xFE70, WordBreakProperty.ALetter], - [/*start*/ 0xFE75, WordBreakProperty.Other], - [/*start*/ 0xFE76, WordBreakProperty.ALetter], - [/*start*/ 0xFEFD, WordBreakProperty.Other], - [/*start*/ 0xFEFF, WordBreakProperty.Format], - [/*start*/ 0xFF00, WordBreakProperty.Other], - [/*start*/ 0xFF07, WordBreakProperty.MidNumLet], - [/*start*/ 0xFF08, WordBreakProperty.Other], - [/*start*/ 0xFF0C, WordBreakProperty.MidNum], - [/*start*/ 0xFF0D, WordBreakProperty.Other], - [/*start*/ 0xFF0E, WordBreakProperty.MidNumLet], - [/*start*/ 0xFF0F, WordBreakProperty.Other], - [/*start*/ 0xFF10, WordBreakProperty.Numeric], - [/*start*/ 0xFF1A, WordBreakProperty.MidLetter], - [/*start*/ 0xFF1B, WordBreakProperty.MidNum], - [/*start*/ 0xFF1C, WordBreakProperty.Other], - [/*start*/ 0xFF21, WordBreakProperty.ALetter], - [/*start*/ 0xFF3B, WordBreakProperty.Other], - [/*start*/ 0xFF3F, WordBreakProperty.ExtendNumLet], - [/*start*/ 0xFF40, WordBreakProperty.Other], - [/*start*/ 0xFF41, WordBreakProperty.ALetter], - [/*start*/ 0xFF5B, WordBreakProperty.Other], - [/*start*/ 0xFF66, WordBreakProperty.Katakana], - [/*start*/ 0xFF9E, WordBreakProperty.Extend], - [/*start*/ 0xFFA0, WordBreakProperty.ALetter], - [/*start*/ 0xFFBF, WordBreakProperty.Other], - [/*start*/ 0xFFC2, WordBreakProperty.ALetter], - [/*start*/ 0xFFC8, WordBreakProperty.Other], - [/*start*/ 0xFFCA, WordBreakProperty.ALetter], - [/*start*/ 0xFFD0, WordBreakProperty.Other], - [/*start*/ 0xFFD2, WordBreakProperty.ALetter], - [/*start*/ 0xFFD8, WordBreakProperty.Other], - [/*start*/ 0xFFDA, WordBreakProperty.ALetter], - [/*start*/ 0xFFDD, WordBreakProperty.Other], - [/*start*/ 0xFFF9, WordBreakProperty.Format], - [/*start*/ 0xFFFC, WordBreakProperty.Other], - [/*start*/ 0x10000, WordBreakProperty.ALetter], - [/*start*/ 0x1000C, WordBreakProperty.Other], - [/*start*/ 0x1000D, WordBreakProperty.ALetter], - [/*start*/ 0x10027, WordBreakProperty.Other], - [/*start*/ 0x10028, WordBreakProperty.ALetter], - [/*start*/ 0x1003B, WordBreakProperty.Other], - [/*start*/ 0x1003C, WordBreakProperty.ALetter], - [/*start*/ 0x1003E, WordBreakProperty.Other], - [/*start*/ 0x1003F, WordBreakProperty.ALetter], - [/*start*/ 0x1004E, WordBreakProperty.Other], - [/*start*/ 0x10050, WordBreakProperty.ALetter], - [/*start*/ 0x1005E, WordBreakProperty.Other], - [/*start*/ 0x10080, WordBreakProperty.ALetter], - [/*start*/ 0x100FB, WordBreakProperty.Other], - [/*start*/ 0x10140, WordBreakProperty.ALetter], - [/*start*/ 0x10175, WordBreakProperty.Other], - [/*start*/ 0x101FD, WordBreakProperty.Extend], - [/*start*/ 0x101FE, WordBreakProperty.Other], - [/*start*/ 0x10280, WordBreakProperty.ALetter], - [/*start*/ 0x1029D, WordBreakProperty.Other], - [/*start*/ 0x102A0, WordBreakProperty.ALetter], - [/*start*/ 0x102D1, WordBreakProperty.Other], - [/*start*/ 0x102E0, WordBreakProperty.Extend], - [/*start*/ 0x102E1, WordBreakProperty.Other], - [/*start*/ 0x10300, WordBreakProperty.ALetter], - [/*start*/ 0x10320, WordBreakProperty.Other], - [/*start*/ 0x1032D, WordBreakProperty.ALetter], - [/*start*/ 0x1034B, WordBreakProperty.Other], - [/*start*/ 0x10350, WordBreakProperty.ALetter], - [/*start*/ 0x10376, WordBreakProperty.Extend], - [/*start*/ 0x1037B, WordBreakProperty.Other], - [/*start*/ 0x10380, WordBreakProperty.ALetter], - [/*start*/ 0x1039E, WordBreakProperty.Other], - [/*start*/ 0x103A0, WordBreakProperty.ALetter], - [/*start*/ 0x103C4, WordBreakProperty.Other], - [/*start*/ 0x103C8, WordBreakProperty.ALetter], - [/*start*/ 0x103D0, WordBreakProperty.Other], - [/*start*/ 0x103D1, WordBreakProperty.ALetter], - [/*start*/ 0x103D6, WordBreakProperty.Other], - [/*start*/ 0x10400, WordBreakProperty.ALetter], - [/*start*/ 0x1049E, WordBreakProperty.Other], - [/*start*/ 0x104A0, WordBreakProperty.Numeric], - [/*start*/ 0x104AA, WordBreakProperty.Other], - [/*start*/ 0x104B0, WordBreakProperty.ALetter], - [/*start*/ 0x104D4, WordBreakProperty.Other], - [/*start*/ 0x104D8, WordBreakProperty.ALetter], - [/*start*/ 0x104FC, WordBreakProperty.Other], - [/*start*/ 0x10500, WordBreakProperty.ALetter], - [/*start*/ 0x10528, WordBreakProperty.Other], - [/*start*/ 0x10530, WordBreakProperty.ALetter], - [/*start*/ 0x10564, WordBreakProperty.Other], - [/*start*/ 0x10600, WordBreakProperty.ALetter], - [/*start*/ 0x10737, WordBreakProperty.Other], - [/*start*/ 0x10740, WordBreakProperty.ALetter], - [/*start*/ 0x10756, WordBreakProperty.Other], - [/*start*/ 0x10760, WordBreakProperty.ALetter], - [/*start*/ 0x10768, WordBreakProperty.Other], - [/*start*/ 0x10800, WordBreakProperty.ALetter], - [/*start*/ 0x10806, WordBreakProperty.Other], - [/*start*/ 0x10808, WordBreakProperty.ALetter], - [/*start*/ 0x10809, WordBreakProperty.Other], - [/*start*/ 0x1080A, WordBreakProperty.ALetter], - [/*start*/ 0x10836, WordBreakProperty.Other], - [/*start*/ 0x10837, WordBreakProperty.ALetter], - [/*start*/ 0x10839, WordBreakProperty.Other], - [/*start*/ 0x1083C, WordBreakProperty.ALetter], - [/*start*/ 0x1083D, WordBreakProperty.Other], - [/*start*/ 0x1083F, WordBreakProperty.ALetter], - [/*start*/ 0x10856, WordBreakProperty.Other], - [/*start*/ 0x10860, WordBreakProperty.ALetter], - [/*start*/ 0x10877, WordBreakProperty.Other], - [/*start*/ 0x10880, WordBreakProperty.ALetter], - [/*start*/ 0x1089F, WordBreakProperty.Other], - [/*start*/ 0x108E0, WordBreakProperty.ALetter], - [/*start*/ 0x108F3, WordBreakProperty.Other], - [/*start*/ 0x108F4, WordBreakProperty.ALetter], - [/*start*/ 0x108F6, WordBreakProperty.Other], - [/*start*/ 0x10900, WordBreakProperty.ALetter], - [/*start*/ 0x10916, WordBreakProperty.Other], - [/*start*/ 0x10920, WordBreakProperty.ALetter], - [/*start*/ 0x1093A, WordBreakProperty.Other], - [/*start*/ 0x10980, WordBreakProperty.ALetter], - [/*start*/ 0x109B8, WordBreakProperty.Other], - [/*start*/ 0x109BE, WordBreakProperty.ALetter], - [/*start*/ 0x109C0, WordBreakProperty.Other], - [/*start*/ 0x10A00, WordBreakProperty.ALetter], - [/*start*/ 0x10A01, WordBreakProperty.Extend], - [/*start*/ 0x10A04, WordBreakProperty.Other], - [/*start*/ 0x10A05, WordBreakProperty.Extend], - [/*start*/ 0x10A07, WordBreakProperty.Other], - [/*start*/ 0x10A0C, WordBreakProperty.Extend], - [/*start*/ 0x10A10, WordBreakProperty.ALetter], - [/*start*/ 0x10A14, WordBreakProperty.Other], - [/*start*/ 0x10A15, WordBreakProperty.ALetter], - [/*start*/ 0x10A18, WordBreakProperty.Other], - [/*start*/ 0x10A19, WordBreakProperty.ALetter], - [/*start*/ 0x10A36, WordBreakProperty.Other], - [/*start*/ 0x10A38, WordBreakProperty.Extend], - [/*start*/ 0x10A3B, WordBreakProperty.Other], - [/*start*/ 0x10A3F, WordBreakProperty.Extend], - [/*start*/ 0x10A40, WordBreakProperty.Other], - [/*start*/ 0x10A60, WordBreakProperty.ALetter], - [/*start*/ 0x10A7D, WordBreakProperty.Other], - [/*start*/ 0x10A80, WordBreakProperty.ALetter], - [/*start*/ 0x10A9D, WordBreakProperty.Other], - [/*start*/ 0x10AC0, WordBreakProperty.ALetter], - [/*start*/ 0x10AC8, WordBreakProperty.Other], - [/*start*/ 0x10AC9, WordBreakProperty.ALetter], - [/*start*/ 0x10AE5, WordBreakProperty.Extend], - [/*start*/ 0x10AE7, WordBreakProperty.Other], - [/*start*/ 0x10B00, WordBreakProperty.ALetter], - [/*start*/ 0x10B36, WordBreakProperty.Other], - [/*start*/ 0x10B40, WordBreakProperty.ALetter], - [/*start*/ 0x10B56, WordBreakProperty.Other], - [/*start*/ 0x10B60, WordBreakProperty.ALetter], - [/*start*/ 0x10B73, WordBreakProperty.Other], - [/*start*/ 0x10B80, WordBreakProperty.ALetter], - [/*start*/ 0x10B92, WordBreakProperty.Other], - [/*start*/ 0x10C00, WordBreakProperty.ALetter], - [/*start*/ 0x10C49, WordBreakProperty.Other], - [/*start*/ 0x10C80, WordBreakProperty.ALetter], - [/*start*/ 0x10CB3, WordBreakProperty.Other], - [/*start*/ 0x10CC0, WordBreakProperty.ALetter], - [/*start*/ 0x10CF3, WordBreakProperty.Other], - [/*start*/ 0x10D00, WordBreakProperty.ALetter], - [/*start*/ 0x10D24, WordBreakProperty.Extend], - [/*start*/ 0x10D28, WordBreakProperty.Other], - [/*start*/ 0x10D30, WordBreakProperty.Numeric], - [/*start*/ 0x10D3A, WordBreakProperty.Other], - [/*start*/ 0x10E80, WordBreakProperty.ALetter], - [/*start*/ 0x10EAA, WordBreakProperty.Other], - [/*start*/ 0x10EAB, WordBreakProperty.Extend], - [/*start*/ 0x10EAD, WordBreakProperty.Other], - [/*start*/ 0x10EB0, WordBreakProperty.ALetter], - [/*start*/ 0x10EB2, WordBreakProperty.Other], - [/*start*/ 0x10F00, WordBreakProperty.ALetter], - [/*start*/ 0x10F1D, WordBreakProperty.Other], - [/*start*/ 0x10F27, WordBreakProperty.ALetter], - [/*start*/ 0x10F28, WordBreakProperty.Other], - [/*start*/ 0x10F30, WordBreakProperty.ALetter], - [/*start*/ 0x10F46, WordBreakProperty.Extend], - [/*start*/ 0x10F51, WordBreakProperty.Other], - [/*start*/ 0x10FB0, WordBreakProperty.ALetter], - [/*start*/ 0x10FC5, WordBreakProperty.Other], - [/*start*/ 0x10FE0, WordBreakProperty.ALetter], - [/*start*/ 0x10FF7, WordBreakProperty.Other], - [/*start*/ 0x11000, WordBreakProperty.Extend], - [/*start*/ 0x11003, WordBreakProperty.ALetter], - [/*start*/ 0x11038, WordBreakProperty.Extend], - [/*start*/ 0x11047, WordBreakProperty.Other], - [/*start*/ 0x11066, WordBreakProperty.Numeric], - [/*start*/ 0x11070, WordBreakProperty.Other], - [/*start*/ 0x1107F, WordBreakProperty.Extend], - [/*start*/ 0x11083, WordBreakProperty.ALetter], - [/*start*/ 0x110B0, WordBreakProperty.Extend], - [/*start*/ 0x110BB, WordBreakProperty.Other], - [/*start*/ 0x110BD, WordBreakProperty.Format], - [/*start*/ 0x110BE, WordBreakProperty.Other], - [/*start*/ 0x110CD, WordBreakProperty.Format], - [/*start*/ 0x110CE, WordBreakProperty.Other], - [/*start*/ 0x110D0, WordBreakProperty.ALetter], - [/*start*/ 0x110E9, WordBreakProperty.Other], - [/*start*/ 0x110F0, WordBreakProperty.Numeric], - [/*start*/ 0x110FA, WordBreakProperty.Other], - [/*start*/ 0x11100, WordBreakProperty.Extend], - [/*start*/ 0x11103, WordBreakProperty.ALetter], - [/*start*/ 0x11127, WordBreakProperty.Extend], - [/*start*/ 0x11135, WordBreakProperty.Other], - [/*start*/ 0x11136, WordBreakProperty.Numeric], - [/*start*/ 0x11140, WordBreakProperty.Other], - [/*start*/ 0x11144, WordBreakProperty.ALetter], - [/*start*/ 0x11145, WordBreakProperty.Extend], - [/*start*/ 0x11147, WordBreakProperty.ALetter], - [/*start*/ 0x11148, WordBreakProperty.Other], - [/*start*/ 0x11150, WordBreakProperty.ALetter], - [/*start*/ 0x11173, WordBreakProperty.Extend], - [/*start*/ 0x11174, WordBreakProperty.Other], - [/*start*/ 0x11176, WordBreakProperty.ALetter], - [/*start*/ 0x11177, WordBreakProperty.Other], - [/*start*/ 0x11180, WordBreakProperty.Extend], - [/*start*/ 0x11183, WordBreakProperty.ALetter], - [/*start*/ 0x111B3, WordBreakProperty.Extend], - [/*start*/ 0x111C1, WordBreakProperty.ALetter], - [/*start*/ 0x111C5, WordBreakProperty.Other], - [/*start*/ 0x111C9, WordBreakProperty.Extend], - [/*start*/ 0x111CD, WordBreakProperty.Other], - [/*start*/ 0x111CE, WordBreakProperty.Extend], - [/*start*/ 0x111D0, WordBreakProperty.Numeric], - [/*start*/ 0x111DA, WordBreakProperty.ALetter], - [/*start*/ 0x111DB, WordBreakProperty.Other], - [/*start*/ 0x111DC, WordBreakProperty.ALetter], - [/*start*/ 0x111DD, WordBreakProperty.Other], - [/*start*/ 0x11200, WordBreakProperty.ALetter], - [/*start*/ 0x11212, WordBreakProperty.Other], - [/*start*/ 0x11213, WordBreakProperty.ALetter], - [/*start*/ 0x1122C, WordBreakProperty.Extend], - [/*start*/ 0x11238, WordBreakProperty.Other], - [/*start*/ 0x1123E, WordBreakProperty.Extend], - [/*start*/ 0x1123F, WordBreakProperty.Other], - [/*start*/ 0x11280, WordBreakProperty.ALetter], - [/*start*/ 0x11287, WordBreakProperty.Other], - [/*start*/ 0x11288, WordBreakProperty.ALetter], - [/*start*/ 0x11289, WordBreakProperty.Other], - [/*start*/ 0x1128A, WordBreakProperty.ALetter], - [/*start*/ 0x1128E, WordBreakProperty.Other], - [/*start*/ 0x1128F, WordBreakProperty.ALetter], - [/*start*/ 0x1129E, WordBreakProperty.Other], - [/*start*/ 0x1129F, WordBreakProperty.ALetter], - [/*start*/ 0x112A9, WordBreakProperty.Other], - [/*start*/ 0x112B0, WordBreakProperty.ALetter], - [/*start*/ 0x112DF, WordBreakProperty.Extend], - [/*start*/ 0x112EB, WordBreakProperty.Other], - [/*start*/ 0x112F0, WordBreakProperty.Numeric], - [/*start*/ 0x112FA, WordBreakProperty.Other], - [/*start*/ 0x11300, WordBreakProperty.Extend], - [/*start*/ 0x11304, WordBreakProperty.Other], - [/*start*/ 0x11305, WordBreakProperty.ALetter], - [/*start*/ 0x1130D, WordBreakProperty.Other], - [/*start*/ 0x1130F, WordBreakProperty.ALetter], - [/*start*/ 0x11311, WordBreakProperty.Other], - [/*start*/ 0x11313, WordBreakProperty.ALetter], - [/*start*/ 0x11329, WordBreakProperty.Other], - [/*start*/ 0x1132A, WordBreakProperty.ALetter], - [/*start*/ 0x11331, WordBreakProperty.Other], - [/*start*/ 0x11332, WordBreakProperty.ALetter], - [/*start*/ 0x11334, WordBreakProperty.Other], - [/*start*/ 0x11335, WordBreakProperty.ALetter], - [/*start*/ 0x1133A, WordBreakProperty.Other], - [/*start*/ 0x1133B, WordBreakProperty.Extend], - [/*start*/ 0x1133D, WordBreakProperty.ALetter], - [/*start*/ 0x1133E, WordBreakProperty.Extend], - [/*start*/ 0x11345, WordBreakProperty.Other], - [/*start*/ 0x11347, WordBreakProperty.Extend], - [/*start*/ 0x11349, WordBreakProperty.Other], - [/*start*/ 0x1134B, WordBreakProperty.Extend], - [/*start*/ 0x1134E, WordBreakProperty.Other], - [/*start*/ 0x11350, WordBreakProperty.ALetter], - [/*start*/ 0x11351, WordBreakProperty.Other], - [/*start*/ 0x11357, WordBreakProperty.Extend], - [/*start*/ 0x11358, WordBreakProperty.Other], - [/*start*/ 0x1135D, WordBreakProperty.ALetter], - [/*start*/ 0x11362, WordBreakProperty.Extend], - [/*start*/ 0x11364, WordBreakProperty.Other], - [/*start*/ 0x11366, WordBreakProperty.Extend], - [/*start*/ 0x1136D, WordBreakProperty.Other], - [/*start*/ 0x11370, WordBreakProperty.Extend], - [/*start*/ 0x11375, WordBreakProperty.Other], - [/*start*/ 0x11400, WordBreakProperty.ALetter], - [/*start*/ 0x11435, WordBreakProperty.Extend], - [/*start*/ 0x11447, WordBreakProperty.ALetter], - [/*start*/ 0x1144B, WordBreakProperty.Other], - [/*start*/ 0x11450, WordBreakProperty.Numeric], - [/*start*/ 0x1145A, WordBreakProperty.Other], - [/*start*/ 0x1145E, WordBreakProperty.Extend], - [/*start*/ 0x1145F, WordBreakProperty.ALetter], - [/*start*/ 0x11462, WordBreakProperty.Other], - [/*start*/ 0x11480, WordBreakProperty.ALetter], - [/*start*/ 0x114B0, WordBreakProperty.Extend], - [/*start*/ 0x114C4, WordBreakProperty.ALetter], - [/*start*/ 0x114C6, WordBreakProperty.Other], - [/*start*/ 0x114C7, WordBreakProperty.ALetter], - [/*start*/ 0x114C8, WordBreakProperty.Other], - [/*start*/ 0x114D0, WordBreakProperty.Numeric], - [/*start*/ 0x114DA, WordBreakProperty.Other], - [/*start*/ 0x11580, WordBreakProperty.ALetter], - [/*start*/ 0x115AF, WordBreakProperty.Extend], - [/*start*/ 0x115B6, WordBreakProperty.Other], - [/*start*/ 0x115B8, WordBreakProperty.Extend], - [/*start*/ 0x115C1, WordBreakProperty.Other], - [/*start*/ 0x115D8, WordBreakProperty.ALetter], - [/*start*/ 0x115DC, WordBreakProperty.Extend], - [/*start*/ 0x115DE, WordBreakProperty.Other], - [/*start*/ 0x11600, WordBreakProperty.ALetter], - [/*start*/ 0x11630, WordBreakProperty.Extend], - [/*start*/ 0x11641, WordBreakProperty.Other], - [/*start*/ 0x11644, WordBreakProperty.ALetter], - [/*start*/ 0x11645, WordBreakProperty.Other], - [/*start*/ 0x11650, WordBreakProperty.Numeric], - [/*start*/ 0x1165A, WordBreakProperty.Other], - [/*start*/ 0x11680, WordBreakProperty.ALetter], - [/*start*/ 0x116AB, WordBreakProperty.Extend], - [/*start*/ 0x116B8, WordBreakProperty.ALetter], - [/*start*/ 0x116B9, WordBreakProperty.Other], - [/*start*/ 0x116C0, WordBreakProperty.Numeric], - [/*start*/ 0x116CA, WordBreakProperty.Other], - [/*start*/ 0x1171D, WordBreakProperty.Extend], - [/*start*/ 0x1172C, WordBreakProperty.Other], - [/*start*/ 0x11730, WordBreakProperty.Numeric], - [/*start*/ 0x1173A, WordBreakProperty.Other], - [/*start*/ 0x11800, WordBreakProperty.ALetter], - [/*start*/ 0x1182C, WordBreakProperty.Extend], - [/*start*/ 0x1183B, WordBreakProperty.Other], - [/*start*/ 0x118A0, WordBreakProperty.ALetter], - [/*start*/ 0x118E0, WordBreakProperty.Numeric], - [/*start*/ 0x118EA, WordBreakProperty.Other], - [/*start*/ 0x118FF, WordBreakProperty.ALetter], - [/*start*/ 0x11907, WordBreakProperty.Other], - [/*start*/ 0x11909, WordBreakProperty.ALetter], - [/*start*/ 0x1190A, WordBreakProperty.Other], - [/*start*/ 0x1190C, WordBreakProperty.ALetter], - [/*start*/ 0x11914, WordBreakProperty.Other], - [/*start*/ 0x11915, WordBreakProperty.ALetter], - [/*start*/ 0x11917, WordBreakProperty.Other], - [/*start*/ 0x11918, WordBreakProperty.ALetter], - [/*start*/ 0x11930, WordBreakProperty.Extend], - [/*start*/ 0x11936, WordBreakProperty.Other], - [/*start*/ 0x11937, WordBreakProperty.Extend], - [/*start*/ 0x11939, WordBreakProperty.Other], - [/*start*/ 0x1193B, WordBreakProperty.Extend], - [/*start*/ 0x1193F, WordBreakProperty.ALetter], - [/*start*/ 0x11940, WordBreakProperty.Extend], - [/*start*/ 0x11941, WordBreakProperty.ALetter], - [/*start*/ 0x11942, WordBreakProperty.Extend], - [/*start*/ 0x11944, WordBreakProperty.Other], - [/*start*/ 0x11950, WordBreakProperty.Numeric], - [/*start*/ 0x1195A, WordBreakProperty.Other], - [/*start*/ 0x119A0, WordBreakProperty.ALetter], - [/*start*/ 0x119A8, WordBreakProperty.Other], - [/*start*/ 0x119AA, WordBreakProperty.ALetter], - [/*start*/ 0x119D1, WordBreakProperty.Extend], - [/*start*/ 0x119D8, WordBreakProperty.Other], - [/*start*/ 0x119DA, WordBreakProperty.Extend], - [/*start*/ 0x119E1, WordBreakProperty.ALetter], - [/*start*/ 0x119E2, WordBreakProperty.Other], - [/*start*/ 0x119E3, WordBreakProperty.ALetter], - [/*start*/ 0x119E4, WordBreakProperty.Extend], - [/*start*/ 0x119E5, WordBreakProperty.Other], - [/*start*/ 0x11A00, WordBreakProperty.ALetter], - [/*start*/ 0x11A01, WordBreakProperty.Extend], - [/*start*/ 0x11A0B, WordBreakProperty.ALetter], - [/*start*/ 0x11A33, WordBreakProperty.Extend], - [/*start*/ 0x11A3A, WordBreakProperty.ALetter], - [/*start*/ 0x11A3B, WordBreakProperty.Extend], - [/*start*/ 0x11A3F, WordBreakProperty.Other], - [/*start*/ 0x11A47, WordBreakProperty.Extend], - [/*start*/ 0x11A48, WordBreakProperty.Other], - [/*start*/ 0x11A50, WordBreakProperty.ALetter], - [/*start*/ 0x11A51, WordBreakProperty.Extend], - [/*start*/ 0x11A5C, WordBreakProperty.ALetter], - [/*start*/ 0x11A8A, WordBreakProperty.Extend], - [/*start*/ 0x11A9A, WordBreakProperty.Other], - [/*start*/ 0x11A9D, WordBreakProperty.ALetter], - [/*start*/ 0x11A9E, WordBreakProperty.Other], - [/*start*/ 0x11AC0, WordBreakProperty.ALetter], - [/*start*/ 0x11AF9, WordBreakProperty.Other], - [/*start*/ 0x11C00, WordBreakProperty.ALetter], - [/*start*/ 0x11C09, WordBreakProperty.Other], - [/*start*/ 0x11C0A, WordBreakProperty.ALetter], - [/*start*/ 0x11C2F, WordBreakProperty.Extend], - [/*start*/ 0x11C37, WordBreakProperty.Other], - [/*start*/ 0x11C38, WordBreakProperty.Extend], - [/*start*/ 0x11C40, WordBreakProperty.ALetter], - [/*start*/ 0x11C41, WordBreakProperty.Other], - [/*start*/ 0x11C50, WordBreakProperty.Numeric], - [/*start*/ 0x11C5A, WordBreakProperty.Other], - [/*start*/ 0x11C72, WordBreakProperty.ALetter], - [/*start*/ 0x11C90, WordBreakProperty.Other], - [/*start*/ 0x11C92, WordBreakProperty.Extend], - [/*start*/ 0x11CA8, WordBreakProperty.Other], - [/*start*/ 0x11CA9, WordBreakProperty.Extend], - [/*start*/ 0x11CB7, WordBreakProperty.Other], - [/*start*/ 0x11D00, WordBreakProperty.ALetter], - [/*start*/ 0x11D07, WordBreakProperty.Other], - [/*start*/ 0x11D08, WordBreakProperty.ALetter], - [/*start*/ 0x11D0A, WordBreakProperty.Other], - [/*start*/ 0x11D0B, WordBreakProperty.ALetter], - [/*start*/ 0x11D31, WordBreakProperty.Extend], - [/*start*/ 0x11D37, WordBreakProperty.Other], - [/*start*/ 0x11D3A, WordBreakProperty.Extend], - [/*start*/ 0x11D3B, WordBreakProperty.Other], - [/*start*/ 0x11D3C, WordBreakProperty.Extend], - [/*start*/ 0x11D3E, WordBreakProperty.Other], - [/*start*/ 0x11D3F, WordBreakProperty.Extend], - [/*start*/ 0x11D46, WordBreakProperty.ALetter], - [/*start*/ 0x11D47, WordBreakProperty.Extend], - [/*start*/ 0x11D48, WordBreakProperty.Other], - [/*start*/ 0x11D50, WordBreakProperty.Numeric], - [/*start*/ 0x11D5A, WordBreakProperty.Other], - [/*start*/ 0x11D60, WordBreakProperty.ALetter], - [/*start*/ 0x11D66, WordBreakProperty.Other], - [/*start*/ 0x11D67, WordBreakProperty.ALetter], - [/*start*/ 0x11D69, WordBreakProperty.Other], - [/*start*/ 0x11D6A, WordBreakProperty.ALetter], - [/*start*/ 0x11D8A, WordBreakProperty.Extend], - [/*start*/ 0x11D8F, WordBreakProperty.Other], - [/*start*/ 0x11D90, WordBreakProperty.Extend], - [/*start*/ 0x11D92, WordBreakProperty.Other], - [/*start*/ 0x11D93, WordBreakProperty.Extend], - [/*start*/ 0x11D98, WordBreakProperty.ALetter], - [/*start*/ 0x11D99, WordBreakProperty.Other], - [/*start*/ 0x11DA0, WordBreakProperty.Numeric], - [/*start*/ 0x11DAA, WordBreakProperty.Other], - [/*start*/ 0x11EE0, WordBreakProperty.ALetter], - [/*start*/ 0x11EF3, WordBreakProperty.Extend], - [/*start*/ 0x11EF7, WordBreakProperty.Other], - [/*start*/ 0x11FB0, WordBreakProperty.ALetter], - [/*start*/ 0x11FB1, WordBreakProperty.Other], - [/*start*/ 0x12000, WordBreakProperty.ALetter], - [/*start*/ 0x1239A, WordBreakProperty.Other], - [/*start*/ 0x12400, WordBreakProperty.ALetter], - [/*start*/ 0x1246F, WordBreakProperty.Other], - [/*start*/ 0x12480, WordBreakProperty.ALetter], - [/*start*/ 0x12544, WordBreakProperty.Other], - [/*start*/ 0x13000, WordBreakProperty.ALetter], - [/*start*/ 0x1342F, WordBreakProperty.Other], - [/*start*/ 0x13430, WordBreakProperty.Format], - [/*start*/ 0x13439, WordBreakProperty.Other], - [/*start*/ 0x14400, WordBreakProperty.ALetter], - [/*start*/ 0x14647, WordBreakProperty.Other], - [/*start*/ 0x16800, WordBreakProperty.ALetter], - [/*start*/ 0x16A39, WordBreakProperty.Other], - [/*start*/ 0x16A40, WordBreakProperty.ALetter], - [/*start*/ 0x16A5F, WordBreakProperty.Other], - [/*start*/ 0x16A60, WordBreakProperty.Numeric], - [/*start*/ 0x16A6A, WordBreakProperty.Other], - [/*start*/ 0x16AD0, WordBreakProperty.ALetter], - [/*start*/ 0x16AEE, WordBreakProperty.Other], - [/*start*/ 0x16AF0, WordBreakProperty.Extend], - [/*start*/ 0x16AF5, WordBreakProperty.Other], - [/*start*/ 0x16B00, WordBreakProperty.ALetter], - [/*start*/ 0x16B30, WordBreakProperty.Extend], - [/*start*/ 0x16B37, WordBreakProperty.Other], - [/*start*/ 0x16B40, WordBreakProperty.ALetter], - [/*start*/ 0x16B44, WordBreakProperty.Other], - [/*start*/ 0x16B50, WordBreakProperty.Numeric], - [/*start*/ 0x16B5A, WordBreakProperty.Other], - [/*start*/ 0x16B63, WordBreakProperty.ALetter], - [/*start*/ 0x16B78, WordBreakProperty.Other], - [/*start*/ 0x16B7D, WordBreakProperty.ALetter], - [/*start*/ 0x16B90, WordBreakProperty.Other], - [/*start*/ 0x16E40, WordBreakProperty.ALetter], - [/*start*/ 0x16E80, WordBreakProperty.Other], - [/*start*/ 0x16F00, WordBreakProperty.ALetter], - [/*start*/ 0x16F4B, WordBreakProperty.Other], - [/*start*/ 0x16F4F, WordBreakProperty.Extend], - [/*start*/ 0x16F50, WordBreakProperty.ALetter], - [/*start*/ 0x16F51, WordBreakProperty.Extend], - [/*start*/ 0x16F88, WordBreakProperty.Other], - [/*start*/ 0x16F8F, WordBreakProperty.Extend], - [/*start*/ 0x16F93, WordBreakProperty.ALetter], - [/*start*/ 0x16FA0, WordBreakProperty.Other], - [/*start*/ 0x16FE0, WordBreakProperty.ALetter], - [/*start*/ 0x16FE2, WordBreakProperty.Other], - [/*start*/ 0x16FE3, WordBreakProperty.ALetter], - [/*start*/ 0x16FE4, WordBreakProperty.Extend], - [/*start*/ 0x16FE5, WordBreakProperty.Other], - [/*start*/ 0x16FF0, WordBreakProperty.Extend], - [/*start*/ 0x16FF2, WordBreakProperty.Other], - [/*start*/ 0x1B000, WordBreakProperty.Katakana], - [/*start*/ 0x1B001, WordBreakProperty.Other], - [/*start*/ 0x1B164, WordBreakProperty.Katakana], - [/*start*/ 0x1B168, WordBreakProperty.Other], - [/*start*/ 0x1BC00, WordBreakProperty.ALetter], - [/*start*/ 0x1BC6B, WordBreakProperty.Other], - [/*start*/ 0x1BC70, WordBreakProperty.ALetter], - [/*start*/ 0x1BC7D, WordBreakProperty.Other], - [/*start*/ 0x1BC80, WordBreakProperty.ALetter], - [/*start*/ 0x1BC89, WordBreakProperty.Other], - [/*start*/ 0x1BC90, WordBreakProperty.ALetter], - [/*start*/ 0x1BC9A, WordBreakProperty.Other], - [/*start*/ 0x1BC9D, WordBreakProperty.Extend], - [/*start*/ 0x1BC9F, WordBreakProperty.Other], - [/*start*/ 0x1BCA0, WordBreakProperty.Format], - [/*start*/ 0x1BCA4, WordBreakProperty.Other], - [/*start*/ 0x1D165, WordBreakProperty.Extend], - [/*start*/ 0x1D16A, WordBreakProperty.Other], - [/*start*/ 0x1D16D, WordBreakProperty.Extend], - [/*start*/ 0x1D173, WordBreakProperty.Format], - [/*start*/ 0x1D17B, WordBreakProperty.Extend], - [/*start*/ 0x1D183, WordBreakProperty.Other], - [/*start*/ 0x1D185, WordBreakProperty.Extend], - [/*start*/ 0x1D18C, WordBreakProperty.Other], - [/*start*/ 0x1D1AA, WordBreakProperty.Extend], - [/*start*/ 0x1D1AE, WordBreakProperty.Other], - [/*start*/ 0x1D242, WordBreakProperty.Extend], - [/*start*/ 0x1D245, WordBreakProperty.Other], - [/*start*/ 0x1D400, WordBreakProperty.ALetter], - [/*start*/ 0x1D455, WordBreakProperty.Other], - [/*start*/ 0x1D456, WordBreakProperty.ALetter], - [/*start*/ 0x1D49D, WordBreakProperty.Other], - [/*start*/ 0x1D49E, WordBreakProperty.ALetter], - [/*start*/ 0x1D4A0, WordBreakProperty.Other], - [/*start*/ 0x1D4A2, WordBreakProperty.ALetter], - [/*start*/ 0x1D4A3, WordBreakProperty.Other], - [/*start*/ 0x1D4A5, WordBreakProperty.ALetter], - [/*start*/ 0x1D4A7, WordBreakProperty.Other], - [/*start*/ 0x1D4A9, WordBreakProperty.ALetter], - [/*start*/ 0x1D4AD, WordBreakProperty.Other], - [/*start*/ 0x1D4AE, WordBreakProperty.ALetter], - [/*start*/ 0x1D4BA, WordBreakProperty.Other], - [/*start*/ 0x1D4BB, WordBreakProperty.ALetter], - [/*start*/ 0x1D4BC, WordBreakProperty.Other], - [/*start*/ 0x1D4BD, WordBreakProperty.ALetter], - [/*start*/ 0x1D4C4, WordBreakProperty.Other], - [/*start*/ 0x1D4C5, WordBreakProperty.ALetter], - [/*start*/ 0x1D506, WordBreakProperty.Other], - [/*start*/ 0x1D507, WordBreakProperty.ALetter], - [/*start*/ 0x1D50B, WordBreakProperty.Other], - [/*start*/ 0x1D50D, WordBreakProperty.ALetter], - [/*start*/ 0x1D515, WordBreakProperty.Other], - [/*start*/ 0x1D516, WordBreakProperty.ALetter], - [/*start*/ 0x1D51D, WordBreakProperty.Other], - [/*start*/ 0x1D51E, WordBreakProperty.ALetter], - [/*start*/ 0x1D53A, WordBreakProperty.Other], - [/*start*/ 0x1D53B, WordBreakProperty.ALetter], - [/*start*/ 0x1D53F, WordBreakProperty.Other], - [/*start*/ 0x1D540, WordBreakProperty.ALetter], - [/*start*/ 0x1D545, WordBreakProperty.Other], - [/*start*/ 0x1D546, WordBreakProperty.ALetter], - [/*start*/ 0x1D547, WordBreakProperty.Other], - [/*start*/ 0x1D54A, WordBreakProperty.ALetter], - [/*start*/ 0x1D551, WordBreakProperty.Other], - [/*start*/ 0x1D552, WordBreakProperty.ALetter], - [/*start*/ 0x1D6A6, WordBreakProperty.Other], - [/*start*/ 0x1D6A8, WordBreakProperty.ALetter], - [/*start*/ 0x1D6C1, WordBreakProperty.Other], - [/*start*/ 0x1D6C2, WordBreakProperty.ALetter], - [/*start*/ 0x1D6DB, WordBreakProperty.Other], - [/*start*/ 0x1D6DC, WordBreakProperty.ALetter], - [/*start*/ 0x1D6FB, WordBreakProperty.Other], - [/*start*/ 0x1D6FC, WordBreakProperty.ALetter], - [/*start*/ 0x1D715, WordBreakProperty.Other], - [/*start*/ 0x1D716, WordBreakProperty.ALetter], - [/*start*/ 0x1D735, WordBreakProperty.Other], - [/*start*/ 0x1D736, WordBreakProperty.ALetter], - [/*start*/ 0x1D74F, WordBreakProperty.Other], - [/*start*/ 0x1D750, WordBreakProperty.ALetter], - [/*start*/ 0x1D76F, WordBreakProperty.Other], - [/*start*/ 0x1D770, WordBreakProperty.ALetter], - [/*start*/ 0x1D789, WordBreakProperty.Other], - [/*start*/ 0x1D78A, WordBreakProperty.ALetter], - [/*start*/ 0x1D7A9, WordBreakProperty.Other], - [/*start*/ 0x1D7AA, WordBreakProperty.ALetter], - [/*start*/ 0x1D7C3, WordBreakProperty.Other], - [/*start*/ 0x1D7C4, WordBreakProperty.ALetter], - [/*start*/ 0x1D7CC, WordBreakProperty.Other], - [/*start*/ 0x1D7CE, WordBreakProperty.Numeric], - [/*start*/ 0x1D800, WordBreakProperty.Other], - [/*start*/ 0x1DA00, WordBreakProperty.Extend], - [/*start*/ 0x1DA37, WordBreakProperty.Other], - [/*start*/ 0x1DA3B, WordBreakProperty.Extend], - [/*start*/ 0x1DA6D, WordBreakProperty.Other], - [/*start*/ 0x1DA75, WordBreakProperty.Extend], - [/*start*/ 0x1DA76, WordBreakProperty.Other], - [/*start*/ 0x1DA84, WordBreakProperty.Extend], - [/*start*/ 0x1DA85, WordBreakProperty.Other], - [/*start*/ 0x1DA9B, WordBreakProperty.Extend], - [/*start*/ 0x1DAA0, WordBreakProperty.Other], - [/*start*/ 0x1DAA1, WordBreakProperty.Extend], - [/*start*/ 0x1DAB0, WordBreakProperty.Other], - [/*start*/ 0x1E000, WordBreakProperty.Extend], - [/*start*/ 0x1E007, WordBreakProperty.Other], - [/*start*/ 0x1E008, WordBreakProperty.Extend], - [/*start*/ 0x1E019, WordBreakProperty.Other], - [/*start*/ 0x1E01B, WordBreakProperty.Extend], - [/*start*/ 0x1E022, WordBreakProperty.Other], - [/*start*/ 0x1E023, WordBreakProperty.Extend], - [/*start*/ 0x1E025, WordBreakProperty.Other], - [/*start*/ 0x1E026, WordBreakProperty.Extend], - [/*start*/ 0x1E02B, WordBreakProperty.Other], - [/*start*/ 0x1E100, WordBreakProperty.ALetter], - [/*start*/ 0x1E12D, WordBreakProperty.Other], - [/*start*/ 0x1E130, WordBreakProperty.Extend], - [/*start*/ 0x1E137, WordBreakProperty.ALetter], - [/*start*/ 0x1E13E, WordBreakProperty.Other], - [/*start*/ 0x1E140, WordBreakProperty.Numeric], - [/*start*/ 0x1E14A, WordBreakProperty.Other], - [/*start*/ 0x1E14E, WordBreakProperty.ALetter], - [/*start*/ 0x1E14F, WordBreakProperty.Other], - [/*start*/ 0x1E2C0, WordBreakProperty.ALetter], - [/*start*/ 0x1E2EC, WordBreakProperty.Extend], - [/*start*/ 0x1E2F0, WordBreakProperty.Numeric], - [/*start*/ 0x1E2FA, WordBreakProperty.Other], - [/*start*/ 0x1E800, WordBreakProperty.ALetter], - [/*start*/ 0x1E8C5, WordBreakProperty.Other], - [/*start*/ 0x1E8D0, WordBreakProperty.Extend], - [/*start*/ 0x1E8D7, WordBreakProperty.Other], - [/*start*/ 0x1E900, WordBreakProperty.ALetter], - [/*start*/ 0x1E944, WordBreakProperty.Extend], - [/*start*/ 0x1E94B, WordBreakProperty.ALetter], - [/*start*/ 0x1E94C, WordBreakProperty.Other], - [/*start*/ 0x1E950, WordBreakProperty.Numeric], - [/*start*/ 0x1E95A, WordBreakProperty.Other], - [/*start*/ 0x1EE00, WordBreakProperty.ALetter], - [/*start*/ 0x1EE04, WordBreakProperty.Other], - [/*start*/ 0x1EE05, WordBreakProperty.ALetter], - [/*start*/ 0x1EE20, WordBreakProperty.Other], - [/*start*/ 0x1EE21, WordBreakProperty.ALetter], - [/*start*/ 0x1EE23, WordBreakProperty.Other], - [/*start*/ 0x1EE24, WordBreakProperty.ALetter], - [/*start*/ 0x1EE25, WordBreakProperty.Other], - [/*start*/ 0x1EE27, WordBreakProperty.ALetter], - [/*start*/ 0x1EE28, WordBreakProperty.Other], - [/*start*/ 0x1EE29, WordBreakProperty.ALetter], - [/*start*/ 0x1EE33, WordBreakProperty.Other], - [/*start*/ 0x1EE34, WordBreakProperty.ALetter], - [/*start*/ 0x1EE38, WordBreakProperty.Other], - [/*start*/ 0x1EE39, WordBreakProperty.ALetter], - [/*start*/ 0x1EE3A, WordBreakProperty.Other], - [/*start*/ 0x1EE3B, WordBreakProperty.ALetter], - [/*start*/ 0x1EE3C, WordBreakProperty.Other], - [/*start*/ 0x1EE42, WordBreakProperty.ALetter], - [/*start*/ 0x1EE43, WordBreakProperty.Other], - [/*start*/ 0x1EE47, WordBreakProperty.ALetter], - [/*start*/ 0x1EE48, WordBreakProperty.Other], - [/*start*/ 0x1EE49, WordBreakProperty.ALetter], - [/*start*/ 0x1EE4A, WordBreakProperty.Other], - [/*start*/ 0x1EE4B, WordBreakProperty.ALetter], - [/*start*/ 0x1EE4C, WordBreakProperty.Other], - [/*start*/ 0x1EE4D, WordBreakProperty.ALetter], - [/*start*/ 0x1EE50, WordBreakProperty.Other], - [/*start*/ 0x1EE51, WordBreakProperty.ALetter], - [/*start*/ 0x1EE53, WordBreakProperty.Other], - [/*start*/ 0x1EE54, WordBreakProperty.ALetter], - [/*start*/ 0x1EE55, WordBreakProperty.Other], - [/*start*/ 0x1EE57, WordBreakProperty.ALetter], - [/*start*/ 0x1EE58, WordBreakProperty.Other], - [/*start*/ 0x1EE59, WordBreakProperty.ALetter], - [/*start*/ 0x1EE5A, WordBreakProperty.Other], - [/*start*/ 0x1EE5B, WordBreakProperty.ALetter], - [/*start*/ 0x1EE5C, WordBreakProperty.Other], - [/*start*/ 0x1EE5D, WordBreakProperty.ALetter], - [/*start*/ 0x1EE5E, WordBreakProperty.Other], - [/*start*/ 0x1EE5F, WordBreakProperty.ALetter], - [/*start*/ 0x1EE60, WordBreakProperty.Other], - [/*start*/ 0x1EE61, WordBreakProperty.ALetter], - [/*start*/ 0x1EE63, WordBreakProperty.Other], - [/*start*/ 0x1EE64, WordBreakProperty.ALetter], - [/*start*/ 0x1EE65, WordBreakProperty.Other], - [/*start*/ 0x1EE67, WordBreakProperty.ALetter], - [/*start*/ 0x1EE6B, WordBreakProperty.Other], - [/*start*/ 0x1EE6C, WordBreakProperty.ALetter], - [/*start*/ 0x1EE73, WordBreakProperty.Other], - [/*start*/ 0x1EE74, WordBreakProperty.ALetter], - [/*start*/ 0x1EE78, WordBreakProperty.Other], - [/*start*/ 0x1EE79, WordBreakProperty.ALetter], - [/*start*/ 0x1EE7D, WordBreakProperty.Other], - [/*start*/ 0x1EE7E, WordBreakProperty.ALetter], - [/*start*/ 0x1EE7F, WordBreakProperty.Other], - [/*start*/ 0x1EE80, WordBreakProperty.ALetter], - [/*start*/ 0x1EE8A, WordBreakProperty.Other], - [/*start*/ 0x1EE8B, WordBreakProperty.ALetter], - [/*start*/ 0x1EE9C, WordBreakProperty.Other], - [/*start*/ 0x1EEA1, WordBreakProperty.ALetter], - [/*start*/ 0x1EEA4, WordBreakProperty.Other], - [/*start*/ 0x1EEA5, WordBreakProperty.ALetter], - [/*start*/ 0x1EEAA, WordBreakProperty.Other], - [/*start*/ 0x1EEAB, WordBreakProperty.ALetter], - [/*start*/ 0x1EEBC, WordBreakProperty.Other], - [/*start*/ 0x1F130, WordBreakProperty.ALetter], - [/*start*/ 0x1F14A, WordBreakProperty.Other], - [/*start*/ 0x1F150, WordBreakProperty.ALetter], - [/*start*/ 0x1F16A, WordBreakProperty.Other], - [/*start*/ 0x1F170, WordBreakProperty.ALetter], - [/*start*/ 0x1F18A, WordBreakProperty.Other], - [/*start*/ 0x1F1E6, WordBreakProperty.Regional_Indicator], - [/*start*/ 0x1F200, WordBreakProperty.Other], - [/*start*/ 0x1F3FB, WordBreakProperty.Extend], - [/*start*/ 0x1F400, WordBreakProperty.Other], - [/*start*/ 0x1FBF0, WordBreakProperty.Numeric], - [/*start*/ 0x1FBFA, WordBreakProperty.Other], - [/*start*/ 0xE0001, WordBreakProperty.Format], - [/*start*/ 0xE0002, WordBreakProperty.Other], - [/*start*/ 0xE0020, WordBreakProperty.Extend], - [/*start*/ 0xE0080, WordBreakProperty.Other], - [/*start*/ 0xE0100, WordBreakProperty.Extend], - [/*start*/ 0xE01F0, WordBreakProperty.Other], -]; diff --git a/common/models/wordbreakers/tsconfig.json b/common/models/wordbreakers/tsconfig.json deleted file mode 100644 index 54d12d5d8c7..00000000000 --- a/common/models/wordbreakers/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "../tsconfig.kmw-worker-base.json", - - "compilerOptions": { - "baseUrl": "./", - "outDir": "build/obj", - "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", - "rootDir": "./src" - }, - "references": [ - { "path": "../types" } - ], - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "test/**/*.ts" - ] -} diff --git a/common/predictive-text/.build-builder b/common/predictive-text/.build-builder deleted file mode 100644 index 4e15741a629..00000000000 --- a/common/predictive-text/.build-builder +++ /dev/null @@ -1 +0,0 @@ -The presence of this file tells CI to use the new builder_ style parameters for build.sh and unit_tests/test.sh. \ No newline at end of file diff --git a/common/test/resources/json/models/future_suggestions/i_got_distracted_by_hazel.json b/common/test/resources/json/models/future_suggestions/i_got_distracted_by_hazel.json index a9aa49753b4..d01c7f5c5af 100644 --- a/common/test/resources/json/models/future_suggestions/i_got_distracted_by_hazel.json +++ b/common/test/resources/json/models/future_suggestions/i_got_distracted_by_hazel.json @@ -48,7 +48,7 @@ [ { "transform": { - "insert": "distracted ", + "insert": "distracted by ", "deleteLeft": 0 }, "displayAs": "distracted by" diff --git a/common/test/resources/model-helpers.mjs b/common/test/resources/model-helpers.mjs index 1de10bdb15e..b2ad083f96f 100644 --- a/common/test/resources/model-helpers.mjs +++ b/common/test/resources/model-helpers.mjs @@ -113,7 +113,18 @@ export function randomToken() { } export function iGotDistractedByHazel() { - return jsonFixture('models/future_suggestions/i_got_distracted_by_hazel'); + return jsonFixture('models/future_suggestions/i_got_distracted_by_hazel').map((set) => set.map((entry) => { + return { + ...entry, + // Dummy-model predictions all claim probability 1; there's no actual probability stuff + // used here. + 'lexical-p': 1, + // We're predicting from a single transform, not a distribution, so probability 1. + 'correction-p': 1, + // Multiply 'em together. + p: 1, + } + })); } export function jsonFixture(name, root, import_root) { diff --git a/common/test/resources/models/simple-dummy.js b/common/test/resources/models/simple-dummy.js index 97180729bd6..86da6494b0f 100644 --- a/common/test/resources/models/simple-dummy.js +++ b/common/test/resources/models/simple-dummy.js @@ -9,9 +9,7 @@ Model.punctuation = { quotesForKeepSuggestion: { open: '“', close: '”'}, - // Important! Set this, or else the model compositor will - // insert something for us! - insertAfterWord: "", + insertAfterWord: " ", }; // A direct import/copy from i_got_distracted_by_hazel.json. @@ -19,21 +17,21 @@ [ { "transform": { - "insert": "I ", + "insert": "I", "deleteLeft": 0 }, "displayAs": "I" }, { "transform": { - "insert": "I'm ", + "insert": "I'm", "deleteLeft": 0 }, "displayAs": "I'm" }, { "transform": { - "insert": "Oh ", + "insert": "Oh", "deleteLeft": 0 }, "displayAs": "Oh" @@ -42,21 +40,21 @@ [ { "transform": { - "insert": "love ", + "insert": "love", "deleteLeft": 0 }, "displayAs": "love" }, { "transform": { - "insert": "am ", + "insert": "am", "deleteLeft": 0 }, "displayAs": "am" }, { "transform": { - "insert": "got ", + "insert": "got", "deleteLeft": 0 }, "displayAs": "got" @@ -65,21 +63,21 @@ [ { "transform": { - "insert": "distracted ", + "insert": "distracted by", "deleteLeft": 0 }, "displayAs": "distracted by" }, { "transform": { - "insert": "distracted ", + "insert": "distracted", "deleteLeft": 0 }, "displayAs": "distracted" }, { "transform": { - "insert": "a ", + "insert": "a", "deleteLeft": 0 }, "displayAs": "a" @@ -88,27 +86,27 @@ [ { "transform": { - "insert": "Hazel ", + "insert": "Hazel", "deleteLeft": 0 }, "displayAs": "Hazel" }, { "transform": { - "insert": "the ", + "insert": "the", "deleteLeft": 0 }, "displayAs": "the" }, { "transform": { - "insert": "a ", + "insert": "a", "deleteLeft": 0 }, "displayAs": "a" } ] - ]; + ]; return Model; }()); diff --git a/common/test/resources/package.json b/common/test/resources/package.json index 9b4ddf1a690..9295190f2d3 100644 --- a/common/test/resources/package.json +++ b/common/test/resources/package.json @@ -5,6 +5,6 @@ "license": "MIT", "devDependencies": { "@keymanapp/resources-gosh": "*", - "typescript": "^4.9.5" + "typescript": "^5.4.5" } } diff --git a/common/test/resources/test-timeouts.mjs b/common/test/resources/test-timeouts.mjs new file mode 100644 index 00000000000..2c5bfd5f2cb --- /dev/null +++ b/common/test/resources/test-timeouts.mjs @@ -0,0 +1 @@ +export const DEFAULT_BROWSER_TIMEOUT = 5000; //ms \ No newline at end of file diff --git a/common/test/resources/timeout-adapter.js b/common/test/resources/timeout-adapter.js deleted file mode 100644 index 0e93ec94a95..00000000000 --- a/common/test/resources/timeout-adapter.js +++ /dev/null @@ -1,43 +0,0 @@ -// Preprocessing of the Karma configuration's client.args parameter. - -var com = com || {}; -com.keyman = com.keyman || {}; -com.keyman.karma = com.keyman.karma || {}; - -(function() { - const testconfig = window['testconfig'] = {}; - - // Default value. - let mobile = false; - - // If we've set things up to support Device dection without loading KMW... - if(com.keyman.Device) { - try { - let device = new com.keyman.Device(); - device.detect(); - - mobile = (device.formFactor != 'desktop'); - } finally { - // no-op; silent failure's fine here. - } - } - - let configArgs = window['__karma__'].config.args; // Where Karma gives us our custom args. - for(var i = 0; i < configArgs.length; configArgs++) { - switch(configArgs[i].type) { - case 'timeouts': - var timeouts = JSON.parse(JSON.stringify(configArgs[i])); - delete timeouts.type; - - if(mobile) { - for(var key in timeouts) { - if(key != 'mobileFactor') { - timeouts[key] = timeouts[key] * timeouts['mobileFactor']; - } - } - } - testconfig['timeouts'] = timeouts; - break; - } - } -})(); diff --git a/common/web/eslint/eslintNoNodeImports.js b/common/tools/eslint/eslintNoNodeImports.js similarity index 100% rename from common/web/eslint/eslintNoNodeImports.js rename to common/tools/eslint/eslintNoNodeImports.js diff --git a/common/tools/hextobin/build.sh b/common/tools/hextobin/build.sh index c1e2338e9d8..9e73f8c4101 100755 --- a/common/tools/hextobin/build.sh +++ b/common/tools/hextobin/build.sh @@ -11,7 +11,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" builder_describe "Build hextobin" clean configure build builder_describe_outputs \ - configure /node_modules \ + configure /common/tools/hextobin/node_modules/commander \ build /common/tools/hextobin/build/index.js builder_parse "$@" diff --git a/common/tools/sourcemap-path-remapper/package.json b/common/tools/sourcemap-path-remapper/package.json index ce5f88b1953..312b3ed964f 100644 --- a/common/tools/sourcemap-path-remapper/package.json +++ b/common/tools/sourcemap-path-remapper/package.json @@ -20,8 +20,7 @@ "@keymanapp/resources-gosh": "*", "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", - "ts-node": "^10.9.1", - "typescript": "^4.9.5" + "typescript": "^5.4.5" }, "dependencies": { "convert-source-map": "^2.0.0" diff --git a/common/tools/sourcemap-path-remapper/tsconfig.json b/common/tools/sourcemap-path-remapper/tsconfig.json index e207097f614..35744ee9887 100644 --- a/common/tools/sourcemap-path-remapper/tsconfig.json +++ b/common/tools/sourcemap-path-remapper/tsconfig.json @@ -10,7 +10,7 @@ "inlineSources": true, "sourceRoot": "/common/tools/sourcemap-path-remapper/src", "lib": ["dom", "es6"], - "target": "es5", + "target": "es6", "types": ["node"], "downlevelIteration": true, "baseUrl": "./", diff --git a/common/web/build.sh b/common/web/build.sh index 554b1e8ebb3..798db3b231f 100755 --- a/common/web/build.sh +++ b/common/web/build.sh @@ -8,16 +8,9 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../resources/build/builder.inc.sh" ## END STANDARD BUILD SCRIPT INCLUDE -# -# TODO: future modules may include -# :lm-message-types \ -# :sentry-manager \ -# - builder_describe "Keyman common web modules" \ :keyman-version \ :types \ - :utils \ clean \ configure \ build \ diff --git a/common/web/gesture-recognizer/.mocharc.env.js b/common/web/gesture-recognizer/.mocharc.env.js deleted file mode 100644 index 81e54c73657..00000000000 --- a/common/web/gesture-recognizer/.mocharc.env.js +++ /dev/null @@ -1,5 +0,0 @@ -import { fileURLToPath } from "url"; -import { dirname } from 'path'; - -// Tells ts-node where to find the tsconfig.json to be used for executing TS unit tests. -process.env.TS_NODE_PROJECT = `${dirname(fileURLToPath(import.meta.url))}/src/test/auto/tsconfig.json`; \ No newline at end of file diff --git a/common/web/gesture-recognizer/.mocharc.json b/common/web/gesture-recognizer/.mocharc.json deleted file mode 100644 index bf90c42bf45..00000000000 --- a/common/web/gesture-recognizer/.mocharc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extensions": ["js", "ts"], - "spec" : [ - "src/test/auto/headless/**/*.*" - ], - "node-option": [ - "loader=ts-node/esm" - ], - "require": ".mocharc.env.js" -} \ No newline at end of file diff --git a/common/web/gesture-recognizer/build.sh b/common/web/gesture-recognizer/build.sh deleted file mode 100755 index a27cd3f8d16..00000000000 --- a/common/web/gesture-recognizer/build.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash - -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "$(dirname "$THIS_SCRIPT")/../../../resources/build/builder.inc.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" - -BUNDLE_CMD="node $KEYMAN_ROOT/common/web/es-bundling/build/common-bundle.mjs" - -################################ Main script ################################ - -builder_describe "Builds the gesture-recognition model for Web-based on-screen keyboards" \ - "@/common/web/es-bundling build" \ - "@/common/web/utils build" \ - "clean" \ - "configure" \ - "build" \ - "test" \ - ":module" \ - ":tools tools for testing & developing test resources for this module" \ - "--ci sets the --ci option for child scripts (i.e, the $(builder_term test) action)" - -builder_describe_outputs \ - configure /node_modules \ - build:module /common/web/gesture-recognizer/build/lib/index.mjs \ - build:tools /common/web/gesture-recognizer/build/tools/lib/index.mjs - -builder_parse "$@" - -# TODO: build if out-of-date if test is specified -# TODO: configure if npm has not been run, and build is specified - -function do_configure() { - verify_npm_setup - - # Configure Web browser-engine testing environments. As is, this should only - # make changes when we update the dependency, even on our CI build agents. - playwright install -} - -builder_run_action configure do_configure - -if builder_start_action clean; then - rm -rf build/ - builder_finish_action success clean -fi - -if builder_start_action build:module; then - # Build - tsc --build $builder_verbose - - $BUNDLE_CMD "${KEYMAN_ROOT}/common/web/gesture-recognizer/build/obj/index.js" \ - --out "${KEYMAN_ROOT}/common/web/gesture-recognizer/build/lib/index.mjs" \ - --format esm - - builder_finish_action success build:module -fi - -if builder_start_action build:tools; then - src/tools/build.sh build - builder_finish_action success build:tools -fi - -if builder_start_action test:module; then - if builder_has_option --ci; then - ./test.sh --ci - else - ./test.sh - fi - builder_finish_action success test:module -fi - -if builder_has_action test:tools && ! builder_has_action test:module; then - echo "The $(builder_term test:tools) action is currently a no-op." -fi \ No newline at end of file diff --git a/common/web/gesture-recognizer/src/test/auto/browser/web-test-runner.config.mjs b/common/web/gesture-recognizer/src/test/auto/browser/web-test-runner.config.mjs deleted file mode 100644 index 0d5bac85080..00000000000 --- a/common/web/gesture-recognizer/src/test/auto/browser/web-test-runner.config.mjs +++ /dev/null @@ -1,69 +0,0 @@ -// @ts-check -import { devices, playwrightLauncher } from '@web/test-runner-playwright'; -import { defaultReporter, summaryReporter } from '@web/test-runner'; -import { LauncherWrapper, sessionStabilityReporter } from '@keymanapp/common-test-resources/test-runner-stability-reporter.mjs'; -import named from '@keymanapp/common-test-resources/test-runner-rename-browser.mjs' -import { esbuildPlugin } from '@web/dev-server-esbuild'; -import { importMapsPlugin } from '@web/dev-server-import-maps'; -import { dirname, resolve } from 'path'; -import { fileURLToPath } from 'url'; - -const dir = dirname(fileURLToPath(import.meta.url)); -const KEYMAN_ROOT = resolve(dir, '../../../../../../../'); - -/** @type {import('@web/test-runner').TestRunnerConfig} */ -export default { - // debug: true, - browsers: [ - new LauncherWrapper(playwrightLauncher({ product: 'chromium' })), - new LauncherWrapper(playwrightLauncher({ product: 'firefox' })), - playwrightLauncher({ product: 'webkit', concurrency: 1}), - named(new LauncherWrapper(playwrightLauncher({ product: 'webkit', concurrency: 1, createBrowserContext({browser}) { - return browser.newContext({...devices['iPhone X'] }); - }})), 'iOS Phone (emulated)'), - named(new LauncherWrapper(playwrightLauncher({ product: 'chromium' , createBrowserContext({browser}) { - return browser.newContext({...devices['Pixel 4'] }) - }})), 'Android Phone (emulated)'), - ], - concurrency: 10, - nodeResolve: true, - files: [ - '**/*.spec.html' - ], - middleware: [ - // Rewrites short-hand paths for test resources, making them fully relative to the repo root. - function rewriteResourcePath(context, next) { - if(context.url.startsWith('/resources/')) { - context.url = '/common/web/gesture-recognizer/src/test' + context.url; - } - - return next(); - } - ], - plugins: [ - esbuildPlugin({ ts: true, target: 'auto'}), - importMapsPlugin({ - inject: { - importMap: { - // Redirects `eventemitter3` imports to the bundled ESM library. The standard import is an - // ESM wrapper around the CommonJS implementation, and WTR fails when it hits the CommonJS. - imports: { - 'eventemitter3': '/node_modules/eventemitter3/dist/eventemitter3.esm.js' - } - } - } - }) - ], - reporters: [ - summaryReporter({}), /* local-dev mocha-style */ - sessionStabilityReporter({}), - defaultReporter({}) - ], - /* - Un-comment the next two lines for easy interactive debugging; it'll launch the - test page in your preferred browser. - */ - // open: true, - // manual: true, - rootDir: KEYMAN_ROOT -} \ No newline at end of file diff --git a/common/web/gesture-recognizer/src/test/auto/tsconfig.json b/common/web/gesture-recognizer/src/test/auto/tsconfig.json deleted file mode 100644 index e091004e2bb..00000000000 --- a/common/web/gesture-recognizer/src/test/auto/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -/* - * VS Code Intellisense needs this helper in order to properly use subpath imports in the - * test specs found under the `headless` subfolder. Otherwise, it'll report errors while - * editing - even if the tests themselves actually work. - */ -{ - "extends": "../../../../tsconfig.kmw-main-base.json", - "compilerOptions": { - // Not needed when testing via Node, and when `true` it seems to desync preset breakpoints - // worse than when `false`. - "importHelpers": false, - // Makes the tests... somewhat tricky to handle properly. - "noImplicitAny": false - } -} diff --git a/common/web/gesture-recognizer/test.sh b/common/web/gesture-recognizer/test.sh deleted file mode 100755 index dca49780a15..00000000000 --- a/common/web/gesture-recognizer/test.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash - -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")" -. "$(dirname "$THIS_SCRIPT")/../../../resources/build/build-utils.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -. $KEYMAN_ROOT/resources/shellHelperFunctions.sh - -# This script runs from its own folder -cd "$(dirname $THIS_SCRIPT)" - -################################ Main script ################################ - -builder_describe "Runs all tests for the gesture-recognizer module" \ - "@/common/web/gesture-recognizer" \ - "test+" \ - ":headless Runs headless user tests" \ - ":browser Runs browser-based user tests" \ - "--ci Uses CI-based test configurations & emits CI-friendly test reports" - -builder_parse "$@" - -# TODO: build if out-of-date if test is specified -# TODO: configure if npm has not been run, and build is specified - -# START - Script parameter configuration -REPORT_STYLE="local" # Default setting. - -if builder_has_option --ci; then - REPORT_STYLE="ci" - - echo "Replacing user-friendly test reports & configurations with CI-friendly versions." -fi - -# END - Script parameter configuration - -test-headless ( ) { - # During debugging, "--slow 0" allows reporting the duration of ALL tests, not just the ones that run long. - # Can be useful... but probably shouldn't be the default. - MOCHA_FLAGS= - - if [ $REPORT_STYLE == "ci" ]; then - MOCHA_FLAGS="$MOCHA_FLAGS --reporter mocha-teamcity-reporter" - fi - - c8 mocha --recursive $MOCHA_FLAGS ./src/test/auto/headless/ -} - -test-browser ( ) { - local WTR_DEBUG= - local WTR_CONFIG= - if [[ $# -eq 1 && $1 == "debug" ]]; then - WTR_DEBUG=" --manual" - elif [ $REPORT_STYLE != "local" ]; then - WTR_CONFIG=.CI - fi - - web-test-runner --config src/test/auto/browser/web-test-runner${WTR_CONFIG}.config.mjs ${WTR_DEBUG} -} - -if builder_start_action test:headless; then - test-headless - builder_finish_action success test:headless -fi - -if builder_start_action test:browser; then - if builder_has_option --debug; then - echo "Running browser-based unit tests in debug-mode configuration..." - echo - echo "${COLOR_YELLOW}You must manually terminate this mode (CTRL-C) for the script to exit.${COLOR_RESET}" - sleep 2 - test-browser debug - else - test-browser - fi - builder_finish_action success test:browser -fi \ No newline at end of file diff --git a/common/web/input-processor/README.md b/common/web/input-processor/README.md deleted file mode 100644 index 70417353a26..00000000000 --- a/common/web/input-processor/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Keyman: Keyboard Processor Module -The Original Code is (C) 2017-2020 SIL International - -The Keyboard Processor module is an internal component of KeymanWeb, seen within this repo at /web/. - -## Minimum Compilation Requirements - -* A local installation of [Node.js](https://nodejs.org/) v8.9+. - * Builds will call `npm install` to automatically install further necessary build dependencies. - - * Linux users can update to LTS version of nodejs by following instructions on the [NodeSource Distributions](https://github.com/nodesource/distributions#table-of-contents) page. - -********************************************************************** - -The build script may be found at src/build.sh. diff --git a/common/web/input-processor/build.sh b/common/web/input-processor/build.sh deleted file mode 100755 index 45d5277db4e..00000000000 --- a/common/web/input-processor/build.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash -# -# Compile KeymanWeb's 'keyboard-processor' module, one of the components of Web's 'core' module. -# -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" - -BUNDLE_CMD="node $KEYMAN_ROOT/common/web/es-bundling/build/common-bundle.mjs" - -################################ Main script ################################ - -builder_describe "Builds the standalone, headless form of Keyman Engine for Web's input-processor module" \ - "@/common/web/keyman-version" \ - "@/common/web/keyboard-processor" \ - "@/common/predictive-text" \ - "@/developer/src/kmc-model test" \ - "clean" \ - "configure" \ - "build" \ - "test" \ - "--ci Sets $(builder_term test) action to use CI-based test configurations & reporting" - -builder_describe_outputs \ - configure /node_modules \ - build /common/web/input-processor/build/lib/index.mjs \ - -builder_parse "$@" - -function do_build() { - tsc -b ./tsconfig.json - - $BUNDLE_CMD "${KEYMAN_ROOT}/common/web/input-processor/build/obj/index.js" \ - --out "${KEYMAN_ROOT}/common/web/input-processor/build/lib/index.mjs" \ - --format esm - - # Declaration bundling. - tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts -} - -function do_test() { - local FLAGS= - if builder_has_option --ci; then - FLAGS="--reporter mocha-teamcity-reporter" - fi - - mocha --recursive $FLAGS ./tests/cases/ -} - -builder_run_action configure verify_npm_setup -builder_run_action clean rm -rf build/ -builder_run_action build do_build -builder_run_action test do_test \ No newline at end of file diff --git a/common/web/input-processor/package.json b/common/web/input-processor/package.json deleted file mode 100644 index eb618b335e8..00000000000 --- a/common/web/input-processor/package.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "name": "@keymanapp/input-processor", - "description": "The core text and prediction processing engine for KeymanWeb", - "repository": { - "type": "git", - "url": "git+https://github.com/keymanapp/keyman.git" - }, - "keywords": [ - "input", - "languages", - "keyboards" - ], - "author": "SIL International", - "license": "MIT", - "bugs": { - "url": "https://github.com/keymanapp/keyman/issues" - }, - "homepage": "https://github.com/keymanapp/keyman#readme", - "devDependencies": { - "@keymanapp/kmc": "*", - "@keymanapp/resources-gosh": "*", - "mocha": "^10.0.0", - "mocha-teamcity-reporter": "^4.0.0", - "ts-node": "^10.9.1", - "typescript": "^4.9.5" - }, - "scripts": { - "test": "gosh ./test.sh" - }, - "dependencies": { - "@keymanapp/keyboard-processor": "*", - "@keymanapp/lexical-model-layer": "*", - "@keymanapp/models-types": "*", - "@keymanapp/keyman-version": "*", - "@keymanapp/web-utils": "*", - "eventemitter3": "^5.0.0" - }, - "type": "module", - "main": "./build/obj/index.js", - "types": "./build/obj/index.d.ts", - "exports": { - ".": { - "es6-bundling": "./src/index.ts", - "default": "./build/obj/index.js" - }, - "./lib": { - "types": "./build/lib/index.d.ts", - "import": "./build/lib/index.mjs" - }, - "./obj/*.js": "./build/obj/*.js" - } -} diff --git a/common/web/input-processor/src/index.ts b/common/web/input-processor/src/index.ts deleted file mode 100644 index 6747f42a234..00000000000 --- a/common/web/input-processor/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// - -export * from './corrections.js'; -export * from './correctionLayout.js'; -export { default as InputProcessor } from './text/inputProcessor.js'; -export { default as ContextWindow } from './text/contextWindow.js'; -export { default as ModelSpec } from './text/prediction/modelSpec.js'; -export { default as LanguageProcessor, StateChangeEnum } from './text/prediction/languageProcessor.js'; -export { default as PredictionContext } from './text/prediction/predictionContext.js'; -export { TranscriptionCache } from './transcriptionCache.js'; \ No newline at end of file diff --git a/common/web/input-processor/tsconfig.json b/common/web/input-processor/tsconfig.json deleted file mode 100644 index 4551a235dc4..00000000000 --- a/common/web/input-processor/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "../tsconfig.kmw-main-base.json", - "compilerOptions": { - "baseUrl": "./", - // Necessary for ES5 iteration of Map.keys(). - "downlevelIteration": true, - "outDir": "build/obj/", - "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", - "rootDir": "./src" - }, - "references": [ - { "path": "../utils" }, - { "path": "../keyboard-processor" }, - { "path": "../../predictive-text/tsconfig.all.json" }, - ], - // The extra .d.ts is necessary to avoid issues with lack of a Worker type definition when not including - // the standard DOM in the TS "lib" configuration. - "include": ["./src/**/*.ts", "../../predictive-text/src/worker-interface.d.ts"] -} diff --git a/common/web/keyboard-processor/.gitignore b/common/web/keyboard-processor/.gitignore deleted file mode 100644 index c613a78f7dc..00000000000 --- a/common/web/keyboard-processor/.gitignore +++ /dev/null @@ -1,18 +0,0 @@ -# Build products -src/environment.inc.ts - -# Legacy build folders. -output/ -build/ -embedded/ - -# Current build output folder (matches standard node publish location). -dist/ - -# Other local files. -node_modules/ -unit_tests/modernizr.js -source/environment.inc.ts -.idea/**/*.xml -*.iml -*.kpj.user \ No newline at end of file diff --git a/common/web/keyboard-processor/build.sh b/common/web/keyboard-processor/build.sh deleted file mode 100755 index 85be0c09980..00000000000 --- a/common/web/keyboard-processor/build.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bash -# -# Compile KeymanWeb's 'keyboard-processor' module, one of the components of Web's 'core' module. -# -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" - -BUNDLE_CMD="node $KEYMAN_ROOT/common/web/es-bundling/build/common-bundle.mjs" - -################################ Main script ################################ - -builder_describe \ - "Compiles the web-oriented utility function module." \ - "@/common/web/recorder test" \ - "@/common/web/keyman-version" \ - "@/common/web/es-bundling" \ - "@/common/web/types" \ - "@/common/web/utils" \ - configure \ - clean \ - build \ - test \ - "--ci For use with action $(builder_term test) - emits CI-friendly test reports" - -builder_describe_outputs \ - configure /node_modules \ - build /common/web/keyboard-processor/build/lib/index.mjs - -builder_parse "$@" - -function do_configure() { - verify_npm_setup - - # Configure Web browser-engine testing environments. As is, this should only - # make changes when we update the dependency, even on our CI build agents. - playwright install -} - -function do_build() { - tsc --build "$THIS_SCRIPT_PATH/tsconfig.all.json" - - # Base product - the main keyboard processor - $BUNDLE_CMD "${KEYMAN_ROOT}/common/web/keyboard-processor/build/obj/index.js" \ - --out "${KEYMAN_ROOT}/common/web/keyboard-processor/build/lib/index.mjs" \ - --format esm - - # The DOM-oriented keyboard loader - $BUNDLE_CMD "${KEYMAN_ROOT}/common/web/keyboard-processor/build/obj/keyboards/loaders/dom-keyboard-loader.js" \ - --out "${KEYMAN_ROOT}/common/web/keyboard-processor/build/lib/dom-keyboard-loader.mjs" \ - --format esm - - # The Node-oriented keyboard loader - $BUNDLE_CMD "${KEYMAN_ROOT}/common/web/keyboard-processor/build/obj/keyboards/loaders/node-keyboard-loader.js" \ - --out "${KEYMAN_ROOT}/common/web/keyboard-processor/build/lib/node-keyboard-loader.mjs" \ - --format esm \ - --platform node - - # Declaration bundling. - tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts - tsc --emitDeclarationOnly --outFile ./build/lib/dom-keyboard-loader.d.ts -p src/keyboards/loaders/tsconfig.dom.json - tsc --emitDeclarationOnly --outFile ./build/lib/node-keyboard-loader.d.ts -p src/keyboards/loaders/tsconfig.node.json -} - -function do_test() { - local MOCHA_FLAGS= - local WTR_CONFIG= - if builder_has_option --ci; then - echo "Replacing user-friendly test reports with CI-friendly versions." - MOCHA_FLAGS="$MOCHA_FLAGS --reporter mocha-teamcity-reporter" - WTR_CONFIG=.CI - fi - - c8 mocha --recursive $MOCHA_FLAGS ./tests/node/ - web-test-runner --config tests/dom/web-test-runner${WTR_CONFIG}.config.mjs -} - -builder_run_action configure do_configure -builder_run_action clean rm -rf ./build -builder_run_action build do_build -builder_run_action test do_test \ No newline at end of file diff --git a/common/web/keyboard-processor/package.json b/common/web/keyboard-processor/package.json deleted file mode 100644 index 3e281508e18..00000000000 --- a/common/web/keyboard-processor/package.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "name": "@keymanapp/keyboard-processor", - "description": "Core module for Keyman keyboard support in KeymanWeb.", - "repository": { - "type": "git", - "url": "git+https://github.com/keymanapp/keyman.git" - }, - "keywords": [ - "input", - "languages", - "keyboards" - ], - "author": "SIL International", - "license": "MIT", - "bugs": { - "url": "https://github.com/keymanapp/keyman/issues" - }, - "homepage": "https://github.com/keymanapp/keyman#readme", - "devDependencies": { - "@keymanapp/resources-gosh": "*", - "c8": "^7.12.0", - "mocha": "^10.0.0", - "mocha-teamcity-reporter": "^4.0.0", - "ts-node": "^10.9.1", - "typescript": "^4.9.5" - }, - "scripts": { - "build": "gosh build.sh", - "clean": "gosh build.sh clean", - "test": "gosh build.sh test" - }, - "dependencies": { - "@keymanapp/common-types": "*", - "@keymanapp/models-types": "*", - "@keymanapp/keyman-version": "*", - "@keymanapp/web-utils": "*" - }, - "type": "module", - "main": "./build/obj/index.js", - "types": "./build/obj/index.d.ts", - "exports": { - ".": { - "es6-bundling": "./src/index.ts", - "default": "./build/obj/index.js" - }, - "./node-keyboard-loader": { - "es6-bundling": "./src/keyboards/loaders/node-keyboard-loader.ts", - "types": "./build/obj/keyboards/loaders/node-keyboard-loader.d.ts", - "import": "./build/obj/keyboards/loaders/node-keyboard-loader.js" - }, - "./dom-keyboard-loader": { - "es6-bundling": "./src/keyboards/loaders/dom-keyboard-loader.ts", - "types": "./build/obj/keyboards/loaders/dom-keyboard-loader.d.ts", - "import": "./build/obj/keyboards/loaders/dom-keyboard-loader.js" - }, - "./lib": { - "types": "./build/lib/index.d.ts", - "import": "./build/lib/index.mjs" - }, - "./lib/node-keyboard-loader": { - "types": "./build/lib/node-keyboard-loader.d.ts", - "import": "./build/lib/node-keyboard-loader.mjs" - }, - "./lib/dom-keyboard-loader": { - "types": "./build/lib/dom-keyboard-loader.d.ts", - "import": "./build/lib/dom-keyboard-loader.mjs" - }, - "./obj/*.js": "./build/obj/*.js" - } -} diff --git a/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.dom.json b/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.dom.json deleted file mode 100644 index 7ba2abe026a..00000000000 --- a/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.dom.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../../../tsconfig.kmw-main-base.json", - "compilerOptions": { - "baseUrl": "../../../", - "outDir": "../../../build/obj/keyboards/loaders/", - "tsBuildInfoFile": "../../../build/obj/keyboards/loaders/tsconfig.dom.tsbuildinfo", - "rootDir": "." - }, - "references": [ - { "path": "../../../tsconfig.json" } - ], - "include": ["dom-keyboard-loader.ts", "domKeyboardLoader.ts"], -} diff --git a/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.node.json b/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.node.json deleted file mode 100644 index 0be3f169ad3..00000000000 --- a/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.node.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../../../tsconfig.kmw-main-base.json", - "compilerOptions": { - "types": [ "node" ], - "baseUrl": "../../../", - "outDir": "../../../build/obj/keyboards/loaders/", - "tsBuildInfoFile": "../../../build/obj/keyboards/loaders/tsconfig.node.tsbuildinfo", - "rootDir": "." - }, - "references": [ - { "path": "../../../tsconfig.json" } - ], - "include": ["node-keyboard-loader.ts", "nodeKeyboardLoader.ts"], -} diff --git a/common/web/keyboard-processor/tests/dom/readme.md b/common/web/keyboard-processor/tests/dom/readme.md deleted file mode 100644 index 153b7f4a1e8..00000000000 --- a/common/web/keyboard-processor/tests/dom/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -Automated tests in this subfolder and its children are designed to facilitate simple, browser-independent -unit tests that are DOM-reliant. - -Tests for anything that may reasonably vary depending upon the browser used to run the code should go under -the "integrated" folder instead. \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/dom/web-test-runner.CI.config.mjs b/common/web/keyboard-processor/tests/dom/web-test-runner.CI.config.mjs deleted file mode 100644 index d18a8a9cb09..00000000000 --- a/common/web/keyboard-processor/tests/dom/web-test-runner.CI.config.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-check -import BASE_CONFIG from './web-test-runner.config.mjs'; -import teamcityReporter from '@keymanapp/common-test-resources/test-runner-TC-reporter.mjs'; -import { sessionStabilityReporter } from '@keymanapp/common-test-resources/test-runner-stability-reporter.mjs'; - -/** @type {import('@web/test-runner').TestRunnerConfig} */ -export default { - ...BASE_CONFIG, - reporters: [ - teamcityReporter(), /* custom-written, for CI-friendly reports */ - sessionStabilityReporter({ciMode: true}) - ] -} \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/dom/web-test-runner.config.mjs b/common/web/keyboard-processor/tests/dom/web-test-runner.config.mjs deleted file mode 100644 index c16759b5090..00000000000 --- a/common/web/keyboard-processor/tests/dom/web-test-runner.config.mjs +++ /dev/null @@ -1,62 +0,0 @@ -// @ts-check -import { devices, playwrightLauncher } from '@web/test-runner-playwright'; -import { esbuildPlugin } from '@web/dev-server-esbuild'; -import { defaultReporter, summaryReporter } from '@web/test-runner'; -import { LauncherWrapper, sessionStabilityReporter } from '@keymanapp/common-test-resources/test-runner-stability-reporter.mjs'; -import { importMapsPlugin } from '@web/dev-server-import-maps'; -import { dirname, resolve } from 'path'; -import { fileURLToPath } from 'url'; - -const dir = dirname(fileURLToPath(import.meta.url)); -const KEYMAN_ROOT = resolve(dir, '../../../../..'); - -/** @type {import('@web/test-runner').TestRunnerConfig} */ -export default { - // debug: true, - browsers: [ - new LauncherWrapper(playwrightLauncher({ product: 'chromium' })), - new LauncherWrapper(playwrightLauncher({ product: 'firefox' })), - new LauncherWrapper(playwrightLauncher({ product: 'webkit', concurrency: 1 })), - ], - concurrency: 10, - nodeResolve: true, - files: [ - '**/*.spec.ts' - ], - middleware: [ - // Rewrites short-hand paths for test resources, making them fully relative to the repo root. - function rewriteResourcePath(context, next) { - if(context.url.startsWith('/resources/')) { - context.url = '/common/test' + context.url; - } - - return next(); - } - ], - plugins: [ - esbuildPlugin({ts: true, target: 'auto'}), - importMapsPlugin({ - inject: { - importMap: { - // Redirects `eventemitter3` imports to the bundled ESM library. The standard import is an - // ESM wrapper around the CommonJS implementation, and WTR fails when it hits the CommonJS. - imports: { - 'eventemitter3': '/node_modules/eventemitter3/dist/eventemitter3.esm.js' - } - } - } - }) - ], - reporters: [ - summaryReporter({}), /* local-dev mocha-style */ - sessionStabilityReporter({}), - defaultReporter({}) - ], - /* - Un-comment the next two lines for easy interactive debugging; it'll launch the - test page in your preferred browser. - */ - // open: true, - // manual: true, - rootDir: KEYMAN_ROOT -} \ No newline at end of file diff --git a/common/web/keyboard-processor/tsconfig.json b/common/web/keyboard-processor/tsconfig.json deleted file mode 100644 index d4c06e2aaa6..00000000000 --- a/common/web/keyboard-processor/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -// Is used by the keyboard-loader submodules. -{ - "extends": "../tsconfig.kmw-main-base.json", - "compilerOptions": { - "baseUrl": "./", - "outDir": "build/obj/", - "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", - "rootDir": "./src" - }, - "references": [ - { "path": "../types" }, - { "path": "../../models/types" }, - { "path": "../keyman-version/" }, - { "path": "../utils/" } - ], - "include": ["./src/**/*.ts"], - "exclude": ["./src/keyboards/loaders/**/*.ts"] -} diff --git a/common/web/keyman-version/package.json b/common/web/keyman-version/package.json index ac33bdc6909..bdd2e9bc84f 100644 --- a/common/web/keyman-version/package.json +++ b/common/web/keyman-version/package.json @@ -13,6 +13,6 @@ "license": "MIT", "type": "module", "devDependencies": { - "typescript": "^4.9.5" + "typescript": "^5.4.5" } } diff --git a/common/web/keyman-version/tsconfig.json b/common/web/keyman-version/tsconfig.json index a5808c432ee..6fc1db1b8d1 100644 --- a/common/web/keyman-version/tsconfig.json +++ b/common/web/keyman-version/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "./build", - "target": "es5", + "target": "es6", "rootDir": "." }, diff --git a/common/web/lm-message-types/tsconfig.json b/common/web/lm-message-types/tsconfig.json deleted file mode 100644 index cc5fb2f861d..00000000000 --- a/common/web/lm-message-types/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "declaration": true, - "outDir": "build/", - "sourceMap": true, - "lib": ["es6"], - "target": "es5" - }, - "include": ["./*.ts"], - "exclude": ["test.ts"] -} diff --git a/common/web/lm-worker/src/main/correction/context-tracker.ts b/common/web/lm-worker/src/main/correction/context-tracker.ts deleted file mode 100644 index 2f15165e5e3..00000000000 --- a/common/web/lm-worker/src/main/correction/context-tracker.ts +++ /dev/null @@ -1,597 +0,0 @@ -import { applyTransform, tokenize } from '@keymanapp/models-templates'; -import { defaultWordbreaker } from '@keymanapp/models-wordbreakers'; - -import { ClassicalDistanceCalculation } from './classical-calculation.js'; -import { SearchSpace } from './distance-modeler.js'; -import TransformUtils from '../transformUtils.js'; - -function textToCharTransforms(text: string, transformId?: number) { - let perCharTransforms: Transform[] = []; - - for(let i=0; i < text.kmwLength(); i++) { - let char = text.kmwCharAt(i); // is SMP-aware - - let transform: Transform = { - insert: char, - deleteLeft: 0, - id: transformId - }; - - perCharTransforms.push(transform); - } - - return perCharTransforms; -} - -export class TrackedContextSuggestion { - suggestion: Suggestion; - tokenWidth: number; -} - -export class TrackedContextToken { - raw: string; - replacementText: string; - - transformDistributions: Distribution[] = []; - replacements: TrackedContextSuggestion[]; - activeReplacementId: number = -1; - - get currentText(): string { - if(this.replacementText === undefined || this.replacementText === null) { - return this.raw; - } else { - return this.replacementText; - } - } - - get replacement(): TrackedContextSuggestion { - let replacementId = this.activeReplacementId; - return this.replacements.find(function(replacement) { - return replacement.suggestion.id == replacementId; - }); - } - - revert() { - delete this.activeReplacementId; - } -} - -export class TrackedContextState { - // Stores the post-transform Context. Useful as a debugging reference, but also used to - // pre-validate context state matches in case of discarded changes from multitaps. - taggedContext: Context; - model: LexicalModel; - - tokens: TrackedContextToken[]; - /** - * How many tokens were removed from the start of the best-matching ancestor. - * Useful for restoring older states, e.g., when the user moves the caret backwards, we can recover the context at that position. - */ - indexOffset: number; - - // Tracks all search spaces starting at the current token. - // In the lm-layer's current form, this should only ever have one entry. - // Leaves 'design space' for if/when we add support for phrase-level corrections/predictions. - searchSpace: SearchSpace[] = []; - - constructor(source: TrackedContextState); - constructor(model: LexicalModel); - constructor(obj: TrackedContextState | LexicalModel) { - if(obj instanceof TrackedContextState) { - let source = obj; - // Be sure to deep-copy the tokens! Pointer-aliasing is bad here. - this.tokens = source.tokens.map(function(token) { - let copy = new TrackedContextToken(); - copy.raw = token.raw; - copy.replacements = [].concat(token.replacements); - copy.activeReplacementId = token.activeReplacementId; - copy.transformDistributions = [].concat(token.transformDistributions); - - if(token.replacementText) { - copy.replacementText = token.replacementText; - } - - return copy; - }); - - this.indexOffset = 0; - const lexicalModel = this.model = obj.model; - this.taggedContext = obj.taggedContext; - - if(lexicalModel?.traverseFromRoot) { - // We need to construct a separate search space from other ContextStates. - // - // In case we are unable to perfectly track context (say, due to multitaps) - // we need to ensure that only fully-utilized keystrokes are considered. - this.searchSpace = obj.searchSpace.map((space) => new SearchSpace(space)); - } - } else { - let lexicalModel = obj; - this.tokens = []; - this.indexOffset = Number.MIN_SAFE_INTEGER; - this.model = lexicalModel; - - if(lexicalModel && lexicalModel.traverseFromRoot) { - this.searchSpace = [new SearchSpace(lexicalModel)]; - } - } - } - - get head(): TrackedContextToken { - return this.tokens[0]; - } - - get tail(): TrackedContextToken { - return this.tokens[this.tokens.length - 1]; - } - - popHead() { - this.tokens.splice(0, 2); - this.indexOffset -= 1; - } - - pushTail(token: TrackedContextToken) { - if(this.model && this.model.traverseFromRoot) { - this.searchSpace = [new SearchSpace(this.model)]; // yeah, need to update SearchSpace for compatibility - } else { - this.searchSpace = []; - } - this.tokens.push(token); - - let state = this; - if(state.searchSpace.length > 0) { - token.transformDistributions.forEach(distrib => state.searchSpace[0].addInput(distrib)); - } - } - - pushWhitespaceToTail(transformDistribution: Distribution = null) { - let whitespaceToken = new TrackedContextToken(); - - // Track the Transform that resulted in the whitespace 'token'. - // Will be needed for phrase-level correction/prediction. - whitespaceToken.transformDistributions = transformDistribution ? [transformDistribution] : []; - - whitespaceToken.raw = null; - this.tokens.push(whitespaceToken); - } - - /** - * Used for 14.0's backspace workaround, which flattens all previous Distribution - * entries because of limitations with direct use of backspace transforms. - * @param tokenText - * @param transformId - */ - replaceTailForBackspace(tokenText: USVString, transformId: number) { - this.tokens.pop(); - - // It's a backspace transform; time for special handling! - // - // For now, with 14.0, we simply compress all remaining Transforms for the token into - // multiple single-char transforms. Probabalistically modeling BKSP is quite complex, - // so we simplify by assuming everything remaining after a BKSP is 'true' and 'intended' text. - // - // Note that we cannot just use a single, monolithic transform at this point b/c - // of our current edit-distance optimization strategy; diagonalization is currently... - // not very compatible with that. - let backspacedTokenContext: Distribution[] = textToCharTransforms(tokenText, transformId).map(function(transform) { - return [{sample: transform, p: 1.0}]; - }); - - let compactedToken = new TrackedContextToken(); - compactedToken.raw = tokenText; - compactedToken.transformDistributions = backspacedTokenContext; - this.pushTail(compactedToken); - } - - updateTail(transformDistribution: Distribution, tokenText?: USVString) { - let editedToken = this.tail; - - // Preserve existing text if new text isn't specified. - tokenText = tokenText || (tokenText === '' ? '' : editedToken.raw); - - if(transformDistribution && transformDistribution.length > 0) { - editedToken.transformDistributions.push(transformDistribution); - if(this.searchSpace) { - this.searchSpace.forEach(space => space.addInput(transformDistribution)); - } - } - // Replace old token's raw-text with new token's raw-text. - editedToken.raw = tokenText; - } - - toRawTokenization() { - let sequence: USVString[] = []; - - for(let token of this.tokens) { - // Hide any tokens representing wordbreaks. (Thinking ahead to phrase-level possibilities) - if(token.currentText !== null) { - sequence.push(token.currentText); - } - } - - return sequence; - } -} - -class CircularArray { - static readonly DEFAULT_ARRAY_SIZE = 5; - private circle: Item[]; - private currentHead: number=0; - private currentTail: number=0; - - constructor(size: number = CircularArray.DEFAULT_ARRAY_SIZE) { - this.circle = Array(size); - } - - get count(): number { - let diff = this.currentHead - this.currentTail; - - if(diff < 0) { - diff = diff + this.circle.length; - } - - return diff; - } - - get maxCount(): number { - return this.circle.length; - } - - get oldest(): Item { - if(this.count == 0) { - return undefined; - } - - return this.item(0); - } - - get newest(): Item { - if(this.count == 0) { - return undefined; - } - - return this.item(this.count - 1); - } - - enqueue(item: Item): Item { - var prevItem = null; - let nextHead = (this.currentHead + 1) % this.maxCount; - - if(nextHead == this.currentTail) { - prevItem = this.circle[this.currentTail]; - this.currentTail = (this.currentTail + 1) % this.maxCount; - } - - this.circle[this.currentHead] = item; - this.currentHead = nextHead; - - return prevItem; - } - - dequeue(): Item { - if(this.currentTail == this.currentHead) { - return null; - } else { - let item = this.circle[this.currentTail]; - this.currentTail = (this.currentTail + 1) % this.maxCount; - return item; - } - } - - popNewest(): Item { - if(this.currentTail == this.currentHead) { - return null; - } else { - let item = this.circle[this.currentHead]; - this.currentHead = (this.currentHead - 1 + this.maxCount) % this.maxCount; - return item; - } - } - - /** - * Returns items contained within the circular array, ordered from 'oldest' to 'newest' - - * the same order in which the items will be dequeued. - * @param index - */ - item(index: number) { - if(index >= this.count) { - throw "Invalid array index"; - } - - let mappedIndex = (this.currentTail + index) % this.maxCount; - return this.circle[mappedIndex]; - } -} - -export class ContextTracker extends CircularArray { - static attemptMatchContext(tokenizedContext: USVString[], - matchState: TrackedContextState, - transformDistribution?: Distribution): TrackedContextState { - // Map the previous tokenized state to an edit-distance friendly version. - let matchContext: USVString[] = matchState.toRawTokenization(); - - // Inverted order, since 'match' existed before our new context. - let mapping = ClassicalDistanceCalculation.computeDistance(matchContext.map(value => ({key: value})), - tokenizedContext.map(value => ({key: value})), - 1); - - let editPath = mapping.editPath(); - - let poppedHead = false; - let pushedTail = false; - - // Matters greatly when starting from a nil context. - if(editPath.length > 1) { - // First entry: may not be an 'insert' or a 'transpose' op. - // 'insert' allowed if the next token is 'substitute', as this may occur with an edit path of length 2. - if((editPath[0] == 'insert' && !(editPath[1] == 'substitute' && editPath.length == 2)) || editPath[0].indexOf('transpose') >= 0) { - return null; - } else if(editPath[0] == 'delete') { - poppedHead = true; // a token from the previous state has been wholly removed. - } - } - - // Last entry: may not be a 'delete' or a 'transpose' op. - let tailIndex = editPath.length -1; - let ignorePenultimateMatch = false; - if(editPath[tailIndex] == 'delete' || editPath[0].indexOf('transpose') >= 0) { - return null; - } else if(editPath[tailIndex] == 'insert') { - pushedTail = true; - } else if(tailIndex > 0 && editPath[tailIndex-1] == 'insert' && editPath[tailIndex] == 'substitute') { - // Tends to happen when accepting suggestions. - pushedTail = true; - ignorePenultimateMatch = true; - } - - // Can happen for the first text input after backspace deletes a wordbreaking character, - // thus the new input continues a previous word while dropping the empty word after - // that prior wordbreaking character. - // - // We can't handle it reliably from this match state, but a previous entry (without the empty token) - // should still be in the cache and will be reliable for this example case. - if(tailIndex > 0 && editPath[tailIndex-1] == 'delete' && editPath[tailIndex] == 'substitute') { - return null; - } - - // Now to check everything in-between: should be exclusively 'match'es. - for(let index = 1; index < editPath.length - (ignorePenultimateMatch ? 2 : 1); index++) { - if(editPath[index] != 'match') { - return null; - } - } - - // If we've made it here... success! We have a context match! - let state: TrackedContextState; - - if(pushedTail) { - // On suggestion acceptance, we should update the previous final token. - // We do it first so that the acceptance is replicated in the new TrackedContextState - // as well. - if(ignorePenultimateMatch) { - // For this case, we were likely called by ModelCompositor.acceptSuggestion(), which - // would have marked the accepted suggestion. - matchState.tail.replacementText = tokenizedContext[tokenizedContext.length-2]; - } - - state = new TrackedContextState(matchState); - } else { - // We're continuing a previously-cached context; create a deep-copy of it. - // We can't just re-use the old instance, unfortunately; predictions break - // with multitaps otherwise - we should avoid tracking keystrokes that were - // rewound. - // - // If there are no incoming transforms, though... yeah, re-use is safe then. - state = !!transformDistribution ? new TrackedContextState(matchState) : matchState; - } - - const hasDistribution = transformDistribution && Array.isArray(transformDistribution); - let primaryInput = hasDistribution ? transformDistribution[0].sample : null; - if(primaryInput && primaryInput.insert == "" && primaryInput.deleteLeft == 0 && !primaryInput.deleteRight) { - primaryInput = null; - } - - const isWhitespace = primaryInput && TransformUtils.isWhitespace(primaryInput); - const isBackspace = primaryInput && TransformUtils.isBackspace(primaryInput); - const finalToken = tokenizedContext[tokenizedContext.length-1]; - - /* Assumption: This is an adequate check for its two sub-branches. - * - * Basis: - * - Assumption: one keystroke may only cause a single token to rotate out of context. - * - That is, no "reasonable" keystroke would emit enough code points to 'bump' two words simultaneously. - * - ... This one may need to be loosened a bit... but it should be enough for initial correction testing as-is. - * - Assumption: one keystroke may only cause a single token to be appended to the context - * - That is, no "reasonable" keystroke would emit a Transform adding two separate word tokens - * - For languages using whitespace to word-break, said keystroke would have to include said whitespace to break the assumption. - */ - - function maintainLastToken() { - if(isWhitespace && editPath[tailIndex] == 'match') { - /* - We can land here if there are multiple whitespaces in a row. - There's already an implied whitespace to the left, so we conceptually - merge the new whitespace with that one. - */ - return; - } else if(isBackspace) { - // Consider backspace entry for this case? - state.replaceTailForBackspace(finalToken, primaryInput.id); - } else { - state.updateTail(primaryInput ? transformDistribution : null, finalToken); - } - } - - // If there is/was more than one context token available... - if(editPath.length > 1) { - // We're removing a context token, but at least one remains. - if(poppedHead) { - state.popHead(); - } - - // We're adding an additional context token. - if(pushedTail) { - const tokenizedTail = tokenizedContext[tokenizedContext.length - 1]; - /* - * Common-case: most transforms that trigger this case are from pure-whitespace Transforms. MOST. - * - * Less-common, but noteworthy: some wordbreaks may occur without whitespace. Example: - * `"o` => ['"', 'o']. Make sure to double-check against `tokenizedContext`! - */ - let pushedToken = new TrackedContextToken(); - pushedToken.raw = tokenizedTail; - - if(isWhitespace || !primaryInput) { - state.pushWhitespaceToTail(transformDistribution ?? []); - // Continuing the earlier assumption, that 'pure-whitespace Transform' does not emit any initial characters - // for the new word (token), so the input keystrokes do not correspond to the new text token. - pushedToken.transformDistributions = []; - } else { - state.pushWhitespaceToTail(); - // Assumption: Since we only allow one-transform-at-a-time changes between states, we shouldn't be missing - // any metadata used to construct the new context state token. - pushedToken.transformDistributions = transformDistribution ? [transformDistribution] : []; - } - - state.pushTail(pushedToken); - } else { - // We're editing the final context token. - // TODO: Assumption: we didn't 'miss' any inputs somehow. - // As is, may be prone to fragility should the lm-layer's tracked context 'desync' from its host's. - maintainLastToken(); - } - // There is only one word in the context. - } else { - // TODO: Assumption: we didn't 'miss' any inputs somehow. - // As is, may be prone to fragility should the lm-layer's tracked context 'desync' from its host's. - - if(editPath[tailIndex] == 'insert') { - // Construct appropriate initial token. - let token = new TrackedContextToken(); - token.raw = tokenizedContext[0]; - token.transformDistributions = [transformDistribution]; - state.pushTail(token); - } else { - // Edit the lone context token. - maintainLastToken(); - } - } - return state; - } - - private static modelContextState(tokenizedContext: USVString[], - transformDistribution: Distribution, - lexicalModel: LexicalModel): TrackedContextState { - let baseTokens = tokenizedContext.map(function(entry) { - let token = new TrackedContextToken(); - token.raw = entry; - if(token.raw) { - token.transformDistributions = textToCharTransforms(token.raw).map(function(transform) { - return [{sample: transform, p: 1.0}]; - }); - } else { - // Helps model context-final wordbreaks. - token.transformDistributions = []; - } - return token; - }); - - // And now build the final context state object, which includes whitespace 'tokens'. - let state = new TrackedContextState(lexicalModel); - - if(baseTokens.length > 0) { - state.pushTail(baseTokens.splice(0, 1)[0]); - } - - while(baseTokens.length > 0) { - state.pushWhitespaceToTail(); - state.pushTail(baseTokens.splice(0, 1)[0]); - } - - if(state.tokens.length == 0) { - let token = new TrackedContextToken(); - token.raw = ''; - - state.pushTail(token); - } - - return state; - } - - /** - * Compares the current, post-input context against the most recently-seen contexts from previous prediction calls, returning - * the most information-rich `TrackedContextState` possible. If a match is found, the state will be annotated with the - * input information provided to previous prediction calls and persisted correction-search calculations for re-use. - * - * @param model - * @param context - * @param transformDistribution - */ - analyzeState(model: LexicalModel, - context: Context, - transformDistribution?: Distribution): TrackedContextState { - if(!model.traverseFromRoot) { - // Assumption: LexicalModel provides a valid traverseFromRoot function. (Is technically optional) - // Without it, no 'corrections' may be made; the model can only be used to predict, not correct. - throw "This lexical model does not provide adequate data for correction algorithms and context reuse"; - } - - let tokenizedContext = tokenize(model.wordbreaker || defaultWordbreaker, context); - - if(tokenizedContext.left.length > 0) { - for(let i = this.count - 1; i >= 0; i--) { - const priorMatchState = this.item(i); - const priorTaggedContext = priorMatchState.taggedContext; - if(priorTaggedContext && transformDistribution && transformDistribution.length > 0) { - // Using the potential `matchState` + the incoming transform, do the results line up for - // our observed context? If not, skip it. - // - // Necessary to properly handle multitaps, as there are context rewinds that the - // predictive-text engine is not otherwise warned about. - // - // `priorTaggedContext` must not be `null`! - const doublecheckContext = applyTransform(transformDistribution[0].sample, priorTaggedContext); - if(doublecheckContext.left != context.left) { - continue; - } - } else if(priorTaggedContext?.left != context.left) { - continue; - } - - let resultState = ContextTracker.attemptMatchContext(tokenizedContext.left, this.item(i), transformDistribution); - - if(resultState) { - // Keep it reasonably current! And it's probably fine to have it more than once - // in the history. However, if it's the most current already, there's no need - // to refresh it. - if(this.newest != resultState && this.newest != priorMatchState) { - // Already has a taggedContext. - this.enqueue(priorMatchState); - } - - resultState.taggedContext = context; - if(resultState != this.item(i)) { - this.enqueue(resultState); - } - return resultState; - } - } - } - - // Else: either empty OR we've detected a 'new context'. Initialize from scratch; no prior input information is - // available. Only the results of the prior inputs are known. - // - // Assumption: as a caret needs to move to context before any actual transform distributions occur, - // this state is only reached on caret moves; thus, transformDistribution is actually just a single null transform. - let state = ContextTracker.modelContextState(tokenizedContext.left, transformDistribution, model); - state.taggedContext = context; - this.enqueue(state); - return state; - } - - clearCache() { - while(this.count > 0) { - this.dequeue(); - } - } -} diff --git a/common/web/lm-worker/src/main/model-compositor.ts b/common/web/lm-worker/src/main/model-compositor.ts deleted file mode 100644 index 1908890b64e..00000000000 --- a/common/web/lm-worker/src/main/model-compositor.ts +++ /dev/null @@ -1,788 +0,0 @@ -import * as models from '@keymanapp/models-templates'; -import * as wordBreakers from '@keymanapp/models-wordbreakers'; -import * as correction from './correction/index.js' - -import TransformUtils from './transformUtils.js'; - -export default class ModelCompositor { - private lexicalModel: LexicalModel; - private contextTracker?: correction.ContextTracker; - private static readonly MAX_SUGGESTIONS = 12; - readonly punctuation: LexicalModelPunctuation; - - /** - * Controls the strength of anti-corrective measures for single-character scenarios. - * The base key probability will be raised to this power for this specific case. - * - * Current selection's motivation: (0.5 / 0.4) ^ 16 ~= 35.5. - * - if the most likely has p=0.5 and second-most has p=0.4 - a highly-inaccurate key - * stroke - the net effect will apply a factor of 35.5 to the lexical probability of - * the best key's prediction roots, favoring it in this manner. - * - less extreme edge cases will have a significantly stronger factor, acting as a - * "soft threshold". - * - truly ambiguous, "coin flip" cases will have a lower factor and thus favor the - * more likely words from the pair. - * - Our OSK key-element borders aren't visible to the user, so the 'spot' where - * behavior changes might feel arbitrary to users if we used a hard threshold instead. - */ - private static readonly SINGLE_CHAR_KEY_PROB_EXPONENT = 16; - - private SUGGESTION_ID_SEED = 0; - - private testMode: boolean = false - - constructor(lexicalModel: LexicalModel, testMode?: boolean) { - this.lexicalModel = lexicalModel; - if(lexicalModel.traverseFromRoot) { - this.contextTracker = new correction.ContextTracker(); - } - this.punctuation = ModelCompositor.determinePunctuationFromModel(lexicalModel); - this.testMode = !!testMode; - } - - private predictFromCorrections(corrections: ProbabilityMass[], context: Context): Distribution { - let returnedPredictions: Distribution = []; - - for(let correction of corrections) { - let predictions = this.lexicalModel.predict(correction.sample, context); - - let predictionSet = predictions.map(function(pair: ProbabilityMass) { - let transform = correction.sample; - let inputProb = correction.p; - // Let's not rely on the model to copy transform IDs. - // Only bother is there IS an ID to copy. - if(transform.id !== undefined) { - pair.sample.transformId = transform.id; - } - - let prediction = {sample: pair.sample, p: pair.p * inputProb}; - return prediction; - }, this); - - returnedPredictions = returnedPredictions.concat(predictionSet); - } - - return returnedPredictions; - } - - predict(transformDistribution: Transform | Distribution, context: Context): Suggestion[] { - let suggestionDistribution: Distribution = []; - let lexicalModel = this.lexicalModel; - let punctuation = this.punctuation; - - if(!(transformDistribution instanceof Array)) { - transformDistribution = [ {sample: transformDistribution, p: 1.0} ]; - } else if(transformDistribution.length == 0) { - /* - Robust stop-gap: if our other filters somehow fail, this fixes the - zero-length array by making it match the form of the array that - would result if it were instead called with the other legal - parameter type - a single Transform. - - Unfortunately, the method will lack all data about even - the original keystroke that resulted in the call... but this way, - we can at least get some predictions rather than shortcutting - and producing none whatsoever. - */ - transformDistribution.push({ - sample: { - insert: '', - deleteLeft: 0 - }, - p: 1.0 - }) - } - - let inputTransform = transformDistribution.sort(function(a, b) { - return b.p - a.p; - })[0].sample; - - // Only allow new-word suggestions if space was the most likely keypress. - let allowSpace = TransformUtils.isWhitespace(inputTransform); - let allowBksp = TransformUtils.isBackspace(inputTransform); - - let postContext = models.applyTransform(inputTransform, context); - let keepOptionText = this.wordbreak(postContext); - let keepOption: Outcome = null; - - let rawPredictions: Distribution = []; - - // Used to restore whitespaces if operations would remove them. - let prefixTransform: Transform; - let postContextState: correction.TrackedContextState = null; - - // Section 1: determining 'prediction roots'. - if(!this.contextTracker) { - let predictionRoots: ProbabilityMass[]; - - // Generates raw prediction distributions for each valid input. Can only 'correct' - // against the final input. - // - // This is the old, 12.0-13.0 'correction' style. - if(allowSpace) { - // Detect start of new word; prevent whitespace loss here. - predictionRoots = [{sample: inputTransform, p: 1.0}]; - prefixTransform = inputTransform; - } else { - predictionRoots = transformDistribution.map((alt) => { - let transform = alt.sample; - - // Filter out special keys unless they're expected. - if(TransformUtils.isWhitespace(transform) && !allowSpace) { - return null; - } else if(TransformUtils.isBackspace(transform) && !allowBksp) { - return null; - } - - return alt; - }); - } - - // Remove `null` entries. - predictionRoots = predictionRoots.filter(tuple => !!tuple); - - // Running in bulk over all suggestions, duplicate entries may be possible. - rawPredictions = this.predictFromCorrections(predictionRoots, context); - } else { - // Token replacement benefits greatly from knowledge of the prior context state. - let contextState = this.contextTracker.analyzeState(this.lexicalModel, context, null); - // Corrections and predictions are based upon the post-context state, though. - postContextState = this.contextTracker.analyzeState(this.lexicalModel, - postContext, - !TransformUtils.isEmpty(inputTransform) ? - transformDistribution: - null - ); - - // TODO: Should we filter backspaces & whitespaces out of the transform distribution? - // Ideally, the answer (in the future) will be no, but leaving it in right now may pose an issue. - - // Rather than go "full hog" and make a priority queue out of the eventual, future competing search spaces... - // let's just note that right now, there will only ever be one. - // - // The 'eventual' logic will be significantly more complex, though still manageable. - let searchSpace = postContextState.searchSpace[0]; - - // No matter the prediction, once we know the root of the prediction, we'll always 'replace' the - // same amount of text. We can handle this before the big 'prediction root' loop. - let deleteLeft = 0; - - // The amount of text to 'replace' depends upon whatever sort of context change occurs - // from the received input. - const postContextTokens = postContextState.tokens; - let postContextLength = postContextTokens.length; - let contextLengthDelta = postContextTokens.length - contextState.tokens.length; - // If the context now has more tokens, the token we'll be 'predicting' didn't originally exist. - if(postContextLength == 0 || contextLengthDelta > 0) { - // As the word/token being corrected/predicted didn't originally exist, there's no - // part of it to 'replace'. - deleteLeft = 0; - - // If the new token is due to whitespace or due to a different input type that would - // likely imply a tokenization boundary... - if(TransformUtils.isWhitespace(inputTransform)) { - /* TODO: consider/implement: the second half of the comment above. - * For example: on input of a `'`, predict new words instead of replacing the `'`. - * (since after a letter, the `'` will be ignored, anyway) - * - * Idea: if the model's most likely prediction (with no root) would make a new - * token if appended to the current token, that's probably a good case. - * Keeps the check simple & quick. - * - * Might need a mixed mode, though: ';' is close enough that `l` is a reasonable - * fat-finger guess. So yeah, we're not addressing this idea right now. - * - so... consider multiple context behavior angles when building prediction roots? - * - * May need something similar to help handle contractions during their construction, - * but that'd be within `ContextTracker`. - * can' => [`can`, `'`] - * can't => [`can't`] (WB6, 7 of https://unicode.org/reports/tr29/#Word_Boundary_Rules) - * - * (Would also helps WB7b+c for Hebrew text) - */ - - // Infer 'new word' mode, even if we received new text when reaching - // this position. That new text didn't exist before, so still - nothing - // to 'replace'. - prefixTransform = inputTransform; - context = postContext; // As far as predictions are concerned, the post-context state - // should not be replaced. Predictions are to be rooted on - // text "up for correction" - so we want a null root for this - // branch. - contextState = postContextState; - } - // If the tokenized context length is shorter... sounds like a backspace (or similar). - } else if (contextLengthDelta < 0) { - /* Ooh, we've dropped context here. Almost certainly from a backspace. - * Even if we drop multiple tokens... well, we know exactly how many chars - * were actually deleted - `inputTransform.deleteLeft`. - * Since we replace a word being corrected/predicted, we take length of the remaining - * context's tail token in addition to however far was deleted to reach that state. - */ - deleteLeft = this.wordbreak(postContext).kmwLength() + inputTransform.deleteLeft; - } else { - // Suggestions are applied to the pre-input context, so get the token's original length. - // We're on the same token, so just delete its text for the replacement op. - deleteLeft = this.wordbreak(context).kmwLength(); - } - - // Is the token under construction newly-constructed / is there no pre-existing root? - // If so, we want to strongly avoid overcorrection, even for 'nearby' keys. - // (Strong lexical frequency differences can easily cause overcorrection when only - // one key's available.) - // - // NOTE: we only want this applied word-initially, when any corrections 'correct' - // 100% of the word. Things are generally fine once it's not "all or nothing." - let tailToken = postContextTokens[postContextTokens.length - 1]; - const isTokenStart = tailToken.transformDistributions.length <= 1; - - // TODO: whitespace, backspace filtering. Do it here. - // Whitespace is probably fine, actually. Less sure about backspace. - - let bestCorrectionCost: number; - const SEARCH_TIMEOUT = this.testMode ? 0 : correction.SearchSpace.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL; - for(let matches of searchSpace.getBestMatches(SEARCH_TIMEOUT)) { - // Corrections obtained: now to predict from them! - let predictionRoots = matches.map(function(match) { - let correction = match.matchString; - - // Worth considering: extend Traversal to allow direct prediction lookups? - // let traversal = match.finalTraversal; - - // Replace the existing context with the correction. - let correctionTransform: Transform = { - insert: correction, // insert correction string - deleteLeft: deleteLeft, - id: inputTransform.id // The correction should always be based on the most recent external transform/transcription ID. - } - - let rootCost = match.totalCost; - - /* If we're dealing with the FIRST keystroke of a new sequence, we'll **dramatically** boost - * the exponent to ensure only VERY nearby corrections have a chance of winning, and only if - * there are significantly more likely words. We only need this to allow very minor fat-finger - * adjustments for 100% keystroke-sequence corrections in order to prevent finickiness on - * key borders. - * - * Technically, the probabilities this produces won't be normalized as-is... but there's no - * true NEED to do so for it, even if it'd be 'nice to have'. Consistently tracking when - * to apply it could become tricky, so it's simpler to leave out. - * - * Worst-case, it's possible to temporarily add normalization if a code deep-dive - * is needed in the future. - */ - if(isTokenStart) { - /* Suppose a key distribution: most likely with p=0.5, second-most with 0.4 - a pretty - * ambiguous case that would only arise very near the center of the boundary between two keys. - * Raising (0.5/0.4)^16 ~= 35.53. (At time of writing, SINGLE_CHAR_KEY_PROB_EXPONENT = 16.) - * That seems 'within reason' for correction very near boundaries. - * - * So, with the second-most-likely key being that close in probability, its best suggestion - * must be ~ 35.5x more likely than that of the truly-most-likely key to "win". So, it's not - * a HARD cutoff, but more of a 'soft' one. Keeping the principles in mind documented above, - * it's possible to tweak this to a more harsh or lenient setting if desired, rather than - * being totally "all or nothing" on which key is taken for highly-ambiguous keypresses. - */ - rootCost *= ModelCompositor.SINGLE_CHAR_KEY_PROB_EXPONENT; // note the `Math.exp` below. - } - - return { - sample: correctionTransform, - p: Math.exp(-rootCost) - }; - }, this); - - // Running in bulk over all suggestions, duplicate entries may be possible. - let predictions = this.predictFromCorrections(predictionRoots, context); - - // Only set 'best correction' cost when a correction ACTUALLY YIELDS predictions. - if(predictions.length > 0 && bestCorrectionCost === undefined) { - bestCorrectionCost = -Math.log(predictionRoots[0].p); - } - - rawPredictions = rawPredictions.concat(predictions); - // TODO: We don't currently de-duplicate predictions at this point quite yet, so - // it's technically possible that we return too few. - - let correctionCost = matches[0].totalCost; - // Searching a bit longer is permitted when no predictions have been found. - if(correctionCost >= bestCorrectionCost + 8) { - break; - // If enough have been found, we're safe to terminate earlier. - } else if(rawPredictions.length >= ModelCompositor.MAX_SUGGESTIONS) { - if(correctionCost >= bestCorrectionCost + 4) { // e^-4 = 0.0183156388. Allows "80%" of an extra edit. - // Very useful for stopping 'sooner' when words reach a sufficient length. - break; - } else { - // Sort the prediction list; we need them in descending order for the next check. - rawPredictions.sort(function(a, b) { - return b.p - a.p; - }); - - // If the best suggestion from the search's current tier fails to beat the worst - // pending suggestion from previous tiers, assume all further corrections will - // similarly fail to win; terminate the search-loop. - if(rawPredictions[ModelCompositor.MAX_SUGGESTIONS-1].p > Math.exp(-correctionCost)) { - break; - } - } - } - } - } - - // Section 2 - post-analysis for our generated predictions, managing 'keep'. - // Assumption: Duplicated 'displayAs' properties indicate duplicated Suggestions. - // When true, we can use an 'associative array' to de-duplicate everything. - let suggestionDistribMap: {[key: string]: ProbabilityMass} = {}; - let currentCasing: CasingForm = null; - if(lexicalModel.languageUsesCasing) { - currentCasing = this.detectCurrentCasing(postContext); - } - - let baseWord = this.wordbreak(context); - - // Deduplicator + annotator of 'keep' suggestions. - for(let prediction of rawPredictions) { - // Combine duplicate samples. - let displayText = prediction.sample.displayAs; - let preserveAsKeep = displayText == keepOptionText; - - // De-duplication should be case-insensitive, but NOT - // diacritic-insensitive. - if(this.lexicalModel.languageUsesCasing) { - preserveAsKeep = preserveAsKeep || displayText == this.lexicalModel.applyCasing('lower', keepOptionText); - } - - if(preserveAsKeep) { - // Preserve the original, pre-keyed version of the text. - if(!keepOption) { - let baseTransform = prediction.sample.transform; - - let keepTransform = { - insert: keepOptionText, - deleteLeft: baseTransform.deleteLeft, - deleteRight: baseTransform.deleteRight, - id: baseTransform.id - } - - let intermediateKeep = models.transformToSuggestion(keepTransform, prediction.p); - keepOption = this.toAnnotatedSuggestion(intermediateKeep, 'keep', models.QuoteBehavior.noQuotes); - keepOption.matchesModel = true; - - // Since we replaced the original Suggestion with a keep-annotated one, - // we must manually preserve the transform ID. - keepOption.transformId = prediction.sample.transformId; - } else if(keepOption.p && prediction.p) { - keepOption.p += prediction.p; - } - } else { - // Apply capitalization rules now; facilitates de-duplication of suggestions - // that may be caused as a result. - // - // Example: "apple" and "Apple" are separate when 'lower', but identical for 'initial' and 'upper'. - if(currentCasing && currentCasing != 'lower') { - this.applySuggestionCasing(prediction.sample, baseWord, currentCasing); - // update the mapping string, too. - displayText = prediction.sample.displayAs; - } - - let existingSuggestion = suggestionDistribMap[displayText]; - if(existingSuggestion) { - existingSuggestion.p += prediction.p; - } else { - suggestionDistribMap[displayText] = prediction; - } - } - } - - // Generate a default 'keep' option if one was not otherwise produced. - if(!keepOption && keepOptionText != '') { - // IMPORTANT: duplicate the original transform. Causes nasty side-effects - // for context-tracking otherwise! - let keepTransform: Transform = { ...inputTransform }; - - // 1 is a filler value; goes unused b/c is for a 'keep'. - let keepSuggestion = models.transformToSuggestion(keepTransform, 1); - // This is the one case where the transform doesn't insert the full word; we need to override the displayAs param. - keepSuggestion.displayAs = keepOptionText; - - keepOption = this.toAnnotatedSuggestion(keepSuggestion, 'keep'); - keepOption.matchesModel = false; - } - - // Section 3: Finalize suggestions, truncate list to the N (MAX_SUGGESTIONS) most optimal, return. - - // Now that we've calculated a unique set of probability masses, time to make them into a proper - // distribution and prep for return. - for(let key in suggestionDistribMap) { - let pair = suggestionDistribMap[key]; - suggestionDistribution.push(pair); - } - - suggestionDistribution = suggestionDistribution.sort(function(a, b) { - return b.p - a.p; // Use descending order - we want the largest probabilty suggestions first! - }); - - let suggestions = suggestionDistribution.splice(0, ModelCompositor.MAX_SUGGESTIONS).map(function(value) { - let sample: Suggestion & { - p?: number, - "lexical-p"?: number, - "correction-p"?: number - } = value.sample; - - if(sample['p']) { - // For analysis / debugging - sample['lexical-p'] = sample['p']; - sample['correction-p'] = value.p / sample['p']; - // Use of the Trie model always exposed the lexical model's probability for a word to KMW. - // It's useful for debugging right now, so may as well repurpose it as the posterior. - // - // We still condition on 'p' existing so that test cases aren't broken. - sample['p'] = value.p; - } - // - return sample; - }); - - if(keepOption) { - suggestions = [ keepOption as Suggestion ].concat(suggestions); - } - - // Apply 'after word' punctuation and casing (when applicable). Also, set suggestion IDs. - // We delay until now so that utility functions relying on the unmodified Transform may execute properly. - - let compositor = this; - suggestions.forEach(function(suggestion) { - // Valid 'keep' suggestions may have zero length; we still need to evaluate the following code - // for such cases. - - // Do we need to manipulate the suggestion's transform based on the current state of the context? - if(!context.right) { - suggestion.transform.insert += punctuation.insertAfterWord; - } else { - // If we're mid-word, delete its original post-caret text. - const tokenization = compositor.tokenize(context); - if(tokenization && tokenization.caretSplitsToken) { - // While we wait on the ability to provide a more 'ideal' solution, let's at least - // go with a more stable, if slightly less ideal, solution for now. - // - // A predictive text default (on iOS, at least) - immediately wordbreak - // on suggestions accepted mid-word. - suggestion.transform.insert += punctuation.insertAfterWord; - } - } - - // If this is a suggestion after wordbreak input, make sure we preserve the wordbreak transform! - if(prefixTransform) { - let mergedTransform = models.buildMergedTransform(prefixTransform, suggestion.transform); - mergedTransform.id = suggestion.transformId; - - // Temporarily and locally drops 'readonly' semantics so that we can reassign the transform. - // See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#improved-control-over-mapped-type-modifiers - let mutableSuggestion = suggestion as {-readonly [transform in keyof Suggestion]: Suggestion[transform]}; - - // Assignment via by-reference behavior, as suggestion is an object - mutableSuggestion.transform = mergedTransform; - } - - suggestion.id = compositor.SUGGESTION_ID_SEED; - compositor.SUGGESTION_ID_SEED++; - }); - - // Store the suggestions on the final token of the current context state (if it exists). - // Or, once phrase-level suggestions are possible, on whichever token serves as each prediction's root. - if(postContextState) { - postContextState.tail.replacements = suggestions.map(function(suggestion) { - return { - suggestion: suggestion, - tokenWidth: 1 - } - }); - } - - return suggestions; - } - - // Responsible for applying casing rules to suggestions. - private applySuggestionCasing(suggestion: Suggestion, baseWord: USVString, casingForm: CasingForm) { - // Step 1: does the suggestion replace the whole word? If not, we should extend the suggestion to do so. - let unchangedLength = baseWord.kmwLength() - suggestion.transform.deleteLeft; - - if(unchangedLength > 0) { - suggestion.transform.deleteLeft += unchangedLength; - suggestion.transform.insert = baseWord.kmwSubstr(0, unchangedLength) + suggestion.transform.insert; - } - - // Step 2: Now that the transform affects the whole word, we may safely apply casing rules. - suggestion.transform.insert = this.lexicalModel.applyCasing(casingForm, suggestion.transform.insert); - suggestion.displayAs = this.lexicalModel.applyCasing(casingForm, suggestion.displayAs); - } - - private toAnnotatedSuggestion(suggestion: Outcome, - annotationType: SuggestionTag, - quoteBehavior?: models.QuoteBehavior): Outcome; - private toAnnotatedSuggestion(suggestion: Outcome, - annotationType: 'keep', - quoteBehavior?: models.QuoteBehavior): Outcome; - private toAnnotatedSuggestion(suggestion: Outcome, - annotationType: 'revert', - quoteBehavior?: models.QuoteBehavior): Outcome; - private toAnnotatedSuggestion(suggestion: Outcome, - annotationType: SuggestionTag, - quoteBehavior: models.QuoteBehavior = models.QuoteBehavior.default): Outcome { - // A method-internal 'import' of the enum. - let QuoteBehavior = models.QuoteBehavior; - - let defaultQuoteBehavior = QuoteBehavior.noQuotes; - if(annotationType == 'keep' || annotationType == 'revert') { - defaultQuoteBehavior = QuoteBehavior.useQuotes; - } - - return { - transform: suggestion.transform, - transformId: suggestion.transformId, - displayAs: QuoteBehavior.apply(quoteBehavior, suggestion.displayAs, this.punctuation, defaultQuoteBehavior), - tag: annotationType, - p: suggestion.p - }; - } - - /** - * Returns the punctuation used for this model, filling out unspecified fields - */ - private static determinePunctuationFromModel(model: LexicalModel): LexicalModelPunctuation { - let defaults = DEFAULT_PUNCTUATION; - - // Use the defaults of the model does not provide any punctuation at all. - if (!model.punctuation) - return defaults; - - let specifiedPunctuation = model.punctuation; - let insertAfterWord = specifiedPunctuation.insertAfterWord; - if (insertAfterWord !== '' && !insertAfterWord) { - insertAfterWord = defaults.insertAfterWord; - } - - let quotesForKeepSuggestion = specifiedPunctuation.quotesForKeepSuggestion; - if (!quotesForKeepSuggestion) { - quotesForKeepSuggestion = defaults.quotesForKeepSuggestion; - } - - let isRTL = specifiedPunctuation.isRTL; - // Default: false / undefined, so no need to directly specify it. - - return { - insertAfterWord, quotesForKeepSuggestion, isRTL - } - } - - acceptSuggestion(suggestion: Suggestion, context: Context, postTransform?: Transform): Reversion { - // Step 1: generate and save the reversion's Transform. - let sourceTransform = suggestion.transform; - let deletedLeftChars = context.left.kmwSubstr(-sourceTransform.deleteLeft, sourceTransform.deleteLeft); - let insertedLength = sourceTransform.insert.kmwLength(); - - let reversionTransform: Transform = { - insert: deletedLeftChars, - deleteLeft: insertedLength - }; - - // Step 2: building the proper 'displayAs' string for the Reversion - let postContext = context; - if(postTransform) { - // The code above restores the state to the context at the time the `Suggestion` was created. - // `postTransform` handles any missing context that came later. - reversionTransform = models.buildMergedTransform(reversionTransform, postTransform); - - // Now that we've built the reversion based upon the Suggestion's original context, - // we manipulate it in order to get a proper 'displayAs' string. - postContext = models.applyTransform(postTransform, postContext); - } - - let revertedPrefix: string; - let postContextTokenization = this.tokenize(postContext); - if(postContextTokenization) { - // Handles display string for reversions triggered by accepting a suggestion mid-token. - if(postContextTokenization.left.length > 0) { - revertedPrefix = postContextTokenization.left[postContextTokenization.left.length-1]; - } else { - revertedPrefix = ''; - } - revertedPrefix += postContextTokenization.caretSplitsToken ? postContextTokenization.right[0] : ''; - } else { - revertedPrefix = this.wordbreak(postContext); - } - - let firstConversion = models.transformToSuggestion(reversionTransform); - firstConversion.displayAs = revertedPrefix; - - // Build the actual Reversion, which is technically an annotated Suggestion. - // Since we're outside of the standard `predict` control path, we'll need to - // set the Reversion's ID directly. - let reversion = this.toAnnotatedSuggestion(firstConversion, 'revert'); - if(suggestion.transformId != null) { - reversion.transformId = -suggestion.transformId; - } - if(suggestion.id != null) { - // Since a reversion inverts its source suggestion, we set its ID to be the - // additive inverse of the source suggestion's ID. Makes easy mapping / - // verification later. - reversion.id = -suggestion.id; - } else { - reversion.id = -this.SUGGESTION_ID_SEED; - this.SUGGESTION_ID_SEED++; - } - - // Step 3: if we track Contexts, update the tracking data as appropriate. - if(this.contextTracker) { - let contextState = this.contextTracker.newest; - if(!contextState) { - contextState = this.contextTracker.analyzeState(this.lexicalModel, context); - } - - contextState.tail.activeReplacementId = suggestion.id; - let acceptedContext = models.applyTransform(suggestion.transform, context); - this.contextTracker.analyzeState(this.lexicalModel, acceptedContext); - } - - return reversion; - } - - applyReversion(reversion: Reversion, context: Context): Suggestion[] { - // If we are unable to track context (because the model does not support LexiconTraversal), - // we need a "fallback" strategy. - let compositor = this; - let fallbackSuggestions = function() { - let revertedContext = models.applyTransform(reversion.transform, context); - let suggestions = compositor.predict({insert: '', deleteLeft: 0}, revertedContext); - suggestions.forEach(function(suggestion) { - // A reversion's transform ID is the additive inverse of its original suggestion; - // we revert to the state of said original suggestion. - suggestion.transformId = -reversion.transformId; - }); - return suggestions; - } - - if(!this.contextTracker) { - return fallbackSuggestions(); - } - - // When the context is tracked, we prefer the tracked information. - let contextMatchFound = false; - for(let c = this.contextTracker.count - 1; c >= 0; c--) { - let contextState = this.contextTracker.item(c); - - if(contextState.tail.activeReplacementId == -reversion.id) { - contextMatchFound = true; - break; - } - } - - if(!contextMatchFound) { - return fallbackSuggestions(); - } - - // Remove all contexts more recent than the one we're reverting to. - while(this.contextTracker.newest.tail.activeReplacementId != -reversion.id) { - this.contextTracker.popNewest(); - } - - this.contextTracker.newest.tail.revert(); - - // Will need to be modified a bit if/when phrase-level suggestions are implemented. - // Those will be tracked on the first token of the phrase, which won't be the tail - // if they cover multiple tokens. - let suggestions = this.contextTracker.newest.tail.replacements.map(function(trackedSuggestion) { - return trackedSuggestion.suggestion; - }); - - suggestions.forEach(function(suggestion) { - // A reversion's transform ID is the additive inverse of its original suggestion; - // we revert to the state of said original suggestion. - suggestion.transformId = -reversion.transformId; - }); - return suggestions; - } - - private wordbreak(context: Context): string { - let model = this.lexicalModel; - - if(model.wordbreaker || !model.wordbreak) { - // We don't need a 12.0 / 13.0 compatibility mode here. - // We're either relying on defaults or on the 14.0+ wordbreaker spec. - let wordbreaker = model.wordbreaker || wordBreakers.default; - - return models.wordbreak(wordbreaker, context); - } else { - // 1. This model does not provide a model following the 14.0+ wordbreaking spec - // 2. This model DOES define a custom wordbreaker following the 12.0-13.0 spec. - - // Since the model relies on custom wordbreaking behavior, we need to use the - // old, deprecated wordbreaking pattern. - return model.wordbreak(context); - } - } - - private tokenize(context: Context): models.Tokenization { - let model = this.lexicalModel; - - if(model.wordbreaker) { - return models.tokenize(model.wordbreaker, context); - } else { - return null; - } - } - - public resetContext(context: Context) { - // Force-resets the context, throwing out any previous fat-finger data, etc. - // Designed for use when the caret has been directly moved and/or the context sourced from a different control - // than before. - if(this.contextTracker) { - this.contextTracker.clearCache(); - this.contextTracker.analyzeState(this.lexicalModel, context, null); - } - } - - private detectCurrentCasing(context: Context): CasingForm { - let model = this.lexicalModel; - - let text = this.wordbreak(context); - - if(!model.languageUsesCasing) { - throw "Invalid attempt to detect casing: languageUsesCasing is set to false"; - } - - if(!model.applyCasing) { - // The worker should automatically 'sub in' default behavior during the model's load if that - // function isn't defined explicitly as part of the model. - throw "Invalid LMLayer state: languageUsesCasing is set to true, but no applyCasing function exists"; - } - - // If the user has selected Shift or Caps layer, that overrides our - // text analysis - if(context.casingForm == 'upper' || context.casingForm == 'initial') { - return context.casingForm; - } - if(model.applyCasing('lower', text) == text) { - return 'lower'; - } else if(model.applyCasing('upper', text) == text) { - // If only a single character has been input, assume we're in 'initial' mode. - return text.kmwLength() > 1 ? 'upper' : 'initial'; - } else if(model.applyCasing('initial', text) == text) { - // We check 'initial' last, as upper-case input is indistinguishable. - return 'initial'; - } else { - // If we do not have a casing form given to us by the keyboard, then - // 'null' is returned when no casing pattern matches the input. - return context.casingForm ?? null; - } - } -} - -/** - * The default punctuation and spacing produced by the model. - */ -const DEFAULT_PUNCTUATION: LexicalModelPunctuation = { - quotesForKeepSuggestion: { open: `“`, close: `”`}, - insertAfterWord: " " , -}; diff --git a/common/web/lm-worker/src/test/mocha/cases/edit-distance/context-tracker.js b/common/web/lm-worker/src/test/mocha/cases/edit-distance/context-tracker.js deleted file mode 100644 index 5c9d4812b68..00000000000 --- a/common/web/lm-worker/src/test/mocha/cases/edit-distance/context-tracker.js +++ /dev/null @@ -1,189 +0,0 @@ -import { assert } from 'chai'; - -import { ContextTracker } from '#./correction/context-tracker.js'; -import ModelCompositor from '#./model-compositor.js'; -import * as models from '#./models/index.js'; - -import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; - -describe('ContextTracker', function() { - function toWrapperDistribution(transform) { - return [{ - sample: transform, - p: 1.0 - }]; - } - - describe('attemptMatchContext', function() { - it("properly matches and aligns when lead token is removed", function() { - let existingContext = ["an", "apple", "a", "day", "keeps", "the", "doctor"]; - let transform = { - insert: '', - deleteLeft: 0 - } - let newContext = existingContext.slice(0); - newContext.splice(0, 1); - let rawTokens = ["apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor"]; - - let existingState = ContextTracker.modelContextState(existingContext); - let state = ContextTracker.attemptMatchContext(newContext, existingState, null, toWrapperDistribution(transform)); - assert.isNotNull(state); - assert.deepEqual(state.tokens.map(token => token.raw), rawTokens); - }); - - it("properly matches and aligns when final token is edited", function() { - let existingContext = ["an", "apple", "a", "day", "keeps", "the", "docto"]; - let transform = { - insert: 'r', - deleteLeft: 0 - } - let newContext = existingContext.slice(0); - newContext[newContext.length - 1] = 'doctor'; - let rawTokens = ["an", null, "apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor"]; - - let existingState = ContextTracker.modelContextState(existingContext); - let state = ContextTracker.attemptMatchContext(newContext, existingState, null, toWrapperDistribution(transform)); - assert.isNotNull(state); - assert.deepEqual(state.tokens.map(token => token.raw), rawTokens); - }); - - it("properly matches and aligns when a 'wordbreak' is added", function() { - let existingContext = ["an", "apple", "a", "day", "keeps", "the", "doctor"]; - let transform = { - insert: ' ', - deleteLeft: 0 - } - let newContext = existingContext.slice(0); - newContext.push(''); - let rawTokens = ["an", null, "apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor", null, ""]; - - let existingState = ContextTracker.modelContextState(existingContext); - let state = ContextTracker.attemptMatchContext(newContext, existingState, toWrapperDistribution(transform)); - assert.isNotNull(state); - assert.deepEqual(state.tokens.map(token => token.raw), rawTokens); - - // The 'wordbreak' transform - assert.isNotEmpty(state.tokens[state.tokens.length - 2].transformDistributions); - assert.isEmpty(state.tokens[state.tokens.length - 1].transformDistributions); - }); - - it("properly matches and aligns when an implied 'wordbreak' occurs (as when following \"'\")", function() { - let existingContext = ["'"]; - let transform = { - insert: 'a', - deleteLeft: 0 - } - let newContext = existingContext.slice(0); - newContext.push('a'); // The incoming transform should produce a new token WITH TEXT. - let rawTokens = ["'", null, "a"]; - - let existingState = ContextTracker.modelContextState(existingContext); - let state = ContextTracker.attemptMatchContext(newContext, existingState, toWrapperDistribution(transform)); - assert.isNotNull(state); - assert.deepEqual(state.tokens.map(token => token.raw), rawTokens); - - // The 'wordbreak' transform - assert.isEmpty(state.tokens[state.tokens.length - 2].transformDistributions); - assert.isNotEmpty(state.tokens[state.tokens.length - 1].transformDistributions); - }); - - it("properly matches and aligns when lead token is removed AND a 'wordbreak' is added'", function() { - let existingContext = ["an", "apple", "a", "day", "keeps", "the", "doctor"]; - let transform = { - insert: ' ', - deleteLeft: 0 - } - let newContext = existingContext.slice(0); - newContext.splice(0, 1); - newContext.push(''); - let rawTokens = ["apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor", null, ""]; - - let existingState = ContextTracker.modelContextState(existingContext); - let state = ContextTracker.attemptMatchContext(newContext, existingState, toWrapperDistribution(transform)); - assert.isNotNull(state); - assert.deepEqual(state.tokens.map(token => token.raw), rawTokens); - - // The 'wordbreak' transform - assert.isNotEmpty(state.tokens[state.tokens.length - 2].transformDistributions); - assert.isEmpty(state.tokens[state.tokens.length - 1].transformDistributions); - }); - }); - - describe('modelContextState', function() { - it('models without final wordbreak', function() { - let context = ["an", "apple", "a", "day", "keeps", "the", "doctor"]; - let rawTokens = ["an", null, "apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor"]; - - let state = ContextTracker.modelContextState(context); - assert.deepEqual(state.tokens.map(token => token.raw), rawTokens); - }); - - it('models with final wordbreak', function() { - let context = ["an", "apple", "a", "day", "keeps", "the", "doctor", ""]; - let rawTokens = ["an", null, "apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor", null, ""]; - - let state = ContextTracker.modelContextState(context); - assert.deepEqual(state.tokens.map(token => token.raw), rawTokens); - }); - }); - - describe('suggestion acceptance tracking', function() { - let englishPunctuation = { - quotesForKeepSuggestion: { open: `“`, close: `”`}, - insertAfterWord: ' ' - }; - - it('tracks an accepted suggestion', function() { - let baseSuggestion = { - transform: { - insert: 'world ', - deleteLeft: 3, - id: 0 - }, - transformId: 0, - id: 1, - displayAs: 'world' - }; - - let baseContext = { - left: 'hello wor', startOfBuffer: true, endOfBuffer: true - }; - - // Represents the keystroke that triggered the suggestion. It's not technically part - // of the Context when the suggestion is built. - let postTransform = { - insert: 'l', - deleteLeft: 0 - }; - - let options = { - punctuation: englishPunctuation - }; - - let model = new models.TrieModel(jsonFixture('models/tries/english-1000'), options); - let compositor = new ModelCompositor(model); - let baseContextState = compositor.contextTracker.analyzeState(model, baseContext); - - baseContextState.tail.replacements = [{ - suggestion: baseSuggestion, - tokenWidth: 1 - }]; - - let reversion = compositor.acceptSuggestion(baseSuggestion, baseContext, postTransform); - - // Actual test assertion - was the replacement tracked? - assert.equal(baseContextState.tail.activeReplacementId, baseSuggestion.id); - assert.equal(reversion.id, -baseSuggestion.id); - - // Next step - on the followup context, is the replacement still active? - let postContext = models.applyTransform(baseSuggestion.transform, baseContext); - let postContextState = compositor.contextTracker.analyzeState(model, postContext); - - // Penultimate token corresponds to whitespace, which does not have a 'raw' representation. - assert.isNull(postContextState.tokens[postContextState.tokens.length - 2].raw); - - // Final token is empty (follows a wordbreak) - assert.equal(postContextState.tail.raw, ''); - }); - }); -}); \ No newline at end of file diff --git a/common/web/lm-worker/src/test/test-runner/web-test-runner.CI.config.mjs b/common/web/lm-worker/src/test/test-runner/web-test-runner.CI.config.mjs deleted file mode 100644 index d18a8a9cb09..00000000000 --- a/common/web/lm-worker/src/test/test-runner/web-test-runner.CI.config.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-check -import BASE_CONFIG from './web-test-runner.config.mjs'; -import teamcityReporter from '@keymanapp/common-test-resources/test-runner-TC-reporter.mjs'; -import { sessionStabilityReporter } from '@keymanapp/common-test-resources/test-runner-stability-reporter.mjs'; - -/** @type {import('@web/test-runner').TestRunnerConfig} */ -export default { - ...BASE_CONFIG, - reporters: [ - teamcityReporter(), /* custom-written, for CI-friendly reports */ - sessionStabilityReporter({ciMode: true}) - ] -} \ No newline at end of file diff --git a/common/web/lm-worker/tsconfig.json b/common/web/lm-worker/tsconfig.json deleted file mode 100644 index f439e969409..00000000000 --- a/common/web/lm-worker/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../models/tsconfig.kmw-worker-base.json", - - "compilerOptions": { - "baseUrl": "./", - "outDir": "build/obj", - "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", - "rootDir": "./src/main", - - // As this one is the one that directly interfaces with the worker (from the inside) - "lib": ["webworker", "es6"], - }, - "references": [ - // types - { "path": "../../models/types" }, - { "path": "../lm-message-types" }, - // modules - { "path": "../keyman-version" }, - { "path": "../utils" }, - { "path": "../../models/templates" }, - { "path": "../../models/wordbreakers" }, - ], - "include": [ - "src/main/**/*.ts" - ] -} diff --git a/common/web/recorder/tsconfig.json b/common/web/recorder/tsconfig.json deleted file mode 100644 index 1a38d48f035..00000000000 --- a/common/web/recorder/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../tsconfig.kmw-main-base.json", - - "compilerOptions": { - "types": ["node"], - "baseUrl": "./", - "outDir": "build/obj/", - "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", - "rootDir": "./src" - }, - - "include": [ - "src/**/*.ts" - ], - - "references": [ - { "path": "../keyman-version" }, - { "path": "../utils/" }, - { "path": "../keyboard-processor/" }, - { "path": "../lm-message-types" } - ], -} diff --git a/common/web/tsconfig.kmw-main-base.json b/common/web/tsconfig.kmw-main-base.json deleted file mode 100644 index 0ed8703e5e2..00000000000 --- a/common/web/tsconfig.kmw-main-base.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../web/tsconfig.base.json" -} diff --git a/common/web/tslib/README.md b/common/web/tslib/README.md deleted file mode 100644 index 4c87474f627..00000000000 --- a/common/web/tslib/README.md +++ /dev/null @@ -1,15 +0,0 @@ -The default import setup for the `tslib` package is unfortunately incompatible with `esbuild` when in ES5 mode. But... -with a little elbow grease, we can fix that with _this_ package by importing its ES5-compatible file and exporting it -as _this_ package's default export for use in anything looking to `"importHelpers"`. - -To utilize this with `esbuild` while enabling the `"importHelpers"` compilation option in your tsconfig.json, you'll want -to set the following in your `esbuild` config: - -```javascript - alias: { - 'tslib': '@keymanapp/tslib' - }, -``` - -Note that esbuild 0.15.16 is the minimum required version to utilize the 'alias' feature necessary to replace `tslib` for -`tsc`-generated `import { /* */ } from 'tslib'` statements that result from enabling `"importHelpers"` in a tsconfig. \ No newline at end of file diff --git a/common/web/tslib/build.sh b/common/web/tslib/build.sh deleted file mode 100755 index 30a770a0ddc..00000000000 --- a/common/web/tslib/build.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" - -################################ Main script ################################ - -builder_describe \ - "A ES5 + esbuild compatibility wrapper for the 'tslib' package." \ - clean configure build - -builder_describe_outputs \ - configure "/node_modules" \ - build "/common/web/tslib/build/index.js" - -builder_parse "$@" - -builder_run_action configure verify_npm_setup -builder_run_action clean rm -rf build/ -builder_run_action build tsc --build \ No newline at end of file diff --git a/common/web/tslib/package.json b/common/web/tslib/package.json deleted file mode 100644 index ceec8c64099..00000000000 --- a/common/web/tslib/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@keymanapp/tslib", - "description": "An ES5 + esbuild-compatible wrapper for the 'tslib' library", - "main": "./build/index.js", - "exports": { - ".": { - "types": "./build/index.d.ts", - "import": "./build/index.js" - }, - "./esbuild-tools": { - "types": "./build/esbuild-tools.d.ts", - "import": "./build/esbuild-tools.js" - } - }, - "scripts": { - "build": "gosh ./build.sh build", - "clean": "gosh ./build.sh clean" - }, - "dependencies": { - "tslib": "^2.5.2", - "typescript": "^4.9.5" - }, - "devDependencies": { - "@keymanapp/resources-gosh": "*", - "esbuild": "^0.15.16" - }, - "type": "module" -} diff --git a/common/web/tslib/src/index.ts b/common/web/tslib/src/index.ts deleted file mode 100644 index 2f173ad83fa..00000000000 --- a/common/web/tslib/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from '../../../../node_modules/tslib/tslib.js'; - -export * as tslib from '../../../../node_modules/tslib/tslib.js'; \ No newline at end of file diff --git a/common/web/tslib/tsconfig.json b/common/web/tslib/tsconfig.json deleted file mode 100644 index c7528861ae4..00000000000 --- a/common/web/tslib/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../tsconfig.kmw-main-base.json", - "compilerOptions": { - "allowJs": true, - "importHelpers": false, // This project exists to help define the helpers for the other KMW components. - "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo", - "lib": ["es6", "DOM"], // The latter is b/c esbuild expects the type. - - "baseUrl": "./src", - "outDir": "./build/", - "rootDir": "./src" - }, - "include": [ - "./src/*.ts" - ] -} diff --git a/common/web/types/.eslintrc.cjs b/common/web/types/.eslintrc.cjs index 98db7f812e5..b28ba0941b1 100644 --- a/common/web/types/.eslintrc.cjs +++ b/common/web/types/.eslintrc.cjs @@ -14,7 +14,7 @@ module.exports = { overrides: [ { files: "src/**/*.ts", - extends: ["../../../common/web/eslint/eslintNoNodeImports.js"], + extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], }, ], rules: { diff --git a/common/web/types/build.sh b/common/web/types/build.sh index df11540b424..7141bc10720 100755 --- a/common/web/types/build.sh +++ b/common/web/types/build.sh @@ -73,40 +73,25 @@ function compile_schemas() { node tools/schema-bundler.js } -function copy_cldr_imports() { - # Store CLDR imports - # load all versions that have a cldr_info.json - for CLDR_INFO_PATH in "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/"*/cldr_info.json - do - # TODO-LDML: developer/src/inst/download.in.mak needs these also... - CLDR_PATH=$(dirname "$CLDR_INFO_PATH") - CLDR_VER=$(basename "$CLDR_PATH") - mkdir -p "$THIS_SCRIPT_PATH/build/src/import/$CLDR_VER" - # TODO-LDML: When these are copied, the DOCTYPE will break due to the wrong path. We don't use the DTD so it should be OK. - cp "$CLDR_INFO_PATH" "$CLDR_PATH/import/"*.xml "$THIS_SCRIPT_PATH/build/src/import/$CLDR_VER/" - done -} function do_configure() { compile_schemas verify_npm_setup } -function do_build() { - copy_cldr_imports - tsc --build -} - function do_test() { eslint . tsc --build test - c8 --skip-full --reporter=lcov --reporter=text mocha "${builder_extra_params[@]}" + readonly C8_THRESHOLD=50 + c8 -skip-full --reporter=lcov --reporter=text --lines $C8_THRESHOLD --statements $C8_THRESHOLD --branches $C8_THRESHOLD --functions $C8_THRESHOLD mocha "${builder_extra_params[@]}" + builder_echo warning "Coverage thresholds are currently $C8_THRESHOLD%, which is lower than ideal." + builder_echo warning "Please increase threshold in build.sh as test coverage improves." } #------------------------------------------------------------------------------------------------------------------- builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure do_configure -builder_run_action build do_build +builder_run_action build tsc --build builder_run_action test do_test builder_run_action publish builder_publish_npm diff --git a/common/web/types/package.json b/common/web/types/package.json index 201354b8974..1ba487aafdf 100644 --- a/common/web/types/package.json +++ b/common/web/types/package.json @@ -8,9 +8,11 @@ "unicode" ], "type": "module", + "types": "./build/src/main.d.ts", "exports": { ".": { "es6-bundling": "./src/main.ts", + "types": "./build/src/main.d.ts", "default": "./build/src/main.js" } }, @@ -31,28 +33,20 @@ "dependencies": { "@keymanapp/ldml-keyboard-constants": "*", "@keymanapp/keyman-version": "*", - "restructure": "3.0.1", - "semver": "^7.5.2", - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" + "restructure": "3.0.1" }, "devDependencies": { "@types/chai": "^4.1.7", - "@types/git-diff": "^2.0.3", "@types/mocha": "^5.2.7", "@types/node": "^20.4.1", - "@types/semver": "^7.3.12", - "@types/xml2js": "^0.4.5", "ajv": "^8.12.0", "ajv-cli": "^5.0.0", "ajv-formats": "^2.1.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "git-diff": "^2.0.6", "hexy": "^0.3.4", "mocha": "^8.4.0", - "ts-node": "^9.1.1", - "typescript": "^4.9.5" + "typescript": "^5.4.5" }, "mocha": { "spec": "build/test/**/test-*.js", @@ -76,6 +70,7 @@ "src/kmx/kmx-plus.ts", "src/kmx/kmx-builder.ts", "src/kmx/element-string.ts", + "src/lexical-model-types.ts", "src/kmx/string-list.ts", "src/ldml-keyboard/ldml-keyboard-testdata-xml.ts", "src/ldml-keyboard/unicodeset-parser-api.ts", diff --git a/common/web/types/src/consts/modifier-key-constants.ts b/common/web/types/src/consts/modifier-key-constants.ts new file mode 100644 index 00000000000..245cbf70f37 --- /dev/null +++ b/common/web/types/src/consts/modifier-key-constants.ts @@ -0,0 +1,33 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Modifier key bit-flags + */ + +export const ModifierKeyConstants = { + // Define Keyman Developer modifier bit-flags (exposed for use by other modules) + // Compare against /common/include/kmx_file.h. CTRL+F "#define LCTRLFLAG" to find the secton. + LCTRLFLAG: 0x0001, // Left Control flag + RCTRLFLAG: 0x0002, // Right Control flag + LALTFLAG: 0x0004, // Left Alt flag + RALTFLAG: 0x0008, // Right Alt flag + K_SHIFTFLAG: 0x0010, // Either shift flag + K_CTRLFLAG: 0x0020, // Either ctrl flag + K_ALTFLAG: 0x0040, // Either alt flag + K_METAFLAG: 0x0080, // Either Meta-key flag (tentative). Not usable in keyboard rules; + // Used internally (currently, only by KMW) to ensure Meta-key + // shortcuts safely bypass rules + // Meta key = Command key on macOS, Windows key on Windows/Linux + CAPITALFLAG: 0x0100, // Caps lock on + NOTCAPITALFLAG: 0x0200, // Caps lock NOT on + NUMLOCKFLAG: 0x0400, // Num lock on + NOTNUMLOCKFLAG: 0x0800, // Num lock NOT on + SCROLLFLAG: 0x1000, // Scroll lock on + NOTSCROLLFLAG: 0x2000, // Scroll lock NOT on + ISVIRTUALKEY: 0x4000, // It is a Virtual Key Sequence + VIRTUALCHARKEY: 0x8000, // Keyman 6.0: Virtual Key Cap Sequence NOT YET + + // Note: OTHER_MODIFIER = 0x10000, used by KMX+ for the + // other modifier flag in layers, > 16 bit so not available here. + // See keys_mod_other in keyman_core_ldml.ts +} diff --git a/common/web/types/src/consts/virtual-key-constants.ts b/common/web/types/src/consts/virtual-key-constants.ts index 3347660f015..7b083241bdc 100644 --- a/common/web/types/src/consts/virtual-key-constants.ts +++ b/common/web/types/src/consts/virtual-key-constants.ts @@ -1,6 +1,5 @@ // Define standard keycode numbers (exposed for use by other modules) -// TODO-LDML: merge with common\web\keyboard-processor\src\text\codes.ts /** * May include non-US virtual key codes @@ -128,7 +127,7 @@ export const USVirtualKeyCodes = { 'k_?C1':193, K_oDF:0xDF, K_ODF:0xDF, - /*K_LOPT:50001, + K_LOPT:50001, K_ROPT:50002, K_NUMERALS:50003, K_SYMBOLS:50004, @@ -139,7 +138,7 @@ export const USVirtualKeyCodes = { K_SHIFTED:50009, K_ALTGR:50010, K_TABBACK:50011, - K_TABFWD:50012*/ + K_TABFWD:50012 }; const k = USVirtualKeyCodes; diff --git a/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts b/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts index 757316a70a9..e24fd295c84 100644 --- a/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts +++ b/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file.ts @@ -7,6 +7,11 @@ // writing // +/** + * On screen keyboard description consisting of specific layouts for tablet, phone, + * and desktop. Despite its name, this format is used for both touch layouts and + * hardware-style layouts. + */ export interface TouchLayoutFile { tablet?: TouchLayoutPlatform; phone?: TouchLayoutPlatform; @@ -17,6 +22,7 @@ export type TouchLayoutFont = string; export type TouchLayoutFontSize = string; export type TouchLayoutDefaultHint = "none"|"dot"|"longpress"|"multitap"|"flick"|"flick-n"|"flick-ne"|"flick-e"|"flick-se"|"flick-s"|"flick-sw"|"flick-w"|"flick-nw"; +/** touch layout specification for a specific platform like phone or tablet */ export interface TouchLayoutPlatform { font?: TouchLayoutFont; fontsize?: TouchLayoutFontSize; @@ -27,6 +33,7 @@ export interface TouchLayoutPlatform { export type TouchLayoutLayerId = string; // pattern = /^[a-zA-Z0-9_-]+$/ +/** a layer with rows of keys on a touch layout */ export interface TouchLayoutLayer { id: TouchLayoutLayerId; row: TouchLayoutRow[]; @@ -34,6 +41,7 @@ export interface TouchLayoutLayer { export type TouchLayoutRowId = number; +/** a row of keys on a touch layout */ export interface TouchLayoutRow { id: TouchLayoutRowId; key: TouchLayoutKey[]; @@ -58,26 +66,44 @@ export const PRIVATE_USE_IDS = [ ] as const; /* A map of key field names with values matching the `typeof` the corresponding property - * exists in common/web/keyboard-processor, keyboards/activeLayout.ts. + * exists in /web/src/engine/keyboard/src/keyboards/activeLayout.ts. * * Make sure that when one is updated, the other also is. TS types are compile-time only, * so the run-time-accessible mapping in activeLayout.ts cannot be auto-generated by TS. */ +/** defines a key on a touch layout */ export interface TouchLayoutKey { + /** key id: used to find key in VKDictionary, or a standard key from the K_ enumeration */ id?: TouchLayoutKeyId; + /** text to display on key cap */ text?: string; + /** + * the modifier combination (not layer) that should be used in key events, + * for this key, overriding the layer that the key is a part of. + */ layer?: TouchLayoutLayerId; + /** the next layer to switch to after this key is pressed */ nextlayer?: TouchLayoutLayerId; + /** font */ font?: TouchLayoutFont; + /** fontsize */ fontsize?: TouchLayoutFontSize; + /** the type of key */ sp?: TouchLayoutKeySp; + /** padding */ pad?: TouchLayoutKeyPad; + /** width of the key */ width?: TouchLayoutKeyWidth; + /** longpress keys, also known as subkeys */ sk?: TouchLayoutSubKey[]; + /** flicks */ flick?: TouchLayoutFlick; + /** multitaps */ multitap?: TouchLayoutSubKey[]; + /** hint e.g. for longpress */ hint?: string; }; +/** key type like regular key, framekeys, deadkeys, blank, etc. */ export const enum TouchLayoutKeySp { normal=0, /** A 'frame' key, such as Shift or Enter, which is styled accordingly; uses @@ -102,29 +128,54 @@ export const enum TouchLayoutKeySp { spacer=10 }; +/** padding for a key */ export type TouchLayoutKeyPad = number; // 0-100000 +/** width of a key */ export type TouchLayoutKeyWidth = number; // 0-100000 +/** defines a subkey */ export interface TouchLayoutSubKey { + /** key id: used to find key in VKDictionary, or a standard key from the K_ enumeration */ id: TouchLayoutKeyId; + /** text to display on key cap */ text?: string; + /** + * the modifier combination (not layer) that should be used in key events, + * for this key, overriding the layer that the key is a part of. + */ layer?: TouchLayoutLayerId; + /** the next layer to switch to after this key is pressed */ nextlayer?: TouchLayoutLayerId; + /** font */ font?: TouchLayoutFont; + /** fontsize */ fontsize?: TouchLayoutFontSize; + /** the type of key */ sp?: TouchLayoutKeySp; + /** padding */ pad?: TouchLayoutKeyPad; + /** width of the key */ width?: TouchLayoutKeyWidth; + /** use this subkey if no other selected */ default?: boolean; // Only used for longpress currently }; +/** defines all possible flicks for a key */ export interface TouchLayoutFlick { + /** flick up (north) */ n?: TouchLayoutSubKey; + /** flick down (south) */ s?: TouchLayoutSubKey; + /** flick right (east) */ e?: TouchLayoutSubKey; + /** flick left (west) */ w?: TouchLayoutSubKey; + /** flick up-right (north-east) */ ne?: TouchLayoutSubKey; + /** flick up-left (north-west) */ nw?: TouchLayoutSubKey; + /** flick down-right (south-east) */ se?: TouchLayoutSubKey; + /** flick down-left (south-west) */ sw?: TouchLayoutSubKey; }; diff --git a/common/web/types/src/kmx/element-string.ts b/common/web/types/src/kmx/kmx-plus/element-string.ts similarity index 94% rename from common/web/types/src/kmx/element-string.ts rename to common/web/types/src/kmx/kmx-plus/element-string.ts index b26878f459e..b389107ccee 100644 --- a/common/web/types/src/kmx/element-string.ts +++ b/common/web/types/src/kmx/kmx-plus/element-string.ts @@ -1,7 +1,9 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; import { DependencySections, StrsItem, UsetItem } from './kmx-plus.js'; -import { ElementParser, ElementSegment, ElementType } from '../ldml-keyboard/pattern-parser.js'; -import { MATCH_HEX_ESCAPE, unescapeOneQuadString } from '../util/util.js'; +import { ElementParser, ElementSegment, ElementType } from '../../ldml-keyboard/pattern-parser.js'; +import * as util from '../../util/util.js'; +import MATCH_HEX_ESCAPE = util.MATCH_HEX_ESCAPE; +import unescapeOneQuadString = util.unescapeOneQuadString; export enum ElemElementFlags { none = 0, diff --git a/common/web/types/src/kmx/kmx-plus.ts b/common/web/types/src/kmx/kmx-plus/kmx-plus.ts similarity index 97% rename from common/web/types/src/kmx/kmx-plus.ts rename to common/web/types/src/kmx/kmx-plus/kmx-plus.ts index 6e3c73d33c8..0cb49e527c7 100644 --- a/common/web/types/src/kmx/kmx-plus.ts +++ b/common/web/types/src/kmx/kmx-plus/kmx-plus.ts @@ -1,12 +1,18 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; import * as r from 'restructure'; import { ElementString } from './element-string.js'; -import { ListItem } from './string-list.js'; -import { isOneChar, toOneChar, unescapeString, escapeStringForRegex } from '../util/util.js'; -import { KMXFile } from './kmx.js'; -import { UnicodeSetParser, UnicodeSet } from '@keymanapp/common-types'; -import { VariableParser } from '../ldml-keyboard/pattern-parser.js'; -import { MarkerParser } from '../ldml-keyboard/pattern-parser.js'; +import { ListItem } from '../../ldml-keyboard/string-list.js'; +import * as util from '../../util/util.js'; +import * as KMX from '../kmx.js'; +import { UnicodeSetParser, UnicodeSet } from '../../ldml-keyboard/unicodeset-parser-api.js'; +import { VariableParser } from '../../ldml-keyboard/pattern-parser.js'; +import { MarkerParser } from '../../ldml-keyboard/pattern-parser.js'; + +import isOneChar = util.isOneChar; +import toOneChar = util.toOneChar; +import unescapeString = util.unescapeString; +import escapeStringForRegex = util.escapeStringForRegex; +import KMXFile = KMX.KMXFile; // Implementation of file structures from /core/src/ldml/C7043_ldml.md // Writer in kmx-builder.ts @@ -285,7 +291,7 @@ export class Vars extends Section { }); } findStringVariableValue(id: string): string { - return Vars.findVariable(this.strings, id)?.value?.value; // Unwrap: Variable, StrsItem + return Vars.findVariable(this.strings, id)?.value?.value ?? null; // Unwrap: Variable, StrsItem } substituteSetRegex(str: string, sections: DependencySections): string { return str.replaceAll(VariableParser.SET_REFERENCE, (_entire, id) => { diff --git a/common/web/types/src/kmx/kmx.ts b/common/web/types/src/kmx/kmx.ts index c5989ec1157..4f53e29fde1 100644 --- a/common/web/types/src/kmx/kmx.ts +++ b/common/web/types/src/kmx/kmx.ts @@ -1,4 +1,5 @@ import * as r from 'restructure'; +import { ModifierKeyConstants } from '../consts/modifier-key-constants.js'; /* Definitions from kmx_file.h. Must be kept in sync */ @@ -7,6 +8,25 @@ import * as r from 'restructure'; // In memory representations of KMX structures // kmx-builder will transform these to the corresponding COMP_xxxx +export enum KMX_Version { + VERSION_30 = 0x00000300, + VERSION_31 = 0x00000301, + VERSION_32 = 0x00000302, + VERSION_40 = 0x00000400, + VERSION_50 = 0x00000500, + VERSION_501 = 0x00000501, + VERSION_60 = 0x00000600, + VERSION_70 = 0x00000700, + VERSION_80 = 0x00000800, + VERSION_90 = 0x00000900, + VERSION_100 = 0x00000A00, + VERSION_140 = 0x00000E00, + VERSION_150 = 0x00000F00, + VERSION_160 = 0x00001000, + VERSION_170 = 0x00001100 +}; + + export class KEYBOARD { fileVersion?: number; // dwFileVersion (TSS_FILEVERSION) @@ -129,22 +149,21 @@ export class KMXFile { // File version identifiers (COMP_KEYBOARD.dwFileVersion) // - public static readonly VERSION_30 = 0x00000300; - public static readonly VERSION_31 = 0x00000301; - public static readonly VERSION_32 = 0x00000302; - public static readonly VERSION_40 = 0x00000400; - public static readonly VERSION_50 = 0x00000500; - public static readonly VERSION_501 = 0x00000501; - public static readonly VERSION_60 = 0x00000600; - public static readonly VERSION_70 = 0x00000700; - public static readonly VERSION_80 = 0x00000800; - public static readonly VERSION_90 = 0x00000900; - public static readonly VERSION_100 = 0x00000A00; - public static readonly VERSION_140 = 0x00000E00; - public static readonly VERSION_150 = 0x00000F00; - - public static readonly VERSION_160 = 0x00001000; - public static readonly VERSION_170 = 0x00001100; + public static readonly VERSION_30 = KMX_Version.VERSION_30; + public static readonly VERSION_31 = KMX_Version.VERSION_31; + public static readonly VERSION_32 = KMX_Version.VERSION_32; + public static readonly VERSION_40 = KMX_Version.VERSION_40; + public static readonly VERSION_50 = KMX_Version.VERSION_50; + public static readonly VERSION_501 = KMX_Version.VERSION_501; + public static readonly VERSION_60 = KMX_Version.VERSION_60; + public static readonly VERSION_70 = KMX_Version.VERSION_70; + public static readonly VERSION_80 = KMX_Version.VERSION_80; + public static readonly VERSION_90 = KMX_Version.VERSION_90; + public static readonly VERSION_100 = KMX_Version.VERSION_100; + public static readonly VERSION_140 = KMX_Version.VERSION_140; + public static readonly VERSION_150 = KMX_Version.VERSION_150; + public static readonly VERSION_160 = KMX_Version.VERSION_160; + public static readonly VERSION_170 = KMX_Version.VERSION_170; public static readonly VERSION_MIN = this.VERSION_50; public static readonly VERSION_MAX = this.VERSION_170; @@ -325,25 +344,25 @@ export class KMXFile { public static readonly HK_CTRL = 0x00020000; public static readonly HK_SHIFT = 0x00040000; - public static readonly LCTRLFLAG = 0x0001; // Left Control flag - public static readonly RCTRLFLAG = 0x0002; // Right Control flag - public static readonly LALTFLAG = 0x0004; // Left Alt flag - public static readonly RALTFLAG = 0x0008; // Right Alt flag - public static readonly K_SHIFTFLAG = 0x0010; // Either shift flag - public static readonly K_CTRLFLAG = 0x0020; // Either ctrl flag - public static readonly K_ALTFLAG = 0x0040; // Either alt flag + public static readonly LCTRLFLAG = ModifierKeyConstants.LCTRLFLAG; // Left Control flag + public static readonly RCTRLFLAG = ModifierKeyConstants.RCTRLFLAG; // Right Control flag + public static readonly LALTFLAG = ModifierKeyConstants.LALTFLAG; // Left Alt flag + public static readonly RALTFLAG = ModifierKeyConstants.RALTFLAG; // Right Alt flag + public static readonly K_SHIFTFLAG = ModifierKeyConstants.K_SHIFTFLAG; // Either shift flag + public static readonly K_CTRLFLAG = ModifierKeyConstants.K_CTRLFLAG; // Either ctrl flag + public static readonly K_ALTFLAG = ModifierKeyConstants.K_ALTFLAG; // Either alt flag //public static readonly K_METAFLAG = 0x0080; // Either Meta-key flag (tentative). Not usable in keyboard rules; // Used internally (currently, only by KMW) to ensure Meta-key // shortcuts safely bypass rules // Meta key = Command key on macOS, Windows key on Windows - public static readonly CAPITALFLAG = 0x0100; // Caps lock on - public static readonly NOTCAPITALFLAG = 0x0200; // Caps lock NOT on - public static readonly NUMLOCKFLAG = 0x0400; // Num lock on - public static readonly NOTNUMLOCKFLAG = 0x0800; // Num lock NOT on - public static readonly SCROLLFLAG = 0x1000; // Scroll lock on - public static readonly NOTSCROLLFLAG = 0x2000; // Scroll lock NOT on - public static readonly ISVIRTUALKEY = 0x4000; // It is a Virtual Key Sequence - public static readonly VIRTUALCHARKEY = 0x8000; // Keyman 6.0: Virtual Key Cap Sequence NOT YET + public static readonly CAPITALFLAG = ModifierKeyConstants.CAPITALFLAG; // Caps lock on + public static readonly NOTCAPITALFLAG = ModifierKeyConstants.NOTCAPITALFLAG; // Caps lock NOT on + public static readonly NUMLOCKFLAG = ModifierKeyConstants.NUMLOCKFLAG; // Num lock on + public static readonly NOTNUMLOCKFLAG = ModifierKeyConstants.NOTNUMLOCKFLAG; // Num lock NOT on + public static readonly SCROLLFLAG = ModifierKeyConstants.SCROLLFLAG; // Scroll lock on + public static readonly NOTSCROLLFLAG = ModifierKeyConstants.NOTSCROLLFLAG; // Scroll lock NOT on + public static readonly ISVIRTUALKEY = ModifierKeyConstants.ISVIRTUALKEY; // It is a Virtual Key Sequence + public static readonly VIRTUALCHARKEY = ModifierKeyConstants.VIRTUALCHARKEY; // Keyman 6.0: Virtual Key Cap Sequence NOT YET // Note: OTHER_MODIFIER = 0x10000, used by KMX+ for the // other modifier flag in layers, > 16 bit so not available here. diff --git a/common/web/types/src/ldml-keyboard/pattern-parser.ts b/common/web/types/src/ldml-keyboard/pattern-parser.ts index 8901de6dddf..3e56096ec67 100644 --- a/common/web/types/src/ldml-keyboard/pattern-parser.ts +++ b/common/web/types/src/ldml-keyboard/pattern-parser.ts @@ -3,7 +3,8 @@ */ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { MATCH_QUAD_ESCAPE, isOneChar, unescapeOneQuadString, unescapeString, hexQuad } from "../util/util.js"; +import { MATCH_QUAD_ESCAPE } from "../util/consts.js"; +import { isOneChar, unescapeOneQuadString, unescapeString, hexQuad } from "../util/util.js"; /** * Helper function for extracting matched items diff --git a/common/web/types/src/kmx/string-list.ts b/common/web/types/src/ldml-keyboard/string-list.ts similarity index 94% rename from common/web/types/src/kmx/string-list.ts rename to common/web/types/src/ldml-keyboard/string-list.ts index f68e9053696..5d11a5ca7c9 100644 --- a/common/web/types/src/kmx/string-list.ts +++ b/common/web/types/src/ldml-keyboard/string-list.ts @@ -1,5 +1,5 @@ -import { OrderedStringList } from '../ldml-keyboard/pattern-parser.js'; -import { DependencySections, StrsItem, StrsOptions } from './kmx-plus.js'; +import { OrderedStringList } from './pattern-parser.js'; +import { DependencySections, StrsItem, StrsOptions } from '../kmx/kmx-plus/kmx-plus.js'; /** * A single entry in a ListItem. diff --git a/common/models/types/index.d.ts b/common/web/types/src/lexical-model-types.ts similarity index 89% rename from common/models/types/index.d.ts rename to common/web/types/src/lexical-model-types.ts index 30aba3ba3d4..7c5cfdf643f 100644 --- a/common/models/types/index.d.ts +++ b/common/web/types/src/lexical-model-types.ts @@ -15,17 +15,36 @@ * * See also: https://developer.mozilla.org/en-US/docs/Web/API/USVString */ -declare type USVString = string; +export type USVString = string; -declare type CasingForm = 'lower' | 'initial' | 'upper'; +export type CasingForm = 'lower' | 'initial' | 'upper'; + +/** + * Represents one lexical entry and its probability.. + */ +export type TextWithProbability = { + /** + * A lexical entry (word) offered by the model. + * + * Note: not the search-term keyed part. This will match the actual, unkeyed form. + */ + text: string; + + /** + * The probability of the lexical entry, directly based upon its frequency. + * + * A real-number weight, from 0 to 1. + */ + p: number; +} /** * Used to facilitate edit-distance calculations by allowing the LMLayer to * efficiently search the model's lexicon in a Trie-like manner. */ -declare interface LexiconTraversal { +export interface LexiconTraversal { /** - * Provides an iterable pattern used to search for words with a prefix matching + * Provides an iterable pattern used to search for words with a 'keyed' prefix matching * the current traversal state's prefix when a new character is appended. Iterating * across `children` provides 'breadth' to a lexical search. * @@ -50,6 +69,20 @@ declare interface LexiconTraversal { */ children(): Generator<{char: USVString, traversal: () => LexiconTraversal}>; + /** + * Allows direct access to the traversal state that results when appending one + * or more codepoints encoded in UTF-16 to the current traversal state's prefix. + * This allows bypassing iteration among all legal child Traversals. + * + * If such a traversal state is not supported, returns `undefined`. + * + * Note: traversals navigate and represent the lexicon in its "keyed" state, + * as produced by use of the search-term keying function defined for the model. + * That is, if a model "keys" `è` to `e`, there will be no `è` child. + * @param char + */ + child(char: USVString): LexiconTraversal | undefined; + /** * Any entries directly keyed by the currently-represented lookup prefix. Entries and * children may exist simultaneously, but `entries` must always exist when no children are @@ -70,13 +103,20 @@ declare interface LexiconTraversal { * - prefix of 'crepe': ['crêpe', 'crêpé'] * - other examples: https://www.thoughtco.com/french-accent-homographs-1371072 */ - entries: USVString[]; + entries: TextWithProbability[]; + + // Note: `p`, not `maxP` - we want to see the same name for `this.entries.p` and `this.p` + /** + * Gives the probability of the highest-frequency lexical entry that is either a member or + * descendent of the represented trie `Node`. + */ + p: number; } /** * The model implementation, within the Worker. */ -declare interface LexicalModel { +export interface LexicalModel { /** * Processes `config` messages, configuring the newly-loaded model based on the host * platform's capability restrictions. @@ -223,7 +263,7 @@ declare interface LexicalModel { * first, you delete the specified amount amount from the left * and right, then you insert the provided text. */ -declare interface Transform { +export interface Transform { /** * Facilitates use of unique identifiers for tracking the Transform and * any related data from its original source, as the reference cannot be @@ -258,7 +298,7 @@ declare interface Transform { /** * A concrete suggestion */ -declare interface Suggestion { +export interface Suggestion { /** * Indicates the externally-supplied id of the Transform that prompted * the Suggestion. Automatically handled by the LMLayer; models should @@ -294,13 +334,18 @@ declare interface Suggestion { * to the input text. Ex: 'keep', 'emoji', 'correction', etc. */ tag?: SuggestionTag; + + /** + * Set to true if this suggestion is a valid auto-accept target. + */ + autoAccept?: boolean } -interface Reversion extends Suggestion { +export interface Reversion extends Suggestion { tag: 'revert'; } -interface Keep extends Suggestion { +export interface Keep extends Suggestion { tag: 'keep'; /** @@ -323,12 +368,12 @@ interface Keep extends Suggestion { * * If left undefined, the consumers will assume this is a prediction. */ -type SuggestionTag = undefined | 'keep' | 'revert' | 'correction' | 'emoji'; +export type SuggestionTag = undefined | 'keep' | 'revert' | 'correction' | 'emoji'; /** * The text and environment surrounding the insertion point (text cursor). */ -declare interface Context { +export interface Context { /** * Up to maxLeftContextCodeUnits code units of Unicode scalar value * (i. e., characters) to the left of the insertion point in the @@ -368,7 +413,7 @@ declare interface Context { * from ambiguous text sequences. Designed for use with fat-finger correction * and similar typing ambiguities. */ -interface ProbabilityMass { +export interface ProbabilityMass { /** * An individual sample from a Distribution over the same type. */ @@ -381,12 +426,12 @@ interface ProbabilityMass { p: number; } -declare type Distribution = ProbabilityMass[]; +export type Distribution = ProbabilityMass[]; /** * A type augmented with an optional probability. */ -type Outcome = T & { +export type Outcome = T & { /** * [optional] probability of this outcome. */ @@ -396,7 +441,7 @@ type Outcome = T & { /** * A type augmented with a probability. */ -type WithOutcome = T & { +export type WithOutcome = T & { /** * Probability of this outcome. */ @@ -413,7 +458,7 @@ type WithOutcome = T & { * prediction, as well as what operations the keyboard is allowed to do on the * underlying buffer. */ -declare interface Capabilities { +export interface Capabilities { /** * The maximum amount of UTF-16 code points that the keyboard will provide to * the left of the cursor, as an integer. @@ -437,7 +482,7 @@ declare interface Capabilities { /** * Configuration of the LMLayer, sent back to the keyboard. */ -declare interface Configuration { +export interface Configuration { /** * How many UTF-16 code units maximum to send as the context to the * left of the cursor ("left" in the Unicode character stream). @@ -493,14 +538,14 @@ declare interface Configuration { * @returns an array of spans from the phrase, in order as they appear in the * phrase, each span which representing a word. */ -declare interface WordBreakingFunction { +export interface WordBreakingFunction { // invariant: span[i].end <= span[i + 1].start // invariant: for all span[i] and span[i + 1], there does not exist a span[k] // where span[i].end <= span[k].start AND span[k].end <= span[i + 1].start (phrase: string): Span[]; } -declare interface CasingFunction { +export interface CasingFunction { (caseToApply: CasingForm, text: string, defaultApplyCasing?: CasingFunction): string; } @@ -508,7 +553,7 @@ declare interface CasingFunction { * A span of text in a phrase. This is usually meant to represent words from a * pharse. */ -declare interface Span { +export interface Span { // invariant: start < end (empty spans not allowed) readonly start: number; // invariant: end > end (empty spans not allowed) @@ -527,7 +572,7 @@ declare interface Span { /** * Options for various punctuation to use in suggestions. */ -interface LexicalModelPunctuation { +export interface LexicalModelPunctuation { /** * The quotes that appear in "keep" suggestions, e.g., keep what the user * typed verbatim. diff --git a/common/web/types/src/main.ts b/common/web/types/src/main.ts index 3d18812876c..df21b0fed35 100644 --- a/common/web/types/src/main.ts +++ b/common/web/types/src/main.ts @@ -1,55 +1,22 @@ export * as KMX from './kmx/kmx.js'; -export * as KMXPlus from './kmx/kmx-plus.js'; -export { default as KMXBuilder } from './kmx/kmx-builder.js'; export { KmxFileReader, KmxFileReaderError } from './kmx/kmx-file-reader.js'; export * as KeymanTargets from './kmx/keyman-targets.js'; export * as VisualKeyboard from './kvk/visual-keyboard.js'; -export { default as KMXPlusBuilder} from './kmx/kmx-plus-builder/kmx-plus-builder.js'; export { default as KvkFileReader } from './kvk/kvk-file-reader.js'; -export { default as KvksFileReader } from './kvk/kvks-file-reader.js'; export { default as KvkFileWriter } from './kvk/kvk-file-writer.js'; -export { default as KvksFileWriter } from './kvk/kvks-file-writer.js'; export * as KvkFile from './kvk/kvk-file.js'; -export * as KvksFile from './kvk/kvks-file.js'; -export * as LDMLKeyboard from './ldml-keyboard/ldml-keyboard-xml.js'; -export { LDMLKeyboardTestDataXMLSourceFile } from './ldml-keyboard/ldml-keyboard-testdata-xml.js'; -export { UnicodeSetParser, UnicodeSet } from './ldml-keyboard/unicodeset-parser-api.js'; -export { VariableParser, MarkerParser } from './ldml-keyboard/pattern-parser.js'; -export { LDMLKeyboardXMLSourceFileReader, LDMLKeyboardXMLSourceFileReaderOptions } from './ldml-keyboard/ldml-keyboard-xml-reader.js'; +export { USVirtualKeyCodes } from './consts/virtual-key-constants.js'; export * as Constants from './consts/virtual-key-constants.js'; - -export { defaultCompilerOptions, CompilerBaseOptions, CompilerCallbacks, CompilerOptions, CompilerEvent, CompilerErrorNamespace, - CompilerErrorSeverity, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerCallbackOptions, - CompilerError, CompilerMessageSpec, CompilerMessageSpecWithException, compilerErrorSeverity, CompilerErrorMask, CompilerFileCallbacks, compilerErrorSeverityName, - compilerErrorFormatCode, CompilerMessageDef, - compilerLogLevelToSeverity, CompilerLogLevel, compilerEventFormat, ALL_COMPILER_LOG_LEVELS, - ALL_COMPILER_LOG_FORMATS, CompilerLogFormat, - CompilerMessageOverride, - CompilerMessageOverrideMap, - - KeymanCompilerArtifact, - KeymanCompilerArtifactOptional, - KeymanCompilerArtifacts, - KeymanCompilerResult, - KeymanCompiler - - } from './util/compiler-interfaces.js'; -export { CommonTypesMessages } from './util/common-events.js'; +export { ModifierKeyConstants } from './consts/modifier-key-constants.js'; export * as TouchLayout from './keyman-touch-layout/keyman-touch-layout-file.js'; -export { TouchLayoutFileReader } from './keyman-touch-layout/keyman-touch-layout-file-reader.js'; -export { TouchLayoutFileWriter, TouchLayoutFileWriterOptions } from './keyman-touch-layout/keyman-touch-layout-file-writer.js'; -export * as KPJ from './kpj/kpj-file.js'; -export { KPJFileReader } from './kpj/kpj-file-reader.js'; -export { KeymanDeveloperProject, KeymanDeveloperProjectFile, KeymanDeveloperProjectType, } from './kpj/keyman-developer-project.js'; - -export * as KpsFile from './package/kps-file.js'; export * as KmpJsonFile from './package/kmp-json-file.js'; +export { Uni_IsSurrogate1, Uni_IsSurrogate2 } from './util/util.js'; export * as util from './util/util.js'; export * as KeymanFileTypes from './util/file-types.js'; @@ -57,4 +24,10 @@ export * as KeymanFileTypes from './util/file-types.js'; export * as Schemas from './schemas.js'; export * as SchemaValidators from './schema-validators.js'; -export * as xml2js from './deps/xml2js/xml2js.js'; \ No newline at end of file +export * as KMXPlus from './kmx/kmx-plus/kmx-plus.js'; +// TODO: these exports are really not well named +export { UnicodeSetParser, UnicodeSet } from './ldml-keyboard/unicodeset-parser-api.js'; +export { VariableParser, MarkerParser } from './ldml-keyboard/pattern-parser.js'; +export { ElementString } from './kmx/kmx-plus/element-string.js'; + +export { USVString, CasingForm, CasingFunction, TextWithProbability, LexiconTraversal, LexicalModel, LexicalModelPunctuation, Transform, Suggestion, Reversion, Keep, SuggestionTag, Context, Distribution, Outcome, WithOutcome, ProbabilityMass, Configuration, Capabilities, WordBreakingFunction, Span } from './lexical-model-types.js'; diff --git a/common/web/types/src/util/consts.ts b/common/web/types/src/util/consts.ts new file mode 100644 index 00000000000..271b3aa6bc7 --- /dev/null +++ b/common/web/types/src/util/consts.ts @@ -0,0 +1,12 @@ +// TODO-LDML: #7569 the below regex works, but captures more than it should +// (it would include \u{fffffffffffffffff } which +// is overlong and has a space at the end.) The second regex does not work yet. +export const MATCH_HEX_ESCAPE = /\\u{([0-9a-fA-F ]{1,})}/g; +// const MATCH_HEX_ESCAPE = /\\u{((?:(?:[0-9a-fA-F]{1,5})|(?:10[0-9a-fA-F]{4})(?: (?!}))?)+)}/g; + +/** regex for single quad escape such as \u0127 or \U00000000 */ +export const CONTAINS_QUAD_ESCAPE = /(?:\\u([0-9a-fA-F]{4})|\\U([0-9a-fA-F]{8}))/; + +/** regex for single quad escape such as \u0127 */ +export const MATCH_QUAD_ESCAPE = new RegExp(CONTAINS_QUAD_ESCAPE, 'g'); + diff --git a/common/web/types/src/util/file-types.ts b/common/web/types/src/util/file-types.ts index e3d3bd54523..f2c44a1d02c 100644 --- a/common/web/types/src/util/file-types.ts +++ b/common/web/types/src/util/file-types.ts @@ -64,6 +64,16 @@ export type All = Source | Binary; */ export type Any = string; +/** + * Standard project file name - history of project in Markdown format + */ +export const HISTORY_MD = 'HISTORY.md'; + +/** + * Standard project file name - README in Markdown format + */ +export const README_MD = 'README.md'; + /** * Gets the file type based on extension, dealing with multi-part file * extensions. Does not sniff contents of file or assume file existence. Does diff --git a/common/web/types/src/util/util.ts b/common/web/types/src/util/util.ts index 5a0e6600a2a..0d13505cfc9 100644 --- a/common/web/types/src/util/util.ts +++ b/common/web/types/src/util/util.ts @@ -1,3 +1,6 @@ +import { MATCH_HEX_ESCAPE, CONTAINS_QUAD_ESCAPE, MATCH_QUAD_ESCAPE } from './consts.js'; +export { MATCH_HEX_ESCAPE, CONTAINS_QUAD_ESCAPE, MATCH_QUAD_ESCAPE }; + /** * xml2js will not place single-entry objects into arrays. Easiest way to fix * this is to box them ourselves as needed. Ensures that o.x is an array. @@ -16,18 +19,6 @@ export function boxXmlArray(o: any, x: string): void { } } -// TODO-LDML: #7569 the below regex works, but captures more than it should -// (it would include \u{fffffffffffffffff } which -// is overlong and has a space at the end.) The second regex does not work yet. -export const MATCH_HEX_ESCAPE = /\\u{([0-9a-fA-F ]{1,})}/g; -// const MATCH_HEX_ESCAPE = /\\u{((?:(?:[0-9a-fA-F]{1,5})|(?:10[0-9a-fA-F]{4})(?: (?!}))?)+)}/g; - -/** regex for single quad escape such as \u0127 or \U00000000 */ -export const CONTAINS_QUAD_ESCAPE = /(?:\\u([0-9a-fA-F]{4})|\\U([0-9a-fA-F]{8}))/; - -/** regex for single quad escape such as \u0127 */ -export const MATCH_QUAD_ESCAPE = new RegExp(CONTAINS_QUAD_ESCAPE, 'g'); - export class UnescapeError extends Error { } @@ -249,14 +240,14 @@ const Uni_PUA_16_END = 0x10FFFD; * @brief True if a lead surrogate * \def Uni_IsSurrogate1 */ -function Uni_IsSurrogate1(ch : number) { +export function Uni_IsSurrogate1(ch : number) { return ((ch) >= Uni_LEAD_SURROGATE_START && (ch) <= Uni_LEAD_SURROGATE_END); } /** * @brief True if a trail surrogate * \def Uni_IsSurrogate2 */ -function Uni_IsSurrogate2(ch : number) { +export function Uni_IsSurrogate2(ch : number) { return ((ch) >= Uni_TRAIL_SURROGATE_START && (ch) <= Uni_TRAIL_SURROGATE_END); } @@ -264,7 +255,7 @@ function Uni_IsSurrogate2(ch : number) { * @brief True if any surrogate * \def UniIsSurrogate */ -function Uni_IsSurrogate(ch : number) { +export function Uni_IsSurrogate(ch : number) { return (Uni_IsSurrogate1(ch) || Uni_IsSurrogate2(ch)); } diff --git a/common/web/types/test/helpers/index.ts b/common/web/types/test/helpers/index.ts index 9b2be1d6056..3e305eeca8a 100644 --- a/common/web/types/test/helpers/index.ts +++ b/common/web/types/test/helpers/index.ts @@ -1,5 +1,4 @@ import * as path from "path"; -import * as fs from "fs"; import { fileURLToPath } from "url"; /** @@ -12,26 +11,3 @@ import { fileURLToPath } from "url"; export function makePathToFixture(...components: string[]): string { return fileURLToPath(new URL(path.join('..', '..', '..', 'test', 'fixtures', ...components), import.meta.url)); } - -export function loadFile(filename: string | URL): Buffer { - return fs.readFileSync(filename); -} - -export function resolveFilename(baseFilename: string, filename: string) { - const basePath = - baseFilename.endsWith('/') || baseFilename.endsWith('\\') ? - baseFilename : - path.dirname(baseFilename); - // Transform separators to platform separators -- we are agnostic - // in our use here but path prefers files may use - // either / or \, although older kps files were always \. - if(path.sep == '/') { - filename = filename.replace(/\\/g, '/'); - } else { - filename = filename.replace(/\//g, '\\'); - } - if(!path.isAbsolute(filename)) { - filename = path.resolve(basePath, filename); - } - return filename; -} diff --git a/common/models/types/test.ts b/common/web/types/test/lexical-model-types.tests.ts similarity index 54% rename from common/models/types/test.ts rename to common/web/types/test/lexical-model-types.tests.ts index a3607418091..8944342ed6e 100644 --- a/common/models/types/test.ts +++ b/common/web/types/test/lexical-model-types.tests.ts @@ -1,12 +1,12 @@ -/// - /** * This file "tests" the exports from the main module. - * + * * Since the exports are all types, the "test" here is that the type can be * imported and compiled without any compiler errors. */ +import { USVString, Transform, Suggestion, SuggestionTag, Context, Capabilities, Configuration, Distribution, WordBreakingFunction, Span, LexicalModelPunctuation, ElementString, KMXPlus } from '@keymanapp/common-types'; + export let u: USVString; export let l: Transform export let s: Suggestion; @@ -17,4 +17,9 @@ export let conf: Configuration; export let d: Distribution; export let wbf: WordBreakingFunction; export let sp: Span; -export let lmp: LexicalModelPunctuation; \ No newline at end of file +export let lmp: LexicalModelPunctuation; + + +// try some of the other types - that should still work +export let elemString: ElementString; +export let section: KMXPlus.Section; diff --git a/common/web/types/test/tsconfig.json b/common/web/types/test/tsconfig.json index b034236bd75..9678c49945f 100644 --- a/common/web/types/test/tsconfig.json +++ b/common/web/types/test/tsconfig.json @@ -11,11 +11,12 @@ }, "include": [ "**/test-*.ts", + "**/*.tests.ts", "./helpers/*.ts", ], "references": [ { "path": "../../keyman-version" }, { "path": "../../../../core/include/ldml/"}, { "path": "../" }, - ] + ], } diff --git a/common/web/types/tsconfig.json b/common/web/types/tsconfig.json index 0473ae92eee..cd779632a0d 100644 --- a/common/web/types/tsconfig.json +++ b/common/web/types/tsconfig.json @@ -9,7 +9,6 @@ "preserveConstEnums": true, }, "include": [ - "src/deps/xml2js/*.js", "src/**/*.ts", "src/schemas/*.mjs", // Import the validators ], diff --git a/common/web/utils/src/surrogates.ts b/common/web/utils/src/surrogates.ts deleted file mode 100644 index 53c1a1e3fae..00000000000 --- a/common/web/utils/src/surrogates.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * The definitions below are duplicated from common/web/types/util/util.ts; - * we can't downcompile the originals to ES5 when bundling with esbuild. - * `import type` stuff is fine, but not non-type `import` statements. - * - * TODO: Use those instead, once we're no longer building ES5 versions of Web. - */ - -export const Uni_LEAD_SURROGATE_START = 0xD800; -export const Uni_LEAD_SURROGATE_END = 0xDBFF; -export const Uni_TRAIL_SURROGATE_START = 0xDC00; -export const Uni_TRAIL_SURROGATE_END = 0xDFFF; - -/** - * @brief True if a lead surrogate - * \def Uni_IsSurrogate1 - */ -export function Uni_IsSurrogate1(ch : number) { - return ((ch) >= Uni_LEAD_SURROGATE_START && (ch) <= Uni_LEAD_SURROGATE_END); -} -/** - * @brief True if a trail surrogate - * \def Uni_IsSurrogate2 - */ -export function Uni_IsSurrogate2(ch : number) { - return ((ch) >= Uni_TRAIL_SURROGATE_START && (ch) <= Uni_TRAIL_SURROGATE_END); -} diff --git a/common/windows/.gitignore b/common/windows/.gitignore index 7af23e7e1aa..a6cb808a340 100644 --- a/common/windows/.gitignore +++ b/common/windows/.gitignore @@ -7,3 +7,4 @@ delphi/**/*.~* delphi/**/__recovery/ delphi/**/__history/ delphi/general/keymanversion_build.inc +delphi/ext/cef4delphi/packages/CEF4Delphi.res diff --git a/common/windows/build.sh b/common/windows/build.sh index 714d6335447..06f5dd02013 100755 --- a/common/windows/build.sh +++ b/common/windows/build.sh @@ -18,7 +18,7 @@ builder_describe "Keyman common Windows modules" \ builder_parse "$@" -if [[ $BUILDER_OS != windows ]]; then +if [[ $BUILDER_OS != win ]]; then builder_echo grey "Platform is not Windows; skipping common/windows" exit 0 fi diff --git a/common/windows/cpp/include/registry.h b/common/windows/cpp/include/registry.h index 67cdfc2f69e..2627d06522f 100644 --- a/common/windows/cpp/include/registry.h +++ b/common/windows/cpp/include/registry.h @@ -110,6 +110,10 @@ #define REGSZ_KeyboardHotkeysAreToggle "hotkeys are toggles" #define REGSZ_DeadkeyConversionMode "deadkey conversion mode" // CU // I4552 #define REGSZ_ZapVirtualKeyCode "zap virtual key code" // LM, defaults to 0x0E (_VK_PREFIX_DEFAULT) +/* Default is to only use left modifier in hotkeys trigger */ +#define REGSZ_AllowRightModifierHotKey "allow right modifier for hotkey" + + /* Debug flags These are all stored in HKCU\Software\Keyman\Debug @@ -121,13 +125,7 @@ #define REGSZ_Flag_ShouldSerializeInput "Flag_ShouldSerializeInput" -/* REGSZ_Keyman_Debug DWORD: Use old non-chiral Win32 API RegisterHotkey instead of left-only hotkeys */ - -#define REGSZ_Flag_UseRegisterHotkey "Flag_UseRegisterHotkey" - -/* REGSZ_Flag_UseCachedHotkeyModifierState DWORD: Use old cached modifier state when checking hotkeys; ignores UseRegisterHotkey if FALSE */ -#define REGSZ_Flag_UseCachedHotkeyModifierState "Flag_UseCachedHotkeyModifierState" /* DWORD: Enable/disable deep TSF integration, default enabled; 0 = disabled, 1 = enabled, 2 = default */ diff --git a/common/windows/cpp/src/registry.cpp b/common/windows/cpp/src/registry.cpp index 4bc1dcf8fca..d44d43f4e1d 100644 --- a/common/windows/cpp/src/registry.cpp +++ b/common/windows/cpp/src/registry.cpp @@ -206,7 +206,7 @@ BOOL RegistryReadOnly::WrapError(DWORD res) { /*if(res != ERROR_SUCCESS) { - SendDebugMessageFormat(0,KDS_PROGRAM,0,"RegistryFullAccess::WrapError = %d", res); + __logerror(0,KDS_PROGRAM,0,"RegistryFullAccess::WrapError = %d", res); }*/ return res == ERROR_SUCCESS; } diff --git a/common/windows/delphi/Defines.mak b/common/windows/delphi/Defines.mak deleted file mode 100644 index 50210e5fea1..00000000000 --- a/common/windows/delphi/Defines.mak +++ /dev/null @@ -1,299 +0,0 @@ -# DEBUG=1 - -# TODO: this should be a shared Defines.mak for common,developer,windows. So we -# need to move any project-specific stuff into a defines-windows.mak, -# defines-etc.mak - -# -# Paths -# - -!IFNDEF KEYMAN_ROOT -!ERROR KEYMAN_ROOT must be defined! -!ENDIF - -# TODO: COMMON_ROOT should be common\windows -COMMON_ROOT=$(KEYMAN_ROOT)\common\windows\delphi -WINDOWS_ROOT=$(KEYMAN_ROOT)\windows -OUTLIB=$(WINDOWS_ROOT)\lib -COMMON_OUTLIB=$(KEYMAN_ROOT)\windows\lib -COMMON_BIN=$(KEYMAN_ROOT)\windows\bin - -# INCLUDE=$(ROOT)\src\global\inc;$(INCLUDE) - -!IFDEF DEBUG -GO_FAST=1 -MAKEFLAG_DEBUG="DEBUG=$(DEBUG)" -DELPHI_MSBUILD_FLAG_DEBUG="/p:Config=Debug" -!ELSE -!IFDEF TEAMCITY_PR_NUMBER -GO_FAST=1 -!ENDIF -DELPHI_MSBUILD_FLAG_DEBUG="/p:Config=Release" -!ENDIF - -!IFDEF USERDEFINES -MAKEFLAG_USERDEFINES="USERDEFINES=$(USERDEFINES)" -!ENDIF - -!IFDEF SC_TIMESTAMP -MAKEFLAG_SC_TIMESTAMP="SC_TIMESTAMP=$(SC_TIMESTAMP)" -!ENDIF - -!IFDEF BUILDHELP -MAKEFLAG_BUILDHELP="BUILDHELP=$(BUILDHELP)" -!ENDIF - -!IFDEF LINT -MAKEFLAG_LINT="LINT=$(LINT)" -!ENDIF - -!IFDEF NOUI -MAKEFLAG_QUIET="NOUI=$(NOUI)" -!ELSE -!IFDEF QUIET -MAKEFLAG_QUIET="QUIET=$(QUIET)" -!ENDIF -!ENDIF - -!IFDEF RELEASE_OEM -MAKEFLAG_RELEASE_OEM="RELEASE_OEM=$(RELEASE_OEM)" -!ENDIF - -# -# USERDEFINES allows the developer to specify overrides for various settings. We need a variable -# because Makefiles cannot test for file existence -# - -# TODO: can we eliminate this? - -!ifdef USERDEFINES -!include $(WINDOWS_ROOT)\src\UserDefines.mak -!endif - -# -# Delphi Compiler Configuration - Delphi 10.3.2 -# - -!IFNDEF DELPHI_VERSION -DELPHI_VERSION=20.0 -!ENDIF - -DCC32PATH=C:\Program Files (x86)\Embarcadero\Studio\$(DELPHI_VERSION)\bin - -# -# Pass local configuration through to sub-instances of MAKE -# - -MAKE="nmake" /C $(MAKEFLAG_QUICK_BUILD_KEYMAN) $(MAKEFLAG_USERDEFINES) $(MAKEFLAG_DEBUG) $(MAKEFLAG_BUILDHELP) $(MAKEFLAG_BUILDRTF) $(MAKEFLAG_SC_TIMESTAMP) $(MAKEFLAG_LINT) $(MAKEFLAG_QUIET) $(MAKEFLAG_RELEASE_OEM) - -# -# Delphi build commands -# - -!IFDEF DEBUG -TARGET_PATH=Debug -!ELSE -TARGET_PATH=Release -!ENDIF - -!IFDEF LINT -DELPHIWARNINGS=-W+MESSAGE_DIRECTIVE -W+IMPLICIT_STRING_CAST -W+IMPLICIT_STRING_CAST_LOSS -W+EXPLICIT_STRING_CAST -W+EXPLICIT_STRING_CAST_LOSS -W+CVT_WCHAR_TO_ACHAR -W+CVT_NARROWING_STRING_LOST -W+CVT_ACHAR_TO_WCHAR -W+CVT_WIDENING_STRING_LOST -W+UNICODE_TO_LOCALE -W+LOCALE_TO_UNICODE -W+IMPLICIT_VARIANTS -!ELSE -DELPHIWARNINGS=-W-MESSAGE_DIRECTIVE -W-IMPLICIT_STRING_CAST -W-IMPLICIT_STRING_CAST_LOSS -W-EXPLICIT_STRING_CAST -W-EXPLICIT_STRING_CAST_LOSS -W-CVT_WCHAR_TO_ACHAR -W-CVT_NARROWING_STRING_LOST -W-CVT_ACHAR_TO_WCHAR -W-CVT_WIDENING_STRING_LOST -W-UNICODE_TO_LOCALE -W-LOCALE_TO_UNICODE -W-IMPLICIT_VARIANTS -W-IMPLICIT_INTEGER_CAST_LOSS -W-IMPLICIT_CONVERSION_LOSS -W-COMBINING_SIGNED_UNSIGNED64 -W-COMBINING_SIGNED_UNSIGNED64 -!ENDIF - -DELPHIDPRPARAMS=-Q -B -GD -H -VT -^$C+ -^$D+ -^$J+ -^$L+ -^$O+ -^$Q- -^$R- -^$W+ -^$Y+ -E. $(DELPHIWARNINGS) -I$(DELPHIINCLUDES) -U$(DELPHIINCLUDES) -R$(DELPHIINCLUDES) -NSVcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;System;Xml;Web;Soap;Winapi;System.Win -NU.\obj\Win32\$(TARGET_PATH) -E.\bin\Win32\$(TARGET_PATH) -DELPHIDPRPARAMS64=-Q -B -GD -H -VT -^$C+ -^$D+ -^$J+ -^$L+ -^$O+ -^$Q- -^$R- -^$W+ -^$Y+ -E. $(DELPHIWARNINGS) -I$(DELPHIINCLUDES) -U$(DELPHIINCLUDES) -R$(DELPHIINCLUDES) -NSVcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;System;Xml;Web;Soap;Winapi;System.Win -NU.\obj\Win64\$(TARGET_PATH) -E.\bin\Win64\$(TARGET_PATH) -DELPHIDPKPARAMS=-Q -B -GD -VT -^$C+ -^$D+ -^$J+ -^$L+ -^$O+ -^$Q- -^$R- -^$W+ -^$Y+ -E. $(DELPHIWARNINGS) -I$(DELPHIINCLUDES) -U$(DELPHIINCLUDES) -R$(DELPHIINCLUDES) -NSVcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;System;Xml;Web;Soap;Winapi;System.Win -LE$(OUTLIB) -LN$(OUTLIB) -NSData -NUobj\Win32\$(TARGET_PATH) - -COMMON_DELPHIDPKPARAMS=-Q -B -GD -VT -^$C+ -^$D+ -^$J+ -^$L+ -^$O+ -^$Q- -^$R- -^$W+ -^$Y+ -E. $(DELPHIWARNINGS) -I$(DELPHIINCLUDES) -U$(DELPHIINCLUDES) -R$(DELPHIINCLUDES) -NSVcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;System;Xml;Web;Soap;Winapi;System.Win -LE$(COMMON_OUTLIB) -LN$(COMMON_OUTLIB) -NSData -NUobj\Win32\$(TARGET_PATH) - -# we are using cmd /c because dcc32/dcc64 are failing on direct execution -# from nmake -DCC32=cmd /c "$(DCC32PATH)\dcc32.exe" $(DELPHIDPRPARAMS) -DCC32DPK=cmd /c "$(DCC32PATH)\dcc32.exe" $(DELPHIDPKPARAMS) -COMMON_DCC32DPK=cmd /c "$(DCC32PATH)\dcc32.exe" $(COMMON_DELPHIDPKPARAMS) -DCC64=cmd /c "$(DCC32PATH)\dcc64.exe" $(DELPHIDPRPARAMS64) -N0x64\ -Ex64\ - -# -# Delphi MSBuild related commands and macros -# - -DELPHI_MSBUILD="$(COMMON_ROOT)\tools\msbuild-wrapper.bat" "$(DCC32PATH)" $(DELPHI_MSBUILD_FLAG_DEBUG) - -!IFDEF NODELPHI -DCC32=echo skipping -DCC32DPK=echo skipping -DCC64=echo skipping -DELPHI_MSBUILD=echo skipping -!ENDIF - -# Visual C++ x86, x64 -WIN32_TARGET_PATH=bin\Win32\$(TARGET_PATH) -X64_TARGET_PATH=bin\x64\$(TARGET_PATH) - -# Delphi x86, x64 -# WIN32_TARGET_PATH=... -WIN64_TARGET_PATH=bin\Win64\$(TARGET_PATH) - -# -# Shared devtools app for common Delphi source manipulation -# - -DEVTOOLS=$(COMMON_ROOT)\tools\devtools\$(WIN32_TARGET_PATH)\devtools.exe - -# -# Other program build commands -# - -BRCC32=rc.exe - -HHC="C:\Program Files (x86)\HTML Help Workshop\hhc.exe" -NMAKE=nmake.exe -CL=cl.exe -MSBUILD=msbuild.exe -# /maxcpucount see https://devblogs.microsoft.com/cppblog/precompiled-header-pch-issues-and-recommendations/ -MT=mt.exe -VCBUILD=error - -!IFDEF DEBUG -MSBUILD_BUILD=/t:Build /p:Configuration=Debug -MSBUILD_CLEAN=/t:Clean /p:Configuration=Debug -!ELSE -MSBUILD_BUILD=/t:Rebuild /p:Configuration=Release -MSBUILD_CLEAN=/t:Clean /p:Configuration=Release -!ENDIF - -COPY=copy -ISXBUILD=C:\PROGRA~1\INSTALLSHIELD\Express\System\IsExpCmdBld -WZZIPPATH="C:\program files\7-zip\7z.exe" -!IFDEF GO_FAST -WZZIP=$(WZZIPPATH) a -mx1 -!ELSE -WZZIP=$(WZZIPPATH) a -mx9 -!ENDIF -WZUNZIP=$(WZZIPPATH) e - -# we are using cmd /c because tds2dbg is failing on direct execution -# from nmake -TDS2DBG=cmd /c $(KEYMAN_ROOT)\common\windows\bin\tools\tds2dbg -SENTRYTOOL=$(COMMON_ROOT)\tools\sentrytool\$(WIN32_TARGET_PATH)\sentrytool.exe -SENTRYTOOL_DELPHIPREP=$(SENTRYTOOL) delphiprep -r $(KEYMAN_ROOT) -i $(DELPHIINCLUDES) - -WIXPATH="c:\program files (x86)\WiX Toolset v3.11\bin" -WIXCANDLE=$(WIXPATH)\candle.exe -wx -nologo - -!IFDEF LINT -WIXLIGHTLINT= -!ELSE -# we suppress ICE82 because it reports spurious errors with merge module keymanengine to do with duplicate sequence numbers. Safely ignored. -WIXLIGHTLINT= -sice:ICE82 -sice:ICE80 -!ENDIF - -!IFDEF GO_FAST -# for debug builds, we turn off compression because it is so hideously slow -# for test builds, we also turn off compression -WIXLIGHTCOMPRESSION=-dcl:none -!ELSE -WIXLIGHTCOMPRESSION=-dcl:high -!ENDIF - -WIXLIGHT=$(WIXPATH)\light.exe -wx -nologo $(WIXLIGHTLINT) $(WIXLIGHTCOMPRESSION) - -WIXLIT=$(WIXPATH)\lit.exe -wx -nologo -WIXHEAT=$(WIXPATH)\heat.exe - -LINKPATH=link.exe - -# -# Certificates and code signing -# - -!ifdef SIGNCODE_BUILD -MAKE=$(MAKE) "SIGNCODE_BUILD=$(SIGNCODE_BUILD)" -!else -MAKE=$(MAKE) -!endif - -# -# To get a .pfx from a .spc and .pvk, run pvk2pfx.exe -# - -!IFNDEF SC_PFX_SHA1 -SC_PFX_SHA1="$(COMMON_ROOT)\tools\certificates\keymantest-sha1.pfx" -!ENDIF - -!IFNDEF SC_PFX_SHA256 -SC_PFX_SHA256="$(COMMON_ROOT)\tools\certificates\keymantest-sha256.pfx" -!ENDIF - -!IFNDEF SC_URL -SC_URL="https://keyman.com/" -!ENDIF - -!IFNDEF SC_PWD -SC_PWD="" -!ENDIF - -SIGNCODE=@"$(KEYMAN_ROOT)\common\windows\signtime.bat" signtool.exe $(SC_PFX_SHA1) $(SC_PFX_SHA256) $(SC_URL) $(SC_PWD) - -# -# On some computers, the PLATFORM environment variable is set to x86. This can break msbuild -# with our projects. This may be resolvable in the future, but for now the easy fix is ... -# - -PLATFORM=Win32 - -# -# mkver commands. mkver determines tag from the local build environment variables -# in the same way as /resources/build/build-utils.sh. -# - -!ifdef GIT_BASH_FOR_KEYMAN -MKVER_SH=$(GIT_BASH_FOR_KEYMAN) $(KEYMAN_ROOT)\common\windows\mkver.sh -!else -MKVER_SH=start /wait $(KEYMAN_ROOT)\common\windows\mkver.sh -!endif - -MKVER_M=$(MKVER_SH) manifest.in manifest.xml -MKVER_U=$(MKVER_SH) - -# -# Symstore -# - -# KEYMAN_SYMSTOREPATH defaults to sibling folder "symbols". If it is not present, -# then we won't attempt to write symbols to the store. -!IFNDEF KEYMAN_SYMSTOREPATH -KEYMAN_SYMSTOREPATH=$(KEYMAN_ROOT)\..\symbols -!ENDIF - -# Nearly matches algorithm from resources/build/build-utils.sh -# For now, we'll use it only for SYMSTORE, where it is for reference -# only. Thus using the variable name __VERSION_WITH_TAG. Issues: -# 1. always appends tier, even for stable -# 2. test builds will append a branch name for master/beta/stable-x.y -# 3. this is only available for `make symbols` (VERSION_WIN, VERSION_TIER -# are defined in Targets.mak only here) -# Fixing this properly would be possible but take a fair bit more -# work than I want to do just now. The intent is to make it possible to find -# symbols in the symstore index which we can purge later on. -__VERSION_WITH_TAG=$(VERSION_WIN)-$(VERSION_TIER) -!IFNDEF TEAMCITY_VERSION -__VERSION_WITH_TAG=$(__VERSION_WITH_TAG)-local -!ELSE -!IFDEF TEAMCITY_PR_NUMBER -__VERSION_WITH_TAG=$(__VERSION_WITH_TAG)-test-$(TEAMCITY_PR_NUMBER) -!ENDIF -!ENDIF - -# This command depends on VERSION_WIN and VERSION_TIER being defined, through -# `make symbols` (i.e. don't call `make wrap-symbols`) -SYMSTORE="C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\symstore.exe" add \ - /s "$(KEYMAN_SYMSTOREPATH)" \ - /v "$(VERSION_WIN)" \ - /c "Version: $(__VERSION_WITH_TAG)" \ - /compress /f - -CLEAN=-del /S /Q diff --git a/common/windows/delphi/Header.mak b/common/windows/delphi/Header.mak deleted file mode 100644 index 6c78b3724e3..00000000000 --- a/common/windows/delphi/Header.mak +++ /dev/null @@ -1,59 +0,0 @@ -# -# Header.mak - used for makefiles which are in parent folders -# - -HEADER_MAK=1 - -!IFNDEF TARGETS -!ERROR You must define the targets before including the Header.mak file! -!ENDIF - -!IFNDEF KEYMAN_ROOT -!ERROR KEYMAN_ROOT must be defined! -!ENDIF - -# This path will need to be updated if the root path changes - -!include $(KEYMAN_ROOT)\common\windows\delphi\Defines.mak - -# -# The targets build, signcode, symbols are standard -# targets for all projects -# - -build: $(BUILDPREREQ) - $(MAKE) "TARGET=build" $(TARGETS) - -!IFNDEF NOTARGET_SIGNCODE -signcode: - $(MAKE) "TARGET=signcode" $(TARGETS) - -symbols: - $(MAKE) "TARGET=symbols" $(TARGETS) -!ELSE -signcode: - rem no signcode required - -symbols: - rem no symbols required -!ENDIF - -build-release: -!IFDEF RELEASE_TARGETS - $(MAKE) "TARGET=build-release" $(RELEASE_TARGETS) -!ELSE - @rem -!ENDIF - -clean: - $(MAKE) "TARGET=clean" $(TARGETS) $(CLEANS) - -install: - $(MAKE) "TARGET=install" $(TARGETS) - -test-manifest: -!IFDEF MANIFESTS - $(MAKE) "TARGET=test-manifest" $(MANIFESTS) -!ELSE - $(MAKE) "TARGET=test-manifest" $(TARGETS) -!ENDIF \ No newline at end of file diff --git a/common/windows/delphi/Target.mak b/common/windows/delphi/Target.mak deleted file mode 100644 index 777bd84beb6..00000000000 --- a/common/windows/delphi/Target.mak +++ /dev/null @@ -1,14 +0,0 @@ -!CMDSWITCHES +S - -def-clean: - $(CLEAN) *.err *.stat *.dproj.local *.Build.CppClean.Log *.suo *.jdbg *.dbg *.dcu *.~* *.dsk *.exe *.rsm *.ncb *.opt *.pch *.plg *.aps *.001 *.sbr *.dep *.drc *.bak *.pdb *.lib *.cod *.ilk *.tds vc80.idb *.map *.bsc version.res manifest.xml manifest.res >nul 2>nul - $(CLEAN) ExcMagic.Debug *.wixpdb *.identcache *.embed.manifest *.embed.manifest.res *.intermediate.manifest error.log >nul 2>nul - if exist bin rd /s/q bin - if exist obj rd /s/q obj - -!CMDSWITCHES -S - -# This virtual rule forces targets which are folders, e.g. `kmshell` to -# always execute -.virtual: - rem always execute diff --git a/common/windows/delphi/components/common_components.dproj b/common/windows/delphi/components/common_components.dproj index 2bd91473abe..4ecda904642 100644 --- a/common/windows/delphi/components/common_components.dproj +++ b/common/windows/delphi/components/common_components.dproj @@ -13,31 +13,6 @@ true - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - true Base @@ -59,6 +34,12 @@ Base true + + true + Cfg_2 + true + true + .\obj\$(Platform)\$(Config) .\$(Platform)\$(Config) @@ -77,23 +58,6 @@ 3081 CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= - - None - android-support-v4.dex.jar;cloud-messaging.dex.jar;com-google-android-gms.play-services-ads-base.17.2.0.dex.jar;com-google-android-gms.play-services-ads-identifier.16.0.0.dex.jar;com-google-android-gms.play-services-ads-lite.17.2.0.dex.jar;com-google-android-gms.play-services-ads.17.2.0.dex.jar;com-google-android-gms.play-services-analytics-impl.16.0.8.dex.jar;com-google-android-gms.play-services-analytics.16.0.8.dex.jar;com-google-android-gms.play-services-base.16.0.1.dex.jar;com-google-android-gms.play-services-basement.16.2.0.dex.jar;com-google-android-gms.play-services-gass.17.2.0.dex.jar;com-google-android-gms.play-services-identity.16.0.0.dex.jar;com-google-android-gms.play-services-maps.16.1.0.dex.jar;com-google-android-gms.play-services-measurement-base.16.4.0.dex.jar;com-google-android-gms.play-services-measurement-sdk-api.16.4.0.dex.jar;com-google-android-gms.play-services-stats.16.0.1.dex.jar;com-google-android-gms.play-services-tagmanager-v4-impl.16.0.8.dex.jar;com-google-android-gms.play-services-tasks.16.0.1.dex.jar;com-google-android-gms.play-services-wallet.16.0.1.dex.jar;com-google-firebase.firebase-analytics.16.4.0.dex.jar;com-google-firebase.firebase-common.16.1.0.dex.jar;com-google-firebase.firebase-iid-interop.16.0.1.dex.jar;com-google-firebase.firebase-iid.17.1.1.dex.jar;com-google-firebase.firebase-measurement-connector.17.0.1.dex.jar;com-google-firebase.firebase-messaging.17.5.0.dex.jar;fmx.dex.jar;google-play-billing.dex.jar;google-play-licensing.dex.jar - - - None - android-support-v4.dex.jar;cloud-messaging.dex.jar;com-google-android-gms.play-services-ads-base.17.2.0.dex.jar;com-google-android-gms.play-services-ads-identifier.16.0.0.dex.jar;com-google-android-gms.play-services-ads-lite.17.2.0.dex.jar;com-google-android-gms.play-services-ads.17.2.0.dex.jar;com-google-android-gms.play-services-analytics-impl.16.0.8.dex.jar;com-google-android-gms.play-services-analytics.16.0.8.dex.jar;com-google-android-gms.play-services-base.16.0.1.dex.jar;com-google-android-gms.play-services-basement.16.2.0.dex.jar;com-google-android-gms.play-services-gass.17.2.0.dex.jar;com-google-android-gms.play-services-identity.16.0.0.dex.jar;com-google-android-gms.play-services-maps.16.1.0.dex.jar;com-google-android-gms.play-services-measurement-base.16.4.0.dex.jar;com-google-android-gms.play-services-measurement-sdk-api.16.4.0.dex.jar;com-google-android-gms.play-services-stats.16.0.1.dex.jar;com-google-android-gms.play-services-tagmanager-v4-impl.16.0.8.dex.jar;com-google-android-gms.play-services-tasks.16.0.1.dex.jar;com-google-android-gms.play-services-wallet.16.0.1.dex.jar;com-google-firebase.firebase-analytics.16.4.0.dex.jar;com-google-firebase.firebase-common.16.1.0.dex.jar;com-google-firebase.firebase-iid-interop.16.0.1.dex.jar;com-google-firebase.firebase-iid.17.1.1.dex.jar;com-google-firebase.firebase-measurement-connector.17.0.1.dex.jar;com-google-firebase.firebase-messaging.17.5.0.dex.jar;fmx.dex.jar;google-play-billing.dex.jar;google-play-licensing.dex.jar - - - None - - - None - - - None - Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) Debug @@ -113,6 +77,7 @@ false true 1033 + ..\..\lib false @@ -120,6 +85,11 @@ 0 0 + + ..\..\lib + true + 1033 + MainSource @@ -164,8 +134,6 @@ common_components.dpk - File c:\projects\keyman\app\windows\lib\DCPdelphi2009.bpl not found - File c:\projects\keyman\app\windows\lib\delphiprojectmanager.bpl not found Microsoft Office 2000 Sample Automation Server Wrapper Components Microsoft Office XP Sample Automation Server Wrapper Components @@ -944,13 +912,6 @@ - False - False - False - False - False - False - False True False diff --git a/common/windows/delphi/ext/cef4delphi/packages/CEF4Delphi.res b/common/windows/delphi/ext/cef4delphi/packages/CEF4Delphi.res deleted file mode 100644 index b9b013c2123..00000000000 Binary files a/common/windows/delphi/ext/cef4delphi/packages/CEF4Delphi.res and /dev/null differ diff --git a/common/windows/delphi/general/RegistryKeys.pas b/common/windows/delphi/general/RegistryKeys.pas index 47e7df754a0..e351b3860ff 100644 --- a/common/windows/delphi/general/RegistryKeys.pas +++ b/common/windows/delphi/general/RegistryKeys.pas @@ -113,10 +113,11 @@ interface SRegValue_ShowWelcome = 'show welcome'; // CU SRegValue_UseAdvancedInstall = 'use advanced install'; // CU - SRegValue_AltGrCtrlAlt = 'simulate altgr'; // CU - SRegValue_KeyboardHotKeysAreToggle = 'hotkeys are toggles'; // CU + SRegValue_AltGrCtrlAlt = 'simulate altgr'; // CU + SRegValue_KeyboardHotKeysAreToggle = 'hotkeys are toggles'; // CU + SRegValue_AllowRightModifierHotKey = 'allow right modifier for hotkey'; // CU SRegValue_ReleaseShiftKeysAfterKeyPress = 'release shift keys after key press'; // CU - SRegValue_TestKeymanFunctioning = 'test keyman functioning'; // CU, default true + SRegValue_TestKeymanFunctioning = 'test keyman functioning'; // CU, default true SRegValue_CreateStartMenuAsSubfolders = 'create start menu as subfolders'; // CU SRegValue_CreateUninstallEntries = 'create uninstall entries'; // CU @@ -379,8 +380,6 @@ interface SRegKey_KeymanEngineDebug_CU = SRegKey_KeymanEngineRoot_CU + '\Debug'; - SRegValue_Flag_UseRegisterHotkey = 'Flag_UseRegisterHotkey'; - SRegValue_Flag_UseCachedHotkeyModifierState = 'Flag_UseCachedHotkeyModifierState'; SRegValue_Flag_ShouldSerializeInput = 'Flag_ShouldSerializeInput'; SRegValue_Flag_UseAutoStartTask = 'Flag_UseAutoStartTask'; SRegValue_Flag_SyncLanguagesToCloud = 'Flag_SyncLanguagesToCloud'; diff --git a/common/windows/delphi/keyboards/kmxfileconsts.pas b/common/windows/delphi/keyboards/kmxfileconsts.pas index 2a32184ec04..e8f0e0d740c 100644 --- a/common/windows/delphi/keyboards/kmxfileconsts.pas +++ b/common/windows/delphi/keyboards/kmxfileconsts.pas @@ -29,7 +29,6 @@ interface SZMAX_GROUPNAME = 80; SZMAX_DEADKEYNAME = 80; SZMAX_VKDICTIONARYNAME = 80; // I3438 - SZMAX_ERRORTEXT = 512; SZMAX_LANGUAGENAME = 80; SZMAX_KEYBOARDNAME = 80; diff --git a/common/windows/delphi/tools/certificates/build.sh b/common/windows/delphi/tools/certificates/build.sh index 378723a1257..10ab4c75cec 100755 --- a/common/windows/delphi/tools/certificates/build.sh +++ b/common/windows/delphi/tools/certificates/build.sh @@ -21,6 +21,8 @@ builder_describe_outputs \ function do_certificates() { rm -f KeymanTestCA-sha1.* KeymanTest-sha1.* KeymanTest-sha256.* KeymanTestCA-sha256.* + source "$KEYMAN_ROOT/resources/build/win/visualstudio_environment.inc.sh" + makecert -r -pe -n "CN=Keyman Test CA SHA1" -ss CA -sr CurrentUser -a sha1 -cy authority -sky signature -sv KeymanTestCA-sha1.pvk KeymanTestCA-sha1.cer certutil -user -addstore Root KeymanTestCA-sha1.cer makecert -pe -n "CN=Keyman Test Certificate SHA1" -a sha1 -cy end -sky signature -ic KeymanTestCA-sha1.cer -iv KeymanTestCA-sha1.pvk -sv KeymanTest-sha1.pvk KeymanTest-sha1.cer diff --git a/core/commands.inc.sh b/core/commands.inc.sh index 9f1365cddae..f2c49d88cac 100644 --- a/core/commands.inc.sh +++ b/core/commands.inc.sh @@ -91,7 +91,14 @@ do_test() { if [[ $target =~ ^(x86|x64)$ ]]; then cmd //C build.bat $target $BUILDER_CONFIGURATION test $testparams else - meson test -C "$MESON_PATH" $testparams + if [[ $target == wasm ]] && [[ $BUILDER_OS == mac ]]; then + # 11794 -- parallel tests failing on some mac build agents; temporary + # mitigation until we diagnose root cause + meson test -j 1 -C "$MESON_PATH" $testparams + else + meson test -C "$MESON_PATH" $testparams + fi + fi builder_finish_action success test:$target } diff --git a/core/cross-mac-arm64.build b/core/cross-mac-arm64.build index 720e9f31007..faf3e6e9c15 100644 --- a/core/cross-mac-arm64.build +++ b/core/cross-mac-arm64.build @@ -5,9 +5,9 @@ cpu = 'arm64' endian = 'little' [binaries] -c = ['clang', '-arch', 'arm64'] -objc = ['clang', '-arch', 'arm64'] -cpp = ['clang++', '-arch', 'arm64'] +c = ['clang', '-arch', 'arm64', '-mmacosx-version-min=10.13'] +objc = ['clang', '-arch', 'arm64', '-mmacosx-version-min=10.13'] +cpp = ['clang++', '-arch', 'arm64', '-mmacosx-version-min=10.13'] ar = 'ar' ld = 'ld' strip = 'strip' diff --git a/core/cross-mac-x86_64.build b/core/cross-mac-x86_64.build index 2ab2c62a600..3378fe11a40 100644 --- a/core/cross-mac-x86_64.build +++ b/core/cross-mac-x86_64.build @@ -5,9 +5,9 @@ cpu = 'x86_64' endian = 'little' [binaries] -c = ['clang', '-arch', 'x86_64'] -objc = ['clang', '-arch', 'x86_64'] -cpp = ['clang++', '-arch', 'x86_64'] +c = ['clang', '-arch', 'x86_64', '-mmacosx-version-min=10.13'] +objc = ['clang', '-arch', 'x86_64', '-mmacosx-version-min=10.13'] +cpp = ['clang++', '-arch', 'x86_64', '-mmacosx-version-min=10.13'] ar = 'ar' ld = 'ld' strip = 'strip' diff --git a/core/doc/meson.build b/core/doc/meson.build index 6039e2e2dda..a0f2bc4e6d5 100644 --- a/core/doc/meson.build +++ b/core/doc/meson.build @@ -20,7 +20,7 @@ if hotdoc.found() deps = files( '../include/keyman/keyman_core_api.h', '../src/jsonpp.hpp', - '../src/utfcodec.hpp' + '../../common/cpp/utfcodec.hpp' ) docs = custom_target('docs', diff --git a/core/meson.build b/core/meson.build index 0049c95741e..4a9ea257ea8 100644 --- a/core/meson.build +++ b/core/meson.build @@ -13,7 +13,7 @@ project('keyman_core', 'cpp', 'c', 'b_vscrt=static_from_buildtype', 'warning_level=2', 'debug=true'], - meson_version: '>=0.57.0') + meson_version: '>=1.0') # Import our standard compiler defines; this is copied from # /resources/build/standard.meson.build by build.sh, because diff --git a/core/src/actions_normalize.cpp b/core/src/actions_normalize.cpp index 3384fda9757..78bada7b6f9 100644 --- a/core/src/actions_normalize.cpp +++ b/core/src/actions_normalize.cpp @@ -15,12 +15,12 @@ #include "state.hpp" #include "option.hpp" #include "debuglog.h" -#include "core_icu.h" +#include "util_normalize.hpp" +#include "utfcodec.hpp" +#include "kmx/kmx_xstring.h" -// forward declarations - -icu::UnicodeString context_items_to_unicode_string(km::core::context const *context); -km_core_usv *unicode_string_to_usv(icu::UnicodeString& src); +// forward declaration +bool context_items_to_unicode_string(km::core::context const *context, std::u32string &str); /** * Normalize the output from an action to NFC, across the context | output @@ -65,32 +65,12 @@ bool km::core::actions_normalize( cached_context. */ - /* - Initialization - */ - - UErrorCode icu_status = U_ZERO_ERROR; - const icu::Normalizer2 *nfc = icu::Normalizer2::getNFCInstance(icu_status); - assert(U_SUCCESS(icu_status)); - if(!U_SUCCESS(icu_status)) { - DebugLog("getNFCInstance failed with %x", icu_status); + std::u32string output(actions.output); + std::u32string cached_context_string, app_context_string; + if (!context_items_to_unicode_string(cached_context, cached_context_string)) { return false; } - - const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(icu_status); - assert(U_SUCCESS(icu_status)); - if(!U_SUCCESS(icu_status)) { - DebugLog("getNFDInstance failed with %x", icu_status); - return false; - } - - icu::UnicodeString output = icu::UnicodeString::fromUTF32(reinterpret_cast(actions.output), -1); - icu::UnicodeString cached_context_string = context_items_to_unicode_string(cached_context); - icu::UnicodeString app_context_string = context_items_to_unicode_string(app_context); - assert(!output.isBogus()); - assert(!cached_context_string.isBogus()); - assert(!app_context_string.isBogus()); - if(output.isBogus() || cached_context_string.isBogus() || app_context_string.isBogus()) { + if (!context_items_to_unicode_string(app_context, app_context_string)) { return false; } int nfu_to_delete = 0; @@ -98,20 +78,25 @@ bool km::core::actions_normalize( /* Further debug assertion of inputs */ - - assert(nfd->isNormalized(output, icu_status) && U_SUCCESS(icu_status)); - assert(nfd->isNormalized(cached_context_string, icu_status) && U_SUCCESS(icu_status)); + assert(km::core::util::is_nfd(output)); + assert(km::core::util::is_nfd(cached_context_string)); /* The keyboard processor will have updated the cached_context already, - applying the transform to it, so we need to rewind this. Remove the output - from cached_context_string to start + applying the transform to it, so we need to rewind this. + + Assert that 'cached_context_string' ends with 'output' + + Remove the output + from cached_context_string to start. */ assert(cached_context_string.length() >= output.length()); - int n = cached_context_string.length() - output.length(); + size_t n = cached_context_string.length() - output.length(); + // auto end_cached = cached_context_string.substr(n, output.length()); + // assert(end_cached == output); assert(cached_context_string.compare(n, output.length(), output) == 0); - cached_context_string.remove(n); + cached_context_string.resize(n); /* While cached_context is guaranteed to be normalized, actions->output may not @@ -119,21 +104,22 @@ bool km::core::actions_normalize( normalization in our output, we now need to look for a normalization boundary prior to the intersection of the cached_context and the output. */ - if(!output.isEmpty()) { - while(n > 0 && !nfd->hasBoundaryBefore(output[0])) { + if(!output.empty()) { + while(n > 0 && !km::core::util::has_nfd_boundary_before(output[0])) { // The output may interact with the context further in normalization. We // need to copy characters back further until we reach a normalization // boundary. // Remove last code point from the context ... - n = cached_context_string.moveIndex32(n, -1); - UChar32 chr = cached_context_string.char32At(n); - cached_context_string.remove(n); + auto len = cached_context_string.length(); + assert(len>0); + auto chr = cached_context_string.at(len-1); + cached_context_string.resize(len-1); // And prepend it to the output ... - output.insert(0, chr); + output.insert(0, 1, chr); } } @@ -148,25 +134,20 @@ bool km::core::actions_normalize( its normalized form matches the cached_context normalized form. */ - while(app_context_string.countChar32()) { - icu::UnicodeString app_context_nfd; - nfd->normalize(app_context_string, app_context_nfd, icu_status); - assert(U_SUCCESS(icu_status)); - if(!U_SUCCESS(icu_status)) { - DebugLog("nfd->normalize failed with %x", icu_status); + while(!app_context_string.empty()) { + auto app_context_nfd = app_context_string; + if(!km::core::util::normalize_nfd(app_context_nfd)) { + DebugLog("nfd->normalize failed"); return false; } - if(app_context_nfd.compare(cached_context_string) == 0) { + if(app_context_nfd == cached_context_string) { break; } - // remove the last UChar32 - int32_t lastUChar32 = app_context_string.length()-1; - // adjust pointer to get the entire char (i.e. so we don't slice a non-BMP char) - lastUChar32 = app_context_string.getChar32Start(lastUChar32); - // remove the UChar32 (1 or 2 code units) - app_context_string.remove(lastUChar32); + size_t len = app_context_string.length(); + // remove the cp at end + app_context_string.resize(len-1); nfu_to_delete++; } @@ -174,17 +155,15 @@ bool km::core::actions_normalize( Normalize our output string */ - icu::UnicodeString output_nfc; - nfc->normalize(output, output_nfc, icu_status); - assert(U_SUCCESS(icu_status)); - if(!U_SUCCESS(icu_status)) { - DebugLog("nfc->normalize failed with %x", icu_status); + auto output_nfc = output; + if(!km::core::util::normalize_nfc(output_nfc)) { + DebugLog("nfc->normalize failed"); return false; } - auto new_output = unicode_string_to_usv(output_nfc); - if(!new_output) { - // error logging handled in unicode_string_to_usv + auto new_output = km::core::util::string_to_usv(output_nfc); + assert(new_output != nullptr); + if(new_output == nullptr) { return false; } @@ -197,8 +176,8 @@ bool km::core::actions_normalize( app_context_string.append(output_nfc); km_core_context_item *app_context_items = nullptr; km_core_status status = KM_CORE_STATUS_OK; - if((status = context_items_from_utf16(app_context_string.getTerminatedBuffer(), &app_context_items)) != KM_CORE_STATUS_OK) { - DebugLog("context_items_from_utf16 failed with %x", status); + if((status = context_items_from_utf32(app_context_string.c_str(), &app_context_items)) != KM_CORE_STATUS_OK) { + DebugLog("context_items_from_string failed with %x", status); delete [] new_output; return false; } @@ -224,21 +203,19 @@ bool km::core::actions_normalize( /** * Helper to convert km_core_context list into a icu::UnicodeString */ -icu::UnicodeString context_items_to_unicode_string(km::core::context const *context) { - icu::UnicodeString nullString; - nullString.setToBogus(); +bool context_items_to_unicode_string(km::core::context const *context, std::u32string &str) { km_core_context_item *items = nullptr; km_core_status status; if((status = km_core_context_get(static_cast(context), &items)) != KM_CORE_STATUS_OK) { DebugLog("Failed to retrieve context with %s", status); - return nullString; + return false; } size_t buf_size = 0; if((status = context_items_to_utf32(items, nullptr, &buf_size)) != KM_CORE_STATUS_OK) { DebugLog("Failed to retrieve context size with %s", status); km_core_context_items_dispose(items); - return nullString; + return false; } km_core_usv *buf = new km_core_usv[buf_size]; @@ -246,39 +223,16 @@ icu::UnicodeString context_items_to_unicode_string(km::core::context const *cont DebugLog("Failed to retrieve context with %s", status); km_core_context_items_dispose(items); delete [] buf; - return nullString; + return false; } - auto result = icu::UnicodeString::fromUTF32(reinterpret_cast(buf), -1); + str = std::u32string(buf, buf_size - 1); // don't include terminating null km_core_context_items_dispose(items); delete [] buf; - return result; -} - -/** - * Helper to convert icu::UnicodeString to a UTF-32 km_core_usv buffer, - * nul-terminated - */ -km_core_usv *unicode_string_to_usv(icu::UnicodeString& src) { - UErrorCode icu_status = U_ZERO_ERROR; - - km_core_usv *dst = new km_core_usv[src.length() + 1]; - - src.toUTF32(reinterpret_cast(dst), src.length(), icu_status); - - assert(U_SUCCESS(icu_status)); - if(!U_SUCCESS(icu_status)) { - DebugLog("toUTF32 failed with %x", icu_status); - delete[] dst; - return nullptr; - } - - dst[src.length()] = 0; - return dst; + return true; } - /** * Refresh app_context to match the cached_context. Does not do normalization, * unlike `actions_normalize`. Used in conjunction with keyboard processors that diff --git a/core/src/context.hpp b/core/src/context.hpp index 91cc9688cd9..f56d637eff1 100644 --- a/core/src/context.hpp +++ b/core/src/context.hpp @@ -87,6 +87,30 @@ km_core_status context_items_from_utf16(km_core_cu const *text, km_core_context_item **out_ptr); +/** + * Convert a UTF32 encoded Unicode string into an array of `km_core_context_item` + * structures. Allocates memory as needed. + * + * @return km_core_status + * * `KM_CORE_STATUS_OK`: On success. + * * `KM_CORE_STATUS_INVALID_ARGUMENT`: If non-optional parameters are + * null. + * * `KM_CORE_STATUS_NO_MEM`: In the event not enough memory can be + * allocated for the output buffer. + * * `KM_CORE_STATUS_INVALID_UTF`: In the event the UTF32 string cannot + * be decoded. + * + * @param text a pointer to a null terminated array of utf32 encoded data. + * @param out_ptr a pointer to the result variable: A pointer to the start of + * the `km_core_context_item` array containing the representation + * of the input string. Terminated with a type of + * `KM_CORE_CT_END`. Must be disposed of with + * `km_core_context_items_dispose`. + */ +km_core_status +context_items_from_utf32(km_core_usv const *text, + km_core_context_item **out_ptr); + /** * Convert a context item array into a UTF-16 encoded string placing it into the * supplied buffer of specified size, and return the number of code units diff --git a/core/src/core_icu.cpp b/core/src/core_icu.cpp new file mode 100644 index 00000000000..cfaac47c509 --- /dev/null +++ b/core/src/core_icu.cpp @@ -0,0 +1,72 @@ +/* + Copyright: © SIL International. + Description: Common LDML utilities + Create Date: 24 May 2024 + Authors: Steven R. Loomis +*/ + +#include "core_icu.h" + +#if !KMN_NO_ICU + +namespace km { +namespace core { +namespace util { + +/** + * Helper to convert icu::UnicodeString to a UTF-32 km_core_usv buffer, + * nul-terminated + */ +km_core_usv *unicode_string_to_usv(icu::UnicodeString& src) { + UErrorCode icu_status = U_ZERO_ERROR; + + km_core_usv *dst = new km_core_usv[src.length() + 1]; + + src.toUTF32(reinterpret_cast(dst), src.length(), icu_status); + + if(!UASSERT_SUCCESS(icu_status)) { + DebugLog("toUTF32 failed with %x", icu_status); + delete[] dst; + return nullptr; + } + + dst[src.length()] = 0; + return dst; +} + +/** + * Internal function to normalize with a specified mode. + * Note: that this function _does_ assert failure, so it is not + * required to assert its return code. The return is provided so + * that callers can exit (such as making no change) if there was failure. + * + * Also note that "failure" here is something catastrophic: ICU not initialized, + * or, more likely, some low memory situation. Does not fail on "bad" data. + * @param n the ICU Normalizer to use + * @param str input/output string + * @param status error code, must be initialized on input + * @return false if failure + */ +bool normalize(const icu::Normalizer2 *n, std::u16string &str, UErrorCode &status) { + if(!UASSERT_SUCCESS(status)) { + return false; + } + assert(n != nullptr); + icu::UnicodeString dest; + icu::UnicodeString src = icu::UnicodeString(str.data(), (int32_t)str.length()); + n->normalize(src, dest, status); + // the next line here will assert + if (!UASSERT_SUCCESS(status)) { + return false; + } else { + str.assign(dest.getBuffer(), dest.length()); + return true; + } +} + + +} +} +} /* end km::core::util */ + +#endif diff --git a/core/src/core_icu.h b/core/src/core_icu.h index d95d198637e..20b13b04567 100644 --- a/core/src/core_icu.h +++ b/core/src/core_icu.h @@ -3,15 +3,42 @@ */ #pragma once +#ifdef __EMSCRIPTEN__ +// define this in tests to keep ICU around +# if !defined(KMN_IN_LDML_TESTS) +# if !defined(KMN_NO_ICU) +// under wasm, turn off ICU except in tests. +# define KMN_NO_ICU 1 +# endif +# endif +#elif !defined(KMN_NO_ICU) +# define KMN_NO_ICU 0 +#endif + +#if KMN_NO_ICU + +// NO ICU + +// any shims needed here for disabling ICU + +#else + +// YES ICU + #if !defined(HAVE_ICU4C) -#error icu4c is required for this code +# error icu4c is required for this code #endif #define U_FALLTHROUGH #include "unicode/utypes.h" #include "unicode/unistr.h" #include "unicode/normalizer2.h" +#include "unicode/uniset.h" +#include "unicode/usetiter.h" +#include "unicode/regex.h" +#include "unicode/utext.h" +#include "keyman_core.h" #include "debuglog.h" #include @@ -29,5 +56,40 @@ inline bool uassert_success(const char *file, int line, const char *function, UE /** * Assert an ICU4C UErrorCode * the first assert is for debug builds, the second triggers the debuglog and has the return value. + * @returns true on success * */ #define UASSERT_SUCCESS(status) (assert(U_SUCCESS(status)), uassert_success(__FILE__, __LINE__, __FUNCTION__, status)) + +// ------------------ some ICU C++ utilities ---------------------------- + +namespace km { +namespace core { +namespace util { + +/** + * Convert a UnicodeString to a km_core_usv array + * @return the 0-terminated array. Caller owns storage. + */ +km_core_usv *unicode_string_to_usv(icu::UnicodeString& src); + +/** + * Internal function to normalize with a specified mode. + * Note: that this function _does_ assert failure, so it is not + * required to assert its return code. The return is provided so + * that callers can exit (such as making no change) if there was failure. + * + * Also note that "failure" here is something catastrophic: ICU not initialized, + * or, more likely, some low memory situation. Does not fail on "bad" data. + * @param n the ICU Normalizer to use + * @param str input/output string + * @param status error code, must be initialized on input + * @return false if failure + */ +bool normalize(const icu::Normalizer2 *n, std::u16string &str, UErrorCode &status); + + +} +} +} + +#endif /* KMN_NO_ICU */ diff --git a/core/src/km_core_context_api.cpp b/core/src/km_core_context_api.cpp index dd27ab7e5c7..594080d8d0d 100644 --- a/core/src/km_core_context_api.cpp +++ b/core/src/km_core_context_api.cpp @@ -115,6 +115,13 @@ context_items_from_utf16(km_core_cu const *text, } +km_core_status +context_items_from_utf32(km_core_usv const *text, + km_core_context_item **out_ptr) +{ + return _context_items_from(reinterpret_cast(text), out_ptr); +} + km_core_status context_items_to_utf8(km_core_context_item const *ci, char *buf, size_t * sz_ptr) { diff --git a/core/src/kmx/kmx_processevent.cpp b/core/src/kmx/kmx_processevent.cpp index 657843c25d1..6e1e8dc2b4a 100644 --- a/core/src/kmx/kmx_processevent.cpp +++ b/core/src/kmx/kmx_processevent.cpp @@ -230,10 +230,6 @@ KMX_BOOL KMX_ProcessEvent::ProcessGroup(LPGROUP gp, KMX_BOOL *pOutputKeystroke) if(kkp->dpContext[0] != 0) break; else continue; } - //if(kkp->Key == m_state.vkey) - //SendDebugMessageFormat(m_state.msg.hwnd, sdmKeyboard, 0, "kkp->Key: %d kkp->ShiftFlags: %x", - // kkp->Key, kkp->ShiftFlags); - /* Keyman 6.0: support Virtual Characters */ if(IsEquivalentShift(kkp->ShiftFlags, m_modifiers)) { diff --git a/core/src/kmx/kmx_xstring.cpp b/core/src/kmx/kmx_xstring.cpp index a1d5ff518ad..794ba26889a 100644 --- a/core/src/kmx/kmx_xstring.cpp +++ b/core/src/kmx/kmx_xstring.cpp @@ -50,6 +50,15 @@ size_t km::core::kmx::u16len(const km_core_cu *p) { return i; } +size_t km::core::kmx::u32len(const km_core_usv *p) { + int i = 0; + while (*p) { + p++; + i++; + } + return i; +} + int km::core::kmx::u16cmp(const km_core_cu *p, const km_core_cu *q) { while (*p && *q) { if (*p != *q) return *p - *q; @@ -107,6 +116,11 @@ km_core_cu *km::core::kmx::u16dup(km_core_cu *src) { memcpy(dup, src, (u16len(src) + 1) * sizeof(km_core_cu)); return dup; } +km_core_usv *km::core::kmx::u32dup(const km_core_usv *src) { + km_core_usv *dup = new km_core_usv[u32len(src) + 1]; + memcpy(dup, src, (u32len(src) + 1) * sizeof(src[0])); + return dup; +} /* * int xstrlen( PKMX_BYTE p ); diff --git a/core/src/kmx/kmx_xstring.h b/core/src/kmx/kmx_xstring.h index a82e959e56d..e17c0030912 100644 --- a/core/src/kmx/kmx_xstring.h +++ b/core/src/kmx/kmx_xstring.h @@ -117,6 +117,9 @@ int u16ncmp(const km_core_cu *p, const km_core_cu *q, size_t count); km_core_cu *u16tok(km_core_cu *p, km_core_cu ch, km_core_cu **ctx); km_core_cu *u16dup(km_core_cu *src); +size_t u32len(const km_core_usv *p); +km_core_usv *u32dup(const km_core_usv *src); + //KMX_BOOL MapUSCharToVK(KMX_WORD ch, PKMX_WORD puKey, PKMX_DWORD puShiftFlags); // --- implementation --- diff --git a/core/src/ldml/ldml_markers.cpp b/core/src/ldml/ldml_markers.cpp index b1d4604dbc2..5d4fb4ff462 100644 --- a/core/src/ldml/ldml_markers.cpp +++ b/core/src/ldml/ldml_markers.cpp @@ -268,15 +268,14 @@ add_pending_markers( marker_map *markers, marker_list &last_markers, const std::u32string::const_iterator &last, - const std::u32string::const_iterator &end, - const icu::Normalizer2 *nfd) { + const std::u32string::const_iterator &end) { // quick check to see if there's no work to do. if(markers == nullptr) { return; } /** which character this marker is 'glued' to. */ char32_t marker_ch; - icu::UnicodeString decomposition; + std::u32string decomposition; if (last == end) { // at end of text, so use a special value to indicate 'EOT'. marker_ch = MARKER_BEFORE_EOT; @@ -285,17 +284,13 @@ add_pending_markers( // if the character is composed, we need to use the first decomposed char // as the 'glue'. - if(!nfd->getDecomposition(ch, decomposition)) { + if(!km::core::util::normalize_nfd(ch, decomposition)) { // char does not have a decomposition - so it may be used for the glue - marker_ch = ch; - decomposition.remove(); // no other entries needed - } else { - // 'glue' is the first codepoint of the decomposition. - marker_ch = decomposition.char32At(0); - if (decomposition.countChar32() == 1) { - decomposition.remove(); // no other entries needed - } // else: will add the remainder below + // the 'if' is only for the assertions here. + assert(decomposition.length() == 1); // should be a single UTF-32 char + assert(decomposition.at(0) == ch); // should be the same char } + marker_ch = decomposition.at(0); // always the first char } markers->emplace_back(marker_ch); // now, update the map with these markers (in order) on this character. @@ -304,10 +299,10 @@ add_pending_markers( markers->emplace_back(marker_ch, *i); } // add any further entries due to decomposition - if (!decomposition.isEmpty()) { - // We already added the base char above, add teh rest - for (auto i=1; iemplace_back(decomposition.char32At(i)); + if (decomposition.length() > 1) { + // We already added the base char above, add the rest + for (size_t i=1; iemplace_back(decomposition.at(i)); } } // clear the list @@ -318,10 +313,6 @@ std::u32string remove_markers(const std::u32string &str, marker_map *markers, marker_encoding encoding) { std::u32string out; marker_list last_markers; - UErrorCode status = U_ZERO_ERROR; - const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); - UASSERT_SUCCESS(status); - auto last = str.begin(); // points to the part of the string after the last matched marker for (auto i = str.begin(); i != str.end();) { auto marker_no = parse_next_marker(i, str.end(), encoding); @@ -329,7 +320,7 @@ remove_markers(const std::u32string &str, marker_map *markers, marker_encoding e // add any markers found before this entry, but only if there is intervening // text. This prevents the sentinel or the '\u' from becoming the attachment char. if (i != last) { - add_pending_markers(markers, last_markers, last, str.end(), nfd); + add_pending_markers(markers, last_markers, last, str.end()); out.append(last, i); // append any non-marker text since the end of the last marker last = i; // advance over text we've already appended } @@ -347,7 +338,7 @@ remove_markers(const std::u32string &str, marker_map *markers, marker_encoding e // add any remaining pending markers. // if last == str.end() then this wil be MARKER_BEFORE_EOT // otherwise it will be the glue character - add_pending_markers(markers, last_markers, last, str.end(), nfd); + add_pending_markers(markers, last_markers, last, str.end()); // get the suffix between the last marker and the end (could be nothing) out.append(last, str.end()); return out; diff --git a/core/src/ldml/ldml_markers.hpp b/core/src/ldml/ldml_markers.hpp index cfd98d1b154..bbec2f23ccd 100644 --- a/core/src/ldml/ldml_markers.hpp +++ b/core/src/ldml/ldml_markers.hpp @@ -17,10 +17,6 @@ #include "debuglog.h" #include "core_icu.h" -#include "unicode/uniset.h" -#include "unicode/usetiter.h" -#include "unicode/regex.h" -#include "unicode/utext.h" namespace km { namespace core { diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index f90fcfaa946..fcadda629cb 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -85,7 +85,7 @@ ldml_processor::ldml_processor(path const & kb_path, const std::vector } else { str = keyEntry->get_to_string(); } - keys.add((km_core_virtual_key)kmapEntry->vkey, (uint16_t)kmapEntry->mod, str); + keys.add((km_core_virtual_key)kmapEntry->vkey, kmapEntry->mod, str); } } // else: no keys! but still valid. Just, no keys. diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index 7b161ff4f9e..8e37011bda4 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -437,20 +437,14 @@ reorder_group::apply(std::u32string &str) const { } transform_entry::transform_entry(const transform_entry &other) - : fFrom(other.fFrom), fTo(other.fTo), fFromPattern(nullptr), fMapFromStrId(other.fMapFromStrId), + : fFrom(other.fFrom), fTo(other.fTo), fFromPattern(other.fFromPattern), fMapFromStrId(other.fMapFromStrId), fMapToStrId(other.fMapToStrId), fMapFromList(other.fMapFromList), fMapToList(other.fMapToList), normalization_disabled(other.normalization_disabled) { - if (other.fFromPattern) { - // clone pattern - fFromPattern.reset(other.fFromPattern->clone()); - } } transform_entry::transform_entry(const std::u32string &from, const std::u32string &to) - : fFrom(from), fTo(to), fFromPattern(nullptr), fMapFromStrId(), fMapToStrId(), fMapFromList(), fMapToList(), normalization_disabled(false) { + : fFrom(from), fTo(to), fFromPattern(from), fMapFromStrId(), fMapToStrId(), fMapFromList(), fMapToList(), normalization_disabled(false) { assert(!fFrom.empty()); - - init(); } transform_entry::transform_entry( @@ -461,7 +455,7 @@ transform_entry::transform_entry( const kmx::kmx_plus &kplus, bool &valid, bool norm_disabled) - : fFrom(from), fTo(to), fFromPattern(nullptr), fMapFromStrId(mapFrom), fMapToStrId(mapTo), normalization_disabled(norm_disabled) { + : fFrom(from), fTo(to), fFromPattern(), fMapFromStrId(mapFrom), fMapToStrId(mapTo), normalization_disabled(norm_disabled) { if (!valid) return; // exit early assert(!fFrom.empty()); // TODO-LDML: should not happen? @@ -469,7 +463,12 @@ transform_entry::transform_entry( assert(kplus.strs != nullptr); assert(kplus.vars != nullptr); assert(kplus.elem != nullptr); - if(!init()) { + std::u32string from2 = fFrom; + if (!normalization_disabled) { + // normalize, including markers, for regex + normalize_nfd_markers(from2, regex_sentinel); + } + if (!fFromPattern.init(from2)) { valid = false; } @@ -506,160 +505,17 @@ transform_entry::transform_entry( } } -bool -transform_entry::init() { - if (fFrom.empty()) { - return false; - } - // TODO-LDML: if we have mapFrom, may need to do other processing. - std::u32string from2 = fFrom; - if (!normalization_disabled) { - // normalize, including markers, for regex - normalize_nfd_markers(from2, regex_sentinel); - } - std::u16string patstr = km::core::kmx::u32string_to_u16string(from2); - UErrorCode status = U_ZERO_ERROR; - /* const */ icu::UnicodeString patustr = icu::UnicodeString(patstr.data(), (int32_t)patstr.length()); - // add '$' to match to end - patustr.append(u'$'); // TODO-LDML: may need to escape some markers. Marker #91 will look like a `[` to the pattern - fFromPattern.reset(icu::RegexPattern::compile(patustr, 0, status)); - return (UASSERT_SUCCESS(status)); -} - size_t transform_entry::apply(const std::u32string &input, std::u32string &output) const { - assert(fFromPattern); - // TODO-LDML: Really? can't go from u32 to UnicodeString? - // TODO-LDML: Also, we could cache the u16 string at the transformGroup level or higher. - UErrorCode status = U_ZERO_ERROR; - const std::u16string matchstr = km::core::kmx::u32string_to_u16string(input); - icu::UnicodeString matchustr = icu::UnicodeString(matchstr.data(), (int32_t)matchstr.length()); - // TODO-LDML: create a new Matcher every time. These could be cached and reset. - std::unique_ptr matcher(fFromPattern->matcher(matchustr, status)); - if (!UASSERT_SUCCESS(status)) { - return 0; // TODO-LDML: return error - } - - if (!matcher->find(status)) { // i.e. matches somewhere, in this case at end of str - return 0; // no match - } - - // TODO-LDML: this is UTF-16 len, not UTF-32 len!! - // TODO-LDML: if we had an underlying UText this would be simpler. - int32_t matchStart = matcher->start(status); - int32_t matchEnd = matcher->end(status); - if (!UASSERT_SUCCESS(status)) { - return 0; // TODO-LDML: return error - } - // extract.. - const icu::UnicodeString substr = matchustr.tempSubStringBetween(matchStart, matchEnd); - // preflight to UTF-32 to get length - UErrorCode substrStatus = U_ZERO_ERROR; // throwaway status - // we need the UTF-32 matchLen for our return. - auto matchLen = substr.toUTF32(nullptr, 0, substrStatus); - - // should have matched something. - assert(matchLen > 0); - - // now, do the replace. - - /** this is the 'to' or other replacement string.*/ - icu::UnicodeString rustr; - if (fMapFromStrId == 0) { - // Normal case: not a map. - // This replace will apply $1, $2 etc. - // Convert the fTo into u16 TODO-LDML (we could cache this?) - const std::u16string rstr = km::core::kmx::u32string_to_u16string(fTo); - rustr = icu::UnicodeString(rstr.data(), (int32_t)rstr.length()); - } else { - // Set map case: mapping from/to - - // we actually need the group(1) string here. - // this is only the content in parenthesis () - icu::UnicodeString group1 = matcher->group(1, status); - if (!UASSERT_SUCCESS(status)) { - // TODO-LDML: could be a malformed from pattern - return 0; // TODO-LDML: return error - } - // now, how long is group1 in UTF-32, hmm? - UErrorCode preflightStatus = U_ZERO_ERROR; // throwaway status - auto group1Len = group1.toUTF32(nullptr, 0, preflightStatus); - char32_t *s = new char32_t[group1Len + 1]; - assert(s != nullptr); // TODO-LDML: OOM - // convert - group1.toUTF32((UChar32 *)s, group1Len + 1, status); - if (!UASSERT_SUCCESS(status)) { - return 0; // TODO-LDML: memory issue - } - std::u32string match32(s, group1Len); // taken from just group1 - // clean up buffer - delete [] s; - - // Now we're ready to do the actual mapping. - - // 1., we need to find the index in the source set. - auto matchIndex = findIndexFrom(match32); - assert(matchIndex != -1L); // TODO-LDML: not matching shouldn't happen, the regex wouldn't have matched. - // we already asserted on load that the from and to sets have the same cardinality. - - // 2. get the target string, convert to utf-16 - // we use the same matchIndex that was just found - const std::u16string rstr = km::core::kmx::u32string_to_u16string(fMapToList.at(matchIndex)); - - // 3. update the UnicodeString for replacement - rustr = icu::UnicodeString(rstr.data(), (int32_t)rstr.length()); - // and we return to the regular code flow. - } - // here we replace the match output. No normalization, yet. - icu::UnicodeString entireOutput = matcher->replaceFirst(rustr, status); - if (!UASSERT_SUCCESS(status)) { - // TODO-LDML: could fail here due to bad input (syntax err) - return 0; - } - // entireOutput includes all of 'input', but modified. Need to substring it. - icu::UnicodeString outu = entireOutput.tempSubString(matchStart); - - // Special case if there's no output, save some allocs - if (outu.length() == 0) { - output.clear(); - } else { - // TODO-LDML: All we are trying to do is to extract the output string. Probably too many steps. - UErrorCode preflightStatus = U_ZERO_ERROR; - // calculate how big the buffer is - auto out32len = outu.toUTF32(nullptr, 0, preflightStatus); // preflightStatus will be an err, because we know the buffer overruns zero bytes - // allocate - std::unique_ptr s(new char32_t[out32len + 1]); - assert(s); - if (!s) { - return 0; // TODO-LDML: allocation failed - } - // convert - outu.toUTF32((UChar32 *)(s.get()), out32len + 1, status); - if (!UASSERT_SUCCESS(status)) { - return 0; // TODO-LDML: memory issue - } - output.assign(s.get(), out32len); - // NOW do a marker-safe normalize - if (!normalization_disabled && !normalize_nfd_markers(output)) { + auto result = fFromPattern.apply(input, output, fTo, fMapFromList, fMapToList); + // NOW do a marker-safe normalize + if (result != 0 && !output.empty() && !normalization_disabled) { + if (!normalize_nfd_markers(output)) { DebugLog("normalize_nfd_markers(output) failed"); - return 0; // TODO-LDML: normalization failed. - } - } - return matchLen; -} - -int32_t transform_entry::findIndexFrom(const std::u32string &match) const { - return findIndex(match, fMapFromList); -} - -int32_t transform_entry::findIndex(const std::u32string &match, const std::deque list) { - int32_t index = 0; - for(auto e = list.begin(); e < list.end(); e++, index++) { - if (match == *e) { - return index; + return 0; // TODO-LDML: normalization failed. } } - return -1; // not found + return result; } any_group::any_group(const transform_group &g) : type(any_group_type::transform), transform(g), reorder() { diff --git a/core/src/ldml/ldml_transforms.hpp b/core/src/ldml/ldml_transforms.hpp index 1b56a762468..90dd5f5e43f 100644 --- a/core/src/ldml/ldml_transforms.hpp +++ b/core/src/ldml/ldml_transforms.hpp @@ -16,11 +16,7 @@ #include #include "debuglog.h" -#include "core_icu.h" -#include "unicode/uniset.h" -#include "unicode/usetiter.h" -#include "unicode/regex.h" -#include "unicode/utext.h" +#include "util_regex.hpp" namespace km { namespace core { @@ -111,14 +107,12 @@ class transform_entry { private: const std::u32string fFrom; const std::u32string fTo; - std::unique_ptr fFromPattern; + km::core::util::km_regex fFromPattern; const KMX_DWORD fMapFromStrId; const KMX_DWORD fMapToStrId; std::deque fMapFromList; std::deque fMapToList; - /** Internal function to setup pattern string @returns true on success */ - bool init(); bool normalization_disabled; /** @returns the index of the item in the fMapFromList list, or -1 */ int32_t findIndexFrom(const std::u32string &match) const; diff --git a/core/src/ldml/ldml_vkeys.cpp b/core/src/ldml/ldml_vkeys.cpp index 07a2ff08ad9..f79f82141fc 100644 --- a/core/src/ldml/ldml_vkeys.cpp +++ b/core/src/ldml/ldml_vkeys.cpp @@ -17,7 +17,7 @@ vkeys::vkeys() : vkey_to_string() { } void -vkeys::add(km_core_virtual_key vk, uint16_t modifier_state, std::u16string output) { +vkeys::add(km_core_virtual_key vk, km_core_ldml_modifier_state modifier_state, std::u16string output) { // construct key const vkey_id id(vk, modifier_state); // assign the string diff --git a/core/src/ldml/ldml_vkeys.hpp b/core/src/ldml/ldml_vkeys.hpp index ed3368dc7e7..6a54d770653 100644 --- a/core/src/ldml/ldml_vkeys.hpp +++ b/core/src/ldml/ldml_vkeys.hpp @@ -19,10 +19,17 @@ namespace km { namespace core { namespace ldml { +/** + * LDML keyboards have 32-bit modifier flags in order to support + * LDML_KEYS_MOD_OTHER (0x10000), unlike the Core APIs which have only 16 bit + * modifier flags. + */ +typedef uint32_t km_core_ldml_modifier_state; + /** * identifier for keybag lookup */ -typedef std::pair vkey_id; +typedef std::pair vkey_id; /** * LDML Class to manage all things key related: vkey remapping and vkey to string @@ -35,9 +42,9 @@ class vkeys { vkeys(); /** - * add a vkey to the bag + * add a vkey to the bag. */ - void add(km_core_virtual_key vk, uint16_t modifier_state, std::u16string output); + void add(km_core_virtual_key vk, km_core_ldml_modifier_state ldml_modifier_state, std::u16string output); /** * Lookup a vkey, returns an empty string if not found diff --git a/core/src/meson.build b/core/src/meson.build index e8008fb78a0..41c198543cb 100644 --- a/core/src/meson.build +++ b/core/src/meson.build @@ -16,11 +16,6 @@ if cpp_compiler.get_id() == 'msvc' version_res += import('windows').compile_resources('version.rc', args:['/n','/c65001']) endif -if cpp_compiler.get_id() == 'emscripten' - # TODO: why do we need this defn here? - defns += ['-DKM_CORE_LIBRARY'] -endif - # ICU4C is used for repertoire tests and core implementation if target_machine.system() == 'linux' @@ -36,10 +31,42 @@ else icu_i18n = icu4c.get_variable('icui18n_dep') endif +if cpp_compiler.get_id() == 'emscripten' + # TODO: why do we need this defn here? + defns += ['-DKM_CORE_LIBRARY'] + icu_if_not_on_wasm = [] +else + # only include this if NOT on wasm. + icu_if_not_on_wasm = [icu_uc, icu_i18n] +endif + if icu_uc.found() defns += '-DHAVE_ICU4C' endif +# On wasm, generate util_normalize_table.h automatically from ICU +generated_headers = [] + +if cpp_compiler.get_id() == 'emscripten' + +util_normalize_table_generator = executable('util_normalize_table_generator', + ['util_normalize_table_generator.cpp'], + cpp_args: defns + warns, + include_directories: [inc], + link_args: links, + dependencies: [icu_uc, icu_i18n], + ) + +util_normalize_table_h = custom_target('util_normalize_table.h', + output: 'util_normalize_table.h', + command: [util_normalize_table_generator], + capture:true) + +generated_headers += util_normalize_table_h + + +endif + kmx_files = files( 'actions_normalize.cpp', @@ -60,6 +87,8 @@ kmx_files = files( 'km_core_processevent_api.cpp', 'jsonpp.cpp', 'util_normalize.cpp', + 'util_regex.cpp', + 'core_icu.cpp', 'ldml/ldml_processor.cpp', 'ldml/ldml_transforms.cpp', 'ldml/ldml_markers.cpp', @@ -97,7 +126,7 @@ core_files = files( 'keyboard.cpp', 'state.cpp', 'jsonpp.cpp', - 'utfcodec.cpp', + '../../common/cpp/utfcodec.cpp', ) mock_files = files( @@ -110,18 +139,19 @@ lib = library('keymancore', kmx_files, mock_files, version_res, + generated_headers, cpp_args: defns + warns + flags, link_args: links, version: lib_version, include_directories: inc, pic: true, install: true, - dependencies: [icu_uc, icu_i18n], + dependencies: icu_if_not_on_wasm, ) headerdirs = [ '.', 'keyman' ] # subdirectories of ${prefix}/include to add to header path -keymancore = declare_dependency(link_with: lib, include_directories: inc, dependencies: [icu_uc, icu_i18n]) +keymancore = declare_dependency(link_with: lib, include_directories: inc, dependencies: icu_if_not_on_wasm) pkg = import('pkgconfig') pkg.generate( diff --git a/core/src/util_normalize.cpp b/core/src/util_normalize.cpp index c21b75ea5b7..bcdfc1bb3f7 100644 --- a/core/src/util_normalize.cpp +++ b/core/src/util_normalize.cpp @@ -13,6 +13,7 @@ #ifdef __EMSCRIPTEN__ #include #include "utfcodec.hpp" +#include // JS implementations EM_JS(char*, NormalizeNFD, (const char* input), { @@ -21,6 +22,17 @@ EM_JS(char*, NormalizeNFD, (const char* input), { const nfd = instr.normalize("NFD"); return stringToNewUTF8(nfd); }); + +EM_JS(char*, NormalizeNFC, (const char* input), { + if (!input) return input; // pass through null + const instr = Module.UTF8ToString(input); + const nfd = instr.normalize("NFC"); + return stringToNewUTF8(nfd); +}); + +// pull in the generated table +#include "util_normalize_table.h" + #endif namespace km { @@ -28,31 +40,15 @@ namespace core { namespace util { #ifndef __EMSCRIPTEN__ - -/** - * Internal function to normalize with a specified mode. - * Note: that this function _does_ assert failure, so it is not - * required to assert its return code. The return is provided so - * that callers can exit (such as making no change) if there was failure. - * - * Also note that "failure" here is something catastrophic: ICU not initialized, - * or, more likely, some low memory situation. Does not fail on "bad" data. - * @param n the ICU Normalizer to use - * @param str input/output string - * @param status error code, must be initialized on input - * @return false if failure - */ -static bool normalize(const icu::Normalizer2 *n, std::u16string &str, UErrorCode &status) { +inline const icu::Normalizer2 *getNFD(UErrorCode &status) { + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); UASSERT_SUCCESS(status); - assert(n != nullptr); - icu::UnicodeString dest; - icu::UnicodeString src = icu::UnicodeString(str.data(), (int32_t)str.length()); - n->normalize(src, dest, status); - // the next line here will assert - if (UASSERT_SUCCESS(status)) { - str.assign(dest.getBuffer(), dest.length()); - } - return U_SUCCESS(status); + return nfd; +} +inline const icu::Normalizer2 *getNFC(UErrorCode &status) { + const icu::Normalizer2 *nfc = icu::Normalizer2::getNFCInstance(status); + UASSERT_SUCCESS(status); + return nfc; } #endif @@ -66,6 +62,16 @@ bool normalize_nfd(std::u32string &str) { } } +bool normalize_nfc(std::u32string &str) { + std::u16string rstr = km::core::kmx::u32string_to_u16string(str); + if(!km::core::util::normalize_nfc(rstr)) { + return false; + } else { + str = km::core::kmx::u16string_to_u32string(rstr); + return true; + } +} + bool normalize_nfd(std::u16string &str) { #ifdef __EMSCRIPTEN__ std::string instr = convert(str); @@ -81,9 +87,26 @@ bool normalize_nfd(std::u16string &str) { return true; #else UErrorCode status = U_ZERO_ERROR; - const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); - UASSERT_SUCCESS(status); - return normalize(nfd, str, status); + return normalize(getNFD(status), str, status); +#endif +} + +bool normalize_nfc(std::u16string &str) { +#ifdef __EMSCRIPTEN__ + std::string instr = convert(str); + const char *in = instr.c_str(); + char *out = NormalizeNFC(in); + if (out == nullptr) { + assert(out != nullptr); + return false; + } + std::string outstr(out); + str = convert(outstr); + free(out); + return true; +#else + UErrorCode status = U_ZERO_ERROR; + return normalize(getNFC(status), str, status); #endif } @@ -91,26 +114,130 @@ bool normalize_nfd(std::u16string &str) { * Normalize the input string using ICU, out of place */ bool normalize_nfd(km_core_cu const * src, std::u16string &dst) { - UErrorCode icu_status = U_ZERO_ERROR; - const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(icu_status); - assert(U_SUCCESS(icu_status)); - if(!U_SUCCESS(icu_status)) { - // TODO: log the failure code +#ifdef __EMSCRIPTEN__ + dst = std::u16string(src); + return normalize_nfd(dst); // vector to above fcn +#else + UErrorCode status = U_ZERO_ERROR; + auto nfd = getNFD(status); + if (nfd == nullptr) { return false; } icu::UnicodeString udst; icu::UnicodeString usrc = icu::UnicodeString(src); - nfd->normalize(usrc, udst, icu_status); - assert(U_SUCCESS(icu_status)); - if(!U_SUCCESS(icu_status)) { - // TODO: log the failure code + nfd->normalize(usrc, udst, status); + if(!UASSERT_SUCCESS(status)) { return false; } dst.assign(udst.getBuffer(), udst.length()); return true; +#endif +} + +bool +normalize_nfd(km_core_usv cp, std::u32string &dst) { + // set the output string to the original string + dst.clear(); + dst.append(1, cp); +#ifdef __EMSCRIPTEN__ + auto str16 = convert(dst); + if (!normalize_nfd(str16)) { + return false; // failed, retain original str + } else { + dst = convert(str16); + return true; + } +#else + UErrorCode icu_status = U_ZERO_ERROR; + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(icu_status); + assert(U_SUCCESS(icu_status)); + if (!U_SUCCESS(icu_status)) { + // TODO: log the failure code + return false; + } + icu::UnicodeString decomposition; + if (!nfd->getDecomposition(cp, decomposition)) { + return false; // no error, just no decomposition + } else { + dst.clear(); + auto len = decomposition.countChar32(); + for (int i = 0; i < len; i++) { + dst.append(1, decomposition.char32At(i)); + } + return true; + } +#endif +} + +bool is_nfd(const std::u16string& str) { +#ifdef __EMSCRIPTEN__ + std::u16string o = str; + normalize_nfd(o); + return (o == str); // false if changed +#else + UErrorCode status = U_ZERO_ERROR; + auto nfd = getNFD(status); + if (nfd == nullptr) return false; + auto ustr = icu::UnicodeString(false, str.c_str(), (int)str.length()); + auto result = nfd->isNormalized(ustr, status); + if (!UASSERT_SUCCESS(status)) { + return false; + } else { + return result; + } +#endif +} + +bool is_nfd(const std::u32string& str) { +#ifdef __EMSCRIPTEN__ + std::u32string o = str; + normalize_nfd(o); + return (o == str); // false if changed +#else + UErrorCode status = U_ZERO_ERROR; + auto nfd = getNFD(status); + if (nfd == nullptr) return false; + auto ustr = icu::UnicodeString::fromUTF32(reinterpret_cast(str.c_str()), (int)str.length()); + auto result = nfd->isNormalized(ustr, status); + if (!UASSERT_SUCCESS(status)) { + return false; + } else { + return result; + } +#endif } +bool has_nfd_boundary_before(km_core_usv cp) { +#ifdef __EMSCRIPTEN__ +// it's a negative table. entries in the table mean returning false. non-entries return true. + for (auto i=0;i<(km_noBoundaryBefore_entries*2);i+=2) { + auto start = km_noBoundaryBefore[i+0]; + if (start > cp) return true; + auto count = km_noBoundaryBefore[i+1]; + auto limit = start+count; + if (cp >= start && cp < limit) return false; + } + return true; // fallthrough +#else + UErrorCode status = U_ZERO_ERROR; + auto nfd = getNFD(status); + if (nfd == nullptr) return false; + return nfd->hasBoundaryBefore(cp); +#endif +} + +/** + * Helper to convert std::u32string to a UTF-32 km_core_usv buffer, + * nul-terminated. + * Parallel to unicode_string_to_usv() + * @returns new buffer, caller owns storage + */ +km_core_usv *string_to_usv(const std::u32string& src) { + return km::core::kmx::u32dup(src.c_str()); +} + + } } } diff --git a/core/src/util_normalize.hpp b/core/src/util_normalize.hpp index 90ed650b089..6321ffacdaa 100644 --- a/core/src/util_normalize.hpp +++ b/core/src/util_normalize.hpp @@ -14,6 +14,12 @@ namespace km { namespace core { namespace util { +/** Normalize a u32string inplace to NFC. @return false on failure */ +bool normalize_nfc(std::u32string &str); + +/** Normalize a u16string inplace to NFC. @return false on failure */ +bool normalize_nfc(std::u16string &str); + /** Normalize a u32string inplace to NFD. @return false on failure */ bool normalize_nfd(std::u32string &str); @@ -23,6 +29,21 @@ bool normalize_nfd(std::u16string &str); /** normalize src to dst in NFD. @return false on failure */ bool normalize_nfd(km_core_cu const * src, std::u16string &dst); +/** normalize (decompose) a single cp to string. @return false on failure */ +bool normalize_nfd(km_core_usv cp, std::u32string &dst); + +/** @return true if string is already NFD */ +bool is_nfd(const std::u16string& str); + +/** @return true if string is already NFD */ +bool is_nfd(const std::u32string& str); + +/** @return true if cp can interacts with prior chars */ +bool has_nfd_boundary_before(km_core_usv cp); + +/** convenience function, caller owns storage */ +km_core_usv *string_to_usv(const std::u32string& src); + } } } diff --git a/core/src/util_normalize_table_generator.cpp b/core/src/util_normalize_table_generator.cpp new file mode 100644 index 00000000000..994f16d8b37 --- /dev/null +++ b/core/src/util_normalize_table_generator.cpp @@ -0,0 +1,108 @@ +/* + Copyright: © SIL International. + Description: Generator for util_normalize_table.h + Create Date: 5 Jun 2024 + Authors: Steven R. Loomis + + util_normalize_table.h is used under wasm by utilities in util_normalize.cpp to implement + normalization functions without needing ICU4C linked. + + This generator is invoked automatically by meson as part of the build. +*/ + +#include "kmx/kmx_plus.h" +#include "kmx/kmx_xstring.h" + +#define KMN_NO_ICU 0 // we will need ICU.. + +#include "core_icu.h" + +#include +#include +#include +#include + +#include + +#include + + + +int +write_nfd_table() { +#ifndef __EMSCRIPTEN__ + std::cerr << "Note: This is unusual - this generator is usually only run under emscripten!" << std::endl; +#endif + + // We write to stdout instead of to a file to avoid dealing with the filesystem under emscripten. + + std::cerr << "Writing to stdout." << std::endl; + + // write preamble + std::cout << "// GENERATED FILE: DO NOT EDIT" << std::endl; + std::cout << "//" << std::endl; + std::cout << "// util_normalize_table.h is generated by util_normalize_table_generator.cpp" << std::endl; + std::cout << "// and used by util_normalize.cpp" << std::endl; + std::cout << std::endl; + std::cout << "#pragma once" << std::endl; + std::cout << "#define KM_HASBOUNDARYBEFORE_UNICODE_VERSION \"" << U_UNICODE_VERSION << "\"" << std::endl; + std::cout << "#define KM_HASBOUNDARYBEFORE_ICU_VERSION \"" << U_ICU_VERSION << "\"" << std::endl; + std::cout << std::endl; + // we're going to need an NFD normalizer + UErrorCode status = U_ZERO_ERROR; + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); + assert(U_SUCCESS(status)); + + // collect the raw list of chars that do NOT have a boundary before them. + std::vector noBoundary; + for (km_core_usv ch = 0; ch < km::core::kmx::Uni_MAX_CODEPOINT; ch++) { + bool bb = nfd->hasBoundaryBefore(ch); + assert(!(ch == 0 && !bb)); // assert that we can use U+0000 as a terminator + if (bb) continue; //only emit nonboundary + noBoundary.push_back(ch); + } + + // now, compress these into runs + std::vector> runs; // start,len + + km_core_usv first = 0; + km_core_usv last = 0; + for(auto i = noBoundary.begin(); i <= noBoundary.end(); i++) { + if (first == 0) { + first = last = *i; + } else { + last++; + if(i == noBoundary.end() || *i != last) { + // end of a run + runs.emplace_back(first, last - first); + if (i != noBoundary.end()) { + // setup for next + first = last = *i; + } + } + } + } + + // finally, write out metadata and the runs themselves. + std::cout << "#define km_noBoundaryBefore_entries " << runs.size() << "\n"; + + std::cout << "static char32_t km_noBoundaryBefore[km_noBoundaryBefore_entries * 2 ] = {" << std::endl; + + std::cout << "/* start codepoint, count (inclusive), ...range end */" << std::endl; + + for (auto i = runs.begin(); i < runs.end(); i++) { + std::cout << "\t0x" << std::hex << i->first << std::dec << ",\t " << i->second << ", // ...0x" << std::hex << (i->first+i->second-1) << std::endl; + } + + // termination + std::cout << "};" << std::endl; + std::cout << "// end" << std::endl; + std::cerr << "Wrote " << runs.size() << " runs representing " << noBoundary.size() << " entries." << std::endl; + return 0; +} + +int +main(int /*argc*/, const char * /*argv*/[]) { + write_nfd_table(); + return 0; +} diff --git a/core/src/util_regex.cpp b/core/src/util_regex.cpp new file mode 100644 index 00000000000..f8982fdf653 --- /dev/null +++ b/core/src/util_regex.cpp @@ -0,0 +1,352 @@ +/* + Copyright: © SIL International. + Description: Core Regex Utilities - abstract out ICU dependencies + Create Date: 5 Jun 2024 + Authors: Steven R. Loomis +*/ + +#include "util_regex.hpp" + +#include "core_icu.h" +#include "kmx/kmx_xstring.h" + +#ifdef __EMSCRIPTEN__ +#include +#include "utfcodec.hpp" +#include +// JS implementations + + +/** + * RegexMatchLen(pattern, input) + * @param pattern the string, sans trailing $, for the pattern + * @param input the text to match against + * @return the length, in code units, of the matched portion + */ +EM_JS(int, RegexMatchLen, (const char* pattern, const char *input), { + const DEBUG_JS = false; + if (!pattern) return -1; + if (!input) return -1; + const patternstr = Module.UTF8ToString(pattern) + '$'; + const inputstr = Module.UTF8ToString(input); + const re = new RegExp(patternstr); + const result = re.exec(inputstr); + if (DEBUG_JS) console.dir({patternstr,inputstr,re,result}); + if (!result) return 0; // no match + const index = result.index; + // code unit indices + const startIndex = index; + const endIndex = inputstr.length; + const matchedText = inputstr.substring(startIndex); + const matchedCodepoints = [...matchedText]; + if (DEBUG_JS) console.dir({index, startIndex,endIndex,matchedText,matchedCodepoints}); + return matchedCodepoints.length; +}); + +/** + * RegexGroup1(pattern, input) + * @param pattern the string, sans trailing $, for the pattern + * @param input the text to match against + * @return string of group 1 match or null + */ +EM_JS(char*, RegexGroup1, (const char* pattern, const char *input), { + const DEBUG_JS = false; + if (!pattern) return 0; + if (!input) return 0; + const patternstr = Module.UTF8ToString(pattern) + '$'; + const inputstr = Module.UTF8ToString(input); + const re = new RegExp(patternstr); + const result = re.exec(inputstr); + if (!result) return 0; // no match + const g1 = result[1]; + if (DEBUG_JS) console.dir({patternstr,inputstr,re,result,g1}); + if (!g1) return 0; + return stringToNewUTF8(g1); +}); + + +/** + * RegexSubstitute + * @param pattern the string, sans trailing $, for the pattern + * @param input the text to match against + * @param to the replacement text + * @return the entire updated output string + */ +EM_JS(char*, RegexSubstitute, (const char* pattern, const char *input, const char *to), { + const DEBUG_JS = false; + if (!pattern) return -1; + if (!input) return -1; + const patternstr = Module.UTF8ToString(pattern) + '$'; + const inputstr = Module.UTF8ToString(input); + const tostr = Module.UTF8ToString(to); + const re = new RegExp(patternstr); + const output = inputstr.replace(re, tostr); + return stringToNewUTF8(output); +}); + +// pull in the generated table +#include "util_normalize_table.h" + +#endif + + +namespace km { +namespace core { +namespace util { + +/** find the */ +int32_t km_regex::findIndex(const std::u32string &match, const std::deque &list) { + int32_t index = 0; + for(auto e = list.begin(); e < list.end(); e++, index++) { + if (match == *e) { + return index; + } + } + return -1; // not found +} + +km_regex::km_regex() +#if KMN_NO_ICU +#else + : fPattern(nullptr) +#endif +{ + +} + + +km_regex::km_regex(const km_regex& other) +#if KMN_NO_ICU + : fPattern(other.fPattern) +#else + : fPattern(nullptr) +#endif +{ +#if KMN_NO_ICU + +#else + if (other.fPattern) { + // clone pattern + fPattern.reset(other.fPattern->clone()); + } +#endif +} + +km_regex::km_regex(const std::u32string &pattern) +#if KMN_NO_ICU + : fPattern(pattern) +#else + : fPattern(nullptr) +#endif +{ + init(pattern); +} + +km_regex::~km_regex() { + +} + +bool km_regex::valid() const { +#if KMN_NO_ICU + return (!fPattern.empty()); +#else + // valid if fPattern is present. + return !!fPattern; +#endif +} + +bool km_regex::init(const std::u32string &pattern) { +#if KMN_NO_ICU +// The current implementation makes a new regex every time, so we always return true. + assert(!pattern.empty()); + fPattern = pattern; + return true; +#else + if (pattern.empty()) { + return false; + } + std::u16string patstr = km::core::kmx::u32string_to_u16string(pattern); + UErrorCode status = U_ZERO_ERROR; + /* const */ icu::UnicodeString patustr = icu::UnicodeString(patstr.data(), (int32_t)patstr.length()); + // add '$' to match to end + patustr.append(u'$'); + fPattern.reset(icu::RegexPattern::compile(patustr, 0, status)); + return (UASSERT_SUCCESS(status)); +#endif +} + +size_t km_regex::apply(const std::u32string &input, std::u32string &output, + const std::u32string &to, + const std::deque &fromList, + const std::deque &toList ) const { +#if KMN_NO_ICU + // length in code points of match from end + std::string patstr = convert(fPattern); + std::string instr = convert(input); + + /** code units */ + const auto matchLen = RegexMatchLen(patstr.c_str(), instr.c_str()); + assert(matchLen != -1); // error + if (matchLen == 0) { + return 0; // Normal case return: no match + } + std::u32string rustr; // replacement + if (fromList.empty()) { + // Normal case: not a map. + // This replace will apply $1, $2 etc. + rustr = to; + } else { + // we actually need the group(1) string here. + // this is only the content in parenthesis () + char *group1 = RegexGroup1(patstr.c_str(), instr.c_str()); + assert(group1 != nullptr); + const std::string group1str(group1); + const std::u32string match32 = convert(group1str); + free(group1); + // Now we're ready to do the actual mapping. + + // 1., we need to find the index in the source set. + auto matchIndex = findIndex(match32, fromList); + assert(matchIndex != -1L); // This indicates that the regex and the fromList are out of sync. + // we already asserted on load that the from and to sets have the same cardinality. + + // 2. get the target string + // we use the same matchIndex that was just found + // 3. update the UnicodeString for replacement + rustr = toList.at(matchIndex); + } + std::string rstr = convert(rustr); + // here we replace the match output. + char *out = RegexSubstitute(patstr.c_str(), instr.c_str(), rstr.c_str()); + assert(out != nullptr); + std::string outstr(out); + free(out); + output = convert(outstr); + // output includes all of 'input', but modified. Need to substring it. + /** code units */ + const auto matchStart = input.length() - matchLen; + // remove the unmatched prefix. + output.erase(0, matchStart); + + return matchLen; +#else + assert(fPattern); + // TODO-LDML: This entire section may have too many conversions and copies. Could be optimized. + UErrorCode status = U_ZERO_ERROR; + const std::u16string matchstr = km::core::kmx::u32string_to_u16string(input); + icu::UnicodeString matchustr = icu::UnicodeString(matchstr.data(), (int32_t)matchstr.length()); + // TODO-LDML: create a new Matcher every time. These could be cached and reset. + std::unique_ptr matcher(fPattern->matcher(matchustr, status)); + if (!UASSERT_SUCCESS(status)) { + return 0; + } + + if (!matcher->find(status)) { // i.e. matches somewhere, in this case at end of str + return 0; // Normal case return: no match + } + + // Note: this is UTF-16 len, not UTF-32 len. + int32_t matchStart = matcher->start(status); + int32_t matchEnd = matcher->end(status); + if (!UASSERT_SUCCESS(status)) { + return 0; // TODO-LDML: return error + } + // extract.. + const icu::UnicodeString substr = matchustr.tempSubStringBetween(matchStart, matchEnd); + // preflight to UTF-32 to get length + UErrorCode substrStatus = U_ZERO_ERROR; // throwaway status + // we need the UTF-32 matchLen for our return. + auto matchLen = substr.toUTF32(nullptr, 0, substrStatus); + + // should have matched something. + assert(matchLen > 0); + + + // now, do the replace. + + /** this is the 'to' or other replacement string.*/ + icu::UnicodeString rustr; + if (fromList.empty()) { + // Normal case: not a map. + // This replace will apply $1, $2 etc. + // Convert the fTo into u16 TODO-LDML (we could cache this?) + const std::u16string rstr = km::core::kmx::u32string_to_u16string(to); + rustr = icu::UnicodeString(rstr.data(), (int32_t)rstr.length()); + } else { + // Set map case: mapping from/to + + // we actually need the group(1) string here. + // this is only the content in parenthesis () + icu::UnicodeString group1 = matcher->group(1, status); + if (!UASSERT_SUCCESS(status)) { + // TODO-LDML: could be a malformed from pattern + return 0; // TODO-LDML: return error + } + // now, how long is group1 in UTF-32, hmm? + UErrorCode preflightStatus = U_ZERO_ERROR; // throwaway status + auto group1Len = group1.toUTF32(nullptr, 0, preflightStatus); + char32_t *s = new char32_t[group1Len + 1]; + assert(s != nullptr); // TODO-LDML: OOM + // convert + group1.toUTF32((UChar32 *)s, group1Len + 1, status); + if (!UASSERT_SUCCESS(status)) { + return 0; // TODO-LDML: memory issue + } + std::u32string match32(s, group1Len); // taken from just group1 + // clean up buffer + delete [] s; + + // Now we're ready to do the actual mapping. + + // 1., we need to find the index in the source set. + auto matchIndex = findIndex(match32, fromList); + assert(matchIndex != -1L); // This indicates that the regex and the fromList are out of sync. + // we already asserted on load that the from and to sets have the same cardinality. + + // 2. get the target string, convert to utf-16 + // we use the same matchIndex that was just found + const std::u16string rstr = km::core::kmx::u32string_to_u16string(toList.at(matchIndex)); + + // 3. update the UnicodeString for replacement + rustr = icu::UnicodeString(rstr.data(), (int32_t)rstr.length()); + // and we return to the regular code flow. + } + // here we replace the match output. + icu::UnicodeString entireOutput = matcher->replaceFirst(rustr, status); + if (!UASSERT_SUCCESS(status)) { + // TODO-LDML: could fail here due to bad input (syntax err) + return 0; + } + // entireOutput includes all of 'input', but modified. Need to substring it. + icu::UnicodeString outu = entireOutput.tempSubString(matchStart); + + // Special case if there's no output, save some allocs + if (outu.length() == 0) { + output.clear(); + } else { + // TODO-LDML: All we are trying to do is to extract the output string. Probably too many steps. + UErrorCode preflightStatus = U_ZERO_ERROR; + // calculate how big the buffer is + auto out32len = outu.toUTF32(nullptr, 0, preflightStatus); // preflightStatus will be an err, because we know the buffer overruns zero bytes + // allocate + std::unique_ptr s(new char32_t[out32len + 1]); + assert(s); + if (!s) { + return 0; + } + // convert + outu.toUTF32((UChar32 *)(s.get()), out32len + 1, status); + if (!UASSERT_SUCCESS(status)) { + return 0; + } + output.assign(s.get(), out32len); + } + return matchLen; + +#endif +} + + +} +} +} diff --git a/core/src/util_regex.hpp b/core/src/util_regex.hpp new file mode 100644 index 00000000000..b10ddff1cfc --- /dev/null +++ b/core/src/util_regex.hpp @@ -0,0 +1,48 @@ +/* + Copyright: © SIL International. + Description: Normalization and Regex utilities + Create Date: 23 May 2024 + Authors: Steven R. Loomis +*/ + +#pragma once + +#include "core_icu.h" +#include "keyman_core.h" +#include +#include + +namespace km { +namespace core { +namespace util { + +class km_regex { +public: + km_regex(); + km_regex(const km_regex &other); + km_regex(const std::u32string &pattern); + ~km_regex(); + bool init(const std::u32string &pattern); + + size_t apply( + const std::u32string &input, + std::u32string &output, + const std::u32string &to, + const std::deque &fromList, + const std::deque &toList) const; + + bool valid() const; +private: +#if KMN_NO_ICU + std::u32string fPattern; // TODO: by value? +#else + std::unique_ptr fPattern; +#endif +// utility functions + public: + static int32_t findIndex(const std::u32string &match, const std::deque &list); +}; + +} // namespace util +} // namespace core +} // namespace km diff --git a/core/tests/meson.build b/core/tests/meson.build index e2e29975822..90e0ab2a54b 100644 --- a/core/tests/meson.build +++ b/core/tests/meson.build @@ -10,7 +10,10 @@ cmpfiles = ['-c', 'import sys; a = open(sys.argv[1], \'r\').read(); b = open(sys.argv[2], \'r\').read(); exit(not (a==b))'] stnds = join_paths(meson.current_source_dir(), 'standards') -libsrc = include_directories(join_paths('../', 'src')) +libsrc = include_directories( + '../src', + '../../common/include' +) # kmx_test_source is required for linux builds, so always enable it even when we # disable all other tests @@ -21,7 +24,7 @@ if get_option('keyman_core_tests') if get_option('default_library') != 'static' ctypes_void_p_size = ['-c', 'import ctypes; print(ctypes.sizeof(ctypes.c_void_p))'] - r = run_command(python, ctypes_void_p_size) + r = run_command(python, ctypes_void_p_size, check: true) python_ctypes_compatible = r.stdout().to_int() == cpp_compiler.sizeof('void *') if not python_ctypes_compatible message('Python ctypes is incompatible with built shared object. Disabling some tests.') diff --git a/core/tests/unit/kmnkbd/meson.build b/core/tests/unit/kmnkbd/meson.build index 0adda0a35e0..7285b9bf117 100644 --- a/core/tests/unit/kmnkbd/meson.build +++ b/core/tests/unit/kmnkbd/meson.build @@ -34,7 +34,7 @@ test_path = join_paths(meson.current_build_dir(), '..', 'kmx') tests_flags = [] if cpp_compiler.get_id() == 'emscripten' - tests_flags += ['-lnodefs.js', '-sEXPORTED_RUNTIME_METHODS=[\'UTF8ToString\']'] + tests_flags += ['-lnodefs.js', wasm_exported_runtime_methods] endif foreach t : tests diff --git a/core/tests/unit/kmx/cat.bat b/core/tests/unit/kmx/cat.bat new file mode 100644 index 00000000000..a5568e39191 --- /dev/null +++ b/core/tests/unit/kmx/cat.bat @@ -0,0 +1,4 @@ +@echo off +set infile=%1 +set infileb=%infile:/=\% +type %infileb% diff --git a/core/tests/unit/kmx/fixtures/binary/meson.build b/core/tests/unit/kmx/fixtures/binary/meson.build index 46b0e78d318..cf96bc75ce7 100644 --- a/core/tests/unit/kmx/fixtures/binary/meson.build +++ b/core/tests/unit/kmx/fixtures/binary/meson.build @@ -17,9 +17,9 @@ binary_tests = [ foreach kbd : binary_tests configure_file( - command: copy_cmd + ['@INPUT@', '@OUTPUT@'], input: kbd + '.kmn', - output: kbd + '.kmn' + output: kbd + '.kmn', + copy: true ) configure_file( diff --git a/core/tests/unit/kmx/meson.build b/core/tests/unit/kmx/meson.build index d244e162a54..548ab895229 100644 --- a/core/tests/unit/kmx/meson.build +++ b/core/tests/unit/kmx/meson.build @@ -89,10 +89,8 @@ kmc_root = meson.current_source_dir() / '../../../../developer/src/kmc/build/src kmc_cmd = [node, '--enable-source-maps', kmc_root] if build_machine.system() == 'windows' - copy_cmd = [find_program('cmd.exe', required: true), '/c', 'copy'] - cat_cmd = [find_program('cmd.exe', required: true), '/c', 'type'] + cat_cmd = [find_program(meson.current_source_dir() / 'cat.bat', required: true)] else - copy_cmd = [find_program('cp', required: true)] cat_cmd = [find_program('cat', required: true)] endif @@ -127,7 +125,7 @@ foreach kbd : tests kbd_src_path = common_test_keyboards_baseline / kbd + '.kmn' content = run_command( - cat_cmd, files(kbd_src_path), + cat_cmd, files(kbd_src_path), check: true, ).stdout().strip() cfg = configuration_data() diff --git a/core/tests/unit/ldml/core_ldml_min.cpp b/core/tests/unit/ldml/core_ldml_min.cpp new file mode 100644 index 00000000000..ed7ea1b3fd5 --- /dev/null +++ b/core/tests/unit/ldml/core_ldml_min.cpp @@ -0,0 +1,39 @@ +/* + Copyright: © SIL International. + Description: Minimal, hermetic Keyman core test. + Create Date: 6 Jan 2024 + Authors: Steven R. Loomis + + This is a different kind of a test. It's a very minimal test of the core API. + It does not have any file i/o other than console output and return code. + + This test is expected to fail with an assertion failure. It's to exercise the API. +*/ + +#include +#include + +#include "keyman_core.h" + +int main(int argc, const char *argv[]) { + + // "load" our "keyboard" + km_core_keyboard * test_kb = nullptr; + + km_core_status status; + km_core_path_name nowhere = {0}; // this is a narrow or wide char string + status = km_core_keyboard_load(nowhere, &test_kb); + + std::cerr << "null km_core_keyboard_load = " << status << std::endl; + assert(status == KM_CORE_STATUS_INVALID_ARGUMENT); + assert(test_kb == nullptr); + km_core_keyboard_dispose(test_kb); + km_core_state_dispose(nullptr); + km_core_cu_dispose(nullptr); + + status = km_core_process_event(nullptr, 0, 0, 0, 0); + /* NOTREACHED - assertion fails above. */ + assert(status == KM_CORE_STATUS_INVALID_ARGUMENT); + + return 0; +} diff --git a/core/tests/unit/ldml/invalid-keyboards/meson.build b/core/tests/unit/ldml/invalid-keyboards/meson.build index da8fb85ad0a..e85646b68ea 100644 --- a/core/tests/unit/ldml/invalid-keyboards/meson.build +++ b/core/tests/unit/ldml/invalid-keyboards/meson.build @@ -8,19 +8,13 @@ invalid_tests = [ 'ik_000_null_invalid' ] -if build_machine.system() == 'windows' - copy_cmd = [find_program('cmd.exe', required: true), '/c', 'copy'] -else - copy_cmd = [find_program('cp', required: true)] -endif - # Build all keyboards in output folder foreach kbd : invalid_tests configure_file( - command: copy_cmd + ['@INPUT@', '@OUTPUT@'], input: kbd + '.xml', - output: kbd + '.xml' + output: kbd + '.xml', + copy: true ) configure_file( diff --git a/core/tests/unit/ldml/keyboards/meson.build b/core/tests/unit/ldml/keyboards/meson.build index 25d5d12ac19..15f458c587f 100644 --- a/core/tests/unit/ldml/keyboards/meson.build +++ b/core/tests/unit/ldml/keyboards/meson.build @@ -56,8 +56,8 @@ tests += tests_from_cldr # Setup kmc -kmc_root = join_paths(meson.source_root(),'..','developer','src','kmc') -ldml_root = join_paths(meson.source_root(),'..','resources','standards-data','ldml-keyboards','45') +kmc_root = meson.global_source_root() / '../developer/src/kmc' +ldml_root = meson.global_source_root() / '../resources/standards-data/ldml-keyboards/45' ldml_data = join_paths(ldml_root, '3.0') ldml_testdata = join_paths(ldml_root, 'test') kmc_cmd = [node, '--enable-source-maps', kmc_root] diff --git a/core/tests/unit/ldml/ldml_test_source.cpp b/core/tests/unit/ldml/ldml_test_source.cpp index 439236fd772..f2cca9ee5fc 100644 --- a/core/tests/unit/ldml/ldml_test_source.cpp +++ b/core/tests/unit/ldml/ldml_test_source.cpp @@ -18,6 +18,9 @@ #include +// Ensure that ICU gets included even on wasm. +#define KMN_IN_LDML_TESTS + #include // for char to vk mapping tables #include // for surrogate pair macros #include diff --git a/core/tests/unit/ldml/meson.build b/core/tests/unit/ldml/meson.build index da60757750d..20ad621b506 100644 --- a/core/tests/unit/ldml/meson.build +++ b/core/tests/unit/ldml/meson.build @@ -22,12 +22,6 @@ invalid_tests = [] # Setup copying of source files, used in child subdir calls -if build_machine.system() == 'windows' - copy_cmd = [find_program('cmd.exe', required: true), '/c', 'copy'] -else - copy_cmd = [find_program('cp', required: true)] -endif - if node.found() # Note: if node is not available, we cannot build the keyboards; build.sh # emits a warning that the ldml keyboard tests will be skipped @@ -88,6 +82,17 @@ ldml = executable('ldml', objects: lib.extract_all_objects(recursive: false), ) +core_ldml_min = executable('core_ldml_min', + ['core_ldml_min.cpp'], + cpp_args: defns + warns, + include_directories: [inc, libsrc], + link_args: links, + dependencies: [], + link_with: [lib], + # objects: lib.extract_all_objects(recursive: false), + ) +test('core_ldml_min', core_ldml_min, suite: 'ldml', should_fail: true) + # Build and run additional test_kmx_plus test @@ -132,7 +137,7 @@ test('test_context_normalization', test_context_normalization, suite: 'ldml') # Build and run additional test_unicode test test_unicode = executable('test_unicode', 'test_unicode.cpp', - ['test_unicode.cpp', common_test_files], + ['test_unicode.cpp', common_test_files, generated_headers], cpp_args: defns + warns, include_directories: [inc, libsrc, '../../../../developer/src/ext/json'], link_args: links + tests_flags, diff --git a/core/tests/unit/ldml/test_kmx_plus.cpp b/core/tests/unit/ldml/test_kmx_plus.cpp index fc3f7e1ffee..5b14043b742 100644 --- a/core/tests/unit/ldml/test_kmx_plus.cpp +++ b/core/tests/unit/ldml/test_kmx_plus.cpp @@ -1,3 +1,4 @@ +#include #include #include "kmx/kmx_plus.h" #include "kmx/kmx_xstring.h" diff --git a/core/tests/unit/ldml/test_transforms.cpp b/core/tests/unit/ldml/test_transforms.cpp index 94c53f38452..fa909516bc9 100644 --- a/core/tests/unit/ldml/test_transforms.cpp +++ b/core/tests/unit/ldml/test_transforms.cpp @@ -1,11 +1,13 @@ #include "../../../src/ldml/ldml_markers.hpp" #include "../../../src/ldml/ldml_transforms.hpp" +#include "../../../src/util_regex.hpp" #include "kmx/kmx_plus.h" #include "kmx/kmx_xstring.h" #include "test_color.h" #include #include #include +#include "../../../src/util_regex.hpp" // TODO-LDML: normal asserts wern't working, so using some hacks. // #include "ldml_test_utils.hpp" @@ -13,14 +15,14 @@ // #include "debuglog.h" #ifndef zassert_string_equal -#define zassert_string_equal(actual, expected) \ - { \ - if (actual != expected) { \ - std::wcerr << __FILE__ << ":" << __LINE__ << ": " << console_color::fg(console_color::BRIGHT_RED) \ - << "got: " << km::core::kmx::Debug_UnicodeString(actual, 0) << " expected " \ - << km::core::kmx::Debug_UnicodeString(expected, 1) << console_color::reset() << std::endl; \ - return EXIT_FAILURE; \ - } \ +#define zassert_string_equal(actual, expected) \ + { \ + if (actual != expected) { \ + std::wcerr << __FILE__ << ":" << __LINE__ << ": " << console_color::fg(console_color::BRIGHT_RED) << "got: " << actual \ + << " " << km::core::kmx::Debug_UnicodeString(actual, 0) << " expected " << expected << " " \ + << km::core::kmx::Debug_UnicodeString(expected, 1) << console_color::reset() << std::endl; \ + return EXIT_FAILURE; \ + } \ } #endif @@ -624,16 +626,16 @@ test_map() { std::cout << __FILE__ << ":" << __LINE__ << " transform_entry::findIndex" << std::endl; { std::deque list; - assert_equal(transform_entry::findIndex(U"Does Not Exist", list), -1); + assert_equal(km::core::util::km_regex::findIndex(U"Does Not Exist", list), -1); list.emplace_back(U"0th"); list.emplace_back(U"First"); list.emplace_back(U"Second"); - assert_equal(transform_entry::findIndex(U"First", list), 1); - assert_equal(transform_entry::findIndex(U"0th", list), 0); - assert_equal(transform_entry::findIndex(U"Second", list), 2); - assert_equal(transform_entry::findIndex(U"Nowhere", list), -1); + assert_equal(km::core::util::km_regex::findIndex(U"First", list), 1); + assert_equal(km::core::util::km_regex::findIndex(U"0th", list), 0); + assert_equal(km::core::util::km_regex::findIndex(U"Second", list), 2); + assert_equal(km::core::util::km_regex::findIndex(U"Nowhere", list), -1); } return EXIT_SUCCESS; @@ -1135,6 +1137,102 @@ test_normalize() { return EXIT_SUCCESS; } +/** test for the util_regex.hpp functions */ +int +test_util_regex() { + std::cout << "== " << __FUNCTION__ << std::endl; + + { + std::cout << __FILE__ << ":" << __LINE__ << " * util_regex.hpp null tests" << std::endl; + km::core::util::km_regex r; + assert(!r.valid()); // not valid because of an empty string + } + { + std::cout << __FILE__ << ":" << __LINE__ << " * util_regex.hpp simple tests" << std::endl; + km::core::util::km_regex r(U"ion"); + assert(r.valid()); + const std::u32string to(U"ivity"); + const std::deque fromList; + const std::deque toList; + std::u32string output; + auto apply0 = r.apply(U"not present", output, to, fromList, toList); + assert_equal(apply0, 0); // not found + + const std::u32string input(U"action"); + auto apply1 = r.apply(input, output, to, fromList, toList); + assert_equal(apply1, 3); // matched last 3 codepoints + std::u32string expect(U"ivity"); + zassert_string_equal(output, expect) + } + { + std::cout << __FILE__ << ":" << __LINE__ << " * util_regex.hpp wide tests" << std::endl; + km::core::util::km_regex r(U"e𐒻"); + assert(r.valid()); + const std::u32string to(U"𐓏"); + const std::deque fromList; + const std::deque toList; + std::u32string output; + const std::u32string input(U":e𐒻"); + auto apply1 = r.apply(input, output, to, fromList, toList); + assert_equal(apply1, 2); // matched last 2 codepoints + std::u32string expect(U"𐓏"); + zassert_string_equal(output, expect) + } + { + std::cout << __FILE__ << ":" << __LINE__ << " * util_regex.hpp simple map tests" << std::endl; + km::core::util::km_regex r(U"(A|B|C)"); + assert(r.valid()); + const std::u32string to(U"$[1:alpha2]"); // ignored + std::deque fromList; + fromList.emplace_back(U"A"); + fromList.emplace_back(U"B"); + fromList.emplace_back(U"C"); + std::deque toList; + toList.emplace_back(U"N"); + toList.emplace_back(U"O"); + toList.emplace_back(U"P"); + std::u32string output; + auto apply0 = r.apply(U"not present", output, to, fromList, toList); + assert_equal(apply0, 0); // not found + + const std::u32string input(U"WHOA"); + auto apply1 = r.apply(input, output, to, fromList, toList); + assert_equal(apply1, 1); // matched last 1 codepoint + std::u32string expect(U"N"); + zassert_string_equal(output, expect) + } + { + std::cout << __FILE__ << ":" << __LINE__ << " * util_regex.hpp wide map tests" << std::endl; + km::core::util::km_regex r(U"(𐒷|𐒻|𐓏𐓏|x)"); + assert(r.valid()); + const std::u32string to(U"$[1:alpha2]"); // ignored + std::deque fromList; + fromList.emplace_back(U"𐒷"); + fromList.emplace_back(U"𐒻"); + fromList.emplace_back(U"𐓏𐓏"); + fromList.emplace_back(U"x"); + std::deque toList; + toList.emplace_back(U"x"); + toList.emplace_back(U"𐒷"); + toList.emplace_back(U"𐒻"); + toList.emplace_back(U"𐓏"); + std::u32string output; + auto apply0 = r.apply(U"not present", output, to, fromList, toList); + assert_equal(apply0, 0); // not found + + assert_equal(r.apply(U"WHO𐓏𐒷", output, to, fromList, toList), 1); + zassert_string_equal(output, U"x"); + + assert_equal(r.apply(U"WHO𐓏x", output, to, fromList, toList), 1); + zassert_string_equal(output, U"𐓏"); + + assert_equal(r.apply(U"WHO𐓏𐓏", output, to, fromList, toList), 2); // 2 codepoints + zassert_string_equal(output, U"𐒻"); + } + + return EXIT_SUCCESS; +} + int main(int argc, const char *argv[]) { int rc = EXIT_SUCCESS; @@ -1152,6 +1250,9 @@ main(int argc, const char *argv[]) { console_color::enabled = console_color::isaterminal() || arg_color; + if (test_util_regex() != EXIT_SUCCESS) { + rc = EXIT_FAILURE; + } if (test_transforms() != EXIT_SUCCESS) { rc = EXIT_FAILURE; diff --git a/core/tests/unit/ldml/test_unicode.cpp b/core/tests/unit/ldml/test_unicode.cpp index 1c2da5444df..937125d1727 100644 --- a/core/tests/unit/ldml/test_unicode.cpp +++ b/core/tests/unit/ldml/test_unicode.cpp @@ -10,6 +10,9 @@ #include #include +// Ensure that ICU gets included even on wasm. +#define KMN_IN_LDML_TESTS + #include "keyman_core.h" #include "path.hpp" @@ -22,6 +25,8 @@ #include #include #include "json.hpp" +#include "util_normalize.hpp" +#include "kmx/kmx_xstring.h" #include #include @@ -40,6 +45,11 @@ } \ } +#ifdef __EMSCRIPTEN__ +// Pull this in to verify versions +#include "util_normalize_table.h" +#endif + //------------------------------------------------------------------------------------- // Unicode version tests //------------------------------------------------------------------------------------- @@ -161,8 +171,9 @@ const std::string &block_unicode_ver) { // the cxx_icu can come from the Ubuntu environment, so do not depend on it // for now. + // TODO: Resolve with ICU4C 76 in #12398 //assert_basic_equal(node_icu_unicode_major, cxx_icu_unicode_major); - assert_basic_equal(node_icu_unicode_major, block_ver_major); + //assert_basic_equal(node_icu_unicode_major, block_ver_major); // seems less important if the C++ ICU verison matches the Node.js ICU version. //assert_basic_equal(cxx_icu_major, node_icu_major); @@ -172,6 +183,42 @@ const std::string &block_unicode_ver) { std::cout << std::endl; } +#ifdef __EMSCRIPTEN__ +inline const char *boolstr(bool b) { + return b?"T":"f"; +} + +void test_has_boundary_before() { + std::cout << "= " << __FUNCTION__ << std::endl; + std::cout << "I see we are on Emscripten / wasm! Now we will do some additional tests." << std::endl; + std::string icu4c_unicode(U_UNICODE_VERSION), header_unicode(KM_HASBOUNDARYBEFORE_UNICODE_VERSION), + icu4c_icu(U_ICU_VERSION), header_icu(KM_HASBOUNDARYBEFORE_ICU_VERSION); + std::cout << "Unicode: " << U_UNICODE_VERSION << ", and from the table file: " << KM_HASBOUNDARYBEFORE_UNICODE_VERSION << std::endl; + std::cout << "It would be very strange for these versions to be out of sync. Some sort of build or tool problem." << std::endl; + assert_basic_equal(icu4c_unicode, header_unicode); + assert_basic_equal(icu4c_icu, header_icu); + + std::cout << std::endl << "Now, let's make sure has_nfd_boundary_before() matches ICU." << std::endl; + + UErrorCode status = U_ZERO_ERROR; + const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); + UASSERT_SUCCESS(status); + + // now, test that hasBoundaryBefore is the same + for (km_core_usv cp = 0; cp < km::core::kmx::Uni_MAX_CODEPOINT; cp++) { + auto km_hbb = km::core::util::has_nfd_boundary_before(cp); + auto icu_hbb = nfd->hasBoundaryBefore(cp); + + if (km_hbb != icu_hbb) { + std::cerr << "Error: util_normalize_table.h said " << boolstr(km_hbb) << " but ICU said " << boolstr(icu_hbb) << " for " + << "has_nfd_boundary_before(0x" << std::hex << cp << std::dec << ")" << std::endl; + } + assert(km_hbb == icu_hbb); + } + std::cout << "All OK!" << std::endl; +} +#endif + int test_all(const char *jsonpath, const char *packagepath, const char *blockspath) { std::cout << "= " << __FUNCTION__ << std::endl; @@ -187,6 +234,10 @@ int test_all(const char *jsonpath, const char *packagepath, const char *blockspa test_unicode_versions(versions, package, block_unicode_ver); +#ifdef __EMSCRIPTEN__ + test_has_boundary_before(); +#endif + return EXIT_SUCCESS; } diff --git a/core/tests/unit/meson.build b/core/tests/unit/meson.build index e3d9f766073..ee141cc12d3 100644 --- a/core/tests/unit/meson.build +++ b/core/tests/unit/meson.build @@ -2,10 +2,10 @@ node = find_program('node', required: true) common_test_files = [ meson.current_source_dir() / 'emscripten_filesystem.cpp', - meson.source_root() / '../common/include/test_color.cpp' + meson.global_source_root() / '../common/include/test_color.cpp' ] -hextobin_root = join_paths(meson.source_root(),'..','common','tools','hextobin','build','hextobin.js') +hextobin_root = meson.global_source_root() / '../common/tools/hextobin/build/hextobin.js' hextobin_cmd = [node, hextobin_root] subdir('json') diff --git a/core/tests/unit/utftest/meson.build b/core/tests/unit/utftest/meson.build index eff79c10b7e..4ab6cb74ca0 100644 --- a/core/tests/unit/utftest/meson.build +++ b/core/tests/unit/utftest/meson.build @@ -5,6 +5,6 @@ # e = executable('utftest', 'utftest.cpp', - objects: lib.extract_objects('utfcodec.cpp'), + objects: lib.extract_objects('../../common/cpp/utfcodec.cpp'), include_directories: [libsrc]) test('utftest', e) diff --git a/core/wasm.build.linux.in b/core/wasm.build.linux.in index d449dcf94a0..27cb7ba68fe 100644 --- a/core/wasm.build.linux.in +++ b/core/wasm.build.linux.in @@ -1,4 +1,4 @@ [binaries] -c = ['$EMSCRIPTEN_BASE/emcc.py'] -cpp = ['$EMSCRIPTEN_BASE/em++.py'] -ar = ['$EMSCRIPTEN_BASE/emar.py'] \ No newline at end of file +c = ['$EMSCRIPTEN_BASE/emcc'] +cpp = ['$EMSCRIPTEN_BASE/em++'] +ar = ['$EMSCRIPTEN_BASE/emar'] diff --git a/core/wasm.build.mac.in b/core/wasm.build.mac.in index d449dcf94a0..27cb7ba68fe 100644 --- a/core/wasm.build.mac.in +++ b/core/wasm.build.mac.in @@ -1,4 +1,4 @@ [binaries] -c = ['$EMSCRIPTEN_BASE/emcc.py'] -cpp = ['$EMSCRIPTEN_BASE/em++.py'] -ar = ['$EMSCRIPTEN_BASE/emar.py'] \ No newline at end of file +c = ['$EMSCRIPTEN_BASE/emcc'] +cpp = ['$EMSCRIPTEN_BASE/em++'] +ar = ['$EMSCRIPTEN_BASE/emar'] diff --git a/core/wasm.defs.build b/core/wasm.defs.build index 4a598a1e1c1..f41af337a38 100644 --- a/core/wasm.defs.build +++ b/core/wasm.defs.build @@ -1,7 +1,7 @@ [binaries] -c = ['emcc.py'] -cpp = ['em++.py'] -ar = ['emar.py'] +c = ['emcc'] +cpp = ['em++'] +ar = ['emar'] exe_wrapper = 'node' [properties] diff --git a/crowdin.yml b/crowdin.yml index f8b9785bc36..812cfa21c77 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -34,6 +34,7 @@ files: languages_mapping: # Prevent invalid region pap-rPAP. Leaving "in" for Indonesian android_code: + el-polyton: b+el # TODO: figure out polyton variant es-419: b+es+419 pap: pap shu-latn-n: b+shu+latn @@ -44,6 +45,7 @@ files: languages_mapping: # Prevent invalid region pap-rPAP android_code: + el-polyton: b+el # TODO: figure out polyton variant es-419: b+es+419 pap: pap shu-latn-n: b+shu+latn @@ -79,6 +81,7 @@ files: languages_mapping: osx_code: pt-PT: pt-PT.lproj + el-polyton: el.lproj - source: /ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.strings dest: /ios/engine/Localizable.strings @@ -86,6 +89,7 @@ files: languages_mapping: osx_code: pt-PT: pt-PT.lproj + el-polyton: el.lproj - source: /ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.stringsdict dest: /ios/engine/Localizable.stringsdict @@ -93,6 +97,7 @@ files: languages_mapping: osx_code: pt-PT: pt-PT.lproj + el-polyton: el.lproj - source: /ios/keyman/Keyman/Keyman/en.lproj/Localizable.strings dest: /ios/app/Localizable.strings @@ -100,6 +105,7 @@ files: languages_mapping: osx_code: pt-PT: pt-PT.lproj + el-polyton: el.lproj # Linux files @@ -121,6 +127,7 @@ files: languages_mapping: osx_code: pt-PT: pt-PT.lproj + el-polyton: el.lproj - source: /mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/en.lproj/preferences.strings dest: /mac/app/preferences.strings @@ -128,6 +135,7 @@ files: languages_mapping: osx_code: pt-PT: pt-PT.lproj + el-polyton: el.lproj - source: /mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/en.lproj/KMInfoWindowController.strings dest: /mac/app/KMInfoWindowController.strings @@ -135,13 +143,15 @@ files: languages_mapping: osx_code: pt-PT: pt-PT.lproj + el-polyton: el.lproj - - source: /mac/Keyman4MacIM/Keyman4MacIM/KMKeyboardHelpWindow/en.lproj/KMKeyboardHelpWindowController.strings + - source: /mac/Keyman4MacIM/Keyman4MacIM/KMKeyboardHelpWindow/en.lproj/KMKeyboardHelpWindowController.strings dest: /mac/app/KMKeyboardHelpWindowController.strings translation: /mac/Keyman4MacIM/Keyman4MacIM/KMKeyboardHelpWindow/%osx_code%/%original_file_name% languages_mapping: osx_code: pt-PT: pt-PT.lproj + el-polyton: el.lproj - source: /mac/Keyman4MacIM/Keyman4MacIM/en.lproj/Localizable.strings dest: /mac/app/Localizable.strings @@ -149,6 +159,7 @@ files: languages_mapping: osx_code: pt-PT: pt-PT.lproj + el-polyton: el.lproj - source: /mac/Keyman4MacIM/Keyman4MacIM/en.lproj/MainMenu.strings dest: /mac/app/MainMenu.strings @@ -156,6 +167,7 @@ files: languages_mapping: osx_code: pt-PT: pt-PT.lproj + el-polyton: el.lproj # crowdin parameters descriptions: diff --git a/developer/docs/api/etc/kmc-analyze.api.md b/developer/docs/api/etc/kmc-analyze.api.md index 490ead7be77..45bbd8101ca 100644 --- a/developer/docs/api/etc/kmc-analyze.api.md +++ b/developer/docs/api/etc/kmc-analyze.api.md @@ -6,7 +6,7 @@ import { CompilerCallbacks } from '@keymanapp/common-types'; import { CompilerEvent } from '@keymanapp/common-types'; -import { Osk } from '@keymanapp/developer-utils'; +import { Osk } from '@keymanapp/kmc-kmn'; // @public export class AnalyzeOskCharacterUse { diff --git a/developer/docs/api/etc/kmc-keyboard-info.api.md b/developer/docs/api/etc/kmc-keyboard-info.api.md index 262c463cf2b..0b1ee4f4279 100644 --- a/developer/docs/api/etc/kmc-keyboard-info.api.md +++ b/developer/docs/api/etc/kmc-keyboard-info.api.md @@ -45,17 +45,17 @@ export class KeyboardInfoCompilerMessages { filename: string; }) => CompilerEvent; // (undocumented) - static ERROR_FileIsNotValid: number; + static ERROR_FontFileCannotBeRead: number; // (undocumented) - static Error_FileIsNotValid: (o: { + static Error_FontFileCannotBeRead: (o: { filename: string; - e: any; }) => CompilerEvent; // (undocumented) - static ERROR_FontFileCannotBeRead: number; + static ERROR_FontFileMetaDataIsInvalid: number; // (undocumented) - static Error_FontFileCannotBeRead: (o: { + static Error_FontFileMetaDataIsInvalid: (o: { filename: string; + message: string; }) => CompilerEvent; // (undocumented) static ERROR_InvalidAuthorEmail: number; @@ -87,37 +87,11 @@ export class KeyboardInfoCompilerMessages { // (undocumented) static Error_NoLicenseFound: () => CompilerEvent; // (undocumented) - static ERROR_OutputValidation: number; - // (undocumented) - static Error_OutputValidation: (o: { - message: any; - }) => CompilerEvent; - // (undocumented) static FATAL_UnexpectedException: number; // (undocumented) static Fatal_UnexpectedException: (o: { e: any; }) => CompilerEvent; - // (undocumented) - static HINT_OutputValidation: number; - // (undocumented) - static Hint_OutputValidation: (o: { - message: any; - }) => CompilerEvent; - // (undocumented) - static WARN_MetadataFieldInconsistent: number; - // (undocumented) - static Warn_MetadataFieldInconsistent: (o: { - field: string; - value: any; - expected: any; - }) => CompilerEvent; - // (undocumented) - static WARN_OutputValidation: number; - // (undocumented) - static Warn_OutputValidation: (o: { - message: any; - }) => CompilerEvent; } // @public diff --git a/developer/docs/api/etc/kmc-kmn.api.md b/developer/docs/api/etc/kmc-kmn.api.md index 28c7aae5e8b..de1bd503f80 100644 --- a/developer/docs/api/etc/kmc-kmn.api.md +++ b/developer/docs/api/etc/kmc-kmn.api.md @@ -11,15 +11,10 @@ import { KeymanCompiler } from '@keymanapp/common-types'; import { KeymanCompilerArtifactOptional } from '@keymanapp/common-types'; import { KeymanCompilerArtifacts } from '@keymanapp/common-types'; import { KeymanCompilerResult } from '@keymanapp/common-types'; -import { Osk } from '@keymanapp/developer-utils'; +import { TouchLayout } from '@keymanapp/common-types'; import { UnicodeSet } from '@keymanapp/common-types'; import { UnicodeSetParser } from '@keymanapp/common-types'; - -// Warning: (ae-internal-missing-underscore) The name "CompilerMessages" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export class CompilerMessages extends KmnCompilerMessages { -} +import { VisualKeyboard } from '@keymanapp/common-types'; // Warning: (ae-internal-missing-underscore) The name "CompilerResultExtraGroup" should be prefixed with an underscore because the declaration is marked as @internal // @@ -184,6 +179,10 @@ export class KmnCompilerMessages { // (undocumented) static Error_CharacterExpansionMustBeFollowedByCharacter: () => CompilerEvent; // (undocumented) + static ERROR_CharacterRangeTooLong: number; + // (undocumented) + static Error_CharacterRangeTooLong: () => CompilerEvent; + // (undocumented) static ERROR_CodeInvalidInKeyStore: number; // (undocumented) static Error_CodeInvalidInKeyStore: () => CompilerEvent; @@ -220,6 +219,10 @@ export class KmnCompilerMessages { // (undocumented) static Error_ExpansionMustFollowCharacterOrVKey: () => CompilerEvent; // (undocumented) + static ERROR_ExtendedStringTooLong: number; + // (undocumented) + static Error_ExtendedStringTooLong: () => CompilerEvent; + // (undocumented) static ERROR_FileNotFound: number; // (undocumented) static Error_FileNotFound: (o: { @@ -381,6 +384,10 @@ export class KmnCompilerMessages { // (undocumented) static Error_InvalidSystemStore: () => CompilerEvent; // (undocumented) + static ERROR_InvalidTarget: number; + // (undocumented) + static Error_InvalidTarget: () => CompilerEvent; + // (undocumented) static ERROR_InvalidToken: number; // (undocumented) static Error_InvalidToken: () => CompilerEvent; @@ -419,6 +426,14 @@ export class KmnCompilerMessages { // (undocumented) static Error_NewContextGroupMustBeReadonly: () => CompilerEvent; // (undocumented) + static ERROR_NonBMPCharactersNotSupportedInKeySection: number; + // (undocumented) + static Error_NonBMPCharactersNotSupportedInKeySection: () => CompilerEvent; + // (undocumented) + static ERROR_NoTargetsSpecified: number; + // (undocumented) + static Error_NoTargetsSpecified: () => CompilerEvent; + // (undocumented) static ERROR_NoTokensFound: number; // (undocumented) static Error_NoTokensFound: () => CompilerEvent; @@ -456,6 +471,10 @@ export class KmnCompilerMessages { // (undocumented) static Error_OutsInVirtualKeySection: () => CompilerEvent; // (undocumented) + static ERROR_OutsTooLong: number; + // (undocumented) + static Error_OutsTooLong: () => CompilerEvent; + // (undocumented) static ERROR_PostKeystrokeGroupMustBeReadonly: number; // (undocumented) static Error_PostKeystrokeGroupMustBeReadonly: () => CompilerEvent; @@ -522,6 +541,10 @@ export class KmnCompilerMessages { line: number; }) => CompilerEvent; // (undocumented) + static ERROR_VirtualKeyExpansionTooLong: number; + // (undocumented) + static Error_VirtualKeyExpansionTooLong: () => CompilerEvent; + // (undocumented) static ERROR_VirtualKeyInContext: number; // (undocumented) static Error_VirtualKeyInContext: () => CompilerEvent; @@ -596,6 +619,10 @@ export class KmnCompilerMessages { // (undocumented) static Fatal_UnicodeSetOutOfRange: () => CompilerEvent; // (undocumented) + static HINT_IndexStoreLong: number; + // (undocumented) + static Hint_IndexStoreLong: () => CompilerEvent; + // (undocumented) static HINT_NonUnicodeFile: number; // (undocumented) static Hint_NonUnicodeFile: () => CompilerEvent; @@ -850,12 +877,6 @@ export class KmwCompilerMessages extends KmnCompilerMessages { msg: string; }) => CompilerEvent; // (undocumented) - static ERROR_NotAnyRequiresVersion14: number; - // (undocumented) - static Error_NotAnyRequiresVersion14: (o: { - line: number; - }) => CompilerEvent; - // (undocumented) static ERROR_TouchLayoutFileDoesNotExist: number; // (undocumented) static Error_TouchLayoutFileDoesNotExist: (o: { @@ -875,6 +896,63 @@ export class KmwCompilerMessages extends KmnCompilerMessages { static Hint_TouchLayoutUsesUnsupportedGesturesDownlevel: (o: { keyId: string; }) => CompilerEvent; + // (undocumented) + static INFO_MinimumEngineVersion: number; + // (undocumented) + static Info_MinimumEngineVersion: (o: { + version: string; + }) => CompilerEvent; +} + +declare namespace Osk { + export { + parseMapping, + remapVisualKeyboard, + remapTouchLayout, + StringRefUsage, + StringRef, + StringResult, + PuaMap + } +} +export { Osk } + +// @public (undocumented) +function parseMapping(mapping: any): PuaMap; + +// @public (undocumented) +type PuaMap = { + [index: string]: string; +}; + +// @public (undocumented) +function remapTouchLayout(source: TouchLayout.TouchLayoutFile, map: PuaMap): boolean; + +// @public (undocumented) +function remapVisualKeyboard(vk: VisualKeyboard.VisualKeyboard, map: PuaMap): boolean; + +// @public (undocumented) +interface StringRef { + // (undocumented) + str: string; + // (undocumented) + usages: StringRefUsage[]; +} + +// @public (undocumented) +interface StringRefUsage { + // (undocumented) + count: number; + // (undocumented) + filename: string; +} + +// @public +interface StringResult { + pua: string; + str: string; + unicode: string; + usages: StringRefUsage[] | string[]; } ``` diff --git a/developer/docs/api/etc/kmc-ldml.api.md b/developer/docs/api/etc/kmc-ldml.api.md index 2ea37258715..cc32c5e3575 100644 --- a/developer/docs/api/etc/kmc-ldml.api.md +++ b/developer/docs/api/etc/kmc-ldml.api.md @@ -16,36 +16,10 @@ import { LDMLKeyboardTestDataXMLSourceFile } from '@keymanapp/common-types'; import { LDMLKeyboardXMLSourceFileReaderOptions } from '@keymanapp/common-types'; import { UnicodeSetParser } from '@keymanapp/common-types'; -// @public -export interface LdmlCompilerOptions extends CompilerOptions { - readerOptions: LDMLKeyboardXMLSourceFileReaderOptions; -} - -// @public -export class LdmlKeyboardCompiler implements KeymanCompiler { - // @internal - compile(source: LDMLKeyboardXMLSourceFile, postValidate?: boolean): Promise; - // @internal - getUsetParser(): Promise; - init(callbacks: CompilerCallbacks, options: LdmlCompilerOptions): Promise; - // Warning: (ae-forgotten-export) The symbol "LDMLKeyboardXMLSourceFile" needs to be exported by the entry point main.d.ts - // - // @internal - load(filename: string): LDMLKeyboardXMLSourceFile | null; - // @internal - loadTestData(filename: string): LDMLKeyboardTestDataXMLSourceFile | null; - // Warning: (ae-forgotten-export) The symbol "LdmlKeyboardCompilerResult" needs to be exported by the entry point main.d.ts - run(inputFilename: string, outputFilename?: string): Promise; - // @internal - validate(source: LDMLKeyboardXMLSourceFile): Promise; - // Warning: (ae-forgotten-export) The symbol "LdmlKeyboardCompilerArtifacts" needs to be exported by the entry point main.d.ts - write(artifacts: LdmlKeyboardCompilerArtifacts): Promise; -} - -// Warning: (ae-internal-missing-underscore) The name "LdmlKeyboardCompilerMessages" should be prefixed with an underscore because the declaration is marked as @internal +// Warning: (ae-internal-missing-underscore) The name "LdmlCompilerMessages" should be prefixed with an underscore because the declaration is marked as @internal // // @internal (undocumented) -export class LdmlKeyboardCompilerMessages { +export class LdmlCompilerMessages { // (undocumented) static ERROR_CantReferenceSetFromUnicodeSet: number; // (undocumented) @@ -108,6 +82,24 @@ export class LdmlKeyboardCompilerMessages { lowestCh: number; }) => CompilerEvent; // (undocumented) + static ERROR_IllegalTransformAsterisk: number; + // (undocumented) + static Error_IllegalTransformAsterisk: (o: { + from: string; + }) => CompilerEvent; + // (undocumented) + static ERROR_IllegalTransformDollarsign: number; + // (undocumented) + static Error_IllegalTransformDollarsign: (o: { + from: string; + }) => CompilerEvent; + // (undocumented) + static ERROR_IllegalTransformPlus: number; + // (undocumented) + static Error_IllegalTransformPlus: (o: { + from: string; + }) => CompilerEvent; + // (undocumented) static ERROR_InvalidFile: number; // (undocumented) static Error_InvalidFile: (o: { @@ -227,6 +219,12 @@ export class LdmlKeyboardCompilerMessages { modifiers: string; }) => CompilerEvent; // (undocumented) + static ERROR_TransformFromMatchesNothing: number; + // (undocumented) + static Error_TransformFromMatchesNothing: (o: { + from: string; + }) => CompilerEvent; + // (undocumented) static ERROR_UnparseableReorderSet: number; // (undocumented) static Error_UnparseableReorderSet: (o: { @@ -241,12 +239,6 @@ export class LdmlKeyboardCompilerMessages { message: string; }) => CompilerEvent; // (undocumented) - static FATAL_SectionCompilerFailed: number; - // (undocumented) - static Fatal_SectionCompilerFailed: (o: { - sect: string; - }) => CompilerEvent; - // (undocumented) static HINT_CharClassImplicitDenorm: number; // (undocumented) static Hint_CharClassImplicitDenorm: (o: { @@ -260,6 +252,18 @@ export class LdmlKeyboardCompilerMessages { locale: string; }) => CompilerEvent; // (undocumented) + static HINT_NoDisplayForMarker: number; + // (undocumented) + static Hint_NoDisplayForMarker: (o: { + id: string; + }) => CompilerEvent; + // (undocumented) + static HINT_NoDisplayForSwitch: number; + // (undocumented) + static Hint_NoDisplayForSwitch: (o: { + id: string; + }) => CompilerEvent; + // (undocumented) static HINT_NormalizationDisabled: number; // (undocumented) static Hint_NormalizationDisabled: () => CompilerEvent; @@ -295,6 +299,32 @@ export class LdmlKeyboardCompilerMessages { }) => CompilerEvent; } +// @public +export interface LdmlCompilerOptions extends CompilerOptions { + readerOptions: LDMLKeyboardXMLSourceFileReaderOptions; +} + +// @public +export class LdmlKeyboardCompiler implements KeymanCompiler { + // @internal + compile(source: LDMLKeyboardXMLSourceFile, postValidate?: boolean): Promise; + // @internal + getUsetParser(): Promise; + init(callbacks: CompilerCallbacks, options: LdmlCompilerOptions): Promise; + // Warning: (ae-forgotten-export) The symbol "LDMLKeyboardXMLSourceFile" needs to be exported by the entry point main.d.ts + // + // @internal + load(filename: string): LDMLKeyboardXMLSourceFile | null; + // @internal + loadTestData(filename: string): LDMLKeyboardTestDataXMLSourceFile | null; + // Warning: (ae-forgotten-export) The symbol "LdmlKeyboardCompilerResult" needs to be exported by the entry point main.d.ts + run(inputFilename: string, outputFilename?: string): Promise; + // @internal + validate(source: LDMLKeyboardXMLSourceFile): Promise; + // Warning: (ae-forgotten-export) The symbol "LdmlKeyboardCompilerArtifacts" needs to be exported by the entry point main.d.ts + write(artifacts: LdmlKeyboardCompilerArtifacts): Promise; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/developer/docs/api/etc/kmc-model.api.md b/developer/docs/api/etc/kmc-model.api.md index df76354c238..51ea5dffb8d 100644 --- a/developer/docs/api/etc/kmc-model.api.md +++ b/developer/docs/api/etc/kmc-model.api.md @@ -4,7 +4,7 @@ ```ts -/// +/// import { CompilerCallbacks } from '@keymanapp/common-types'; import { CompilerEvent } from '@keymanapp/common-types'; diff --git a/developer/docs/api/etc/kmc-package.api.md b/developer/docs/api/etc/kmc-package.api.md index 1215057dfea..eb2c535160a 100644 --- a/developer/docs/api/etc/kmc-package.api.md +++ b/developer/docs/api/etc/kmc-package.api.md @@ -21,6 +21,8 @@ export class KmpCompiler implements KeymanCompiler { init(callbacks: CompilerCallbacks, options: KmpCompilerOptions): Promise; // @internal (undocumented) loadKpsFile(kpsFilename: string): KpsFile.KpsFile; + // (undocumented) + readonly normalizePath: (path: string) => string; run(inputFilename: string, outputFilename?: string): Promise; // @internal (undocumented) transformKpsFileToKmpObject(kpsFilename: string, kps: KpsFile.KpsFile): KmpJsonFile.KmpJsonFile; @@ -61,6 +63,12 @@ export class PackageCompilerMessages { filename: string; }) => CompilerEvent; // (undocumented) + static ERROR_FileRecordIsMissingName: number; + // (undocumented) + static Error_FileRecordIsMissingName: (o: { + description: string; + }) => CompilerEvent; + // (undocumented) static ERROR_FollowKeyboardVersionButNoKeyboards: number; // (undocumented) static Error_FollowKeyboardVersionButNoKeyboards: () => CompilerEvent; @@ -69,6 +77,12 @@ export class PackageCompilerMessages { // (undocumented) static Error_FollowKeyboardVersionNotAllowedForModelPackages: () => CompilerEvent; // (undocumented) + static ERROR_InvalidAuthorEmail: number; + // (undocumented) + static Error_InvalidAuthorEmail: (o: { + email: string; + }) => CompilerEvent; + // (undocumented) static ERROR_InvalidPackageFile: number; // (undocumented) static Error_InvalidPackageFile: (o: { diff --git a/developer/src/.build-builder b/developer/src/.build-builder new file mode 100644 index 00000000000..a06440b36fa --- /dev/null +++ b/developer/src/.build-builder @@ -0,0 +1 @@ +The presence of this file tells CI that we can build Developer on mac and linux, as of 18.0. We will be able to remove this file in 19.0. diff --git a/developer/src/.gitignore b/developer/src/.gitignore index 7ce2ad52302..9cc0d7e9567 100644 --- a/developer/src/.gitignore +++ b/developer/src/.gitignore @@ -86,3 +86,4 @@ kmc/build/ kmc-*/build/ +setup/icons.res \ No newline at end of file diff --git a/developer/src/README.md b/developer/src/README.md index 7e7ea9d336d..d789b63f08c 100644 --- a/developer/src/README.md +++ b/developer/src/README.md @@ -2,6 +2,42 @@ This is the current home for Keyman Developer. +## Build Prerequisites + +* See [Windows Build Environment Configuration](../../docs/build/windows.md). + +## Building Keyman Developer + +1. Start 'Git Bash' (part of Git for Windows). +2. Run `developer/src/build.sh`. +3. Artifacts from a successful build will be placed in **developer/bin** folder. + +*Note*: running `build.sh` will currently reset the packages and path settings +in your Delphi environment. If you use Delphi for other projects, you should +consider building Keyman under a login user dedicated to it, or in a VM. + +Type `build.sh` to see build targets and actions. Common build actions are: + +* `build.sh build` +: builds Keyman Developer + +* `build.sh clean` +: remove temporary files and build artifacts + +* `build.sh publish` +: makes a release of all Keyman Developer projects + +* `build.sh install` +: install some or all components to Program Files (requires elevated command prompt). + +### Building without Delphi + +It is possible to build all components that do _not_ require Delphi. Currently a +few components are Delphi-based (TIKE, setup, a few others), but you may be able +to get away without building them. In this situation, we recommend copying the +relevant Delphi-built components into developer/bin folders from a compatible +installed version of Keyman Developer for testing and debugging purposes. + # Folders ## common @@ -106,4 +142,6 @@ testing and packing keyboards for distribution. ## tools -Various build-time tools for Keyman Developer. \ No newline at end of file +Various build-time tools for Keyman Developer. + + diff --git a/developer/src/build.sh b/developer/src/build.sh index de7ab491716..c85851f1f65 100755 --- a/developer/src/build.sh +++ b/developer/src/build.sh @@ -42,11 +42,29 @@ builder_describe \ "--npm-publish+ For publish, do a npm publish, not npm pack (only for CI)" \ "--dry-run,-n+ Don't actually publish anything to external endpoints, just dry run" +builder_describe_platform \ + :ext win,delphi \ + :kmanalyze win \ + :kmdbrowserhost win,delphi \ + :kmdecomp win \ + :kmconvert win,delphi \ + :samples win \ + :server win \ + :setup win,delphi \ + :test win,delphi \ + :tike win,delphi \ + :inst win,delphi + +# TODO: in future :server could be built on other platforms, potentially, but it +# has addons that are Windows-specific currently + builder_parse "$@" #------------------------------------------------------------------------------------------------------------------- -source "$KEYMAN_ROOT/resources/build/win/environment.inc.sh" +if [[ $BUILDER_OS == win ]]; then + source "$KEYMAN_ROOT/resources/build/win/environment.inc.sh" +fi # # We want to do some checks before we head down the long publish path @@ -133,7 +151,6 @@ function do_publish() { ../../common/web/keyman-version/build.sh publish $DRY_RUN $NPM_PUBLISH ../../common/web/types/build.sh publish $DRY_RUN $NPM_PUBLISH - ../../common/models/types/build.sh publish $DRY_RUN $NPM_PUBLISH ../../core/include/ldml/build.sh publish $DRY_RUN $NPM_PUBLISH # end TODO #-------------------------------------------------------- diff --git a/developer/src/common/build.sh b/developer/src/common/build.sh index 53358c80b05..b37c2bc948f 100755 --- a/developer/src/common/build.sh +++ b/developer/src/common/build.sh @@ -11,5 +11,8 @@ builder_describe \ ":delphi Delphi components" \ ":web Web components" +builder_describe_platform \ + :delphi win,delphi + builder_parse "$@" builder_run_child_actions clean configure build test diff --git a/developer/src/common/include/kmn_compiler_errors.h b/developer/src/common/include/kmn_compiler_errors.h index 4973f58abe5..4955df5e738 100644 --- a/developer/src/common/include/kmn_compiler_errors.h +++ b/developer/src/common/include/kmn_compiler_errors.h @@ -23,21 +23,38 @@ #ifndef _kmn_compiler_errors_h #define _kmn_compiler_errors_h -// Compiler Error Masks - -#define CERR_SEVERITY_MASK 0x0000F000 -#define CERR_MESSAGE_MASK 0x00000FFF -#define CERR_MASK 0x0000FFFF - -// Severity codes -// -// Note: these may not be combined, and are not a bitmask, for historical -// reasons they are separate bits -#define CERR_FATAL 0x00008000 -#define CERR_ERROR 0x00004000 -#define CERR_WARNING 0x00002000 -#define CERR_HINT 0x00001000 -#define CERR_INFO 0x00000000 +// Compiler Error Masks -- matches compiler-interfaces.ts + +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 + }; +}; + +#define MESSAGE_SEVERITY_MASK 0x00F00000 // includes reserved bits, 16 possible severity levels +#define MESSAGE_ERROR_MASK 0x000FFFFF // error | namespace +#define MESSAGE_NAMESPACE_MASK 0x000FF000 // 256 possible namespaces +#define MESSAGE_BASEERROR_MASK 0x00000FFF // error code, 2,048 possible error codes per namespace +#define MESSAGE_RESERVED_MASK 0xFF000000 // do not use these error values at this time + +#define MESSAGE_NAMESPACE_KmnCompiler 0x2000 + +// Severity codes + namespace for shorthand use +#define SevFatal (MESSAGE_NAMESPACE_KmnCompiler | CompilerErrorSeverity::Fatal) +#define SevError (MESSAGE_NAMESPACE_KmnCompiler | CompilerErrorSeverity::Error) +#define SevWarn (MESSAGE_NAMESPACE_KmnCompiler | CompilerErrorSeverity::Warn) +#define SevHint (MESSAGE_NAMESPACE_KmnCompiler | CompilerErrorSeverity::Hint) +#define SevInfo (MESSAGE_NAMESPACE_KmnCompiler | CompilerErrorSeverity::Info) + +// These two values are not true error codes; they are used as +// return values in kmcmplib and are never passed to kmc-kmn +#define STATUS_Success 0x00000000 // NOTE: Not a message code +#define STATUS_EndOfFile 0x00000001 // NOTE: Not a message code // Message codes // @@ -46,209 +63,216 @@ // sure to update that file also. Note that this correlation is currently // maintained manually. All values must be below 0x1000 (exclusive of severity // code). -#define CERR_None 0x00000000 -#define CERR_EndOfFile 0x00000001 - -#define CERR_BadCallParams 0x00008002 -#define CERR_CannotAllocateMemory 0x00008004 -#define CERR_InfileNotExist 0x00004005 // #10678: reduced from fatal to error in 17.0 -// #define CERR_CannotCreateOutfile 0x00004006 // #10678: reduced from fatal to error in 17.0, but unused -#define CERR_UnableToWriteFully 0x00008007 -#define CERR_CannotReadInfile 0x00004008 // #10678: reduced from fatal to error in 17.0 -#define CERR_SomewhereIGotItWrong 0x00008009 - -#define CERR_InvalidToken 0x0000400A -#define CERR_InvalidBegin 0x0000400B -#define CERR_InvalidName 0x0000400C -#define CERR_InvalidVersion 0x0000400D -#define CERR_InvalidLanguageLine 0x0000400E -#define CERR_LayoutButNoLanguage 0x0000400F -#define CERR_InvalidLayoutLine 0x00004010 -#define CERR_NoVersionLine 0x00004011 -#define CERR_InvalidGroupLine 0x00004012 -#define CERR_InvalidStoreLine 0x00004013 -#define CERR_InvalidCodeInKeyPartOfRule 0x00004014 -#define CERR_InvalidDeadkey 0x00004015 -#define CERR_InvalidValue 0x00004016 -#define CERR_ZeroLengthString 0x00004017 -#define CERR_TooManyIndexToKeyRefs 0x00004018 -#define CERR_UnterminatedString 0x00004019 -#define CERR_StringInVirtualKeySection 0x0000401A -#define CERR_AnyInVirtualKeySection 0x0000401B -#define CERR_InvalidAny 0x0000401C -#define CERR_StoreDoesNotExist 0x0000401D -#define CERR_BeepInVirtualKeySection 0x0000401E -#define CERR_IndexInVirtualKeySection 0x0000401F -#define CERR_InvalidIndex 0x00004020 -#define CERR_OutsInVirtualKeySection 0x00004021 -#define CERR_InvalidOuts 0x00004022 -#define CERR_ContextInVirtualKeySection 0x00004024 -#define CERR_InvalidUse 0x00004025 -#define CERR_GroupDoesNotExist 0x00004026 -#define CERR_VirtualKeyNotAllowedHere 0x00004027 -#define CERR_InvalidSwitch 0x00004028 -#define CERR_NoTokensFound 0x00004029 -#define CERR_InvalidLineContinuation 0x0000402A -#define CERR_LineTooLong 0x0000402B -#define CERR_InvalidCopyright 0x0000402C -#define CERR_CodeInvalidInThisSection 0x0000402D -#define CERR_InvalidMessage 0x0000402E -#define CERR_InvalidLanguageName 0x0000402F -#define CERR_InvalidBitmapLine 0x00004030 -#define CERR_CannotReadBitmapFile 0x00004031 -#define CERR_IndexDoesNotPointToAny 0x00004032 -#define CERR_ReservedCharacter 0x00004033 -#define CERR_InvalidCharacter 0x00004034 -#define CERR_InvalidCall 0x00004035 -#define CERR_CallInVirtualKeySection 0x00004036 -#define CERR_CodeInvalidInKeyStore 0x00004037 -#define CERR_CannotLoadIncludeFile 0x00004038 - -#define CERR_60FeatureOnly_EthnologueCode 0x00004039 -#define CERR_60FeatureOnly_MnemonicLayout 0x0000403A -#define CERR_60FeatureOnly_OldCharPosMatching 0x0000403B -#define CERR_60FeatureOnly_NamedCodes 0x0000403C -#define CERR_60FeatureOnly_Contextn 0x0000403D -#define CERR_501FeatureOnly_Call 0x0000403E - -#define CERR_InvalidNamedCode 0x0000403F -#define CERR_InvalidSystemStore 0x00004040 - -#define CERR_60FeatureOnly_VirtualCharKey 0x00004044 - -#define CERR_VersionAlreadyIncluded 0x00004045 - -#define CERR_70FeatureOnly 0x00004046 - -#define CERR_80FeatureOnly 0x00004047 -#define CERR_InvalidInVirtualKeySection 0x00004048 -#define CERR_InvalidIf 0x00004049 -#define CERR_InvalidReset 0x0000404A -#define CERR_InvalidSet 0x0000404B -#define CERR_InvalidSave 0x0000404C - -#define CERR_InvalidEthnologueCode 0x0000404D - -#define CERR_CannotCreateTempfile 0x0000804E - -#define CERR_90FeatureOnly_IfSystemStores 0x0000404F -#define CERR_IfSystemStore_NotFound 0x00004050 -#define CERR_90FeatureOnly_SetSystemStores 0x00004051 -#define CERR_SetSystemStore_NotFound 0x00004052 -#define CERR_90FeatureOnlyVirtualKeyDictionary 0x00004053 - -#define CERR_NotSupportedInKeymanWebContext 0x00004054 -#define CERR_NotSupportedInKeymanWebOutput 0x00004055 -#define CERR_NotSupportedInKeymanWebStore 0x00004056 -#define CERR_VirtualCharacterKeysNotSupportedInKeymanWeb 0x00004057 -#define CERR_VirtualKeysNotValidForMnemonicLayouts 0x00004058 -#define CERR_InvalidTouchLayoutFile 0x00004059 -#define CERR_TouchLayoutInvalidIdentifier 0x0000405A -#define CERR_InvalidKeyCode 0x0000405B -#define CERR_90FeatureOnlyLayoutFile 0x0000405C -#define CERR_90FeatureOnlyKeyboardVersion 0x0000405D -#define CERR_KeyboardVersionFormatInvalid 0x0000405E -#define CERR_ContextExHasInvalidOffset 0x0000405F -#define CERR_90FeatureOnlyEmbedCSS 0x00004060 -#define CERR_90FeatureOnlyTargets 0x00004061 -#define CERR_ContextAndIndexInvalidInMatchNomatch 0x00004062 -#define CERR_140FeatureOnlyContextAndNotAnyWeb 0x00004063 - -#define CERR_ExpansionMustFollowCharacterOrVKey 0x00004064 -#define CERR_VKeyExpansionMustBeFollowedByVKey 0x00004065 -#define CERR_CharacterExpansionMustBeFollowedByCharacter 0x00004066 -#define CERR_VKeyExpansionMustUseConsistentShift 0x00004067 -#define CERR_ExpansionMustBePositive 0x00004068 - -#define CERR_CasedKeysMustContainOnlyVirtualKeys 0x00004069 -#define CERR_CasedKeysMustNotIncludeShiftStates 0x0000406A -#define CERR_CasedKeysNotSupportedWithMnemonicLayout 0x0000406B - -#define CERR_CannotUseReadWriteGroupFromReadonlyGroup 0x0000406C -#define CERR_StatementNotPermittedInReadonlyGroup 0x0000406D -#define CERR_OutputInReadonlyGroup 0x0000406E -#define CERR_NewContextGroupMustBeReadonly 0x0000406F -#define CERR_PostKeystrokeGroupMustBeReadonly 0x00004070 - -#define CERR_DuplicateGroup 0x00004071 -#define CERR_DuplicateStore 0x00004072 -#define CERR_RepeatedBegin 0x00004073 -#define CERR_VirtualKeyInContext 0x00004074 - -#define CERR_OutsTooLong 0x00004075 -#define CERR_ExtendedStringTooLong 0x00004076 -#define CERR_VirtualKeyExpansionTooLong 0x00004077 -#define CERR_CharacterRangeTooLong 0x00004078 - -#define CWARN_TooManyWarnings 0x00002080 -#define CWARN_OldVersion 0x00002081 -#define CWARN_BitmapNotUsed 0x00002082 -#define CWARN_CustomLanguagesNotSupported 0x00002083 -#define CWARN_KeyBadLength 0x00002084 -#define CWARN_IndexStoreShort 0x00002085 -#define CWARN_UnicodeInANSIGroup 0x00002086 -#define CWARN_ANSIInUnicodeGroup 0x00002087 -#define CWARN_UnicodeSurrogateUsed 0x00002088 -#define CWARN_ReservedCharacter 0x00002089 -// Note: CWARN_Info has an "info" severity; this changed in 17.0. Earlier versions -// had a special case for CWARN_Info in message output. -#define CWARN_Info 0x0000008A -#define CINFO_Info CWARN_Info -#define CWARN_VirtualKeyWithMnemonicLayout 0x0000208B -#define CWARN_VirtualCharKeyWithPositionalLayout 0x0000208C -#define CWARN_StoreAlreadyUsedAsOptionOrCall 0x0000208D -#define CWARN_StoreAlreadyUsedAsStoreOrCall 0x0000208E -#define CWARN_StoreAlreadyUsedAsStoreOrOption 0x0000208F - -#define CWARN_PunctuationInEthnologueCode 0x00002090 - -#define CWARN_TouchLayoutMissingLayer 0x00002091 -#define CWARN_TouchLayoutCustomKeyNotDefined 0x00002092 -#define CWARN_TouchLayoutMissingRequiredKeys 0x00002093 -#define CWARN_HelpFileMissing 0x00002094 -#define CWARN_EmbedJsFileMissing 0x00002095 -#define CWARN_TouchLayoutFileMissing 0x00002096 -#define CWARN_VisualKeyboardFileMissing 0x00002097 -#define CWARN_ExtendedShiftFlagsNotSupportedInKeymanWeb 0x00002098 // I4118 -#define CWARN_TouchLayoutUnidentifiedKey 0x00002099 -#define CHINT_UnreachableKeyCode 0x0000109A - -#define CWARN_CouldNotCopyJsonFile 0x0000209B -#define CWARN_PlatformNotInTargets 0x0000209C - -#define CWARN_HeaderStatementIsDeprecated 0x0000209D -#define CWARN_UseNotLastStatementInRule 0x0000209E - -#define CWARN_TouchLayoutFontShouldBeSameForAllPlatforms 0x0000209F -#define CWARN_InvalidJSONMetadataFile 0x000020A0 -#define CWARN_JSONMetadataOSKFontShouldMatchTouchFont 0x000020A1 -#define CWARN_KVKFileIsInSourceFormat 0x000020A2 - -#define CWARN_DontMixChiralAndNonChiralModifiers 0x000020A3 -#define CWARN_MixingLeftAndRightModifiers 0x000020A4 - -#define CWARN_LanguageHeadersDeprecatedInKeyman10 0x000020A5 - -#define CHINT_NonUnicodeFile 0x000010A6 - -#define CWARN_TooManyErrorsOrWarnings 0x000020A7 - -#define CWARN_HotkeyHasInvalidModifier 0x000020A8 - -#define CWARN_TouchLayoutSpecialLabelOnNormalKey 0x000020A9 - -#define CWARN_OptionStoreNameInvalid 0x000020AA - -#define CWARN_NulNotFirstStatementInContext 0x000020AB -#define CWARN_IfShouldBeAtStartOfContext 0x000020AC - -#define CWARN_KeyShouldIncludeNCaps 0x000020AD - -#define CHINT_UnreachableRule 0x000010AE - -#define CWARN_VirtualKeyInOutput 0x000020AF - -#define CERR_BufferOverflow 0x000080C0 -#define CERR_Break 0x000080C1 + +namespace KmnCompilerMessages { + enum { + FATAL_BadCallParams = SevFatal | 0x002, + FATAL_CannotAllocateMemory = SevFatal | 0x004, + ERROR_InfileNotExist = SevError | 0x005, // #10678: reduced from fatal to error in 17.0 +// ERROR_CannotCreateOutfile = SevError | 0x006, // #10678: reduced from fatal to error in 17.0, but unused +// FATAL_UnableToWriteFully = SevFatal | 0x007, // unused + ERROR_CannotReadInfile = SevError | 0x008, // #10678: reduced from fatal to error in 17.0 + FATAL_SomewhereIGotItWrong = SevFatal | 0x009, + + ERROR_InvalidToken = SevError | 0x00A, + ERROR_InvalidBegin = SevError | 0x00B, + ERROR_InvalidName = SevError | 0x00C, + ERROR_InvalidVersion = SevError | 0x00D, + ERROR_InvalidLanguageLine = SevError | 0x00E, + ERROR_LayoutButNoLanguage = SevError | 0x00F, + ERROR_InvalidLayoutLine = SevError | 0x010, + ERROR_NoVersionLine = SevError | 0x011, + ERROR_InvalidGroupLine = SevError | 0x012, + ERROR_InvalidStoreLine = SevError | 0x013, + ERROR_InvalidCodeInKeyPartOfRule = SevError | 0x014, + ERROR_InvalidDeadkey = SevError | 0x015, + ERROR_InvalidValue = SevError | 0x016, + ERROR_ZeroLengthString = SevError | 0x017, + ERROR_TooManyIndexToKeyRefs = SevError | 0x018, + ERROR_UnterminatedString = SevError | 0x019, + ERROR_StringInVirtualKeySection = SevError | 0x01A, + ERROR_AnyInVirtualKeySection = SevError | 0x01B, + ERROR_InvalidAny = SevError | 0x01C, + ERROR_StoreDoesNotExist = SevError | 0x01D, + ERROR_BeepInVirtualKeySection = SevError | 0x01E, + ERROR_IndexInVirtualKeySection = SevError | 0x01F, + ERROR_InvalidIndex = SevError | 0x020, + ERROR_OutsInVirtualKeySection = SevError | 0x021, + ERROR_InvalidOuts = SevError | 0x022, + ERROR_ContextInVirtualKeySection = SevError | 0x024, + ERROR_InvalidUse = SevError | 0x025, + ERROR_GroupDoesNotExist = SevError | 0x026, + ERROR_VirtualKeyNotAllowedHere = SevError | 0x027, + ERROR_InvalidSwitch = SevError | 0x028, + ERROR_NoTokensFound = SevError | 0x029, + ERROR_InvalidLineContinuation = SevError | 0x02A, + ERROR_LineTooLong = SevError | 0x02B, + ERROR_InvalidCopyright = SevError | 0x02C, + ERROR_CodeInvalidInThisSection = SevError | 0x02D, + ERROR_InvalidMessage = SevError | 0x02E, + ERROR_InvalidLanguageName = SevError | 0x02F, + ERROR_InvalidBitmapLine = SevError | 0x030, + ERROR_CannotReadBitmapFile = SevError | 0x031, + ERROR_IndexDoesNotPointToAny = SevError | 0x032, + ERROR_ReservedCharacter = SevError | 0x033, + ERROR_InvalidCharacter = SevError | 0x034, + ERROR_InvalidCall = SevError | 0x035, + ERROR_CallInVirtualKeySection = SevError | 0x036, + ERROR_CodeInvalidInKeyStore = SevError | 0x037, + ERROR_CannotLoadIncludeFile = SevError | 0x038, + + ERROR_60FeatureOnly_EthnologueCode = SevError | 0x039, + ERROR_60FeatureOnly_MnemonicLayout = SevError | 0x03A, + ERROR_60FeatureOnly_OldCharPosMatching = SevError | 0x03B, + ERROR_60FeatureOnly_NamedCodes = SevError | 0x03C, + ERROR_60FeatureOnly_Contextn = SevError | 0x03D, + ERROR_501FeatureOnly_Call = SevError | 0x03E, + + ERROR_InvalidNamedCode = SevError | 0x03F, + ERROR_InvalidSystemStore = SevError | 0x040, + + ERROR_60FeatureOnly_VirtualCharKey = SevError | 0x044, + + ERROR_VersionAlreadyIncluded = SevError | 0x045, + + ERROR_70FeatureOnly = SevError | 0x046, + + ERROR_80FeatureOnly = SevError | 0x047, + ERROR_InvalidInVirtualKeySection = SevError | 0x048, + ERROR_InvalidIf = SevError | 0x049, + ERROR_InvalidReset = SevError | 0x04A, + ERROR_InvalidSet = SevError | 0x04B, + ERROR_InvalidSave = SevError | 0x04C, + + ERROR_InvalidEthnologueCode = SevError | 0x04D, + + FATAL_CannotCreateTempfile = SevFatal | 0x04E, + + ERROR_90FeatureOnly_IfSystemStores = SevError | 0x04F, + ERROR_IfSystemStore_NotFound = SevError | 0x050, + ERROR_90FeatureOnly_SetSystemStores = SevError | 0x051, + ERROR_SetSystemStore_NotFound = SevError | 0x052, + ERROR_90FeatureOnlyVirtualKeyDictionary = SevError | 0x053, + + ERROR_NotSupportedInKeymanWebContext = SevError | 0x054, + ERROR_NotSupportedInKeymanWebOutput = SevError | 0x055, + ERROR_NotSupportedInKeymanWebStore = SevError | 0x056, + ERROR_VirtualCharacterKeysNotSupportedInKeymanWeb = SevError | 0x057, + ERROR_VirtualKeysNotValidForMnemonicLayouts = SevError | 0x058, + ERROR_InvalidTouchLayoutFile = SevError | 0x059, + ERROR_TouchLayoutInvalidIdentifier = SevError | 0x05A, + ERROR_InvalidKeyCode = SevError | 0x05B, + ERROR_90FeatureOnlyLayoutFile = SevError | 0x05C, + ERROR_90FeatureOnlyKeyboardVersion = SevError | 0x05D, + ERROR_KeyboardVersionFormatInvalid = SevError | 0x05E, + ERROR_ContextExHasInvalidOffset = SevError | 0x05F, + ERROR_90FeatureOnlyEmbedCSS = SevError | 0x060, + ERROR_90FeatureOnlyTargets = SevError | 0x061, + ERROR_ContextAndIndexInvalidInMatchNomatch = SevError | 0x062, + ERROR_140FeatureOnlyContextAndNotAnyWeb = SevError | 0x063, + + ERROR_ExpansionMustFollowCharacterOrVKey = SevError | 0x064, + ERROR_VKeyExpansionMustBeFollowedByVKey = SevError | 0x065, + ERROR_CharacterExpansionMustBeFollowedByCharacter = SevError | 0x066, + ERROR_VKeyExpansionMustUseConsistentShift = SevError | 0x067, + ERROR_ExpansionMustBePositive = SevError | 0x068, + + ERROR_CasedKeysMustContainOnlyVirtualKeys = SevError | 0x069, + ERROR_CasedKeysMustNotIncludeShiftStates = SevError | 0x06A, + ERROR_CasedKeysNotSupportedWithMnemonicLayout = SevError | 0x06B, + + ERROR_CannotUseReadWriteGroupFromReadonlyGroup = SevError | 0x06C, + ERROR_StatementNotPermittedInReadonlyGroup = SevError | 0x06D, + ERROR_OutputInReadonlyGroup = SevError | 0x06E, + ERROR_NewContextGroupMustBeReadonly = SevError | 0x06F, + ERROR_PostKeystrokeGroupMustBeReadonly = SevError | 0x070, + + ERROR_DuplicateGroup = SevError | 0x071, + ERROR_DuplicateStore = SevError | 0x072, + ERROR_RepeatedBegin = SevError | 0x073, + ERROR_VirtualKeyInContext = SevError | 0x074, + + ERROR_OutsTooLong = SevError | 0x075, + ERROR_ExtendedStringTooLong = SevError | 0x076, + ERROR_VirtualKeyExpansionTooLong = SevError | 0x077, + ERROR_CharacterRangeTooLong = SevError | 0x078, + ERROR_NonBMPCharactersNotSupportedInKeySection = SevError | 0x079, + + ERROR_InvalidTarget = SevError | 0x07A, + ERROR_NoTargetsSpecified = SevError | 0x07B, + + WARN_TooManyWarnings = SevWarn | 0x080, + WARN_OldVersion = SevWarn | 0x081, + WARN_BitmapNotUsed = SevWarn | 0x082, + WARN_CustomLanguagesNotSupported = SevWarn | 0x083, + WARN_KeyBadLength = SevWarn | 0x084, + WARN_IndexStoreShort = SevWarn | 0x085, + WARN_UnicodeInANSIGroup = SevWarn | 0x086, + WARN_ANSIInUnicodeGroup = SevWarn | 0x087, + WARN_UnicodeSurrogateUsed = SevWarn | 0x088, + WARN_ReservedCharacter = SevWarn | 0x089, + + INFO_MinimumCoreEngineVersion = SevInfo | 0x08A, // renamed from INFO_Info in 18.0-alpha + + WARN_VirtualKeyWithMnemonicLayout = SevWarn | 0x08B, + WARN_VirtualCharKeyWithPositionalLayout = SevWarn | 0x08C, + WARN_StoreAlreadyUsedAsOptionOrCall = SevWarn | 0x08D, + WARN_StoreAlreadyUsedAsStoreOrCall = SevWarn | 0x08E, + WARN_StoreAlreadyUsedAsStoreOrOption = SevWarn | 0x08F, + + WARN_PunctuationInEthnologueCode = SevWarn | 0x090, + + WARN_TouchLayoutMissingLayer = SevWarn | 0x091, + WARN_TouchLayoutCustomKeyNotDefined = SevWarn | 0x092, + WARN_TouchLayoutMissingRequiredKeys = SevWarn | 0x093, + WARN_HelpFileMissing = SevWarn | 0x094, + WARN_EmbedJsFileMissing = SevWarn | 0x095, + // WARN_TouchLayoutFileMissing = SevWarn | 0x096, + // WARN_VisualKeyboardFileMissing = SevWarn | 0x097, + WARN_ExtendedShiftFlagsNotSupportedInKeymanWeb = SevWarn | 0x098, // I4118 + WARN_TouchLayoutUnidentifiedKey = SevWarn | 0x099, + HINT_UnreachableKeyCode = SevHint | 0x09A, + + // WARN_CouldNotCopyJsonFile = SevWarn | 0x09B, + WARN_PlatformNotInTargets = SevWarn | 0x09C, + + WARN_HeaderStatementIsDeprecated = SevWarn | 0x09D, + WARN_UseNotLastStatementInRule = SevWarn | 0x09E, + + WARN_TouchLayoutFontShouldBeSameForAllPlatforms = SevWarn | 0x09F, + // WARN_InvalidJSONMetadataFile = SevWarn | 0x0A0, + // WARN_JSONMetadataOSKFontShouldMatchTouchFont = SevWarn | 0x0A1, + WARN_KVKFileIsInSourceFormat = SevWarn | 0x0A2, + + WARN_DontMixChiralAndNonChiralModifiers = SevWarn | 0x0A3, + WARN_MixingLeftAndRightModifiers = SevWarn | 0x0A4, + + WARN_LanguageHeadersDeprecatedInKeyman10 = SevWarn | 0x0A5, + + HINT_NonUnicodeFile = SevHint | 0x0A6, + + // WARN_TooManyErrorsOrWarnings = SevWarn | 0x0A7, + + WARN_HotkeyHasInvalidModifier = SevWarn | 0x0A8, + + WARN_TouchLayoutSpecialLabelOnNormalKey = SevWarn | 0x0A9, + + WARN_OptionStoreNameInvalid = SevWarn | 0x0AA, + + WARN_NulNotFirstStatementInContext = SevWarn | 0x0AB, + WARN_IfShouldBeAtStartOfContext = SevWarn | 0x0AC, + + WARN_KeyShouldIncludeNCaps = SevWarn | 0x0AD, + + HINT_UnreachableRule = SevHint | 0x0AE, + + WARN_VirtualKeyInOutput = SevWarn | 0x0AF, + + HINT_IndexStoreLong = SevHint | 0x0B0, + + FATAL_BufferOverflow = SevFatal | 0x0C0 +// FATAL_Break = SevFatal | 0x0C1, unused + }; +} #endif // _kmn_compiler_errors_h diff --git a/developer/src/common/web/test-helpers/build.sh b/developer/src/common/web/test-helpers/build.sh index 175bce66166..c1664cfddd8 100755 --- a/developer/src/common/web/test-helpers/build.sh +++ b/developer/src/common/web/test-helpers/build.sh @@ -8,7 +8,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" builder_describe "Keyman Developer unit test helpers" \ - "@/common/web/types" \ + "@/developer/src/common/web/utils" \ clean configure build test builder_describe_outputs \ diff --git a/developer/src/common/web/test-helpers/index.ts b/developer/src/common/web/test-helpers/index.ts index be9157f69ce..93dfb89b2cc 100644 --- a/developer/src/common/web/test-helpers/index.ts +++ b/developer/src/common/web/test-helpers/index.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CompilerEvent, CompilerCallbacks, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerError } from '@keymanapp/common-types'; +import { CompilerEvent, CompilerCallbacks, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerError } from '@keymanapp/developer-utils'; export { verifyCompilerMessagesObject } from './verifyCompilerMessagesObject.js'; /** diff --git a/developer/src/common/web/test-helpers/package.json b/developer/src/common/web/test-helpers/package.json index 40b9943314c..d5418948305 100644 --- a/developer/src/common/web/test-helpers/package.json +++ b/developer/src/common/web/test-helpers/package.json @@ -10,14 +10,13 @@ ], "devDependencies": { "@types/node": "^20.4.1", - "ts-node": "^9.1.1", - "typescript": "^4.9.5" + "typescript": "^5.4.5" }, "scripts": { "build": "tsc -b" }, "license": "MIT", "dependencies": { - "@keymanapp/common-types": "*" + "@keymanapp/developer-utils": "*" } } diff --git a/developer/src/common/web/test-helpers/tsconfig.json b/developer/src/common/web/test-helpers/tsconfig.json index d7089c703f2..dd0e738096d 100644 --- a/developer/src/common/web/test-helpers/tsconfig.json +++ b/developer/src/common/web/test-helpers/tsconfig.json @@ -5,9 +5,6 @@ "rootDir": ".", "outDir": "./build/", "baseUrl": ".", - "paths": { - "@keymanapp/common-types": ["../../../../../common/web/types/src/main"], - }, }, "include": [ "./*.ts", diff --git a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts index e9ed7b45a7c..7f0d9aee766 100644 --- a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts +++ b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts @@ -1,4 +1,4 @@ -import { CompilerError, CompilerErrorMask } from '@keymanapp/common-types'; +import { CompilerError, CompilerErrorMask } from '@keymanapp/developer-utils'; import {assert, expect} from 'chai'; // diff --git a/developer/src/common/web/utils/build.sh b/developer/src/common/web/utils/build.sh index e58d585c5d3..2209463e184 100755 --- a/developer/src/common/web/utils/build.sh +++ b/developer/src/common/web/utils/build.sh @@ -21,20 +21,42 @@ builder_describe "Build Keyman Developer web utility module" \ builder_describe_outputs \ configure /node_modules \ - build /developer/src/common/web/utils/build/index.js + build /developer/src/common/web/utils/build/src/index.js builder_parse "$@" #------------------------------------------------------------------------------------------------------------------- +function copy_cldr_imports() { + # Store CLDR imports + # load all versions that have a cldr_info.json + for CLDR_INFO_PATH in "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/"*/cldr_info.json + do + # TODO-LDML: developer/src/inst/download.in.mak needs these also... + CLDR_PATH=$(dirname "$CLDR_INFO_PATH") + CLDR_VER=$(basename "$CLDR_PATH") + mkdir -p "$THIS_SCRIPT_PATH/build/src/types/import/$CLDR_VER" + # TODO-LDML: When these are copied, the DOCTYPE will break due to the wrong path. We don't use the DTD so it should be OK. + cp "$CLDR_INFO_PATH" "$CLDR_PATH/import/"*.xml "$THIS_SCRIPT_PATH/build/src/types/import/$CLDR_VER/" + done +} + +function do_build() { + copy_cldr_imports + tsc --build +} + builder_run_action clean rm -rf ./build/ builder_run_action configure verify_npm_setup -builder_run_action build tsc --build +builder_run_action build do_build if builder_start_action test; then eslint . tsc --build test - c8 --reporter=lcov --reporter=text --exclude-after-remap mocha + readonly C8_THRESHOLD=40 + c8 --reporter=lcov --reporter=text --exclude-after-remap --lines $C8_THRESHOLD --statements $C8_THRESHOLD --branches $C8_THRESHOLD --functions $C8_THRESHOLD mocha + builder_echo warning "Coverage thresholds are currently $C8_THRESHOLD%, which is lower than ideal." + builder_echo warning "Please increase threshold in build.sh as test coverage improves." builder_finish_action success test fi diff --git a/developer/src/common/web/utils/index.ts b/developer/src/common/web/utils/index.ts deleted file mode 100644 index 84651750639..00000000000 --- a/developer/src/common/web/utils/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { validateMITLicense } from './src/validate-mit-license.js'; -export { KeymanSentry, SentryNodeOptions } from './src/KeymanSentry.js'; -export { getOption, loadOptions, clearOptions } from './src/options.js'; -export { escapeMarkdownChar } from './src/markdown.js'; -export { KeymanUrls } from './src/keyman-urls.js'; \ No newline at end of file diff --git a/developer/src/common/web/utils/package.json b/developer/src/common/web/utils/package.json index 62119daea8c..1ab9daddbf8 100644 --- a/developer/src/common/web/utils/package.json +++ b/developer/src/common/web/utils/package.json @@ -3,20 +3,29 @@ "description": "Keyman Developer utilities", "type": "module", "exports": { - ".": "./build/index.js" + ".": "./build/src/index.js" }, "files": [ "/build/" ], "dependencies": { - "@sentry/node": "^7.57.0" + "@sentry/node": "^7.57.0", + "@keymanapp/common-types": "*", + "eventemitter3": "^5.0.0", + "restructure": "^3.0.1", + "semver": "^7.5.4", + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" }, "devDependencies": { + "@types/git-diff": "^2.0.3", "@types/node": "^20.4.1", - "ts-node": "^9.1.1", - "typescript": "^4.9.5", + "@types/semver": "^7.3.12", + "@types/xml2js": "^0.4.5", "c8": "^7.12.0", - "mocha": "^8.4.0" + "git-diff": "^2.0.6", + "mocha": "^8.4.0", + "typescript": "^5.4.5" }, "scripts": { "build": "tsc -b" diff --git a/common/web/types/src/util/common-events.ts b/developer/src/common/web/utils/src/common-messages.ts similarity index 91% rename from common/web/types/src/util/common-events.ts rename to developer/src/common/web/utils/src/common-messages.ts index eee8a6c84ca..5b3fd7d5070 100644 --- a/common/web/types/src/util/common-events.ts +++ b/developer/src/common/web/utils/src/common-messages.ts @@ -1,3 +1,6 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m } from './compiler-interfaces.js'; import { constants } from '@keymanapp/ldml-keyboard-constants'; @@ -43,4 +46,8 @@ export class CommonTypesMessages { static Error_TestDataUnexpectedArray = (o: {subtag: string}) => m(this.ERROR_TestDataUnexpectedArray, `Problem reading test data: expected single ${def(o.subtag)} element, found multiple`); + + static ERROR_InvalidXml = SevError | 0x0008; + static Error_InvalidXml = (o:{e: any}) => + m(this.ERROR_InvalidXml, `The XML file could not be read: ${(o.e ?? '').toString()}`); }; diff --git a/common/web/types/src/util/compiler-interfaces.ts b/developer/src/common/web/utils/src/compiler-interfaces.ts similarity index 99% rename from common/web/types/src/util/compiler-interfaces.ts rename to developer/src/common/web/utils/src/compiler-interfaces.ts index df8ab48f33e..e4ed62d7500 100644 --- a/common/web/types/src/util/compiler-interfaces.ts +++ b/developer/src/common/web/utils/src/compiler-interfaces.ts @@ -181,7 +181,7 @@ export class CompilerError { */ static severityNameToValue(name: string): CompilerErrorSeverity { name = name.toLowerCase(); - for(let level of CompilerErrorSeverityValues) { + for(const level of CompilerErrorSeverityValues) { if(errorSeverityName[level].startsWith(name)) { return level; } diff --git a/common/web/types/src/deps/xml2js/LICENSE b/developer/src/common/web/utils/src/deps/xml2js/LICENSE similarity index 100% rename from common/web/types/src/deps/xml2js/LICENSE rename to developer/src/common/web/utils/src/deps/xml2js/LICENSE diff --git a/common/web/types/src/deps/xml2js/README.md b/developer/src/common/web/utils/src/deps/xml2js/README.md similarity index 100% rename from common/web/types/src/deps/xml2js/README.md rename to developer/src/common/web/utils/src/deps/xml2js/README.md diff --git a/common/web/types/src/deps/xml2js/bom.js b/developer/src/common/web/utils/src/deps/xml2js/bom.js similarity index 100% rename from common/web/types/src/deps/xml2js/bom.js rename to developer/src/common/web/utils/src/deps/xml2js/bom.js diff --git a/common/web/types/src/deps/xml2js/builder.js b/developer/src/common/web/utils/src/deps/xml2js/builder.js similarity index 100% rename from common/web/types/src/deps/xml2js/builder.js rename to developer/src/common/web/utils/src/deps/xml2js/builder.js diff --git a/common/web/types/src/deps/xml2js/defaults.js b/developer/src/common/web/utils/src/deps/xml2js/defaults.js similarity index 100% rename from common/web/types/src/deps/xml2js/defaults.js rename to developer/src/common/web/utils/src/deps/xml2js/defaults.js diff --git a/common/web/types/src/deps/xml2js/parser.js b/developer/src/common/web/utils/src/deps/xml2js/parser.js similarity index 98% rename from common/web/types/src/deps/xml2js/parser.js rename to developer/src/common/web/utils/src/deps/xml2js/parser.js index aa0101ea7d2..1c7f148d906 100644 --- a/common/web/types/src/deps/xml2js/parser.js +++ b/developer/src/common/web/utils/src/deps/xml2js/parser.js @@ -4,7 +4,7 @@ hasProp = {}.hasOwnProperty; import sax from 'sax'; -import { EventEmitter } from 'events'; +import { EventEmitter } from 'eventemitter3'; import * as bom from './bom.js'; import * as processors from './processors.js'; import { setImmediate } from 'timers'; @@ -323,7 +323,11 @@ export class Parser extends EventEmitter { } catch (error1) { err = error1; if (!(this.saxParser.errThrown || this.saxParser.ended)) { - this.emit('error', err); + if(this.listenerCount('error') > 0) { + this.emit('error', err); + } else { + throw err; + } return this.saxParser.errThrown = true; } else if (this.saxParser.ended) { throw err; diff --git a/common/web/types/src/deps/xml2js/processors.js b/developer/src/common/web/utils/src/deps/xml2js/processors.js similarity index 100% rename from common/web/types/src/deps/xml2js/processors.js rename to developer/src/common/web/utils/src/deps/xml2js/processors.js diff --git a/common/web/types/src/deps/xml2js/xml2js.js b/developer/src/common/web/utils/src/deps/xml2js/xml2js.js similarity index 100% rename from common/web/types/src/deps/xml2js/xml2js.js rename to developer/src/common/web/utils/src/deps/xml2js/xml2js.js diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts new file mode 100644 index 00000000000..7ee5c507b80 --- /dev/null +++ b/developer/src/common/web/utils/src/index.ts @@ -0,0 +1,47 @@ +export { validateMITLicense } from './utils/validate-mit-license.js'; +export { KeymanSentry, SentryNodeOptions } from './utils/KeymanSentry.js'; +export { getOption, loadOptions, clearOptions } from './utils/options.js'; +export { escapeMarkdownChar } from './utils/markdown.js'; +export { KeymanUrls } from './utils/keyman-urls.js'; + +export * as KPJ from './types/kpj/kpj-file.js'; +export { KPJFileReader } from './types/kpj/kpj-file-reader.js'; +export { KeymanDeveloperProject, KeymanDeveloperProjectFile, KeymanDeveloperProjectType, } from './types/kpj/keyman-developer-project.js'; +export { isValidEmail } from './is-valid-email.js'; + +export * as KpsFile from './types/kps/kps-file.js'; + +export { default as KvksFileReader } from './types/kvks/kvks-file-reader.js'; +export { default as KvksFileWriter } from './types/kvks/kvks-file-writer.js'; +export * as KvksFile from './types/kvks/kvks-file.js'; + +export { TouchLayoutFileReader } from './types/keyman-touch-layout/keyman-touch-layout-file-reader.js'; +export { TouchLayoutFileWriter, TouchLayoutFileWriterOptions } from './types/keyman-touch-layout/keyman-touch-layout-file-writer.js'; + +export { default as KMXBuilder } from './types/kmx/kmx-builder.js'; +export { default as KMXPlusBuilder} from './types/kmx/kmx-plus-builder/kmx-plus-builder.js'; + +export * as LDMLKeyboard from './types/ldml-keyboard/ldml-keyboard-xml.js'; +export { LDMLKeyboardTestDataXMLSourceFile } from './types/ldml-keyboard/ldml-keyboard-testdata-xml.js'; +export { LDMLKeyboardXMLSourceFileReader, LDMLKeyboardXMLSourceFileReaderOptions } from './types/ldml-keyboard/ldml-keyboard-xml-reader.js'; + +export { defaultCompilerOptions, CompilerBaseOptions, CompilerCallbacks, CompilerOptions, CompilerEvent, CompilerErrorNamespace, + CompilerErrorSeverity, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerCallbackOptions, + CompilerError, CompilerMessageSpec, CompilerMessageSpecWithException, compilerErrorSeverity, CompilerErrorMask, CompilerFileCallbacks, compilerErrorSeverityName, + compilerErrorFormatCode, CompilerMessageDef, + compilerLogLevelToSeverity, CompilerLogLevel, compilerEventFormat, ALL_COMPILER_LOG_LEVELS, + ALL_COMPILER_LOG_FORMATS, CompilerLogFormat, + CompilerMessageOverride, + CompilerMessageOverrideMap, + + KeymanCompilerArtifact, + KeymanCompilerArtifactOptional, + KeymanCompilerArtifacts, + KeymanCompilerResult, + KeymanCompiler + + } from './compiler-interfaces.js'; + +export { CommonTypesMessages } from './common-messages.js'; + +export * as xml2js from './deps/xml2js/xml2js.js'; diff --git a/developer/src/common/web/utils/src/is-valid-email.ts b/developer/src/common/web/utils/src/is-valid-email.ts new file mode 100644 index 00000000000..364010498a7 --- /dev/null +++ b/developer/src/common/web/utils/src/is-valid-email.ts @@ -0,0 +1,18 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Verify email address format, following WHATWG guidelines + */ + +// There is no "good" definition of a valid email address. Email addresses are +// horrific. They can contain comments, whitespace, and all manner of ugly +// things. Because we use AJV to verify JSON files, we use their specification +// on what is a valid email address. Some useful references: +// * https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address +// * http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address#answer-8829363 +// * https://github.com/ajv-validator/ajv-formats/blob/4ca86d21bd07571a30178cbb3714133db6eada9a/src/formats.ts#L122 +// * https://github.com/ajv-validator/ajv-formats/blob/4ca86d21bd07571a30178cbb3714133db6eada9a/src/formats.ts#L65 + +export function isValidEmail(email: string) { + return /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i.test(email); +} \ No newline at end of file diff --git a/developer/src/common/web/utils/src/restructure.d.ts b/developer/src/common/web/utils/src/restructure.d.ts new file mode 100644 index 00000000000..9750d24473a --- /dev/null +++ b/developer/src/common/web/utils/src/restructure.d.ts @@ -0,0 +1,2 @@ +// TODO: consider defining more types? +declare module 'restructure'; \ No newline at end of file diff --git a/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file-reader.ts b/developer/src/common/web/utils/src/types/keyman-touch-layout/keyman-touch-layout-file-reader.ts similarity index 82% rename from common/web/types/src/keyman-touch-layout/keyman-touch-layout-file-reader.ts rename to developer/src/common/web/utils/src/types/keyman-touch-layout/keyman-touch-layout-file-reader.ts index e0f4ea7917e..81be30e3874 100644 --- a/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file-reader.ts +++ b/developer/src/common/web/utils/src/types/keyman-touch-layout/keyman-touch-layout-file-reader.ts @@ -1,5 +1,6 @@ -import { TouchLayoutFile } from "./keyman-touch-layout-file.js"; -import SchemaValidators from '../schema-validators.js'; +import { TouchLayout } from "@keymanapp/common-types"; +import TouchLayoutFile = TouchLayout.TouchLayoutFile; +import { SchemaValidators } from '@keymanapp/common-types'; export class TouchLayoutFileReader { public read(source: Uint8Array): TouchLayoutFile { @@ -22,7 +23,7 @@ export class TouchLayoutFileReader { // `row.id` should be number, but may have been stringified; we use // presence of `key` property to recognise this as a `TouchLayoutRow`. if(this.key && key == 'id' && typeof value == 'string') { - let newValue = parseInt(value, 10); + const newValue = parseInt(value, 10); /* c8 ignore next 3 */ if(isNaN(newValue)) { throw new TypeError(`Invalid row.id: "${value}"`); @@ -39,7 +40,7 @@ export class TouchLayoutFileReader { return undefined; } - let newValue = parseInt(value, 10); + const newValue = parseInt(value, 10); /* c8 ignore next 3 */ if(isNaN(newValue)) { throw new TypeError(`Invalid [sub]key.${key}: "${value}"`); @@ -67,10 +68,10 @@ export class TouchLayoutFileReader { } public validate(source: TouchLayoutFile): void { - if(!SchemaValidators.touchLayoutClean(source)) + if(!SchemaValidators.default.touchLayoutClean(source)) /* c8 ignore next 3 */ { - throw new Error(JSON.stringify((SchemaValidators.touchLayoutClean).errors)); + throw new Error(JSON.stringify((SchemaValidators.default.touchLayoutClean).errors)); } } diff --git a/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file-writer.ts b/developer/src/common/web/utils/src/types/keyman-touch-layout/keyman-touch-layout-file-writer.ts similarity index 86% rename from common/web/types/src/keyman-touch-layout/keyman-touch-layout-file-writer.ts rename to developer/src/common/web/utils/src/types/keyman-touch-layout/keyman-touch-layout-file-writer.ts index 5de702eb1c6..6261f80d4bd 100644 --- a/common/web/types/src/keyman-touch-layout/keyman-touch-layout-file-writer.ts +++ b/developer/src/common/web/utils/src/types/keyman-touch-layout/keyman-touch-layout-file-writer.ts @@ -1,4 +1,8 @@ -import { TouchLayoutFile, TouchLayoutPlatform, TouchLayoutKey, TouchLayoutSubKey } from "./keyman-touch-layout-file.js"; +import { TouchLayout } from "@keymanapp/common-types"; +import TouchLayoutFile = TouchLayout.TouchLayoutFile; +import TouchLayoutPlatform = TouchLayout.TouchLayoutPlatform;; +import TouchLayoutKey = TouchLayout.TouchLayoutKey; +import TouchLayoutSubKey = TouchLayout.TouchLayoutSubKey; export interface TouchLayoutFileWriterOptions { formatted?: boolean; @@ -87,24 +91,24 @@ export class TouchLayoutFileWriter { // displayUnderlying is always written out by kmcomp, so we do the same for kmc: platform.displayUnderlying = !!platform.displayUnderlying; - for(let layer of platform.layer) { - for(let row of layer.row) { + for(const layer of platform.layer) { + for(const row of layer.row) { // this matches the old spec for touch layout files (row.id as any) = row.id.toString(); - for(let key of row.key) { + for(const key of row.key) { fixupKey(key); if(key.sk) { - for(let sk of key.sk) { + for(const sk of key.sk) { fixupKey(sk); } } if(key.multitap) { - for(let sk of key.multitap) { + for(const sk of key.multitap) { fixupKey(sk); } } if(key.flick) { - for(let id of Object.keys(key.flick)) { + for(const id of Object.keys(key.flick)) { fixupKey((key.flick as any)[id] as TouchLayoutSubKey); } } diff --git a/common/web/types/src/kmx/kmx-builder.ts b/developer/src/common/web/utils/src/types/kmx/kmx-builder.ts similarity index 85% rename from common/web/types/src/kmx/kmx-builder.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-builder.ts index 4a319bfe450..a8ce169d303 100644 --- a/common/web/types/src/kmx/kmx-builder.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-builder.ts @@ -1,8 +1,18 @@ +import { KMXPlus, KMX } from '@keymanapp/common-types'; import * as r from 'restructure'; -import { KMXFile, GROUP, KEY, STORE, BUILDER_COMP_KEYBOARD, BUILDER_COMP_KEYBOARD_KMXPLUSINFO, BUILDER_COMP_STORE, BUILDER_COMP_GROUP, BUILDER_COMP_KEY } from './kmx.js'; -import { KMXPlusFile } from './kmx-plus.js'; import KMXPlusBuilder from './kmx-plus-builder/kmx-plus-builder.js'; +import KMXPlusFile = KMXPlus.KMXPlusFile; +import KMXFile = KMX.KMXFile; +import GROUP = KMX.GROUP; +import KEY = KMX.KEY; +import STORE = KMX.STORE; +import BUILDER_COMP_KEYBOARD = KMX.BUILDER_COMP_KEYBOARD; +import BUILDER_COMP_KEYBOARD_KMXPLUSINFO = KMX.BUILDER_COMP_KEYBOARD_KMXPLUSINFO; +import BUILDER_COMP_STORE = KMX.BUILDER_COMP_STORE; +import BUILDER_COMP_GROUP = KMX.BUILDER_COMP_GROUP; +import BUILDER_COMP_KEY = KMX.BUILDER_COMP_KEY; + export default class KMXBuilder { file: KMXFile; base_keyboard: number = 0; @@ -69,8 +79,8 @@ export default class KMXBuilder { this.comp_header.dpStoreArray = this.comp_header.cxStoreArray ? size : 0; let storeBase = size; size += this.file.keyboard.stores.length * KMXFile.COMP_STORE_SIZE; - for(let store of this.file.keyboard.stores) { - let comp_store: BUILDER_COMP_STORE = { + for(const store of this.file.keyboard.stores) { + const comp_store: BUILDER_COMP_STORE = { dwSystemID: store.dwSystemID, dpName: 0, dpString: 0 @@ -90,8 +100,8 @@ export default class KMXBuilder { this.comp_header.dpGroupArray = this.comp_header.cxGroupArray ? size : 0; let groupBase = size; size += this.file.keyboard.groups.length * KMXFile.COMP_GROUP_SIZE; - for(let group of this.file.keyboard.groups) { - let comp_group: BUILDER_COMP_GROUP = { + for(const group of this.file.keyboard.groups) { + const comp_group: BUILDER_COMP_GROUP = { dpName: 0, dpKeyArray: 0, dpMatch: 0, @@ -100,7 +110,7 @@ export default class KMXBuilder { fUsingKeys: group.fUsingKeys ? 1 : 0 }; - let comp_keys: {base: number, key: KEY, obj: BUILDER_COMP_KEY}[] = []; + const comp_keys: {base: number, key: KEY, obj: BUILDER_COMP_KEY}[] = []; this.comp_groups.push({base: groupBase, group: group, obj: comp_group, keys: comp_keys}); @@ -116,8 +126,8 @@ export default class KMXBuilder { let keyBase = size; size += group.keys.length * KMXFile.COMP_KEY_SIZE; - for(let key of group.keys) { - let comp_key: BUILDER_COMP_KEY = { + for(const key of group.keys) { + const comp_key: BUILDER_COMP_KEY = { Key: key.Key, _padding: 0, Line: key.Line, @@ -164,11 +174,11 @@ export default class KMXBuilder { if(requireString && !str.length) { // Just write zero terminator, as r.String for a zero-length string // seems to fail. - let sbuf = r.uint16le; + const sbuf = r.uint16le; file.set(sbuf.toBuffer(0), pos); } else if(pos && str.length) { - let sbuf = new r.String(null, 'utf16le'); // null-terminated string + const sbuf = new r.String(null, 'utf16le'); // null-terminated string file.set(sbuf.toBuffer(str), pos); } } @@ -176,7 +186,7 @@ export default class KMXBuilder { compile(): Uint8Array { const fileSize = this.build(); - let file: Uint8Array = new Uint8Array(fileSize); + const file: Uint8Array = new Uint8Array(fileSize); // Write headers @@ -190,7 +200,7 @@ export default class KMXBuilder { // Write store array and data - for(let store of this.comp_stores) { + for(const store of this.comp_stores) { file.set(this.file.COMP_STORE.toBuffer(store.obj), store.base); if(this.writeDebug) { this.setString(file, store.obj.dpName, store.store.dpName); @@ -200,7 +210,7 @@ export default class KMXBuilder { // Write group array and data - for(let group of this.comp_groups) { + for(const group of this.comp_groups) { file.set(this.file.COMP_GROUP.toBuffer(group.obj), group.base); if(this.writeDebug) { this.setString(file, group.obj.dpName, group.group.dpName); @@ -208,7 +218,7 @@ export default class KMXBuilder { this.setString(file, group.obj.dpMatch, group.group.dpMatch); this.setString(file, group.obj.dpNoMatch, group.group.dpNoMatch); - for(let key of group.keys) { + for(const key of group.keys) { file.set(this.file.COMP_KEY.toBuffer(key.obj), key.base); // for back-compat reasons, these are never NULL strings this.setString(file, key.obj.dpContext, key.key.dpContext, true); diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-disp.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-disp.ts similarity index 88% rename from common/web/types/src/kmx/kmx-plus-builder/build-disp.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-disp.ts index 712dafac274..6984aa2cfa1 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-disp.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-disp.ts @@ -1,8 +1,10 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; -import { KMXPlusData } from "../kmx-plus.js"; +import { KMXPlus } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION } from './builder-section.js'; +import KMXPlusData = KMXPlus.KMXPlusData; + /* ------------------------------------------------------------------ * disp section ------------------------------------------------------------------ */ @@ -27,7 +29,7 @@ export function build_disp(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS): BUILD return null; } - let disp: BUILDER_DISP = { + const disp: BUILDER_DISP = { ident: constants.hex_section_id(constants.section.disp), size: constants.length_disp + constants.length_disp_item * kmxplus.disp.disps.length, _offset: 0, @@ -36,7 +38,7 @@ export function build_disp(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS): BUILD items: [] }; - for(let item of kmxplus.disp.disps) { + for(const item of kmxplus.disp.disps) { disp.items.push({ to: build_strs_index(sect_strs, item.to), id: build_strs_index(sect_strs, item.id), diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-elem.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-elem.ts similarity index 94% rename from common/web/types/src/kmx/kmx-plus-builder/build-elem.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-elem.ts index 8e325f9591a..8dafccced21 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-elem.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-elem.ts @@ -1,10 +1,11 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { ElementString } from "../element-string.js"; -import { Elem } from "../kmx-plus.js"; +import { KMXPlus, ElementString } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION, BUILDER_U32CHAR } from "./builder-section.js"; import { build_uset_index, BUILDER_USET, BUILDER_USET_REF } from "./build-uset.js"; +import Elem = KMXPlus.Elem; + /* ------------------------------------------------------------------ * elem section ------------------------------------------------------------------ */ @@ -46,7 +47,7 @@ function binaryElemCompare(a: BUILDER_ELEM_STRING, b: BUILDER_ELEM_STRING): numb } export function build_elem(source_elem: Elem, sect_strs: BUILDER_STRS, sect_uset: BUILDER_USET): BUILDER_ELEM { - let result: BUILDER_ELEM = { + const result: BUILDER_ELEM = { ident: constants.hex_section_id(constants.section.elem), size: 0, // finalized below _offset: 0, @@ -55,7 +56,7 @@ export function build_elem(source_elem: Elem, sect_strs: BUILDER_STRS, sect_uset }; result.strings = source_elem.strings.map(item => { - let res: BUILDER_ELEM_STRING = { + const res: BUILDER_ELEM_STRING = { offset: 0, // finalized below length: item.length, items: [], @@ -87,7 +88,7 @@ export function build_elem(source_elem: Elem, sect_strs: BUILDER_STRS, sect_uset /* Calculate offsets and total size */ let offset = constants.length_elem + constants.length_elem_item * result.count; - for(let item of result.strings) { + for(const item of result.strings) { if (item.length === 0) { // no length gets a zero offset item.offset = 0; diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-keys.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-keys.ts similarity index 93% rename from common/web/types/src/kmx/kmx-plus-builder/build-keys.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-keys.ts index ec7e505c57f..fd48ec49ec9 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-keys.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-keys.ts @@ -1,10 +1,14 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { KeysFlick, KMXPlusData, StrsItem } from "../kmx-plus.js"; +import { KMXPlus } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { build_list_index, BUILDER_LIST, BUILDER_LIST_REF } from "./build-list.js"; import { BUILDER_SECTION, BUILDER_U32CHAR } from "./builder-section.js"; +import KeysFlick = KMXPlus.KeysFlick; +import KMXPlusData = KMXPlus.KMXPlusData; +import StrsItem = KMXPlus.StrsItem; + /* ------------------------------------------------------------------ * keys section ------------------------------------------------------------------ */ @@ -71,7 +75,7 @@ export function build_keys(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l return null; } - let keys: BUILDER_KEYS = { + const keys: BUILDER_KEYS = { ident: constants.hex_section_id(constants.section.keys), size: 0, keyCount: kmxplus.keys.keys.length, @@ -89,7 +93,7 @@ export function build_keys(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l // Note that per the Keys class and spec, there is always a flicks=0 meaning 'no flicks' keys.flicks = kmxplus.keys.flicks.map((flicks) => { - let result : BUILDER_KEYS_FLICKS = { + const result : BUILDER_KEYS_FLICKS = { count: flicks.flicks.length, flick: keys.flick.length, // index of first flick id: build_strs_index(sect_strs, flicks.id), @@ -113,7 +117,7 @@ export function build_keys(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l // now, keys keys.keys = kmxplus.keys.keys.map((key) => { - let result : BUILDER_KEYS_KEY = { + const result : BUILDER_KEYS_KEY = { to: build_strs_index(sect_strs, key.to), flags: key.flags, id: build_strs_index(sect_strs, key.id), @@ -136,7 +140,7 @@ export function build_keys(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l // finally, kmap keys.kmap = kmxplus.keys.kmap.map(({vkey, mod, key}) => { - let result : BUILDER_KEYS_KMAP = { + const result : BUILDER_KEYS_KMAP = { vkey, mod, key: keys.keys.findIndex(k => k._id === key), @@ -160,7 +164,7 @@ export function build_keys(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l return rc; }); - let offset = constants.length_keys + + const offset = constants.length_keys + (constants.length_keys_key * keys.keyCount) + (constants.length_keys_flick_element * keys.flickCount) + (constants.length_keys_flick_list * keys.flicksCount) + diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-layr.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-layr.ts similarity index 94% rename from common/web/types/src/kmx/kmx-plus-builder/build-layr.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-layr.ts index c0038246e27..07249368f4e 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-layr.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-layr.ts @@ -1,10 +1,15 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { KMXPlusData, LayrEntry, LayrRow, StrsItem } from "../kmx-plus.js"; +import { KMXPlus } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_LIST } from "./build-list.js"; import { BUILDER_SECTION } from "./builder-section.js"; +import KMXPlusData = KMXPlus.KMXPlusData; +import LayrEntry = KMXPlus.LayrEntry; +import LayrRow = KMXPlus.LayrRow; +import StrsItem = KMXPlus.StrsItem; + /* ------------------------------------------------------------------ * layr section - ------------------------------------------------------------------ */ @@ -66,7 +71,7 @@ export function build_layr(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l return null; // if there aren't any layers at all (which should be an invalid keyboard) } - let layr: BUILDER_LAYR = { + const layr: BUILDER_LAYR = { ident: constants.hex_section_id(constants.section.layr), size: constants.length_layr, _offset: 0, @@ -145,7 +150,7 @@ export function build_layr(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l layr.rowCount = layr.rows.length; layr.keyCount = layr.keys.length; - let offset = constants.length_layr + + const offset = constants.length_layr + (constants.length_layr_list * layr.listCount) + (constants.length_layr_entry * layr.layerCount) + (constants.length_layr_row * layr.rowCount) + diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-list.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-list.ts similarity index 88% rename from common/web/types/src/kmx/kmx-plus-builder/build-list.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-list.ts index 26659f8eb6a..759f6498f4f 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-list.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-list.ts @@ -1,8 +1,11 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { List, ListItem } from "../kmx-plus.js"; +import { KMXPlus } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION } from "./builder-section.js"; +import List = KMXPlus.List; +import ListItem = KMXPlus.ListItem; + /* ------------------------------------------------------------------ * list section ------------------------------------------------------------------ */ @@ -40,7 +43,7 @@ export function build_list(source_list: List, sect_strs: BUILDER_STRS): BUILDER_ return null; } - let result: BUILDER_LIST = { + const result: BUILDER_LIST = { ident: constants.hex_section_id(constants.section.list), size: 0, _offset: 0, @@ -51,13 +54,13 @@ export function build_list(source_list: List, sect_strs: BUILDER_STRS): BUILDER_ }; result.lists = source_list.lists.map(array => { - let list : BUILDER_LIST_LIST = { + const list : BUILDER_LIST_LIST = { index: result.indices.length, // the next indexcount count: array.length, _value: array }; array.forEach((i) => { - let index : BUILDER_LIST_INDEX = { + const index : BUILDER_LIST_INDEX = { // Get the final string index str: build_strs_index(sect_strs, i.value), _value: i.value.value, // unwrap the actual string value @@ -71,7 +74,7 @@ export function build_list(source_list: List, sect_strs: BUILDER_STRS): BUILDER_ // Sort the lists. result.lists.sort((a,b) => a._value.compareTo(b._value)); - let offset = constants.length_list + + const offset = constants.length_list + (constants.length_list_item * result.listCount) + (constants.length_list_index * result.indexCount); result.size = offset; @@ -86,14 +89,14 @@ export function build_list(source_list: List, sect_strs: BUILDER_STRS): BUILDER_ * @returns */ export function build_list_index(sect_list: BUILDER_LIST, value: ListItem) : BUILDER_LIST_REF { - if (!value) { + if (!value) { return 0; // empty list } if(!(value instanceof ListItem)) { throw new Error('unexpected value '+ value); } - let result = sect_list.lists.findIndex(v => v._value === value); + const result = sect_list.lists.findIndex(v => v._value === value); if(result < 0) { throw new Error('unexpectedly missing ListItem ' + value); // TODO-LDML: it's an array of strs } diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-loca.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-loca.ts similarity index 84% rename from common/web/types/src/kmx/kmx-plus-builder/build-loca.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-loca.ts index 9a72402c8c0..4a81fcd9abd 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-loca.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-loca.ts @@ -4,10 +4,12 @@ ------------------------------------------------------------------ */ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { KMXPlusData } from "../kmx-plus.js"; +import { KMXPlus } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION } from "./builder-section.js"; +import KMXPlusData = KMXPlus.KMXPlusData; + /** * Builder for the 'loca' section */ @@ -17,7 +19,7 @@ export interface BUILDER_LOCA extends BUILDER_SECTION { }; export function build_loca(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS): BUILDER_LOCA { - let loca: BUILDER_LOCA = { + const loca: BUILDER_LOCA = { ident: constants.hex_section_id(constants.section.loca), size: constants.length_loca + constants.length_loca_item * kmxplus.loca.locales.length, _offset: 0, @@ -25,7 +27,7 @@ export function build_loca(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS): BUILD items: [] }; - for(let item of kmxplus.loca.locales) { + for(const item of kmxplus.loca.locales) { loca.items.push(build_strs_index(sect_strs, item)); } diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-meta.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-meta.ts similarity index 93% rename from common/web/types/src/kmx/kmx-plus-builder/build-meta.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-meta.ts index 23dd303a7d5..208051356eb 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-meta.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-meta.ts @@ -4,10 +4,12 @@ ------------------------------------------------------------------ */ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { KMXPlusData } from "../kmx-plus.js"; +import { KMXPlus } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION } from "./builder-section.js"; +import KMXPlusData = KMXPlus.KMXPlusData; + /** * Builder for the 'meta' section */ diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-sect.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-sect.ts similarity index 100% rename from common/web/types/src/kmx/kmx-plus-builder/build-sect.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-sect.ts diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-strs.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-strs.ts similarity index 89% rename from common/web/types/src/kmx/kmx-plus-builder/build-strs.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-strs.ts index 503de2d404a..89d733f22d3 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-strs.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-strs.ts @@ -1,7 +1,10 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { Strs, StrsItem } from "../kmx-plus.js"; +import { KMXPlus } from "@keymanapp/common-types"; import { BUILDER_SECTION } from "./builder-section.js"; +import Strs = KMXPlus.Strs; +import StrsItem = KMXPlus.StrsItem; + /** reference from build_strs_index */ export type BUILDER_STR_REF = number; @@ -26,7 +29,7 @@ export interface BUILDER_STRS extends BUILDER_SECTION { }; export function build_strs(source_strs: Strs): BUILDER_STRS { - let result: BUILDER_STRS = { + const result: BUILDER_STRS = { ident: constants.hex_section_id(constants.section.strs), size: 0, // finalized later _offset: 0, @@ -39,7 +42,7 @@ export function build_strs(source_strs: Strs): BUILDER_STRS { let offset = constants.length_strs + constants.length_strs_item * result.count; // TODO: consider padding - for(let item of result.items) { + for(const item of result.items) { item.offset = offset; offset += item.length * 2 + 2; /* UTF-16 code units + sizeof null terminator */ } @@ -64,7 +67,7 @@ export function build_strs_index(sect_strs: BUILDER_STRS, value: StrsItem) : BUI return value.char; } - let result = sect_strs.items.findIndex(v => v._value === value.value); + const result = sect_strs.items.findIndex(v => v._value === value.value); if(result < 0) { throw new Error('unexpectedly missing StrsItem '+value.value); } diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-tran.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-tran.ts similarity index 91% rename from common/web/types/src/kmx/kmx-plus-builder/build-tran.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-tran.ts index 316df698539..2473651b889 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-tran.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-tran.ts @@ -1,10 +1,13 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { Bksp, Tran } from "../kmx-plus.js"; +import { KMXPlus } from "@keymanapp/common-types"; import { BUILDER_ELEM, BUILDER_ELEM_REF, build_elem_index } from "./build-elem.js"; import { BUILDER_STRS, BUILDER_STR_REF, build_strs_index } from "./build-strs.js"; import { BUILDER_SECTION } from "./builder-section.js"; +import Bksp = KMXPlus.Bksp; +import Tran = KMXPlus.Tran; + /* ------------------------------------------------------------------ * tran section ------------------------------------------------------------------ */ @@ -44,7 +47,7 @@ export function build_tran(source_tran: Tran | Bksp, sect_strs: BUILDER_STRS, se return null; } - let tran: BUILDER_TRAN = { + const tran: BUILDER_TRAN = { ident: constants.hex_section_id(source_tran.id), size: 0, // need to compute total transforms + reorders _offset: 0, @@ -56,14 +59,14 @@ export function build_tran(source_tran: Tran | Bksp, sect_strs: BUILDER_STRS, se reorders: [], }; - for (let group of source_tran.groups) { + for (const group of source_tran.groups) { if (group.type === constants.tran_group_type_transform) { tran.groups.push({ type: group.type, count: group.transforms.length, index: tran.transforms.length, // index of first item }); - for (let transform of group.transforms) { + for (const transform of group.transforms) { tran.transforms.push({ from: build_strs_index(sect_strs, transform.from), to: build_strs_index(sect_strs, transform.to), @@ -77,7 +80,7 @@ export function build_tran(source_tran: Tran | Bksp, sect_strs: BUILDER_STRS, se count: group.reorders.length, index: tran.reorders.length, // index of first item }); - for (let reorder of group.reorders) { + for (const reorder of group.reorders) { tran.reorders.push({ elements: build_elem_index(sect_elem, reorder.elements), before: build_elem_index(sect_elem, reorder.before), diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-uset.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-uset.ts similarity index 89% rename from common/web/types/src/kmx/kmx-plus-builder/build-uset.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-uset.ts index 8c73afb796e..1edd4d339fc 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-uset.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-uset.ts @@ -1,8 +1,12 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { KMXPlusData, StrsItem, UsetItem } from "../kmx-plus.js"; +import { KMXPlus } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION, BUILDER_U32CHAR } from "./builder-section.js"; +import KMXPlusData = KMXPlus.KMXPlusData; +import StrsItem = KMXPlus.StrsItem; +import UsetItem = KMXPlus.UsetItem; + /** reference from build_uset_index */ export type BUILDER_USET_REF = number; @@ -80,7 +84,7 @@ export function build_uset_index(sect_uset: BUILDER_USET, value: UsetItem) { } } - let result = sect_uset.usets.findIndex(v => v._pattern.value === value.str.value); + const result = sect_uset.usets.findIndex(v => v._pattern.value === value.str.value); if(result < 0) { throw new Error('unexpectedly missing UsetItem ' + value.uset.toString()); } diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-vars.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-vars.ts similarity index 95% rename from common/web/types/src/kmx/kmx-plus-builder/build-vars.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-vars.ts index 8c09a169f68..92acbba9be1 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-vars.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-vars.ts @@ -1,10 +1,11 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { KMXPlusData } from "../kmx-plus.js"; +import { KMXPlus } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION } from "./builder-section.js"; import { build_list_index, BUILDER_LIST, BUILDER_LIST_REF } from "./build-list.js"; import { build_elem_index, BUILDER_ELEM, BUILDER_ELEM_REF } from "./build-elem.js"; +import KMXPlusData = KMXPlus.KMXPlusData; interface BUILDER_VARS_ITEM { type: number; diff --git a/common/web/types/src/kmx/kmx-plus-builder/builder-section.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/builder-section.ts similarity index 100% rename from common/web/types/src/kmx/kmx-plus-builder/builder-section.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/builder-section.ts diff --git a/common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/kmx-plus-builder.ts similarity index 93% rename from common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts rename to developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/kmx-plus-builder.ts index 381e1d6d19c..acc732c932a 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/kmx-plus-builder.ts @@ -1,5 +1,5 @@ import * as r from 'restructure'; -import { KMXPlusFile } from "../kmx-plus.js"; +import { KMXPlus } from "@keymanapp/common-types"; import { constants, SectionIdent } from '@keymanapp/ldml-keyboard-constants'; import { BUILDER_SECTION } from './builder-section.js'; import { BUILDER_SECT, build_sect } from './build-sect.js'; @@ -15,6 +15,8 @@ import { BUILDER_TRAN, build_tran } from './build-tran.js'; import { BUILDER_USET, build_uset } from './build-uset.js'; import { BUILDER_VARS, build_vars } from './build-vars.js'; +import KMXPlusFile = KMXPlus.KMXPlusFile; + type BUILDER_BKSP = BUILDER_TRAN; // type BUILDER_FINL = BUILDER_TRAN; @@ -50,7 +52,7 @@ export default class KMXPlusBuilder { public compile(): Uint8Array { const fileSize = this.build(); - let file: Uint8Array = new Uint8Array(fileSize); + const file: Uint8Array = new Uint8Array(fileSize); this.emitSection(file, this.file.COMP_PLUS_SECT, this.sect.sect); // Keep the rest of these in order. @@ -155,13 +157,13 @@ export default class KMXPlusBuilder { } private emitStrings(file: Uint8Array) { - for(let item of this.sect.strs.items) { + for(const item of this.sect.strs.items) { if(item._value === '') { // We have a special case for the zero-length string - let sbuf = r.uint16le; + const sbuf = r.uint16le; file.set(sbuf.toBuffer(0), item.offset + this.sect.strs._offset); } else { - let sbuf = new r.String(null, 'utf16le'); + const sbuf = new r.String(null, 'utf16le'); file.set(sbuf.toBuffer(item._value), item.offset + this.sect.strs._offset); } } @@ -169,9 +171,9 @@ export default class KMXPlusBuilder { private emitElements(file: Uint8Array) { if(this.sect.elem) { - for(let str of this.sect.elem.strings) { + for(const str of this.sect.elem.strings) { if(str.items.length > 0) { - let COMP_PLUS_ELEM_ELEMENTS = new r.Array(this.file.COMP_PLUS_ELEM_ELEMENT, str.items.length); + const COMP_PLUS_ELEM_ELEMENTS = new r.Array(this.file.COMP_PLUS_ELEM_ELEMENT, str.items.length); file.set(COMP_PLUS_ELEM_ELEMENTS.toBuffer(str.items), str.offset + this.sect.elem._offset); } } diff --git a/common/web/types/src/kpj/keyman-developer-project.ts b/developer/src/common/web/utils/src/types/kpj/keyman-developer-project.ts similarity index 92% rename from common/web/types/src/kpj/keyman-developer-project.ts rename to developer/src/common/web/utils/src/types/kpj/keyman-developer-project.ts index 76f3ab709a2..f678106a4cc 100644 --- a/common/web/types/src/kpj/keyman-developer-project.ts +++ b/developer/src/common/web/utils/src/types/kpj/keyman-developer-project.ts @@ -2,8 +2,8 @@ // Version 1.0 and 2.0 of Keyman Developer Project .kpj file // -import { KeymanFileTypes } from '../main.js'; -import { CompilerCallbacks } from '../util/compiler-interfaces.js'; +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerCallbacks } from '../../compiler-interfaces.js'; export class KeymanDeveloperProject { options: KeymanDeveloperProjectOptions; @@ -29,13 +29,13 @@ export class KeymanDeveloperProject { if(this.options.version != '2.0') { throw new Error('populateFiles can only be called on a v2.0 project'); } - let sourcePath = this.resolveProjectPath(this.options.sourcePath); + const sourcePath = this.resolveProjectPath(this.options.sourcePath); if(!this.callbacks.fs.existsSync(sourcePath)) { return false; } - let files = this.callbacks.fs.readdirSync(sourcePath); - for(let filename of files) { - let fullPath = this.callbacks.path.join(sourcePath, filename); + const files = this.callbacks.fs.readdirSync(sourcePath); + for(const filename of files) { + const fullPath = this.callbacks.path.join(sourcePath, filename); if(KeymanFileTypes.filenameIs(filename, KeymanFileTypes.Source.LdmlKeyboard)) { try { const data = this.callbacks.loadFile(fullPath); @@ -51,7 +51,7 @@ export class KeymanDeveloperProject { } } if(KeymanFileTypes.sourceTypeFromFilename(filename) !== null) { - let file = new KeymanDeveloperProjectFile20(fullPath, this.callbacks); + const file = new KeymanDeveloperProjectFile20(fullPath, this.callbacks); this.files.push(file); } } @@ -109,7 +109,7 @@ export class KeymanDeveloperProject { p = this.resolveProjectPath(p); - let f = file.filename.replace(new RegExp(`\\${sourceExt}$`, 'i'), targetExt); + const f = file.filename.replace(new RegExp(`\\${sourceExt}$`, 'i'), targetExt); return this.callbacks.path.normalize(this.callbacks.path.join(p, f)); } diff --git a/common/web/types/src/kpj/kpj-file-reader.ts b/developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts similarity index 81% rename from common/web/types/src/kpj/kpj-file-reader.ts rename to developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts index eb2a168a48c..d7768987531 100644 --- a/common/web/types/src/kpj/kpj-file-reader.ts +++ b/developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts @@ -1,9 +1,9 @@ -import * as xml2js from '../deps/xml2js/xml2js.js'; +import { xml2js } from '../../index.js'; import { KPJFile, KPJFileProject } from './kpj-file.js'; -import { boxXmlArray } from '../util/util.js'; +import { util } from '@keymanapp/common-types'; import { KeymanDeveloperProject, KeymanDeveloperProjectFile10, KeymanDeveloperProjectType } from './keyman-developer-project.js'; -import { CompilerCallbacks } from '../util/compiler-interfaces.js'; -import SchemaValidators from '../schema-validators.js'; +import { SchemaValidators } from '@keymanapp/common-types'; +import { CompilerCallbacks } from '../../compiler-interfaces.js'; export class KPJFileReader { constructor(private callbacks: CompilerCallbacks) { @@ -29,7 +29,7 @@ export class KPJFileReader { }); data = this.boxArrays(data); if(data.KeymanDeveloperProject?.Files?.File?.length) { - for(let file of data.KeymanDeveloperProject?.Files?.File) { + for(const file of data.KeymanDeveloperProject?.Files?.File) { // xml2js imports
as '' so we will just delete the empty string if(typeof file.Details == 'string') { delete file.Details; @@ -40,11 +40,11 @@ export class KPJFileReader { } public validate(source: KPJFile): void { - if(!SchemaValidators.kpj(source)) { - if(!SchemaValidators.kpj90(source)) { + if(!SchemaValidators.default.kpj(source)) { + if(!SchemaValidators.default.kpj90(source)) { // If the legacy schema also does not validate, then we will only report // the errors against the modern schema - throw new Error(JSON.stringify((SchemaValidators.kpj).errors)); + throw new Error(JSON.stringify((SchemaValidators.default.kpj).errors)); } } } @@ -60,8 +60,8 @@ export class KPJFileReader { // NOTE: at this point, the xml should have been validated // and matched the schema result so we can assume the source // is a valid shape - let project = source.KeymanDeveloperProject; - let result: KeymanDeveloperProject = new KeymanDeveloperProject(projectFilename, project.Options?.Version || "1.0", this.callbacks); + const project = source.KeymanDeveloperProject; + const result: KeymanDeveloperProject = new KeymanDeveloperProject(projectFilename, project.Options?.Version || "1.0", this.callbacks); if(result.options.version == '2.0') { result.options.buildPath = (project.Options?.BuildPath || result.options.buildPath).replace(/\\/g, '/'); result.options.sourcePath = (project.Options?.SourcePath || result.options.sourcePath).replace(/\\/g, '/'); @@ -88,9 +88,9 @@ export class KPJFileReader { } private transformFilesVersion10(project: KPJFileProject, result: KeymanDeveloperProject) { - let ids: { [id: string]: KeymanDeveloperProjectFile10; } = {}; - for (let sourceFile of project.Files?.File) { - let file: KeymanDeveloperProjectFile10 = new KeymanDeveloperProjectFile10( + const ids: { [id: string]: KeymanDeveloperProjectFile10; } = {}; + for (const sourceFile of project.Files?.File) { + const file: KeymanDeveloperProjectFile10 = new KeymanDeveloperProjectFile10( sourceFile.ID || '', (sourceFile.Filepath || '').replace(/\\/g, '/'), sourceFile.FileVersion || '', @@ -123,7 +123,7 @@ export class KPJFileReader { if(!source.KeymanDeveloperProject.Files || typeof source.KeymanDeveloperProject.Files == 'string') { source.KeymanDeveloperProject.Files = {File:[]}; } - boxXmlArray(source.KeymanDeveloperProject.Files, 'File'); + util.boxXmlArray(source.KeymanDeveloperProject.Files, 'File'); return source; } } \ No newline at end of file diff --git a/common/web/types/src/kpj/kpj-file.ts b/developer/src/common/web/utils/src/types/kpj/kpj-file.ts similarity index 100% rename from common/web/types/src/kpj/kpj-file.ts rename to developer/src/common/web/utils/src/types/kpj/kpj-file.ts diff --git a/common/web/types/src/package/kps-file.ts b/developer/src/common/web/utils/src/types/kps/kps-file.ts similarity index 100% rename from common/web/types/src/package/kps-file.ts rename to developer/src/common/web/utils/src/types/kps/kps-file.ts diff --git a/common/web/types/src/kvk/kvks-file-reader.ts b/developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts similarity index 78% rename from common/web/types/src/kvk/kvks-file-reader.ts rename to developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts index ff3a094e6f0..6b2ad8e0a98 100644 --- a/common/web/types/src/kvk/kvks-file-reader.ts +++ b/developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts @@ -1,10 +1,19 @@ -import * as xml2js from '../deps/xml2js/xml2js.js'; +import { SchemaValidators as SV, KvkFile, util, Constants } from '@keymanapp/common-types'; +import { xml2js } from '../../index.js' import KVKSourceFile from './kvks-file.js'; -import { boxXmlArray } from '../util/util.js'; -import { DEFAULT_KVK_FONT, VisualKeyboard, VisualKeyboardHeaderFlags, VisualKeyboardKey, VisualKeyboardKeyFlags, VisualKeyboardLegalShiftStates, VisualKeyboardShiftState } from './visual-keyboard.js'; -import { USVirtualKeyCodes } from '../consts/virtual-key-constants.js'; -import { BUILDER_KVK_HEADER_VERSION, KVK_HEADER_IDENTIFIER_BYTES } from './kvk-file.js'; -import SchemaValidators from '../schema-validators.js'; +const SchemaValidators = SV.default; +import boxXmlArray = util.boxXmlArray; +import USVirtualKeyCodes = Constants.USVirtualKeyCodes; +import { VisualKeyboard as VK } from '@keymanapp/common-types'; +import DEFAULT_KVK_FONT = VK.DEFAULT_KVK_FONT; +import VisualKeyboard = VK.VisualKeyboard; +import VisualKeyboardHeaderFlags = VK.VisualKeyboardHeaderFlags; +import VisualKeyboardKey = VK.VisualKeyboardKey; +import VisualKeyboardKeyFlags = VK.VisualKeyboardKeyFlags; +import VisualKeyboardLegalShiftStates = VK.VisualKeyboardLegalShiftStates; +import VisualKeyboardShiftState = VK.VisualKeyboardShiftState; +import BUILDER_KVK_HEADER_VERSION = KvkFile.BUILDER_KVK_HEADER_VERSION; +import KVK_HEADER_IDENTIFIER_BYTES = KvkFile.KVK_HEADER_IDENTIFIER_BYTES; export default class KVKSFileReader { @@ -69,9 +78,9 @@ export default class KVKSFileReader { } } - for(let key of Object.keys(source)) { + for(const key of Object.keys(source)) { if(Array.isArray(source[key])) { - for(let item of source[key]) { + for(const item of source[key]) { if(typeof(item) === 'object') { this.cleanupUnderscore(key, item); } @@ -92,7 +101,7 @@ export default class KVKSFileReader { // NOTE: at this point, the xml should have been validated // and matched the schema result so we can assume properties exist - let result: VisualKeyboard = { + const result: VisualKeyboard = { header: { version: BUILDER_KVK_HEADER_VERSION, flags: 0, @@ -117,22 +126,22 @@ export default class KVKSFileReader { result.header.flags |= VisualKeyboardHeaderFlags.kvkhUseUnderlying; } - for(let encoding of source.visualkeyboard.encoding) { - let isUnicode = (encoding.$?.name == 'unicode'), + for(const encoding of source.visualkeyboard.encoding) { + const isUnicode = (encoding.$?.name == 'unicode'), font = isUnicode ? result.header.unicodeFont : result.header.ansiFont; font.name = encoding.$?.fontname ?? DEFAULT_KVK_FONT.name; font.size = parseInt(encoding.$?.fontsize ?? DEFAULT_KVK_FONT.size.toString(), 10); - for(let layer of encoding.layer) { - let shift = this.kvksShiftToKvkShift(layer.$?.shift); - for(let sourceKey of layer.key) { - let vkey = (USVirtualKeyCodes as any)[sourceKey.$?.vkey]; + for(const layer of encoding.layer) { + const shift = this.kvksShiftToKvkShift(layer.$?.shift); + for(const sourceKey of layer.key) { + const vkey = (USVirtualKeyCodes as any)[sourceKey.$?.vkey]; if(!vkey) { if(typeof invalidVkeys !== 'undefined') { invalidVkeys.push(sourceKey.$?.vkey); } continue; } - let key: VisualKeyboardKey = { + const key: VisualKeyboardKey = { flags: (isUnicode ? VisualKeyboardKeyFlags.kvkkUnicode : 0) | (sourceKey.bitmap ? VisualKeyboardKeyFlags.kvkkBitmap : 0), @@ -167,9 +176,9 @@ export default class KVKSFileReader { */ private boxArrays(source: KVKSourceFile) { boxXmlArray(source.visualkeyboard, 'encoding'); - for(let encoding of source.visualkeyboard.encoding) { + for(const encoding of source.visualkeyboard.encoding) { boxXmlArray(encoding, 'layer'); - for(let layer of encoding.layer) { + for(const layer of encoding.layer) { boxXmlArray(layer, 'key'); } } @@ -181,7 +190,7 @@ export default class KVKSFileReader { shift = shift.toUpperCase(); // TODO-LDML(lowpri): make a map of this? - for(let state of VisualKeyboardLegalShiftStates) { + for(const state of VisualKeyboardLegalShiftStates) { if(state.name == shift) { return state.shift; } diff --git a/common/web/types/src/kvk/kvks-file-writer.ts b/developer/src/common/web/utils/src/types/kvks/kvks-file-writer.ts similarity index 73% rename from common/web/types/src/kvk/kvks-file-writer.ts rename to developer/src/common/web/utils/src/types/kvks/kvks-file-writer.ts index 19327e779da..dd4962a8fad 100644 --- a/common/web/types/src/kvk/kvks-file-writer.ts +++ b/developer/src/common/web/utils/src/types/kvks/kvks-file-writer.ts @@ -1,7 +1,13 @@ -import * as xml2js from '../deps/xml2js/xml2js.js'; +import { VisualKeyboard as VK, Constants } from '@keymanapp/common-types'; import KVKSourceFile, { KVKSEncoding, KVKSFlags, KVKSKey, KVKSLayer } from './kvks-file.js'; -import { VisualKeyboard, VisualKeyboardHeaderFlags, VisualKeyboardKeyFlags, VisualKeyboardLegalShiftStates, VisualKeyboardShiftState } from './visual-keyboard.js'; -import { USVirtualKeyCodes } from '../consts/virtual-key-constants.js'; +import { xml2js } from '../../index.js'; + +import USVirtualKeyCodes = Constants.USVirtualKeyCodes; +import VisualKeyboard = VK.VisualKeyboard; +import VisualKeyboardHeaderFlags = VK.VisualKeyboardHeaderFlags; +import VisualKeyboardKeyFlags = VK.VisualKeyboardKeyFlags; +import VisualKeyboardLegalShiftStates = VK.VisualKeyboardLegalShiftStates; +import VisualKeyboardShiftState = VK.VisualKeyboardShiftState; export default class KVKSFileWriter { public write(vk: VisualKeyboard): string { @@ -17,7 +23,7 @@ export default class KVKSFileWriter { } }) - let flags: KVKSFlags = {}; + const flags: KVKSFlags = {}; if(vk.header.flags & VisualKeyboardHeaderFlags.kvkhDisplayUnderlying) { flags.displayunderlying = ''; } @@ -33,7 +39,7 @@ export default class KVKSFileWriter { - let kvks: KVKSourceFile = { + const kvks: KVKSourceFile = { visualkeyboard: { header: { version: '10.0', @@ -46,9 +52,9 @@ export default class KVKSFileWriter { if(vk.header.underlyingLayout) kvks.visualkeyboard.header.layout = vk.header.underlyingLayout; - let encodings: {ansi: {o: KVKSEncoding, l: {[name:string]:KVKSLayer}}, unicode: {o: KVKSEncoding, l: {[name:string]:KVKSLayer}}} = {ansi:null,unicode:null}; + const encodings: {ansi: {o: KVKSEncoding, l: {[name:string]:KVKSLayer}}, unicode: {o: KVKSEncoding, l: {[name:string]:KVKSLayer}}} = {ansi:null,unicode:null}; - for(let key of vk.keys) { + for(const key of vk.keys) { const encoding = key.flags & VisualKeyboardKeyFlags.kvkkUnicode ? 'unicode' : 'ansi'; const shift = this.kvkShiftToKvksShift(key.shift); @@ -65,7 +71,7 @@ export default class KVKSFileWriter { }; kvks.visualkeyboard.encoding.push(encodings[encoding].o); } - let e = encodings[encoding]; + const e = encodings[encoding]; if(!e.l[shift]) { e.l[shift] = { key: [], @@ -73,11 +79,11 @@ export default class KVKSFileWriter { }; e.o.layer.push(e.l[shift]); } - let l = e.l[shift]; + const l = e.l[shift]; // TODO-LDML: map let vkeyName = ''; - for(let vkey of Object.keys(USVirtualKeyCodes)) { + for(const vkey of Object.keys(USVirtualKeyCodes)) { if((USVirtualKeyCodes as any)[vkey] == key.vkey) { vkeyName = vkey; break; @@ -88,7 +94,7 @@ export default class KVKSFileWriter { //TODO-LDML: warn continue; } - let k: KVKSKey = { + const k: KVKSKey = { $: {vkey: vkeyName}, _: key.text, } @@ -99,7 +105,7 @@ export default class KVKSFileWriter { l.key.push(k); } - let result = builder.buildObject(kvks); + const result = builder.buildObject(kvks); return result; //Uint8Array.from(result); } @@ -111,7 +117,7 @@ export default class KVKSFileWriter { public kvkShiftToKvksShift(shift: VisualKeyboardShiftState): string { // TODO-LDML(lowpri): make a map of this? - for(let state of VisualKeyboardLegalShiftStates) { + for(const state of VisualKeyboardLegalShiftStates) { if(state.shift == shift) { return state.name; } diff --git a/common/web/types/src/kvk/kvks-file.ts b/developer/src/common/web/utils/src/types/kvks/kvks-file.ts similarity index 100% rename from common/web/types/src/kvk/kvks-file.ts rename to developer/src/common/web/utils/src/types/kvks/kvks-file.ts diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-testdata-xml.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-testdata-xml.ts similarity index 100% rename from common/web/types/src/ldml-keyboard/ldml-keyboard-testdata-xml.ts rename to developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-testdata-xml.ts diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts similarity index 87% rename from common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts rename to developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts index 9055e8ed1de..d364c16d7dc 100644 --- a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -1,11 +1,17 @@ -import * as xml2js from '../deps/xml2js/xml2js.js'; +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Reads a LDML XML keyboard file into JS object tree and resolves imports + */ +import { SchemaValidators, util } from '@keymanapp/common-types'; +import { xml2js } from '../../index.js'; +import { CommonTypesMessages } from '../../common-messages.js'; +import { CompilerCallbacks } from '../../compiler-interfaces.js'; import { LDMLKeyboardXMLSourceFile, LKImport, ImportStatus } from './ldml-keyboard-xml.js'; -import { boxXmlArray } from '../util/util.js'; -import { CompilerCallbacks } from '../util/compiler-interfaces.js'; import { constants } from '@keymanapp/ldml-keyboard-constants'; -import { CommonTypesMessages } from '../util/common-events.js'; import { LDMLKeyboardTestDataXMLSourceFile, LKTTest, LKTTests } from './ldml-keyboard-testdata-xml.js'; -import SchemaValidators from '../schema-validators.js'; + +import boxXmlArray = util.boxXmlArray; interface NameAndProps { '$'?: any; // content @@ -63,10 +69,10 @@ export class LDMLKeyboardXMLSourceFileReader { boxXmlArray(source?.keyboard3?.locales, 'locale'); boxXmlArray(source?.keyboard3, 'transforms'); if(source?.keyboard3?.layers) { - for(let layers of source?.keyboard3?.layers) { + for(const layers of source?.keyboard3?.layers) { boxXmlArray(layers, 'layer'); if(layers?.layer) { - for(let layer of layers?.layer) { + for(const layer of layers?.layer) { boxXmlArray(layer, 'row'); } } @@ -74,13 +80,13 @@ export class LDMLKeyboardXMLSourceFileReader { } if(source?.keyboard3?.forms?.form) { boxXmlArray(source?.keyboard3?.forms, 'form'); - for(let form of source?.keyboard3?.forms?.form) { + for(const form of source?.keyboard3?.forms?.form) { boxXmlArray(form, 'scanCodes'); } } if(source?.keyboard3?.flicks) { boxXmlArray(source?.keyboard3?.flicks, 'flick'); - for(let flick of source?.keyboard3?.flicks?.flick) { + for(const flick of source?.keyboard3?.flicks?.flick) { boxXmlArray(flick, 'flickSegment'); } } @@ -90,9 +96,9 @@ export class LDMLKeyboardXMLSourceFileReader { boxXmlArray(source?.keyboard3?.variables, 'uset'); } if(source?.keyboard3?.transforms) { - for(let transforms of source.keyboard3.transforms) { + for(const transforms of source.keyboard3.transforms) { boxXmlArray(transforms, 'transformGroup'); - for (let transformGroup of transforms.transformGroup) { + for (const transformGroup of transforms.transformGroup) { boxXmlArray(transformGroup, 'transform'); boxXmlArray(transformGroup, 'reorder'); } @@ -113,6 +119,7 @@ export class LDMLKeyboardXMLSourceFileReader { for (const sub of obj) { // retain the same subtag if (!this.boxImportsAndSpecials(sub, subtag)) { + // resolveImports has already logged a message return false; } } @@ -125,6 +132,7 @@ export class LDMLKeyboardXMLSourceFileReader { boxXmlArray(obj, key); // Now, resolve the import if (!this.resolveImports(obj, subtag)) { + // resolveImports has already logged a message return false; } // now delete the import array we so carefully constructed, the caller does not @@ -132,6 +140,7 @@ export class LDMLKeyboardXMLSourceFileReader { delete obj['import']; } else { if (!this.boxImportsAndSpecials(obj[key], key)) { + // resolveImports has already logged a message return false; } } @@ -151,6 +160,7 @@ export class LDMLKeyboardXMLSourceFileReader { // first, the explicit imports for (const asImport of ([...obj['import'] as LKImport[]].reverse())) { if (!this.resolveOneImport(obj, subtag, asImport)) { + // resolveOneImport has already logged a message return false; } } @@ -161,6 +171,7 @@ export class LDMLKeyboardXMLSourceFileReader { base: constants.cldr_import_base, path: constants.cldr_implied_keys_import }, true)) { + // resolveOneImport has already logged a message return false; } } else if (subtag === 'forms') { @@ -169,6 +180,7 @@ export class LDMLKeyboardXMLSourceFileReader { base: constants.cldr_import_base, path: constants.cldr_implied_forms_import }, true)) { + // resolveOneImport has already logged a message return false; } } @@ -235,8 +247,8 @@ export class LDMLKeyboardXMLSourceFileReader { * @returns true if valid, false if invalid */ public validate(source: LDMLKeyboardXMLSourceFile | LDMLKeyboardTestDataXMLSourceFile): boolean { - if(!SchemaValidators.ldmlKeyboard3(source)) { - for (let err of (SchemaValidators.ldmlKeyboard3).errors) { + if(!SchemaValidators.default.ldmlKeyboard3(source)) { + for (const err of (SchemaValidators.default.ldmlKeyboard3).errors) { this.callbacks.reportMessage(CommonTypesMessages.Error_SchemaValidationError({ instancePath: err.instancePath, keyword: err.keyword, @@ -250,9 +262,9 @@ export class LDMLKeyboardXMLSourceFileReader { } loadUnboxed(file: Uint8Array): LDMLKeyboardXMLSourceFile { - let source = (() => { + const source = (() => { let a: LDMLKeyboardXMLSourceFile; - let parser = new xml2js.Parser({ + const parser = new xml2js.Parser({ explicitArray: false, mergeAttrs: true, includeWhiteChars: false, @@ -266,7 +278,8 @@ export class LDMLKeyboardXMLSourceFileReader { // An alternative fix would be to pull xml2js directly from github // rather than using the version tagged on npmjs.com. }); - parser.parseString(file, (e: unknown, r: unknown) => { a = r as LDMLKeyboardXMLSourceFile }); // TODO-LDML: isn't 'e' the error? + const data = new TextDecoder().decode(file); + parser.parseString(data, (e: unknown, r: unknown) => { if(e) throw e; a = r as LDMLKeyboardXMLSourceFile }); // TODO-LDML: isn't 'e' the error? return a; })(); return source; @@ -278,20 +291,29 @@ export class LDMLKeyboardXMLSourceFileReader { */ public load(file: Uint8Array): LDMLKeyboardXMLSourceFile | null { if (!file) { + throw new Error(`file parameter must not be null`); + } + + let source: LDMLKeyboardXMLSourceFile = null; + try { + source = this.loadUnboxed(file); + } catch(e) { + this.callbacks.reportMessage(CommonTypesMessages.Error_InvalidXml({e})); return null; } - const source = this.loadUnboxed(file); - if(this.boxArrays(source)) { + + if (this.boxArrays(source)) { return source; - } else { - return null; } + + // boxArrays ... resolveImports has already logged a message + return null; } loadTestDataUnboxed(file: Uint8Array): any { - let source = (() => { + const source = (() => { let a: any; - let parser = new xml2js.Parser({ + const parser = new xml2js.Parser({ // explicitArray: false, preserveChildrenOrder:true, // needed for test data explicitChildren: true, // needed for test data diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml.ts similarity index 100% rename from common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts rename to developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml.ts diff --git a/developer/src/common/web/utils/src/KeymanSentry.ts b/developer/src/common/web/utils/src/utils/KeymanSentry.ts similarity index 100% rename from developer/src/common/web/utils/src/KeymanSentry.ts rename to developer/src/common/web/utils/src/utils/KeymanSentry.ts diff --git a/developer/src/common/web/utils/src/keyman-urls.ts b/developer/src/common/web/utils/src/utils/keyman-urls.ts similarity index 100% rename from developer/src/common/web/utils/src/keyman-urls.ts rename to developer/src/common/web/utils/src/utils/keyman-urls.ts diff --git a/developer/src/common/web/utils/src/markdown.ts b/developer/src/common/web/utils/src/utils/markdown.ts similarity index 100% rename from developer/src/common/web/utils/src/markdown.ts rename to developer/src/common/web/utils/src/utils/markdown.ts diff --git a/developer/src/common/web/utils/src/options.ts b/developer/src/common/web/utils/src/utils/options.ts similarity index 100% rename from developer/src/common/web/utils/src/options.ts rename to developer/src/common/web/utils/src/utils/options.ts diff --git a/developer/src/common/web/utils/src/validate-mit-license.ts b/developer/src/common/web/utils/src/utils/validate-mit-license.ts similarity index 100% rename from developer/src/common/web/utils/src/validate-mit-license.ts rename to developer/src/common/web/utils/src/utils/validate-mit-license.ts diff --git a/common/web/types/test/helpers/TestCompilerCallbacks.ts b/developer/src/common/web/utils/test/TestCompilerCallbacks.ts similarity index 87% rename from common/web/types/test/helpers/TestCompilerCallbacks.ts rename to developer/src/common/web/utils/test/TestCompilerCallbacks.ts index d231290fef2..d1413e477a9 100644 --- a/common/web/types/test/helpers/TestCompilerCallbacks.ts +++ b/developer/src/common/web/utils/test/TestCompilerCallbacks.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { loadFile, resolveFilename } from '../helpers/index.js'; -import { CompilerCallbacks, CompilerError, CompilerEvent, CompilerFileSystemCallbacks, CompilerPathCallbacks } from '../../src/util/compiler-interfaces.js'; +import { loadFile, resolveFilename } from './helpers/index.js'; +import { CompilerCallbacks, CompilerError, CompilerEvent, CompilerFileSystemCallbacks, CompilerPathCallbacks } from '../src/compiler-interfaces.js'; // This is related to developer/src/common/web/test-helpers/index.ts but has a slightly different API surface // as this runs at a lower level than the compiler. @@ -40,7 +40,7 @@ export class TestCompilerCallbacks implements CompilerCallbacks { return loadFile(filename); } catch (e) { if (e.code === 'ENOENT') { - return null; + return null; } else { throw e; } diff --git a/common/web/types/test/fixtures/keyman-touch-layout/khmer_angkor.keyman-touch-layout b/developer/src/common/web/utils/test/fixtures/keyman-touch-layout/khmer_angkor.keyman-touch-layout similarity index 100% rename from common/web/types/test/fixtures/keyman-touch-layout/khmer_angkor.keyman-touch-layout rename to developer/src/common/web/utils/test/fixtures/keyman-touch-layout/khmer_angkor.keyman-touch-layout diff --git a/common/web/types/test/fixtures/keyman-touch-layout/legacy.keyman-touch-layout b/developer/src/common/web/utils/test/fixtures/keyman-touch-layout/legacy.keyman-touch-layout similarity index 100% rename from common/web/types/test/fixtures/keyman-touch-layout/legacy.keyman-touch-layout rename to developer/src/common/web/utils/test/fixtures/keyman-touch-layout/legacy.keyman-touch-layout diff --git a/common/web/types/test/fixtures/kpj/khmer_angkor.kpj b/developer/src/common/web/utils/test/fixtures/kpj/khmer_angkor.kpj similarity index 100% rename from common/web/types/test/fixtures/kpj/khmer_angkor.kpj rename to developer/src/common/web/utils/test/fixtures/kpj/khmer_angkor.kpj diff --git a/common/web/types/test/fixtures/kpj/project-missing-file/project_missing_file.kpj b/developer/src/common/web/utils/test/fixtures/kpj/project-missing-file/project_missing_file.kpj similarity index 100% rename from common/web/types/test/fixtures/kpj/project-missing-file/project_missing_file.kpj rename to developer/src/common/web/utils/test/fixtures/kpj/project-missing-file/project_missing_file.kpj diff --git a/common/web/types/test/fixtures/kpj/project-missing-file/project_missing_files.kpj b/developer/src/common/web/utils/test/fixtures/kpj/project-missing-file/project_missing_files.kpj similarity index 100% rename from common/web/types/test/fixtures/kpj/project-missing-file/project_missing_files.kpj rename to developer/src/common/web/utils/test/fixtures/kpj/project-missing-file/project_missing_files.kpj diff --git a/developer/src/common/web/utils/test/fixtures/kvks/balochi_inpage.kvk b/developer/src/common/web/utils/test/fixtures/kvks/balochi_inpage.kvk new file mode 100644 index 00000000000..56fee9dfbd3 Binary files /dev/null and b/developer/src/common/web/utils/test/fixtures/kvks/balochi_inpage.kvk differ diff --git a/common/web/types/test/fixtures/kvk/balochi_inpage.kvks b/developer/src/common/web/utils/test/fixtures/kvks/balochi_inpage.kvks similarity index 100% rename from common/web/types/test/fixtures/kvk/balochi_inpage.kvks rename to developer/src/common/web/utils/test/fixtures/kvks/balochi_inpage.kvks diff --git a/developer/src/common/web/utils/test/fixtures/kvks/khmer_angkor.kvk b/developer/src/common/web/utils/test/fixtures/kvks/khmer_angkor.kvk new file mode 100644 index 00000000000..c64b3a5a544 Binary files /dev/null and b/developer/src/common/web/utils/test/fixtures/kvks/khmer_angkor.kvk differ diff --git a/common/web/types/test/fixtures/kvk/khmer_angkor.kvks b/developer/src/common/web/utils/test/fixtures/kvks/khmer_angkor.kvks similarity index 100% rename from common/web/types/test/fixtures/kvk/khmer_angkor.kvks rename to developer/src/common/web/utils/test/fixtures/kvks/khmer_angkor.kvks diff --git a/common/web/types/test/fixtures/import-minimal.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-minimal.xml similarity index 100% rename from common/web/types/test/fixtures/import-minimal.xml rename to developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-minimal.xml diff --git a/common/web/types/test/fixtures/import-minimal1.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-minimal1.xml similarity index 100% rename from common/web/types/test/fixtures/import-minimal1.xml rename to developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-minimal1.xml diff --git a/common/web/types/test/fixtures/import-minimal2.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-minimal2.xml similarity index 100% rename from common/web/types/test/fixtures/import-minimal2.xml rename to developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-minimal2.xml diff --git a/common/web/types/test/fixtures/import-symbols.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-symbols.xml similarity index 100% rename from common/web/types/test/fixtures/import-symbols.xml rename to developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-symbols.xml diff --git a/common/web/types/test/fixtures/invalid-conforms-to.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-conforms-to.xml similarity index 100% rename from common/web/types/test/fixtures/invalid-conforms-to.xml rename to developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-conforms-to.xml diff --git a/common/web/types/test/fixtures/invalid-import-base.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-base.xml similarity index 100% rename from common/web/types/test/fixtures/invalid-import-base.xml rename to developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-base.xml diff --git a/common/web/types/test/fixtures/invalid-import-path.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-path.xml similarity index 100% rename from common/web/types/test/fixtures/invalid-import-path.xml rename to developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-path.xml diff --git a/common/web/types/test/fixtures/invalid-import-readfail.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-readfail.xml similarity index 100% rename from common/web/types/test/fixtures/invalid-import-readfail.xml rename to developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-readfail.xml diff --git a/common/web/types/test/fixtures/invalid-import-wrongroot.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-wrongroot.xml similarity index 100% rename from common/web/types/test/fixtures/invalid-import-wrongroot.xml rename to developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-wrongroot.xml diff --git a/common/web/types/test/fixtures/invalid-structure-per-dtd.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-structure-per-dtd.xml similarity index 100% rename from common/web/types/test/fixtures/invalid-structure-per-dtd.xml rename to developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-structure-per-dtd.xml diff --git a/common/web/types/test/fixtures/test-fr.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/test-fr.xml similarity index 100% rename from common/web/types/test/fixtures/test-fr.xml rename to developer/src/common/web/utils/test/fixtures/ldml-keyboard/test-fr.xml diff --git a/developer/src/common/web/utils/test/helpers/index.ts b/developer/src/common/web/utils/test/helpers/index.ts index 9968fbfea6d..e36d07429b2 100644 --- a/developer/src/common/web/utils/test/helpers/index.ts +++ b/developer/src/common/web/utils/test/helpers/index.ts @@ -2,6 +2,7 @@ * Helpers and utilities for the Mocha tests. */ import * as path from 'path'; +import * as fs from "fs"; import { fileURLToPath } from 'url'; /** @@ -16,3 +17,25 @@ import { fileURLToPath } from 'url'; return fileURLToPath(new URL(path.join('..', '..', '..', 'test', 'fixtures', ...components), import.meta.url)); } +export function loadFile(filename: string | URL): Buffer { + return fs.readFileSync(filename); +} + +export function resolveFilename(baseFilename: string, filename: string) { + const basePath = + baseFilename.endsWith('/') || baseFilename.endsWith('\\') ? + baseFilename : + path.dirname(baseFilename); + // Transform separators to platform separators -- we are agnostic + // in our use here but path prefers files may use + // either / or \, although older kps files were always \. + if(path.sep == '/') { + filename = filename.replace(/\\/g, '/'); + } else { + filename = filename.replace(/\//g, '\\'); + } + if(!path.isAbsolute(filename)) { + filename = path.resolve(basePath, filename); + } + return filename; +} diff --git a/common/web/types/test/helpers/reader-callback-test.ts b/developer/src/common/web/utils/test/helpers/reader-callback-test.ts similarity index 88% rename from common/web/types/test/helpers/reader-callback-test.ts rename to developer/src/common/web/utils/test/helpers/reader-callback-test.ts index 907793cdf7e..c67d49dfdb4 100644 --- a/common/web/types/test/helpers/reader-callback-test.ts +++ b/developer/src/common/web/utils/test/helpers/reader-callback-test.ts @@ -1,11 +1,11 @@ import 'mocha'; import {assert} from 'chai'; -import { loadFile, makePathToFixture } from '../helpers/index.js'; -import { LDMLKeyboardXMLSourceFileReader, LDMLKeyboardXMLSourceFileReaderOptions } from '../../src/ldml-keyboard/ldml-keyboard-xml-reader.js'; -import { CompilerEvent } from '../../src/util/compiler-interfaces.js'; -import { LDMLKeyboardXMLSourceFile } from '../../src/ldml-keyboard/ldml-keyboard-xml.js'; -import { LDMLKeyboardTestDataXMLSourceFile } from '../ldml-keyboard/ldml-keyboard-testdata-xml.js'; -import { TestCompilerCallbacks } from './TestCompilerCallbacks.js'; +import { loadFile, makePathToFixture } from './index.js'; +import { LDMLKeyboardXMLSourceFileReader, LDMLKeyboardXMLSourceFileReaderOptions } from '../../src/types/ldml-keyboard/ldml-keyboard-xml-reader.js'; +import { CompilerEvent } from '../../src/compiler-interfaces.js'; +import { LDMLKeyboardXMLSourceFile } from '../../src/types/ldml-keyboard/ldml-keyboard-xml.js'; +import { LDMLKeyboardTestDataXMLSourceFile } from '../../src/types/ldml-keyboard/ldml-keyboard-testdata-xml.js'; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { fileURLToPath } from 'url'; const readerOptions: LDMLKeyboardXMLSourceFileReaderOptions = { @@ -77,14 +77,14 @@ export function testReaderCases(cases : CompilationCase[]) { // we need our own callbacks rather than using the global so messages don't get mixed const callbacks = new TestCompilerCallbacks(); const reader = new LDMLKeyboardXMLSourceFileReader(readerOptions, callbacks); - for (let testcase of cases) { + for (const testcase of cases) { const expectFailure = testcase.throws || !!(testcase.errors); // if true, we expect this to fail const testHeading = expectFailure ? `should fail to load: ${testcase.subpath}`: `should load: ${testcase.subpath}`; it(testHeading, function () { callbacks.clear(); - const data = loadFile(makePathToFixture(testcase.subpath)); + const data = loadFile(makePathToFixture('ldml-keyboard', testcase.subpath)); assert.ok(data, `reading ${testcase.subpath}`); const source = reader.load(data); if (!testcase.loadfail) { @@ -128,14 +128,14 @@ export function testTestdataReaderCases(cases : TestDataCase[]) { // we need our own callbacks rather than using the global so messages don't get mixed const callbacks = new TestCompilerCallbacks(); const reader = new LDMLKeyboardXMLSourceFileReader(readerOptions, callbacks); - for (let testcase of cases) { + for (const testcase of cases) { const expectFailure = testcase.throws || !!(testcase.errors); // if true, we expect this to fail const testHeading = expectFailure ? `should fail to load: ${testcase.subpath}`: `should load: ${testcase.subpath}`; it(testHeading, function () { callbacks.clear(); - const data = loadFile(makePathToFixture(testcase.subpath)); + const data = loadFile(makePathToFixture('ldml-keyboard', testcase.subpath)); assert.ok(data, `reading ${testcase.subpath}`); const source = reader.loadTestData(data); if (!testcase.loadfail) { diff --git a/common/web/types/test/keyman-touch-layout/test-keyman-touch-layout-file-reader.ts b/developer/src/common/web/utils/test/keyman-touch-layout/test-keyman-touch-layout-file-reader.ts similarity index 96% rename from common/web/types/test/keyman-touch-layout/test-keyman-touch-layout-file-reader.ts rename to developer/src/common/web/utils/test/keyman-touch-layout/test-keyman-touch-layout-file-reader.ts index 479f2c4ccbc..bfebec85e8f 100644 --- a/common/web/types/test/keyman-touch-layout/test-keyman-touch-layout-file-reader.ts +++ b/developer/src/common/web/utils/test/keyman-touch-layout/test-keyman-touch-layout-file-reader.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import 'mocha'; import { assert } from 'chai'; import { makePathToFixture } from '../helpers/index.js'; -import { TouchLayoutFileReader } from "../../src/keyman-touch-layout/keyman-touch-layout-file-reader.js"; +import { TouchLayoutFileReader } from "../../src/types/keyman-touch-layout/keyman-touch-layout-file-reader.js"; describe('TouchLayoutFileReader', function () { it('should read a valid file', function() { diff --git a/common/web/types/test/keyman-touch-layout/test-keyman-touch-layout-round-trip.ts b/developer/src/common/web/utils/test/keyman-touch-layout/test-keyman-touch-layout-round-trip.ts similarity index 81% rename from common/web/types/test/keyman-touch-layout/test-keyman-touch-layout-round-trip.ts rename to developer/src/common/web/utils/test/keyman-touch-layout/test-keyman-touch-layout-round-trip.ts index f6a09d08d3b..25870e6e09b 100644 --- a/common/web/types/test/keyman-touch-layout/test-keyman-touch-layout-round-trip.ts +++ b/developer/src/common/web/utils/test/keyman-touch-layout/test-keyman-touch-layout-round-trip.ts @@ -2,8 +2,8 @@ import * as fs from 'fs'; import 'mocha'; import { assert } from 'chai'; import { makePathToFixture } from '../helpers/index.js'; -import { TouchLayoutFileReader } from "../../src/keyman-touch-layout/keyman-touch-layout-file-reader.js"; -import { TouchLayoutFileWriter } from "../../src/keyman-touch-layout/keyman-touch-layout-file-writer.js"; +import { TouchLayoutFileReader } from "../../src/types/keyman-touch-layout/keyman-touch-layout-file-reader.js"; +import { TouchLayoutFileWriter } from "../../src/types/keyman-touch-layout/keyman-touch-layout-file-writer.js"; describe('TouchLayoutFile', function () { it('should round-trip from TouchLayoutFileReader to TouchLayoutFileWriter', function() { @@ -26,7 +26,7 @@ describe('TouchLayoutFile', function () { assert.deepEqual(layout, newLayout); // And do the same without any options - let output2 = writer.write(layout); + const output2 = writer.write(layout); assert.deepEqual(output, output2); newLayout = reader.read(output2); diff --git a/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts b/developer/src/common/web/utils/test/kmx/test-ldml-keyboard-testdata-reader.ts similarity index 96% rename from common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts rename to developer/src/common/web/utils/test/kmx/test-ldml-keyboard-testdata-reader.ts index 794adbbe661..d2ce6dc72a9 100644 --- a/common/web/types/test/ldml-keyboard/test-ldml-keyboard-testdata-reader.ts +++ b/developer/src/common/web/utils/test/kmx/test-ldml-keyboard-testdata-reader.ts @@ -2,7 +2,7 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; import { assert } from 'chai'; import 'mocha'; import { testTestdataReaderCases } from '../helpers/reader-callback-test.js'; -import { LKTAnyAction } from './ldml-keyboard-testdata-xml.js'; +import { LKTAnyAction } from '../../src/types/ldml-keyboard/ldml-keyboard-testdata-xml.js'; describe('ldml keyboard xml reader tests', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while diff --git a/common/web/types/test/ldml-keyboard/test-ldml-keyboard-xml-reader.ts b/developer/src/common/web/utils/test/kmx/test-ldml-keyboard-xml-reader.ts similarity index 95% rename from common/web/types/test/ldml-keyboard/test-ldml-keyboard-xml-reader.ts rename to developer/src/common/web/utils/test/kmx/test-ldml-keyboard-xml-reader.ts index fa99be52670..17c35660245 100644 --- a/common/web/types/test/ldml-keyboard/test-ldml-keyboard-xml-reader.ts +++ b/developer/src/common/web/utils/test/kmx/test-ldml-keyboard-xml-reader.ts @@ -1,9 +1,13 @@ -import { LKKey, ImportStatus } from './../../src/ldml-keyboard/ldml-keyboard-xml.js'; +import { LKKey, ImportStatus } from '../../src/types/ldml-keyboard/ldml-keyboard-xml.js'; import 'mocha'; import {assert} from 'chai'; -import { CommonTypesMessages } from '../../src/util/common-events.js'; +import { CommonTypesMessages } from '../../src/common-messages.js'; import { testReaderCases } from '../helpers/reader-callback-test.js'; -import { CLDRScanToVkey, CLDRScanToKeyMap, USVirtualKeyCodes } from '../../src/consts/virtual-key-constants.js'; +import { Constants } from '@keymanapp/common-types'; + +import CLDRScanToVkey = Constants.CLDRScanToVkey; +import CLDRScanToKeyMap = Constants.CLDRScanToKeyMap; +import USVirtualKeyCodes = Constants.USVirtualKeyCodes; function pluckKeysFromKeybag(keys: LKKey[], ids: string[]) { return keys.filter(({id}) => ids.indexOf(id) !== -1); diff --git a/common/web/types/test/kpj/test-kpj-file-reader.ts b/developer/src/common/web/utils/test/kpj/test-kpj-file-reader.ts similarity index 97% rename from common/web/types/test/kpj/test-kpj-file-reader.ts rename to developer/src/common/web/utils/test/kpj/test-kpj-file-reader.ts index cbebdd79207..4b154af0606 100644 --- a/common/web/types/test/kpj/test-kpj-file-reader.ts +++ b/developer/src/common/web/utils/test/kpj/test-kpj-file-reader.ts @@ -2,9 +2,9 @@ import * as fs from 'fs'; import 'mocha'; import {assert} from 'chai'; import { makePathToFixture } from '../helpers/index.js'; -import { KPJFileReader } from "../../src/kpj/kpj-file-reader.js"; -import { KeymanDeveloperProjectFile10, KeymanDeveloperProjectType } from '../../src/kpj/keyman-developer-project.js'; -import { TestCompilerCallbacks } from '../helpers/TestCompilerCallbacks.js'; +import { KPJFileReader } from "../../src/types/kpj/kpj-file-reader.js"; +import { KeymanDeveloperProjectFile10, KeymanDeveloperProjectType } from '../../src/types/kpj/keyman-developer-project.js'; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; const callbacks = new TestCompilerCallbacks(); diff --git a/common/web/types/test/kvk/test-kvk-round-trip.ts b/developer/src/common/web/utils/test/kvks/test-kvk-round-trip.ts similarity index 85% rename from common/web/types/test/kvk/test-kvk-round-trip.ts rename to developer/src/common/web/utils/test/kvks/test-kvk-round-trip.ts index 7211ac3a355..b056bf4eedc 100644 --- a/common/web/types/test/kvk/test-kvk-round-trip.ts +++ b/developer/src/common/web/utils/test/kvks/test-kvk-round-trip.ts @@ -5,10 +5,9 @@ import Hexy from 'hexy'; import gitDiff from 'git-diff'; const { hexy } = Hexy; import { makePathToFixture } from '../helpers/index.js'; -import KvksFileReader from "../../src/kvk/kvks-file-reader.js"; -import KvkFileReader from "../../src/kvk/kvk-file-reader.js"; -import KvkFileWriter from "../../src/kvk/kvk-file-writer.js"; -import KvksFileWriter from "../../src/kvk/kvks-file-writer.js"; +import KvksFileReader from "../../src/types/kvks/kvks-file-reader.js"; +import { KvkFileReader, KvkFileWriter } from "@keymanapp/common-types"; +import KvksFileWriter from "../../src/types/kvks/kvks-file-writer.js"; /** * @@ -28,7 +27,7 @@ function assertBufferMatch(actual: Buffer, expected: Buffer) { describe('kvk-file-reader', function () { it('kvk-file-reader should round-trip with kvk-file-writer', function() { - const path = makePathToFixture('kvk', 'khmer_angkor.kvk'); + const path = makePathToFixture('kvks', 'khmer_angkor.kvk'); const input = fs.readFileSync(path); const reader = new KvkFileReader(); const vk = reader.read(input); @@ -44,8 +43,8 @@ describe('kvk-file-reader', function () { describe('kvks-file-reader', function () { it('kvks-file-reader should compile with kvk-file-writer', function() { - const inputPath = makePathToFixture('kvk', 'khmer_angkor.kvks'); - const compiledPath = makePathToFixture('kvk', 'khmer_angkor.kvk'); + const inputPath = makePathToFixture('kvks', 'khmer_angkor.kvks'); + const compiledPath = makePathToFixture('kvks', 'khmer_angkor.kvk'); const input = fs.readFileSync(inputPath); const compiled = fs.readFileSync(compiledPath); const reader = new KvksFileReader(); @@ -63,7 +62,7 @@ describe('kvks-file-reader', function () { describe('kvks-file-writer', function() { it('kvks-file-writer should match what kvk-file-reader reads', function() { - const kvkIn = makePathToFixture('kvk', 'khmer_angkor.kvk'); + const kvkIn = makePathToFixture('kvks', 'khmer_angkor.kvk'); const kvkBuf = fs.readFileSync(kvkIn); const kvkReader = new KvkFileReader(); const kvksReader = new KvksFileReader(); @@ -85,7 +84,7 @@ describe('kvks-file-reader', function () { }); it('should have identical input and output for kvk and kvks', function() { - const path = makePathToFixture('kvk', 'balochi_inpage.kvks'); + const path = makePathToFixture('kvks', 'balochi_inpage.kvks'); const input = fs.readFileSync(path); const reader = new KvksFileReader(); @@ -104,7 +103,7 @@ describe('kvks-file-reader', function () { assert.deepEqual(vk, vkExpected); // Then compare against the .kvk - const kvkPath = makePathToFixture('kvk', 'balochi_inpage.kvk'); + const kvkPath = makePathToFixture('kvks', 'balochi_inpage.kvk'); const kvkInput = fs.readFileSync(kvkPath); const kvkReader = new KvkFileReader(); diff --git a/developer/src/common/web/utils/test/kvks/test-kvk-utils.ts b/developer/src/common/web/utils/test/kvks/test-kvk-utils.ts new file mode 100644 index 00000000000..fe4c626d729 --- /dev/null +++ b/developer/src/common/web/utils/test/kvks/test-kvk-utils.ts @@ -0,0 +1,48 @@ +// NOTE: this is a copy of common/web/types/test/kvk/test-kvk-utils.ts +import 'mocha'; +import {assert} from 'chai'; +import { VisualKeyboard as VK } from "@keymanapp/common-types"; +import VisualKeyboard = VK.VisualKeyboard; +import VisualKeyboardHeaderFlags = VK.VisualKeyboardHeaderFlags; +import VisualKeyboardKeyFlags = VK.VisualKeyboardKeyFlags; +import VisualKeyboardShiftState = VK.VisualKeyboardShiftState; +import { Constants } from '@keymanapp/common-types'; +import USVirtualKeyCodes = Constants.USVirtualKeyCodes; + +export function verify_khmer_angkor(vk: VisualKeyboard) { + assert.equal(vk.header.flags, VisualKeyboardHeaderFlags.kvkhAltGr); + assert.equal(vk.header.associatedKeyboard, 'khmer_angkor'); + assert.equal(vk.header.ansiFont.name, 'Arial'); + assert.equal(vk.header.ansiFont.size, -12); + assert.equal(vk.header.unicodeFont.name, 'Khmer Busra Kbd'); + assert.equal(vk.header.unicodeFont.size, 16); + assert.equal(vk.keys.length, 186); + assert.equal(vk.keys[0].flags, VisualKeyboardKeyFlags.kvkkUnicode); + assert.equal(vk.keys[0].vkey, USVirtualKeyCodes.K_B); + assert.equal(vk.keys[0].shift, VisualKeyboardShiftState.KVKS_RALT); + assert.equal(vk.keys[0].text, 'ឞ'); + assert.equal(vk.keys[185].flags, VisualKeyboardKeyFlags.kvkkUnicode); + assert.equal(vk.keys[185].vkey, USVirtualKeyCodes.K_COMMA); + assert.equal(vk.keys[185].shift, VisualKeyboardShiftState.KVKS_SHIFT); + assert.equal(vk.keys[185].text, ''); +} + +export function verify_balochi_inpage(vk: VisualKeyboard) { + assert.equal(vk.header.flags, + VisualKeyboardHeaderFlags.kvkhAltGr | VisualKeyboardHeaderFlags.kvkhDisplayUnderlying); + assert.equal(vk.header.associatedKeyboard, 'balochi_inpage'); + assert.equal(vk.header.unicodeFont.name, 'Lateef'); + assert.equal(vk.header.unicodeFont.size, 14); + assert.equal(vk.keys.length, 147); + assert.equal(vk.keys[0].flags, VisualKeyboardKeyFlags.kvkkUnicode); + assert.equal(vk.keys[0].vkey, USVirtualKeyCodes.K_BKQUOTE); + assert.equal(vk.keys[0].shift, VisualKeyboardShiftState.KVKS_RALT); + assert.equal(vk.keys[0].text, '‍'); + assert.equal(vk.keys[30].flags, + VisualKeyboardKeyFlags.kvkkUnicode | VisualKeyboardKeyFlags.kvkkBitmap); + assert.equal(vk.keys[30].vkey, USVirtualKeyCodes.K_COMMA); + assert.equal(vk.keys[30].shift, + VisualKeyboardShiftState.KVKS_LCTRL); + assert.equal(vk.keys[30].text, ''); + assert.equal(vk.keys[30].bitmap.byteLength, 35766); +} \ No newline at end of file diff --git a/common/web/types/test/kvk/test-kvks-file.ts b/developer/src/common/web/utils/test/kvks/test-kvks-file.ts similarity index 82% rename from common/web/types/test/kvk/test-kvks-file.ts rename to developer/src/common/web/utils/test/kvks/test-kvks-file.ts index 2cd1a965c67..80851647bd1 100644 --- a/common/web/types/test/kvk/test-kvks-file.ts +++ b/developer/src/common/web/utils/test/kvks/test-kvks-file.ts @@ -1,14 +1,14 @@ import * as fs from 'fs'; import 'mocha'; import { makePathToFixture } from '../helpers/index.js'; -import KvksFileReader from "../../src/kvk/kvks-file-reader.js"; -import KvksFileWriter from "../../src/kvk/kvks-file-writer.js"; +import KvksFileReader from "../../src/types/kvks/kvks-file-reader.js"; +import KvksFileWriter from "../../src/types/kvks/kvks-file-writer.js"; import { verify_khmer_angkor, verify_balochi_inpage } from './test-kvk-utils.js'; import { assert } from 'chai'; describe('kvks-file-reader', function() { it('should read a valid file', function() { - const path = makePathToFixture('kvk', 'khmer_angkor.kvks'); + const path = makePathToFixture('kvks', 'khmer_angkor.kvks'); const input = fs.readFileSync(path); const reader = new KvksFileReader(); @@ -23,7 +23,7 @@ describe('kvks-file-reader', function() { }); it('should read a valid file with bitmaps', function() { - const path = makePathToFixture('kvk', 'balochi_inpage.kvks'); + const path = makePathToFixture('kvks', 'balochi_inpage.kvks'); const input = fs.readFileSync(path); const reader = new KvksFileReader(); const kvks = reader.read(input); @@ -34,7 +34,7 @@ describe('kvks-file-reader', function() { }); it('should give a sensible error on a .kvk file', function() { - const path = makePathToFixture('kvk', 'khmer_angkor.kvk'); + const path = makePathToFixture('kvks', 'khmer_angkor.kvk'); const input = fs.readFileSync(path); const reader = new KvksFileReader(); @@ -44,7 +44,7 @@ describe('kvks-file-reader', function() { describe('kvks-file-writer', function() { it('should write a valid file', function() { - const path = makePathToFixture('kvk', 'khmer_angkor.kvks'); + const path = makePathToFixture('kvks', 'khmer_angkor.kvks'); const input = fs.readFileSync(path); const reader = new KvksFileReader(); diff --git a/developer/src/common/web/utils/test/test-is-valid-email.ts b/developer/src/common/web/utils/test/test-is-valid-email.ts new file mode 100644 index 00000000000..af2aff17a78 --- /dev/null +++ b/developer/src/common/web/utils/test/test-is-valid-email.ts @@ -0,0 +1,35 @@ +import { assert } from 'chai'; +import 'mocha'; +import { isValidEmail } from '../src/is-valid-email.js'; + +describe('test-is-valid-email', function () { + it('should accept a valid email address', function() { + [ + 'email@example.com', + 'email.example-true@example.com', + 'email@example-example.com', + 'email...@example.com', + 'email_example1@1example.com', + 'email+example@mail.example.com', + 'Email@Example.Com' + ].forEach(email => assert.isTrue(isValidEmail(email), `expected '${email}' to be valid`)); + + // This is accepted, but it's really a bit wonky. But that's an upstream + // issue with overly lax regex and not something we'll attempt to fix: + // + // assert.isTrue(isValidEmail('.@example.com')); + }); + + it('should reject invalid email addresses', function() { + [ + 'email@example.com, email2@example.com', + ' email@example.com', + 'email@example_domain.com', + 'email', + 'email@.', + 'email@example..com', + 'email@', + '@example', + ].forEach(email => assert.isFalse(isValidEmail(email), `expected '${email}' to be invalid`)); + }); +}); diff --git a/developer/src/common/web/utils/test/test-license.ts b/developer/src/common/web/utils/test/test-license.ts index 653f89ba69c..501f4f4711e 100644 --- a/developer/src/common/web/utils/test/test-license.ts +++ b/developer/src/common/web/utils/test/test-license.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import { assert } from 'chai'; import 'mocha'; import { makePathToFixture } from './helpers/index.js'; -import { validateMITLicense } from '../src/validate-mit-license.js'; +import { validateMITLicense } from '../src/utils/validate-mit-license.js'; function verifyLicenseFile(filename: string) { return validateMITLicense(fs.readFileSync(makePathToFixture('license', filename), 'utf-8')); diff --git a/developer/src/common/web/utils/test/tsconfig.json b/developer/src/common/web/utils/test/tsconfig.json index a28eeb03ab2..fdf5fd474f9 100644 --- a/developer/src/common/web/utils/test/tsconfig.json +++ b/developer/src/common/web/utils/test/tsconfig.json @@ -9,9 +9,6 @@ "moduleResolution": "node16", "allowSyntheticDefaultImports": true, "baseUrl": ".", - "paths": { - "@keymanapp/developer-test-helpers": ["../../test-helpers/index"], - }, }, "include": [ "**/test-*.ts", diff --git a/developer/src/common/web/utils/tsconfig.json b/developer/src/common/web/utils/tsconfig.json index c5980c82f9c..3be972dbcb6 100644 --- a/developer/src/common/web/utils/tsconfig.json +++ b/developer/src/common/web/utils/tsconfig.json @@ -7,7 +7,7 @@ "baseUrl": ".", }, "include": [ - "index.ts", "src/**/*.ts", + "src/deps/xml2js/*.js", ], } \ No newline at end of file diff --git a/developer/src/kmc-analyze/.eslintrc.cjs b/developer/src/kmc-analyze/.eslintrc.cjs index bd0835f0be3..148ad7579f8 100644 --- a/developer/src/kmc-analyze/.eslintrc.cjs +++ b/developer/src/kmc-analyze/.eslintrc.cjs @@ -5,7 +5,7 @@ module.exports = { overrides: [ { files:"src/**/*.ts", - extends: ["../../../common/web/eslint/eslintNoNodeImports.js"], + extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } ], rules: { diff --git a/developer/src/kmc-analyze/build.sh b/developer/src/kmc-analyze/build.sh index c07dd41a09a..cb7b79449be 100755 --- a/developer/src/kmc-analyze/build.sh +++ b/developer/src/kmc-analyze/build.sh @@ -11,6 +11,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" builder_describe "Keyman Developer Compiler Analysis Tools" \ "@/common/web/types" \ "@/developer/src/kmc-kmn" \ + "@/developer/src/common/web/utils" \ clean configure build api test publish \ "--npm-publish+ For publish, do a npm publish, not npm pack (only for CI)" \ "--dry-run,-n don't actually publish, just dry run" diff --git a/developer/src/kmc-analyze/package.json b/developer/src/kmc-analyze/package.json index f851aaa9878..8577ed25b33 100644 --- a/developer/src/kmc-analyze/package.json +++ b/developer/src/kmc-analyze/package.json @@ -34,8 +34,7 @@ "c8": "^7.12.0", "chalk": "^2.4.2", "mocha": "^8.4.0", - "ts-node": "^9.1.1", - "typescript": "^4.9.5" + "typescript": "^5.4.5" }, "repository": { "type": "git", diff --git a/developer/src/kmc-analyze/src/messages.ts b/developer/src/kmc-analyze/src/analyzer-messages.ts similarity index 92% rename from developer/src/kmc-analyze/src/messages.ts rename to developer/src/kmc-analyze/src/analyzer-messages.ts index 5c5ced08299..c895a2f3675 100644 --- a/developer/src/kmc-analyze/src/messages.ts +++ b/developer/src/kmc-analyze/src/analyzer-messages.ts @@ -1,5 +1,4 @@ -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/common-types"; -import { KeymanUrls } from "@keymanapp/developer-utils"; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException, KeymanUrls } from "@keymanapp/developer-utils"; const Namespace = CompilerErrorNamespace.Analyzer; const SevInfo = CompilerErrorSeverity.Info | Namespace; diff --git a/developer/src/kmc-analyze/src/index.ts b/developer/src/kmc-analyze/src/index.ts index c0999f43160..d8e750b41b7 100644 --- a/developer/src/kmc-analyze/src/index.ts +++ b/developer/src/kmc-analyze/src/index.ts @@ -5,4 +5,4 @@ export { AnalyzeOskCharacterUse, AnalyzeOskCharacterUseOptions } from './osk-character-use/index.js'; export { AnalyzeOskRewritePua } from './osk-rewrite-pua/index.js'; -export { AnalyzerMessages } from './messages.js'; \ No newline at end of file +export { AnalyzerMessages } from './analyzer-messages.js'; diff --git a/developer/src/kmc-analyze/src/osk-character-use/index.ts b/developer/src/kmc-analyze/src/osk-character-use/index.ts index e0570b99497..36fbf0e78cd 100644 --- a/developer/src/kmc-analyze/src/osk-character-use/index.ts +++ b/developer/src/kmc-analyze/src/osk-character-use/index.ts @@ -1,8 +1,8 @@ -import { CompilerCallbacks, KeymanFileTypes, KvksFile, KvksFileReader, TouchLayout, TouchLayoutFileReader } from "@keymanapp/common-types"; -import { CompilerMessages, Osk } from '@keymanapp/kmc-kmn'; -import { escapeMarkdownChar } from '@keymanapp/developer-utils'; +import { KeymanFileTypes, TouchLayout } from "@keymanapp/common-types"; +import { KmnCompilerMessages, Osk } from '@keymanapp/kmc-kmn'; +import { CompilerCallbacks, escapeMarkdownChar, KvksFile, KvksFileReader, TouchLayoutFileReader } from '@keymanapp/developer-utils'; import { getOskFromKmnFile } from "../util/get-osk-from-kmn-file.js"; -import { AnalyzerMessages } from "../messages.js"; +import { AnalyzerMessages } from "../analyzer-messages.js"; type StringRefUsageMap = {[index:string]: Osk.StringRefUsage[]}; @@ -167,13 +167,13 @@ export class AnalyzeOskCharacterUse { try { source = reader.read(this.callbacks.loadFile(filename)); } catch(e) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidKvksFile({filename, e})); + this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e})); return null; } let invalidKeys: string[] = []; const vk = reader.transform(source, invalidKeys); if(!vk) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidKvksFile({filename, e:null})); + this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e:null})); return null; } for(let key of vk.keys) { diff --git a/developer/src/kmc-analyze/src/osk-rewrite-pua/index.ts b/developer/src/kmc-analyze/src/osk-rewrite-pua/index.ts index eff545bb8e7..dc56ebfb3ad 100644 --- a/developer/src/kmc-analyze/src/osk-rewrite-pua/index.ts +++ b/developer/src/kmc-analyze/src/osk-rewrite-pua/index.ts @@ -1,7 +1,8 @@ -import { CompilerCallbacks, KeymanFileTypes, KvksFile, KvksFileReader, KvksFileWriter, TouchLayoutFileReader, TouchLayoutFileWriter } from "@keymanapp/common-types"; -import { CompilerMessages, Osk } from '@keymanapp/kmc-kmn'; +import { KeymanFileTypes } from "@keymanapp/common-types"; +import { CompilerCallbacks, KvksFile, KvksFileReader, KvksFileWriter, TouchLayoutFileReader, TouchLayoutFileWriter } from '@keymanapp/developer-utils'; +import { KmnCompilerMessages, Osk } from '@keymanapp/kmc-kmn'; import { getOskFromKmnFile } from "../util/get-osk-from-kmn-file.js"; -import { AnalyzerMessages } from "../messages.js"; +import { AnalyzerMessages } from "../analyzer-messages.js"; /** * @public @@ -106,13 +107,13 @@ export class AnalyzeOskRewritePua { try { source = reader.read(this.callbacks.loadFile(filename)); } catch(e) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidKvksFile({filename, e})); + this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e})); return null; } let invalidKeys: string[] = []; const vk = reader.transform(source, invalidKeys); if(!vk) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidKvksFile({filename, e:null})); + this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e:null})); return null; } const dirty = Osk.remapVisualKeyboard(vk, map); diff --git a/developer/src/kmc-analyze/src/util/get-osk-from-kmn-file.ts b/developer/src/kmc-analyze/src/util/get-osk-from-kmn-file.ts index 0e2ec289956..b9fd9cb6020 100644 --- a/developer/src/kmc-analyze/src/util/get-osk-from-kmn-file.ts +++ b/developer/src/kmc-analyze/src/util/get-osk-from-kmn-file.ts @@ -1,4 +1,5 @@ -import { CompilerCallbacks, KMX, KmxFileReader } from "@keymanapp/common-types"; +import { KMX, KmxFileReader } from "@keymanapp/common-types"; +import { CompilerCallbacks } from "@keymanapp/developer-utils"; import { KmnCompiler } from "@keymanapp/kmc-kmn"; export async function getOskFromKmnFile(callbacks: CompilerCallbacks, filename: string): Promise<{ diff --git a/developer/src/kmc-analyze/tsconfig.json b/developer/src/kmc-analyze/tsconfig.json index 30b4149bcf9..3951ed3a40b 100644 --- a/developer/src/kmc-analyze/tsconfig.json +++ b/developer/src/kmc-analyze/tsconfig.json @@ -5,11 +5,6 @@ "outDir": "build/src/", "rootDir": "src/", "baseUrl": ".", - "paths": { - "@keymanapp/common-types": [ "../../../common/web/types/src/main" ], - "@keymanapp/kmc-kmn": [ "../kmc-kmn/src/main" ], - "@keymanapp/developer-utils": ["../common/web/utils/index"], - }, }, "include": [ "src/**/*.ts" diff --git a/developer/src/kmc-keyboard-info/build.sh b/developer/src/kmc-keyboard-info/build.sh index cd425c31bb0..c85d76db015 100755 --- a/developer/src/kmc-keyboard-info/build.sh +++ b/developer/src/kmc-keyboard-info/build.sh @@ -10,6 +10,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" builder_describe "Build Keyman kmc keyboard-info Compiler module" \ "@/common/web/types" \ "@/developer/src/common/web/utils" \ + "@/developer/src/kmc-package" \ "clean" \ "configure" \ "build" \ diff --git a/developer/src/kmc-keyboard-info/package.json b/developer/src/kmc-keyboard-info/package.json index f02e3f6db01..4d4a4210257 100644 --- a/developer/src/kmc-keyboard-info/package.json +++ b/developer/src/kmc-keyboard-info/package.json @@ -33,8 +33,7 @@ "c8": "^7.12.0", "chalk": "^2.4.2", "mocha": "^8.4.0", - "ts-node": "^9.1.1", - "typescript": "^4.9.5" + "typescript": "^5.4.5" }, "mocha": { "spec": "build/test/**/test-*.js", diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts index d1a4c632c03..bf5d989718b 100644 --- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts +++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts @@ -1,4 +1,4 @@ -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/common-types"; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/developer-utils"; const Namespace = CompilerErrorNamespace.KeyboardInfoCompiler; // const SevInfo = CompilerErrorSeverity.Info | Namespace; @@ -52,8 +52,14 @@ export class KeyboardInfoCompilerMessages { static Error_FontFileCannotBeRead = (o:{filename: string}) => m(this.ERROR_FontFileCannotBeRead, `Font ${def(o.filename)} could not be parsed to extract a font family.`); -static ERROR_FontFileMetaDataIsInvalid = SevError | 0x000F; -static Error_FontFileMetaDataIsInvalid = (o:{filename: string,message:string}) => m(this.ERROR_FontFileMetaDataIsInvalid, + static ERROR_FontFileMetaDataIsInvalid = SevError | 0x000F; + static Error_FontFileMetaDataIsInvalid = (o:{filename: string,message:string}) => m( + this.ERROR_FontFileMetaDataIsInvalid, `Font ${def(o.filename)} meta data invalid: ${def(o.message)}.`); + + static ERROR_DescriptionIsMissing = SevError | 0x0010; + static Error_DescriptionIsMissing = (o:{filename:string}) => m( + this.ERROR_DescriptionIsMissing, + `The Info.Description field in the package ${def(o.filename)} is required, but is missing or empty.`); } diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts index 5337ba2fd4e..8e89bb81646 100644 --- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts +++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts @@ -5,10 +5,10 @@ import { minKeymanVersion } from "./min-keyman-version.js"; import { KeyboardInfoFile, KeyboardInfoFileIncludes, KeyboardInfoFileLanguageFont, KeyboardInfoFilePlatform } from "./keyboard-info-file.js"; -import { KeymanFileTypes, CompilerCallbacks, KmpJsonFile, KmxFileReader, KMX, KeymanTargets, KeymanCompiler, CompilerOptions, KeymanCompilerResult, KeymanCompilerArtifacts, KeymanCompilerArtifact } from "@keymanapp/common-types"; +import { KeymanFileTypes, KmpJsonFile, KmxFileReader, KMX, KeymanTargets } from "@keymanapp/common-types"; import { KeyboardInfoCompilerMessages } from "./keyboard-info-compiler-messages.js"; import langtags from "./imports/langtags.js"; -import { KeymanUrls, validateMITLicense } from "@keymanapp/developer-utils"; +import { CompilerCallbacks, KeymanCompiler, CompilerOptions, KeymanCompilerResult, KeymanCompilerArtifacts, KeymanCompilerArtifact, KeymanUrls, isValidEmail, validateMITLicense } from "@keymanapp/developer-utils"; import { KmpCompiler } from "@keymanapp/kmc-package"; import { SchemaValidators } from "@keymanapp/common-types"; @@ -238,6 +238,11 @@ export class KeyboardInfoCompiler implements KeymanCompiler { return null; } + if(!isValidEmail(match[2])) { + this.callbacks.reportMessage(KeyboardInfoCompilerMessages.Error_InvalidAuthorEmail({email:author.url})); + return null; + } + keyboard_info.authorEmail = match[2]; } } @@ -246,6 +251,9 @@ export class KeyboardInfoCompiler implements KeymanCompiler { if(kmpJsonData.info.description?.description) { keyboard_info.description = kmpJsonData.info.description.description.trim(); + } else { + this.callbacks.reportMessage(KeyboardInfoCompilerMessages.Error_DescriptionIsMissing({filename:sources.kpsFilename})); + return null; } // extract the language identifiers from the language metadata arrays for @@ -294,7 +302,7 @@ export class KeyboardInfoCompiler implements KeymanCompiler { } keyboard_info.packageIncludes = [...includes]; - keyboard_info.version = kmpJsonData.info.version.description; + keyboard_info.version = kmpJsonData.info?.version?.description ?? '1.0'; let minVersion = minKeymanVersion; const m = jsFile?.match(/this.KMINVER\s*=\s*(['"])(.*?)\1/); diff --git a/developer/src/kmc-keyboard-info/test/fixtures/missing-info-version-in-kps-11856/khmer_angkor.kps b/developer/src/kmc-keyboard-info/test/fixtures/missing-info-version-in-kps-11856/khmer_angkor.kps new file mode 100644 index 00000000000..dd50bee5d55 --- /dev/null +++ b/developer/src/kmc-keyboard-info/test/fixtures/missing-info-version-in-kps-11856/khmer_angkor.kps @@ -0,0 +1,67 @@ + + + + 15.0.266.0 + 7.0 + + + + + + ..\khmer_angkor\LICENSE.md + + + + + + + Khmer Angkor + © 2015-2022 SIL International + Makara Sok + https://keyman.com/keyboards/khmer_angkor + Khmer Unicode keyboard layout based on the NiDA keyboard layout. Automatically corrects many common keying errors. + + + + + + + ..\khmer_angkor\LICENSE.md + File LICENSE.md + 0 + .md + + + ..\khmer_angkor\build\khmer_angkor.js + File khmer_angkor.js + 0 + .js + + + ..\khmer_angkor\build\khmer_angkor.kvk + File khmer_angkor.kvk + 0 + .kvk + + + ..\khmer_angkor\build\khmer_angkor.kmx + Keyboard Khmer Angkor + 0 + .kmx + + + + + Khmer Angkor + khmer_angkor + 1.3 + + Central Khmer (Khmer, Cambodia) + + + + + + + + diff --git a/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/LICENSE.md b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/LICENSE.md new file mode 100644 index 00000000000..0a8c9054319 --- /dev/null +++ b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2022 SIL International + +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. diff --git a/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/.gitattributes b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/.gitattributes new file mode 100644 index 00000000000..7e3549570d7 --- /dev/null +++ b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/.gitattributes @@ -0,0 +1 @@ +khmer_angkor.js -text \ No newline at end of file diff --git a/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.js b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.js new file mode 100644 index 00000000000..f95f54848cf --- /dev/null +++ b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.js @@ -0,0 +1,5462 @@ +if(typeof keyman === 'undefined') { + console.log('Keyboard requires KeymanWeb 10.0 or later'); + if(typeof tavultesoft !== 'undefined') tavultesoft.keymanweb.util.alert("This keyboard requires KeymanWeb 10.0 or later"); +} else { +KeymanWeb.KR(new Keyboard_khmer_angkor()); +} +function Keyboard_khmer_angkor() +{ + var modCodes = keyman.osk.modifierCodes; + var keyCodes = keyman.osk.keyCodes; + + this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9; + this.KI="Keyboard_khmer_angkor"; + this.KN="Khmer Angkor"; + this.KMINVER="10.0"; + this.KV={F:' 1em "Khmer Busra Kbd"',K102:0}; + this.KV.KLS={ + "rightalt": ["‍","‌","@","","$","€","៙","៚","*","{","}","≈","","","","","ៜ","","ឯ","ឫ","ឨ","[","]","ឦ","ឱ","ឰ","ឩ","ឳ","\\","","","","+","-","×","÷",":","‘","’","ឝ","៘","៖","ៈ","","","","","","","<",">","#","&","ឞ",";","",",",".","/","","","","",""," "], + "rightalt-shift": ["","៱","៲","៳","៴","៵","៶","៷","៸","៹","៰","","","","","","᧠","᧡","᧢","᧣","᧤","᧥","᧦","᧧","᧨","᧩","᧪","᧫","","","","","᧬","᧭","᧮","᧯","᧰","᧱","᧲","᧳","᧴","᧵","᧶","","","","","","","᧷","᧸","᧹","᧺","᧻","᧼","᧽","᧾","᧿","","","","","","",""], + "default": ["«","១","២","៣","៤","៥","៦","៧","៨","៩","០","ឥ","ឲ","","","","ឆ","","","រ","ត","យ","","","","ផ","","ឪ","ឮ","","","","","ស","ដ","ថ","ង","ហ","","ក","ល","","","","","","","","","ឋ","ខ","ច","វ","ប","ន","ម","","។","","","","","","","​"], + "shift": ["»","!","ៗ","\"","៛","%","","","","(",")","","=","","","","ឈ","","","ឬ","ទ","","","","","ភ","","ឧ","ឭ","","","","","","ឌ","ធ","អ","ះ","ញ","គ","ឡ","","","","","","","","","ឍ","ឃ","ជ","","ព","ណ","","","៕","?","","","","","",""] + }; + this.KV.BK=(function(x){ + var + empty=Array.apply(null, Array(65)).map(String.prototype.valueOf,""), + result=[], v, i, + modifiers=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt']; + for(i=modifiers.length-1;i>=0;i--) { + v = x[modifiers[i]]; + if(v || result.length > 0) { + result=(v ? v : empty).slice().concat(result); + } + } + return result; + })(this.KV.KLS); + this.KDU=0; + this.KH=''; + this.KM=0; + this.KBVER="1.3"; + this.KMBM=modCodes.RALT | modCodes.SHIFT /* 0x0018 */; + this.KVKD="T_17D2_1780 T_17D2_1781 T_17D2_1782 T_17D2_1783 T_17D2_1784 T_17D2_1785 T_17D2_1786 T_17D2_1787 T_17D2_1788 T_17D2_1789 T_17D2_178A T_17D2_178B T_17D2_178C T_17D2_178D T_17D2_178E T_17D2_178F T_17D2_1790 T_17D2_1791 T_17D2_1792 T_17D2_1793 T_17D2_1794 T_17D2_1795 T_17D2_1796 T_17D2_1797 T_17D2_1798 T_17D2_1799 T_17D2_179A T_17D2_179B T_17D2_179C T_17D2_179D T_17D2_179E T_17D2_179F T_17D2_17A0 T_17D2_17A1 T_17D2_17A2 U_0030 U_0031 U_0032 U_0033 U_0034 U_0035 U_0036 U_0037 U_0038 U_0039"; + this.KVKL={ + "tablet": { + "displayUnderlying": false, + "layer": [ + { + "id": "default", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_1", + "text": "១" + }, + { + "id": "K_2", + "text": "២" + }, + { + "id": "K_3", + "text": "៣" + }, + { + "id": "K_4", + "text": "៤" + }, + { + "id": "K_5", + "text": "៥" + }, + { + "id": "K_6", + "text": "៦" + }, + { + "id": "K_7", + "text": "៧" + }, + { + "id": "K_8", + "text": "៨" + }, + { + "id": "K_9", + "text": "៩" + }, + { + "id": "K_0", + "text": "០" + }, + { + "id": "K_HYPHEN", + "text": "ឥ" + }, + { + "id": "K_EQUAL", + "text": "ឲ" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": "2", + "key": [ + { + "id": "K_Q", + "text": "ឆ", + "pad": "75" + }, + { + "id": "K_W", + "text": "" + }, + { + "id": "K_E", + "text": "" + }, + { + "id": "K_R", + "text": "រ" + }, + { + "id": "K_T", + "text": "ត" + }, + { + "id": "K_Y", + "text": "យ" + }, + { + "id": "K_U", + "text": "" + }, + { + "id": "K_I", + "text": "" + }, + { + "id": "K_O", + "text": "" + }, + { + "id": "K_P", + "text": "ផ" + }, + { + "id": "K_LBRKT", + "text": "" + }, + { + "id": "K_RBRKT", + "text": "ឪ" + }, + { + "id": "T_new_138", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "id": "K_BKQUOTE", + "text": "«" + }, + { + "id": "K_A", + "text": "" + }, + { + "id": "K_S", + "text": "ស" + }, + { + "id": "K_D", + "text": "ដ" + }, + { + "id": "K_F", + "text": "ថ" + }, + { + "id": "K_G", + "text": "ង" + }, + { + "id": "K_H", + "text": "ហ" + }, + { + "id": "K_J", + "text": "" + }, + { + "id": "K_K", + "text": "ក" + }, + { + "id": "K_L", + "text": "ល" + }, + { + "id": "K_COLON", + "text": "" + }, + { + "id": "K_QUOTE", + "text": "" + }, + { + "id": "K_BKSLASH", + "text": "ឮ" + } + ] + }, + { + "id": "4", + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "shift" + }, + { + "id": "K_oE2" + }, + { + "id": "K_Z", + "text": "ឋ" + }, + { + "id": "K_X", + "text": "ខ" + }, + { + "id": "K_C", + "text": "ច" + }, + { + "id": "K_V", + "text": "វ" + }, + { + "id": "K_B", + "text": "ប" + }, + { + "id": "K_N", + "text": "ន" + }, + { + "id": "K_M", + "text": "ម" + }, + { + "id": "K_COMMA", + "text": "" + }, + { + "id": "K_PERIOD", + "text": "។" + }, + { + "id": "K_SLASH", + "text": "" + }, + { + "id": "T_new_164", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "5", + "key": [ + { + "id": "K_LCONTROL", + "text": "*AltGr*", + "width": "160", + "sp": "1", + "nextlayer": "rightalt" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "160", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "​", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "160", + "sp": "1" + } + ] + } + ] + }, + { + "id": "rightalt", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_1", + "text": "‌" + }, + { + "id": "K_2", + "text": "@" + }, + { + "id": "K_3", + "text": "" + }, + { + "id": "K_4", + "text": "$" + }, + { + "id": "K_5", + "text": "€" + }, + { + "id": "K_6", + "text": "៙" + }, + { + "id": "K_7", + "text": "៚" + }, + { + "id": "K_8", + "text": "*" + }, + { + "id": "K_9", + "text": "{" + }, + { + "id": "K_0", + "text": "}" + }, + { + "id": "K_HYPHEN", + "text": "≈" + }, + { + "id": "K_EQUAL", + "text": "" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": "2", + "key": [ + { + "id": "K_Q", + "text": "ៜ", + "pad": "75" + }, + { + "id": "K_W", + "text": "" + }, + { + "id": "K_E", + "text": "ឯ" + }, + { + "id": "K_R", + "text": "ឫ" + }, + { + "id": "K_T", + "text": "ឨ" + }, + { + "id": "K_Y", + "text": "[" + }, + { + "id": "K_U", + "text": "]" + }, + { + "id": "K_I", + "text": "ឦ" + }, + { + "id": "K_O", + "text": "ឱ" + }, + { + "id": "K_P", + "text": "ឰ" + }, + { + "id": "K_LBRKT", + "text": "ឩ" + }, + { + "id": "K_RBRKT", + "text": "ឳ" + }, + { + "id": "T_new_307", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "id": "K_BKQUOTE", + "text": "‍" + }, + { + "id": "K_A", + "text": "+" + }, + { + "id": "K_S", + "text": "-" + }, + { + "id": "K_D", + "text": "×" + }, + { + "id": "K_F", + "text": "÷" + }, + { + "id": "K_G", + "text": ":" + }, + { + "id": "K_H", + "text": "‘" + }, + { + "id": "K_J", + "text": "’" + }, + { + "id": "K_K", + "text": "ឝ" + }, + { + "id": "K_L", + "text": "៘" + }, + { + "id": "K_COLON", + "text": "៖" + }, + { + "id": "K_QUOTE", + "text": "ៈ" + }, + { + "id": "K_BKSLASH", + "text": "\\" + } + ] + }, + { + "id": "4", + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "shift" + }, + { + "id": "K_oE2" + }, + { + "id": "K_Z", + "text": "<" + }, + { + "id": "K_X", + "text": ">" + }, + { + "id": "K_C", + "text": "#" + }, + { + "id": "K_V", + "text": "&" + }, + { + "id": "K_B", + "text": "ឞ" + }, + { + "id": "K_N", + "text": ";" + }, + { + "id": "K_M", + "text": "" + }, + { + "id": "K_COMMA", + "text": "," + }, + { + "id": "K_PERIOD", + "text": "." + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "id": "T_new_333", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "5", + "key": [ + { + "id": "K_LCONTROL", + "text": "*AltGr*", + "width": "160", + "sp": "2", + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "160", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": " ", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "160", + "sp": "1" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_1", + "text": "!" + }, + { + "id": "K_2", + "text": "ៗ" + }, + { + "id": "K_3", + "text": "\"" + }, + { + "id": "K_4", + "text": "៛" + }, + { + "id": "K_5", + "text": "%" + }, + { + "id": "K_6", + "text": "" + }, + { + "id": "K_7", + "text": "" + }, + { + "id": "K_8", + "text": "" + }, + { + "id": "K_9", + "text": "(" + }, + { + "id": "K_0", + "text": ")" + }, + { + "id": "K_HYPHEN", + "text": "" + }, + { + "id": "K_EQUAL", + "text": "=" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": "2", + "key": [ + { + "id": "K_Q", + "text": "ឈ", + "pad": "75" + }, + { + "id": "K_W", + "text": "" + }, + { + "id": "K_E", + "text": "" + }, + { + "id": "K_R", + "text": "ឬ" + }, + { + "id": "K_T", + "text": "ទ" + }, + { + "id": "K_Y", + "text": "" + }, + { + "id": "K_U", + "text": "" + }, + { + "id": "K_I", + "text": "" + }, + { + "id": "K_O", + "text": "" + }, + { + "id": "K_P", + "text": "ភ" + }, + { + "id": "K_LBRKT", + "text": "" + }, + { + "id": "K_RBRKT", + "text": "ឧ" + }, + { + "id": "T_new_364", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "id": "K_BKQUOTE", + "text": "»" + }, + { + "id": "K_A", + "text": "" + }, + { + "id": "K_S", + "text": "" + }, + { + "id": "K_D", + "text": "ឌ" + }, + { + "id": "K_F", + "text": "ធ" + }, + { + "id": "K_G", + "text": "អ" + }, + { + "id": "K_H", + "text": "ះ" + }, + { + "id": "K_J", + "text": "ញ" + }, + { + "id": "K_K", + "text": "គ" + }, + { + "id": "K_L", + "text": "ឡ" + }, + { + "id": "K_COLON", + "text": "" + }, + { + "id": "K_QUOTE", + "text": "" + }, + { + "id": "K_BKSLASH", + "text": "ឭ" + } + ] + }, + { + "id": "4", + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "2", + "nextlayer": "default" + }, + { + "id": "K_oE2" + }, + { + "id": "K_Z", + "text": "ឍ" + }, + { + "id": "K_X", + "text": "ឃ" + }, + { + "id": "K_C", + "text": "ជ" + }, + { + "id": "K_V", + "text": "" + }, + { + "id": "K_B", + "text": "ព" + }, + { + "id": "K_N", + "text": "ណ" + }, + { + "id": "K_M", + "text": "" + }, + { + "id": "K_COMMA", + "text": "" + }, + { + "id": "K_PERIOD", + "text": "៕" + }, + { + "id": "K_SLASH", + "text": "?" + }, + { + "id": "T_new_390", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "5", + "key": [ + { + "id": "K_LCONTROL", + "text": "*AltGr*", + "width": "160", + "sp": "1", + "nextlayer": "rightalt" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "160", + "sp": "1" + }, + { + "id": "K_SPACE", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "160", + "sp": "1" + } + ] + } + ] + } + ], + "font": "Khmer Busra Kbd", + "fontsize": "0.8em" + }, + "phone": { + "layer": [ + { + "id": "default", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_Q", + "text": "ឆ", + "sk": [ + { + "text": "ឈ", + "id": "K_Q", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_1786" + }, + { + "text": "", + "id": "T_17D2_1788" + } + ] + }, + { + "id": "K_W", + "text": "", + "sk": [ + { + "text": "", + "id": "K_W", + "layer": "shift" + } + ] + }, + { + "id": "K_E", + "text": "", + "sk": [ + { + "text": "", + "id": "K_E", + "layer": "shift" + }, + { + "text": "", + "id": "K_S", + "layer": "shift" + }, + { + "text": "", + "id": "K_V", + "layer": "shift" + }, + { + "text": "ឯ", + "id": "U_17AF" + }, + { + "text": "ឰ", + "id": "U_17B0" + } + ] + }, + { + "id": "K_R", + "text": "រ", + "sk": [ + { + "text": "", + "id": "T_17D2_179A" + }, + { + "text": "ឫ", + "id": "U_17AB" + }, + { + "text": "ឬ", + "id": "U_17AC" + } + ] + }, + { + "id": "K_T", + "text": "ត", + "sk": [ + { + "text": "ទ", + "id": "K_T", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_178F" + }, + { + "text": "", + "id": "T_17D2_1791", + "layer": "default" + } + ] + }, + { + "id": "K_Y", + "text": "យ", + "sk": [ + { + "text": "", + "id": "T_17D2_1799" + } + ] + }, + { + "id": "K_U", + "text": "", + "sk": [ + { + "text": "", + "id": "K_U", + "layer": "shift" + }, + { + "text": "", + "id": "K_Y", + "layer": "shift" + }, + { + "text": "ឧ", + "id": "U_17A7" + }, + { + "text": "ឪ", + "id": "U_17AA", + "layer": "shift" + }, + { + "text": "ឩ", + "id": "U_17A9", + "layer": "shift" + }, + { + "text": "ឨ", + "id": "U_17A8" + } + ] + }, + { + "id": "K_I", + "text": "", + "sk": [ + { + "text": "", + "id": "K_I", + "layer": "shift" + }, + { + "text": "ឥ", + "id": "U_17A5" + }, + { + "text": "ឦ", + "id": "U_17A6", + "layer": "shift" + } + ] + }, + { + "id": "K_O", + "text": "", + "sk": [ + { + "text": "", + "id": "K_O", + "layer": "shift" + }, + { + "text": "", + "id": "K_LBRKT" + }, + { + "text": "", + "id": "K_LBRKT", + "layer": "shift" + }, + { + "text": "", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": "ឱ", + "id": "U_17B1" + }, + { + "text": "ឲ", + "id": "U_17B2" + }, + { + "text": "ឳ", + "id": "U_17B3", + "layer": "shift" + } + ] + }, + { + "id": "K_P", + "text": "ផ", + "sk": [ + { + "text": "ភ", + "id": "K_P", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_1795" + }, + { + "text": "", + "id": "T_17D2_1797", + "layer": "default" + } + ] + } + ] + }, + { + "id": "2", + "key": [ + { + "id": "K_A", + "text": "", + "width": "100", + "sk": [ + { + "text": "", + "id": "K_A", + "layer": "shift" + } + ] + }, + { + "id": "K_S", + "text": "ស", + "sk": [ + { + "text": "", + "id": "T_17D2_179F" + }, + { + "text": "ឝ", + "id": "U_179D" + }, + { + "text": "ឞ", + "id": "U_179E" + } + ] + }, + { + "id": "K_D", + "text": "ដ", + "sk": [ + { + "text": "ឌ", + "id": "K_D", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_178A" + }, + { + "text": "", + "id": "T_17D2_178C", + "layer": "default" + } + ] + }, + { + "id": "K_F", + "text": "ថ", + "sk": [ + { + "text": "ធ", + "id": "K_F", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_1790" + }, + { + "text": "", + "id": "T_17D2_1792", + "layer": "default" + } + ] + }, + { + "id": "K_G", + "text": "ង", + "sk": [ + { + "text": "អ", + "id": "K_G", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_1784" + }, + { + "text": "", + "id": "T_17D2_17A2", + "layer": "default" + } + ] + }, + { + "id": "K_H", + "text": "ហ", + "sk": [ + { + "text": "", + "id": "T_17D2_17A0" + }, + { + "text": "ះ", + "id": "K_H", + "layer": "shift" + }, + { + "text": "ៈ", + "id": "U_17C8" + } + ] + }, + { + "id": "K_J", + "text": "ញ", + "layer": "shift", + "sk": [ + { + "text": "", + "id": "T_17D2_1789" + } + ] + }, + { + "id": "K_K", + "text": "ក", + "sk": [ + { + "text": "គ", + "id": "K_K", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_1780" + }, + { + "text": "", + "id": "T_17D2_1782" + } + ] + }, + { + "id": "K_L", + "text": "ល", + "sk": [ + { + "text": "ឡ", + "id": "K_L", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_179B" + }, + { + "text": "ឭ", + "id": "U_17AD" + }, + { + "text": "ឮ", + "id": "U_17AE" + } + ] + }, + { + "id": "K_COLON", + "text": "" + } + ] + }, + { + "id": "3", + "key": [ + { + "id": "K_Z", + "text": "ឋ", + "sk": [ + { + "text": "ឍ", + "id": "K_Z", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_178B" + }, + { + "text": "", + "id": "T_17D2_178D", + "layer": "default" + } + ] + }, + { + "id": "K_X", + "text": "ខ", + "sk": [ + { + "text": "ឃ", + "id": "K_X", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_1781" + }, + { + "text": "", + "id": "T_17D2_1783", + "layer": "default" + } + ] + }, + { + "id": "K_C", + "text": "ច", + "sk": [ + { + "text": "ជ", + "id": "K_C", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_1785" + }, + { + "text": "", + "id": "T_17D2_1787", + "layer": "default" + } + ] + }, + { + "id": "K_V", + "text": "វ", + "sk": [ + { + "text": "", + "id": "T_17D2_179C" + } + ] + }, + { + "id": "K_B", + "text": "ប", + "sk": [ + { + "text": "ព", + "id": "K_B", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_1794" + }, + { + "text": "", + "id": "T_17D2_1796", + "layer": "default" + } + ] + }, + { + "id": "K_N", + "text": "ន", + "sk": [ + { + "text": "ណ", + "id": "K_N", + "layer": "shift" + }, + { + "text": "", + "id": "T_17D2_1793" + }, + { + "text": "", + "id": "T_17D2_178E", + "layer": "default" + } + ] + }, + { + "id": "K_M", + "text": "ម", + "sk": [ + { + "text": "", + "id": "T_17D2_1798" + }, + { + "text": "", + "id": "K_M", + "layer": "shift" + } + ] + }, + { + "id": "K_COMMA", + "text": "", + "sk": [ + { + "text": "", + "id": "K_COMMA", + "layer": "shift" + }, + { + "text": "", + "id": "K_6", + "layer": "shift" + }, + { + "text": "", + "id": "K_7", + "layer": "shift" + }, + { + "text": "", + "id": "K_8", + "layer": "shift" + }, + { + "text": "", + "id": "K_HYPHEN", + "layer": "shift" + }, + { + "text": "", + "id": "U_17D1", + "layer": "shift" + }, + { + "text": "", + "id": "U_17DD", + "layer": "shift" + }, + { + "text": "", + "id": "U_17CE", + "layer": "shift" + } + ] + }, + { + "id": "K_QUOTE", + "text": "", + "width": "100", + "sk": [ + { + "text": "", + "id": "K_QUOTE", + "layer": "shift" + }, + { + "text": "", + "id": "K_SLASH" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": "4", + "key": [ + { + "id": "K_NUMLOCK", + "text": "១២៣", + "width": "140", + "sp": "1", + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "​", + "width": "555", + "sk": [ + { + "text": " ", + "id": "U_0020", + "layer": "default" + } + ] + }, + { + "id": "K_PERIOD", + "text": "។", + "width": "120", + "sk": [ + { + "text": "៕", + "id": "K_PERIOD", + "layer": "shift" + }, + { + "text": "!", + "id": "U_0021" + }, + { + "text": "?", + "id": "U_003F" + } + ] + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "140", + "sp": "1" + } + ] + } + ] + }, + { + "id": "numeric", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_1", + "text": "១", + "sk": [ + { + "text": "1", + "id": "U_0031" + } + ] + }, + { + "id": "K_2", + "text": "២", + "sk": [ + { + "text": "2", + "id": "U_0032" + } + ] + }, + { + "id": "K_3", + "text": "៣", + "sk": [ + { + "text": "3", + "id": "U_0033" + } + ] + }, + { + "id": "K_4", + "text": "៤", + "sk": [ + { + "text": "4", + "id": "U_0034" + } + ] + }, + { + "id": "K_5", + "text": "៥", + "sk": [ + { + "text": "5", + "id": "U_0035" + } + ] + }, + { + "id": "K_6", + "text": "៦", + "sk": [ + { + "text": "6", + "id": "U_0036" + } + ] + }, + { + "id": "K_7", + "text": "៧", + "sk": [ + { + "text": "7", + "id": "U_0037" + } + ] + }, + { + "id": "K_8", + "text": "៨", + "sk": [ + { + "text": "8", + "id": "U_0038" + } + ] + }, + { + "id": "K_9", + "text": "៩", + "sk": [ + { + "text": "9", + "id": "U_0039" + } + ] + }, + { + "id": "K_0", + "text": "០", + "sk": [ + { + "text": "0", + "id": "U_0030" + }, + { + "text": "", + "id": "U_17D3" + } + ] + } + ] + }, + { + "id": "2", + "key": [ + { + "id": "U_0040", + "text": "@", + "sk": [ + { + "text": "©", + "id": "U_00A9" + }, + { + "text": "®", + "id": "U_00AE" + } + ] + }, + { + "id": "U_0023", + "text": "#", + "sk": [ + { + "text": "№", + "id": "U_2116" + }, + { + "text": "~", + "id": "U_007E" + } + ] + }, + { + "id": "U_17DB", + "text": "៛", + "sk": [ + { + "text": "$", + "id": "U_0024" + }, + { + "text": "฿", + "id": "U_0E3F" + }, + { + "text": "¢", + "id": "U_00A2" + }, + { + "text": "£", + "id": "U_00A3" + }, + { + "text": "¥", + "id": "U_00A5" + } + ] + }, + { + "id": "U_0026", + "text": "&" + }, + { + "id": "U_0025", + "text": "%", + "sk": [ + { + "text": "‰", + "id": "U_2030" + }, + { + "text": "‱", + "id": "U_2031" + } + ] + }, + { + "id": "U_002B", + "text": "+", + "sk": [ + { + "text": "-", + "id": "U_002D" + }, + { + "text": "×", + "id": "U_00D7" + }, + { + "text": "÷", + "id": "U_00F7" + }, + { + "text": "±", + "id": "U_00B1" + } + ] + }, + { + "id": "U_003D", + "text": "=", + "sk": [ + { + "text": "_", + "id": "U_005F" + }, + { + "text": "≠", + "id": "U_2260" + } + ] + }, + { + "id": "U_002A", + "text": "*", + "sk": [ + { + "text": "^", + "id": "U_005E" + } + ] + }, + { + "id": "U_003F", + "text": "?", + "sk": [ + { + "text": "¿", + "id": "U_00BF" + } + ] + }, + { + "id": "U_0021", + "text": "!", + "sk": [ + { + "text": "¡", + "id": "U_00A1" + } + ] + } + ] + }, + { + "id": "3", + "key": [ + { + "id": "U_2018", + "text": "‘", + "sk": [ + { + "text": "’", + "id": "U_2019" + } + ] + }, + { + "id": "U_201C", + "text": "“", + "sk": [ + { + "text": "”", + "id": "U_201D" + } + ] + }, + { + "id": "U_00AB", + "text": "«", + "sk": [ + { + "text": "»", + "id": "U_00BB" + } + ] + }, + { + "id": "U_002F", + "text": "/", + "sk": [ + { + "text": "\\", + "id": "U_005C" + }, + { + "text": "|", + "id": "U_007C" + }, + { + "text": "¦", + "id": "U_00A6" + } + ] + }, + { + "id": "U_0028", + "text": "(", + "sk": [ + { + "text": ")", + "id": "U_0029" + }, + { + "text": "[", + "id": "U_005B" + }, + { + "text": "]", + "id": "U_005D" + }, + { + "text": "{", + "id": "U_007B" + }, + { + "text": "}", + "id": "U_007D" + } + ] + }, + { + "id": "U_17D9", + "text": "៙", + "sk": [ + { + "text": "៚", + "id": "U_17DA" + }, + { + "text": "ៜ", + "id": "U_17DC" + }, + { + "text": "§", + "id": "U_00A7" + }, + { + "text": "Ø", + "id": "U_00D8" + } + ] + }, + { + "id": "U_17D7", + "text": "ៗ" + }, + { + "id": "U_003C", + "text": "<", + "sk": [ + { + "text": "≤", + "id": "U_2264" + }, + { + "text": ">", + "id": "U_003E" + }, + { + "text": "≥", + "id": "U_2265" + } + ] + }, + { + "id": "U_17D6", + "text": "៖", + "sk": [ + { + "text": ":", + "id": "U_003A" + }, + { + "text": ";", + "id": "U_003B" + }, + { + "text": "…", + "id": "U_2026" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "sp": "1" + } + ] + }, + { + "id": "4", + "key": [ + { + "id": "K_LCONTROL", + "text": "១២៣", + "width": "140", + "sp": "2", + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "​", + "width": "555", + "layer": "shift" + }, + { + "id": "K_PERIOD", + "text": "។", + "width": "120", + "sk": [ + { + "text": "៕", + "id": "K_PERIOD", + "layer": "shift" + }, + { + "text": "!", + "id": "U_0021" + }, + { + "text": "?", + "id": "U_003F" + } + ] + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "140", + "sp": "1" + } + ] + } + ] + } + ], + "displayUnderlying": false, + "font": "Khmer Busra Kbd", + "fontsize": "0.8em" + } +}; + this.s_c_key_11=['','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','']; + this.s_c_out_12="កខគឃងចឆជឈញដឋឌឍណតថទធនបផពភមយរលវសហឡអឝឞ"; + this.s_v_gen_key_13=['','','','','','','','','','','','','','','','']; + this.s_v_gen_14="ាិីឹឺុូួើឿៀេែៃោៅ"; + this.s_v_pseudo_key_15=['','','']; + this.s_v_pseudo_16="ំះៈ"; + this.s_v_key_17=['','','','','','','','','','','','','','','','','','','']; + this.s_v_out_18="ាិីឹឺុូួើឿៀេែៃោៅំះៈ"; + this.s_v_any_19="ាិីឹឺុូួើឿៀេែៃោៅំះៈ"; + this.s_v_combo_R_20="េោុិីឹែ"; + this.s_v_combo_N_21="ាុ"; + this.s_v_combo_22="េោុិីឹែាុ"; + this.s_ind_v_key_23=['','','','','','','','','','','','','','','']; + this.s_ind_v_out_24="ឥឦឧឨឩឪឫឬឭឮឯឰឱឲឳ"; + this.s_diacritic_key_25=['','','','','','','','','','','']; + this.s_diacritic_out_26="់័៌៏៍ៈ៎៑៝ៜ្"; + this.s_c_shifter_key_27=['','']; + this.s_c_shifter_28="៉៊"; + this.s_punct_key_29=['','','','','','','','']; + this.s_punct_out_30="។៕៖ៗ៘៙៚៓"; + this.s_latin_punct_key_31=['','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','']; + this.s_latin_punct_out_32="«»()!\"%=?{}\\@*,×./[]‍‌+-÷:≈‘’;<>#&"; + this.s_spaces_key_33=['','','']; + this.s_spaces_out_34="​  "; + this.s_currency_key_35=['','','']; + this.s_currency_out_36="៛$€"; + this.s_digit_key_37=['','','','','','','','','','']; + this.s_digit_out_38="០១២៣៤៥៦៧៨៩"; + this.s_lek_attak_key_39=['','','','','','','','','','']; + this.s_lek_attak_out_40="៰៱៲៳៴៵៶៷៸៹"; + this.s_lunar_date_key_41=['','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','']; + this.s_lunar_date_out_42="᧬᧻᧹᧮᧢᧯᧰᧱᧧᧲᧳᧴᧽᧼᧨᧩᧠᧣᧭᧤᧦᧺᧡᧸᧥᧷᧵᧾᧿᧪᧫᧶"; + this.s_input_subcons_43=['','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','']; + this.s_subcons_44="កខគឃងចឆជឈញដឋឌឍណតថទធនបផពភមយរលវឝឞសហឡអ"; + this.s_arabic_digit_key_45=['','','','','','','','','','']; + this.s_arabic_digit_out_46="0123456789"; + this.s_v_above_47="ិីឹឺើ័"; + this.s_shiftable_c_1st_48="សហអ"; + this.s_shiftable_BA_49="ប"; + this.s_shiftable_c_2nd_50="ងញមយរវនល"; + this.s_shiftable_c_2nd_with_BA_51="ងញមយរវនលប"; + this.s_c_2nd_combo_LO_52="យមងបវ"; + this.s_c_2nd_combo_MO_53="យលងរ"; + this.s_c_1st_combo_LO_54="បហអ"; + this.s_c_1st_combo_MO_55="ហសអ"; + this.s_c_combo_SA_56="បយលមនញងរវអ"; + this.s_c_combo_QA_57="ឆឈបផតទ"; + this.s_c_combo_HA_58="វឣ"; + this.s62="touch"; + this.KVS=[]; + this.gs=function(t,e) { + return this.g_main_0(t,e); + }; + this.gs=function(t,e) { + return this.g_main_0(t,e); + }; + this.g_main_0=function(t,e) { + var k=KeymanWeb,r=0,m=0; + if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_BKSP /* 0x08 */)) { + if(k.KFCM(2,t,['្',{t:'a',a:this.s_c_out_12}])&&k.KIFS(31,this.s62,t)){ + r=m=1; // Line 266 + k.KDC(2,t); + } + else if(k.KFCM(2,t,[{t:'a',a:this.s_v_combo_N_21},'ំ'])){ + r=m=1; // Line 229 + k.KDC(2,t); + } + else if(k.KFCM(2,t,[{t:'a',a:this.s_v_combo_R_20},'ះ'])){ + r=m=1; // Line 230 + k.KDC(2,t); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_B /* 0x42 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ឞ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_K /* 0x4B */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ឝ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_QUOTE /* 0xDE */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ៈ"); + } + else if(1){ + r=m=1; // Line 191 + k.KDC(0,t); + k.KO(-1,t,"ៈ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_E /* 0x45 */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឯ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_I /* 0x49 */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឦ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_O /* 0x4F */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឱ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_P /* 0x50 */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឰ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_R /* 0x52 */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឫ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_T /* 0x54 */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឨ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_LBRKT /* 0xDB */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឩ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_RBRKT /* 0xDD */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឳ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_3 /* 0x33 */)) { + if(1){ + r=m=1; // Line 191 + k.KDC(0,t); + k.KO(-1,t,"៑"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_Q /* 0x51 */)) { + if(1){ + r=m=1; // Line 191 + k.KDC(0,t); + k.KO(-1,t,"ៜ"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_W /* 0x57 */)) { + if(1){ + r=m=1; // Line 191 + k.KDC(0,t); + k.KO(-1,t,"៝"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_EQUAL /* 0xBB */)) { + if(1){ + r=m=1; // Line 191 + k.KDC(0,t); + k.KO(-1,t,"៎"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_6 /* 0x36 */)) { + if(1){ + r=m=1; // Line 193 + k.KDC(0,t); + k.KO(-1,t,"៙"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_7 /* 0x37 */)) { + if(1){ + r=m=1; // Line 193 + k.KDC(0,t); + k.KO(-1,t,"៚"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_L /* 0x4C */)) { + if(1){ + r=m=1; // Line 193 + k.KDC(0,t); + k.KO(-1,t,"៘"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_M /* 0x4D */)) { + if(1){ + r=m=1; // Line 193 + k.KDC(0,t); + k.KO(-1,t,"៓"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_COLON /* 0xBA */)) { + if(1){ + r=m=1; // Line 193 + k.KDC(0,t); + k.KO(-1,t,"៖"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_0 /* 0x30 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"}"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_1 /* 0x31 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"‌"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_2 /* 0x32 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"@"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_8 /* 0x38 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"*"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_9 /* 0x39 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"{"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_A /* 0x41 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"+"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_C /* 0x43 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"#"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_D /* 0x44 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"×"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_F /* 0x46 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"÷"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_G /* 0x47 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,":"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_H /* 0x48 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"‘"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_J /* 0x4A */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"’"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_N /* 0x4E */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,";"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_S /* 0x53 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"-"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_U /* 0x55 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"]"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_V /* 0x56 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"&"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_X /* 0x58 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,">"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_Y /* 0x59 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"["); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_Z /* 0x5A */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"<"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_COMMA /* 0xBC */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,","); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_HYPHEN /* 0xBD */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"≈"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_PERIOD /* 0xBE */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"."); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_SLASH /* 0xBF */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"/"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_BKQUOTE /* 0xC0 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"‍"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_BKSLASH /* 0xDC */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"\\"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_4 /* 0x34 */)) { + if(1){ + r=m=1; // Line 195 + k.KDC(0,t); + k.KO(-1,t,"$"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_5 /* 0x35 */)) { + if(1){ + r=m=1; // Line 195 + k.KDC(0,t); + k.KO(-1,t,"€"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_0 /* 0x30 */)) { + if(1){ + r=m=1; // Line 197 + k.KDC(0,t); + k.KO(-1,t,"៰"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_1 /* 0x31 */)) { + if(1){ + r=m=1; // Line 197 + k.KDC(0,t); + k.KO(-1,t,"៱"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_2 /* 0x32 */)) { + if(1){ + r=m=1; // Line 197 + k.KDC(0,t); + k.KO(-1,t,"៲"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_3 /* 0x33 */)) { + if(1){ + r=m=1; // Line 197 + k.KDC(0,t); + k.KO(-1,t,"៳"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_4 /* 0x34 */)) { + if(1){ + r=m=1; // Line 197 + k.KDC(0,t); + k.KO(-1,t,"៴"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_5 /* 0x35 */)) { + if(1){ + r=m=1; // Line 197 + k.KDC(0,t); + k.KO(-1,t,"៵"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_6 /* 0x36 */)) { + if(1){ + r=m=1; // Line 197 + k.KDC(0,t); + k.KO(-1,t,"៶"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_7 /* 0x37 */)) { + if(1){ + r=m=1; // Line 197 + k.KDC(0,t); + k.KO(-1,t,"៷"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_8 /* 0x38 */)) { + if(1){ + r=m=1; // Line 197 + k.KDC(0,t); + k.KO(-1,t,"៸"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_9 /* 0x39 */)) { + if(1){ + r=m=1; // Line 197 + k.KDC(0,t); + k.KO(-1,t,"៹"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_A /* 0x41 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧬"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_B /* 0x42 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧻"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_C /* 0x43 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧹"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_D /* 0x44 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧮"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_E /* 0x45 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧢"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_F /* 0x46 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧯"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_G /* 0x47 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧰"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_H /* 0x48 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧱"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_I /* 0x49 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧧"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_J /* 0x4A */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧲"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_K /* 0x4B */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧳"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_L /* 0x4C */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧴"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_M /* 0x4D */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧽"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_N /* 0x4E */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧼"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_O /* 0x4F */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧨"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_P /* 0x50 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧩"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_Q /* 0x51 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧠"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_R /* 0x52 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧣"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_S /* 0x53 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧭"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_T /* 0x54 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧤"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_U /* 0x55 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧦"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_V /* 0x56 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧺"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_W /* 0x57 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧡"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_X /* 0x58 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧸"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_Y /* 0x59 */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧥"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_Z /* 0x5A */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧷"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_COLON /* 0xBA */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧵"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_COMMA /* 0xBC */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧾"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_PERIOD /* 0xBE */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧿"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_LBRKT /* 0xDB */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧪"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_RBRKT /* 0xDD */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧫"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4018 */, keyCodes.K_QUOTE /* 0xDE */)) { + if(1){ + r=m=1; // Line 198 + k.KDC(0,t); + k.KO(-1,t,"᧶"); + } + } + else if(k.KKM(e, modCodes.RALT | modCodes.VIRTUAL_KEY /* 0x4008 */, keyCodes.K_SPACE /* 0x20 */)) { + if(1){ + r=m=1; // Line 199 + k.KDC(0,t); + k.KO(-1,t," "); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x100)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ក"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x101)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ខ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x102)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្គ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x103)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ឃ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x104)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ង"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x105)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ច"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x106)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ឆ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x107)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ជ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x108)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ឈ"); + } + } + if(m) {} + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x109)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ញ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x10A)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ដ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x10B)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ឋ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x10C)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ឌ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x10D)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ឍ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x10E)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ណ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x10F)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ត"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x110)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ថ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x111)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ទ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x112)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ធ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x113)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ន"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x114)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ប"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x115)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ផ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x116)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ព"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x117)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ភ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x118)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ម"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x119)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្យ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x11A)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្រ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x11B)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ល"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x11C)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្វ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x11D)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ឝ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x11E)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ឞ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x11F)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ស"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x120)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ហ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x121)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្ឡ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, 0x122)) { + if(1){ + r=m=1; // Line 262 + k.KDC(0,t); + k.KO(-1,t,"្អ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_NPSTAR /* 0x6A */)) { + if(1){ + r=m=1; // Line 268 + k.KDC(0,t); + k.KO(-1,t,"*"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_NPSTAR /* 0x6A */)) { + if(1){ + r=m=1; // Line 269 + k.KDC(0,t); + k.KO(-1,t,"*"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_NPPLUS /* 0x6B */)) { + if(1){ + r=m=1; // Line 270 + k.KDC(0,t); + k.KO(-1,t,"+"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_NPPLUS /* 0x6B */)) { + if(1){ + r=m=1; // Line 271 + k.KDC(0,t); + k.KO(-1,t,"+"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_NPMINUS /* 0x6D */)) { + if(1){ + r=m=1; // Line 272 + k.KDC(0,t); + k.KO(-1,t,"-"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_NPMINUS /* 0x6D */)) { + if(1){ + r=m=1; // Line 273 + k.KDC(0,t); + k.KO(-1,t,"-"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_NPDOT /* 0x6E */)) { + if(1){ + r=m=1; // Line 274 + k.KDC(0,t); + k.KO(-1,t,"."); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_NPDOT /* 0x6E */)) { + if(1){ + r=m=1; // Line 275 + k.KDC(0,t); + k.KO(-1,t,"."); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_NPSLASH /* 0x6F */)) { + if(1){ + r=m=1; // Line 276 + k.KDC(0,t); + k.KO(-1,t,"/"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_NPSLASH /* 0x6F */)) { + if(1){ + r=m=1; // Line 277 + k.KDC(0,t); + k.KO(-1,t,"/"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_SPACE /* 0x20 */)) { + if(k.KFCM(1,t,['​'])){ + r=m=1; // Line 225 + k.KDC(1,t); + k.KO(-1,t," "); + } + else if(1){ + r=m=1; // Line 199 + k.KDC(0,t); + k.KO(-1,t,"​"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_SPACE /* 0x20 */)) { + if(1){ + r=m=1; // Line 199 + k.KDC(0,t); + k.KO(-1,t," "); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_1 /* 0x31 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"!"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_QUOTE /* 0xDE */)) { + if(k.KFCM(3,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ'])){ + r=m=1; // Line 244 + k.KDC(3,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្អ៉"); + k.KB(t); + } + else if(k.KFCM(3,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54}])){ + r=m=1; // Line 245 + k.KDC(3,t); + k.KO(-1,t,"ល្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៉"); + k.KB(t); + } + else if(k.KFCM(3,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55}])){ + r=m=1; // Line 246 + k.KDC(3,t); + k.KO(-1,t,"ម្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៉"); + k.KB(t); + } + else if(k.KFCM(3,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56}])){ + r=m=1; // Line 247 + k.KDC(3,t); + k.KO(-1,t,"ស្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៉"); + k.KB(t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ'])){ + r=m=1; // Line 248 + k.KDC(3,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្ហ៉"); + k.KB(t); + } + else if(k.KFCM(3,t,['អ','្','ង'])){ + r=m=1; // Line 249 + k.KDC(3,t); + k.KO(-1,t,"អ្ង៉"); + k.KB(t); + } + else if(k.KFCM(3,t,['អ','្','វ'])){ + r=m=1; // Line 250 + k.KDC(3,t); + k.KO(-1,t,"អ្វ៉"); + k.KB(t); + } + else if(k.KFCM(1,t,[{t:'a',a:this.s_c_shifter_28}])){ + r=m=1; // Line 215 + k.KDC(1,t); + k.KIO(-1,this.s_c_shifter_28,1,t); + k.KB(t); + } + else if(k.KFCM(1,t,[{t:'a',a:this.s_shiftable_c_1st_48}])){ + r=m=1; // Line 239 + k.KDC(1,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៉"); + k.KB(t); + } + else if(1){ + r=m=1; // Line 192 + k.KDC(0,t); + k.KO(-1,t,"៉"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_3 /* 0x33 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"\""); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_4 /* 0x34 */)) { + if(1){ + r=m=1; // Line 195 + k.KDC(0,t); + k.KO(-1,t,"៛"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_5 /* 0x35 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"%"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_7 /* 0x37 */)) { + if(1){ + r=m=1; // Line 191 + k.KDC(0,t); + k.KO(-1,t,"័"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_QUOTE /* 0xDE */)) { + if(k.KFCM(2,t,['្',{t:'a',a:this.s_c_out_12}])){ + r=m=1; // Line 214 + k.KDC(2,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_out_12,2,t); + k.KB(t); + } + else if(k.KFCM(1,t,[{t:'a',a:this.s_v_gen_14}])){ + r=m=1; // Line 211 + k.KDC(1,t); + k.KIO(-1,this.s_v_gen_14,1,t); + k.KB(t); + } + else if(k.KFCM(1,t,[{t:'a',a:this.s_v_pseudo_16}])){ + r=m=1; // Line 212 + k.KDC(1,t); + k.KIO(-1,this.s_v_pseudo_16,1,t); + k.KB(t); + } + else if(k.KFCM(1,t,[{t:'a',a:this.s_c_shifter_28}])){ + r=m=1; // Line 213 + k.KDC(1,t); + k.KIO(-1,this.s_c_shifter_28,1,t); + k.KB(t); + } + else if(1){ + r=m=1; // Line 191 + k.KDC(0,t); + k.KO(-1,t,"់"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_9 /* 0x39 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"("); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_0 /* 0x30 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,")"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_8 /* 0x38 */)) { + if(1){ + r=m=1; // Line 191 + k.KDC(0,t); + k.KO(-1,t,"៏"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_EQUAL /* 0xBB */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"="); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_COMMA /* 0xBC */)) { + if(1){ + r=m=1; // Line 206 + k.KDC(0,t); + k.KO(-1,t,"ុំ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_HYPHEN /* 0xBD */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឥ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_PERIOD /* 0xBE */)) { + if(1){ + r=m=1; // Line 193 + k.KDC(0,t); + k.KO(-1,t,"។"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_SLASH /* 0xBF */)) { + if(k.KFCM(3,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52}])){ + r=m=1; // Line 254 + k.KDC(3,t); + k.KO(-1,t,"ល្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៊"); + k.KB(t); + } + else if(k.KFCM(3,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53}])){ + r=m=1; // Line 255 + k.KDC(3,t); + k.KO(-1,t,"ម្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៊"); + k.KB(t); + } + else if(k.KFCM(1,t,[{t:'a',a:this.s_c_shifter_28}])){ + r=m=1; // Line 215 + k.KDC(1,t); + k.KIO(-1,this.s_c_shifter_28,1,t); + k.KB(t); + } + else if(k.KFCM(1,t,[{t:'a',a:this.s_shiftable_c_2nd_with_BA_51}])){ + r=m=1; // Line 240 + k.KDC(1,t); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,1,t); + k.KO(-1,t,"៊"); + k.KB(t); + } + else if(1){ + r=m=1; // Line 192 + k.KDC(0,t); + k.KO(-1,t,"៊"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_0 /* 0x30 */)) { + if(1){ + r=m=1; // Line 196 + k.KDC(0,t); + k.KO(-1,t,"០"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_1 /* 0x31 */)) { + if(1){ + r=m=1; // Line 196 + k.KDC(0,t); + k.KO(-1,t,"១"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_2 /* 0x32 */)) { + if(1){ + r=m=1; // Line 196 + k.KDC(0,t); + k.KO(-1,t,"២"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_3 /* 0x33 */)) { + if(1){ + r=m=1; // Line 196 + k.KDC(0,t); + k.KO(-1,t,"៣"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_4 /* 0x34 */)) { + if(1){ + r=m=1; // Line 196 + k.KDC(0,t); + k.KO(-1,t,"៤"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_5 /* 0x35 */)) { + if(1){ + r=m=1; // Line 196 + k.KDC(0,t); + k.KO(-1,t,"៥"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_6 /* 0x36 */)) { + if(1){ + r=m=1; // Line 196 + k.KDC(0,t); + k.KO(-1,t,"៦"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_7 /* 0x37 */)) { + if(1){ + r=m=1; // Line 196 + k.KDC(0,t); + k.KO(-1,t,"៧"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_8 /* 0x38 */)) { + if(1){ + r=m=1; // Line 196 + k.KDC(0,t); + k.KO(-1,t,"៨"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_9 /* 0x39 */)) { + if(1){ + r=m=1; // Line 196 + k.KDC(0,t); + k.KO(-1,t,"៩"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_COLON /* 0xBA */)) { + if(1){ + r=m=1; // Line 205 + k.KDC(0,t); + k.KO(-1,t,"ោះ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_COLON /* 0xBA */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ើ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_COMMA /* 0xBC */)) { + if(1){ + r=m=1; // Line 207 + k.KDC(0,t); + k.KO(-1,t,"ុះ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_EQUAL /* 0xBB */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឲ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_PERIOD /* 0xBE */)) { + if(1){ + r=m=1; // Line 193 + k.KDC(0,t); + k.KO(-1,t,"៕"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_SLASH /* 0xBF */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"?"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_2 /* 0x32 */)) { + if(1){ + r=m=1; // Line 193 + k.KDC(0,t); + k.KO(-1,t,"ៗ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_A /* 0x41 */)) { + if(1){ + r=m=1; // Line 203 + k.KDC(0,t); + k.KO(-1,t,"ាំ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_B /* 0x42 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ព"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_C /* 0x43 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ជ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_D /* 0x44 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ឌ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_E /* 0x45 */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ែ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_F /* 0x46 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ធ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_G /* 0x47 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"អ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_H /* 0x48 */)) { + if(k.KFCM(1,t,['ះ'])){ + r=m=1; // Line 219 + k.KDC(1,t); + k.KO(-1,t,"ៈ"); + } + else if(k.KFCM(1,t,['ៈ'])){ + r=m=1; // Line 220 + k.KDC(1,t); + k.KO(-1,t,"ះ"); + } + else if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ះ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_I /* 0x49 */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ី"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_J /* 0x4A */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ញ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_K /* 0x4B */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"គ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_L /* 0x4C */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ឡ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_M /* 0x4D */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ំ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_N /* 0x4E */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ណ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_O /* 0x4F */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ៅ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_P /* 0x50 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ភ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_Q /* 0x51 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ឈ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_R /* 0x52 */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឬ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_S /* 0x53 */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ៃ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_T /* 0x54 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ទ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_U /* 0x55 */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ូ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_V /* 0x56 */)) { + if(1){ + r=m=1; // Line 204 + k.KDC(0,t); + k.KO(-1,t,"េះ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_W /* 0x57 */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ឺ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_X /* 0x58 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ឃ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_Y /* 0x59 */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ួ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_Z /* 0x5A */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ឍ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_LBRKT /* 0xDB */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ៀ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_BKSLASH /* 0xDC */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឮ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_RBRKT /* 0xDD */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឪ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_6 /* 0x36 */)) { + if(1){ + r=m=1; // Line 191 + k.KDC(0,t); + k.KO(-1,t,"៍"); + } + } + if(m) {} + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_HYPHEN /* 0xBD */)) { + if(1){ + r=m=1; // Line 191 + k.KDC(0,t); + k.KO(-1,t,"៌"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_BKQUOTE /* 0xC0 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"«"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_A /* 0x41 */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ា"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_B /* 0x42 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ប"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_C /* 0x43 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ច"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_D /* 0x44 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ដ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_E /* 0x45 */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"េ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_F /* 0x46 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ថ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_G /* 0x47 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ង"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_H /* 0x48 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ហ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_I /* 0x49 */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ិ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_J /* 0x4A */)) { + if(1){ + r=m=1; // Line 191 + k.KDC(0,t); + k.KO(-1,t,"្"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_K /* 0x4B */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ក"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_L /* 0x4C */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ល"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_M /* 0x4D */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ម"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_N /* 0x4E */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ន"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_O /* 0x4F */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ោ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_P /* 0x50 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ផ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_Q /* 0x51 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ឆ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_R /* 0x52 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"រ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_S /* 0x53 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ស"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_T /* 0x54 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ត"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_U /* 0x55 */)) { + if(k.KFCM(3,t,[{t:'a',a:this.s_shiftable_c_1st_48},'ា','ំ'])){ + r=m=1; // Line 234 + k.KDC(3,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_shiftable_c_2nd_with_BA_51},'ា','ំ'])){ + r=m=1; // Line 235 + k.KDC(3,t); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,1,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ុ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_V /* 0x56 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"វ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_W /* 0x57 */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ឹ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_X /* 0x58 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ខ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_Y /* 0x59 */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"យ"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_Z /* 0x5A */)) { + if(1){ + r=m=1; // Line 188 + k.KDC(0,t); + k.KO(-1,t,"ឋ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_LBRKT /* 0xDB */)) { + if(1){ + r=m=1; // Line 189 + k.KDC(0,t); + k.KO(-1,t,"ឿ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_BKSLASH /* 0xDC */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឭ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_RBRKT /* 0xDD */)) { + if(1){ + r=m=1; // Line 190 + k.KDC(0,t); + k.KO(-1,t,"ឧ"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_BKQUOTE /* 0xC0 */)) { + if(1){ + r=m=1; // Line 194 + k.KDC(0,t); + k.KO(-1,t,"»"); + } + } + if(m==1) { + + k.KDC(-1,t); + r=this.g_normalise_1(t,e); + m=2; + } + return r; + }; + this.g_normalise_1=function(t,e) { + var k=KeymanWeb,r=1,m=0; + if(k.KFCM(7,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ','ុ','ំ','ា','ំ'])){ + m=1; // Line 376 + k.KDC(7,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"អ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(7,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54},'ុ','ំ','ា','ំ'])){ + m=1; // Line 381 + k.KDC(7,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(7,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55},'ុ','ំ','ា','ំ'])){ + m=1; // Line 386 + k.KDC(7,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(7,t,['ស','្','ប','ុ','ំ','ា','ំ'])){ + m=1; // Line 391 + k.KDC(7,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ប៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(7,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56},'ុ','ំ','ា','ំ'])){ + m=1; // Line 396 + k.KDC(7,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(7,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ','ុ','ំ','ា','ំ'])){ + m=1; // Line 401 + k.KDC(7,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"ហ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(7,t,['អ','្','ង','ុ','ំ','ា','ំ'])){ + m=1; // Line 406 + k.KDC(7,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ង៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(7,t,['អ','្','វ','ុ','ំ','ា','ំ'])){ + m=1; // Line 411 + k.KDC(7,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(7,t,['ហ','្','ប','ុ','ំ','ា','ំ'])){ + m=1; // Line 416 + k.KDC(7,t); + k.KO(-1,t,"ហ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ប៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(7,t,['ហ','្',{t:'a',a:this.s_shiftable_c_2nd_with_BA_51},'ុ','ំ','ា','ំ'])){ + m=1; // Line 422 + k.KDC(7,t); + k.KO(-1,t,"ហ"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(7,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52},'ុ','ំ','ា','ំ'])){ + m=1; // Line 429 + k.KDC(7,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(7,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53},'ុ','ំ','ា','ំ'])){ + m=1; // Line 434 + k.KDC(7,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['្','ដ',{t:'a',a:this.s_v_combo_N_21},'ំ','្','រ'])){ + m=1; // Line 340 + k.KDC(6,t); + k.KO(-1,t,"្ត"); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + k.KIO(-1,this.s_v_combo_N_21,3,t); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['្','ដ',{t:'a',a:this.s_v_combo_R_20},'ះ','្','រ'])){ + m=1; // Line 341 + k.KDC(6,t); + k.KO(-1,t,"្ត"); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + k.KIO(-1,this.s_v_combo_R_20,3,t); + k.KO(-1,t,"ះ"); + } + else if(k.KFCM(6,t,['្','រ',{t:'a',a:this.s_v_combo_N_21},'ំ','្','ដ'])){ + m=1; // Line 344 + k.KDC(6,t); + k.KO(-1,t,"្ត"); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + k.KIO(-1,this.s_v_combo_N_21,3,t); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['្','រ',{t:'a',a:this.s_v_combo_R_20},'ះ','្','ដ'])){ + m=1; // Line 345 + k.KDC(6,t); + k.KO(-1,t,"្ត"); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + k.KIO(-1,this.s_v_combo_R_20,3,t); + k.KO(-1,t,"ះ"); + } + else if(k.KFCM(6,t,['្','រ',{t:'a',a:this.s_v_combo_N_21},'ំ','្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 350 + k.KDC(6,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,6,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + k.KIO(-1,this.s_v_combo_N_21,3,t); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['្','រ',{t:'a',a:this.s_v_combo_R_20},'ះ','្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 351 + k.KDC(6,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,6,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + k.KIO(-1,this.s_v_combo_R_20,3,t); + k.KO(-1,t,"ះ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ','ុ','ា','ំ'])){ + m=1; // Line 374 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"អ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54},'ុ','ា','ំ'])){ + m=1; // Line 379 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55},'ុ','ា','ំ'])){ + m=1; // Line 384 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ស','្','ប','ុ','ា','ំ'])){ + m=1; // Line 389 + k.KDC(6,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ប៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56},'ុ','ា','ំ'])){ + m=1; // Line 394 + k.KDC(6,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ','ុ','ា','ំ'])){ + m=1; // Line 399 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"ហ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['អ','្','ង','ុ','ា','ំ'])){ + m=1; // Line 404 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ង៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['អ','្','វ','ុ','ា','ំ'])){ + m=1; // Line 409 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ហ','្','ប','ុ','ា','ំ'])){ + m=1; // Line 414 + k.KDC(6,t); + k.KO(-1,t,"ហ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ប៉"); + k.KO(-1,t,"ប"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ហ','្',{t:'a',a:this.s_shiftable_c_2nd_with_BA_51},'ុ','ា','ំ'])){ + m=1; // Line 420 + k.KDC(6,t); + k.KO(-1,t,"ហ"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,3,t); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52},'ុ','ា','ំ'])){ + m=1; // Line 427 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53},'ុ','ា','ំ'])){ + m=1; // Line 432 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52},'៊',{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16}])){ + m=1; // Line 454 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_gen_14,5,t); + k.KIO(-1,this.s_v_pseudo_16,6,t); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53},'៊',{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16}])){ + m=1; // Line 455 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_gen_14,5,t); + k.KIO(-1,this.s_v_pseudo_16,6,t); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ','៉','ា','ំ'])){ + m=1; // Line 489 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"អ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54},'៉','ា','ំ'])){ + m=1; // Line 490 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55},'៉','ា','ំ'])){ + m=1; // Line 491 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ស','្','ប','៉','ា','ំ'])){ + m=1; // Line 492 + k.KDC(6,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ប៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56},'៉','ា','ំ'])){ + m=1; // Line 493 + k.KDC(6,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ','៉','ា','ំ'])){ + m=1; // Line 494 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"ហ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['អ','្','ង','៉','ា','ំ'])){ + m=1; // Line 495 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ង៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['អ','្','វ','៉','ា','ំ'])){ + m=1; // Line 496 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ','ា','ុ','ំ'])){ + m=1; // Line 503 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"អ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ','ុ','ំ','ា'])){ + m=1; // Line 504 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"អ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54},'ា','ុ','ំ'])){ + m=1; // Line 506 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54},'ុ','ំ','ា'])){ + m=1; // Line 507 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55},'ា','ុ','ំ'])){ + m=1; // Line 509 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55},'ុ','ំ','ា'])){ + m=1; // Line 510 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56},'ា','ុ','ំ'])){ + m=1; // Line 512 + k.KDC(6,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56},'ុ','ំ','ា'])){ + m=1; // Line 513 + k.KDC(6,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ','ា','ុ','ំ'])){ + m=1; // Line 515 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"ហ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ','ុ','ំ','ា'])){ + m=1; // Line 516 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"ហ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['អ','្','ង','ា','ុ','ំ'])){ + m=1; // Line 518 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ង៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['អ','្','ង','ុ','ំ','ា'])){ + m=1; // Line 519 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ង៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['អ','្','វ','ា','ុ','ំ'])){ + m=1; // Line 521 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វ៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['អ','្','វ','ុ','ំ','ា'])){ + m=1; // Line 522 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វ៊"); + k.KO(-1,t,"ុ"); + k.KO(-1,t,"ា"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52},'ា','ុ','ំ'])){ + m=1; // Line 529 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52},'ុ','ំ','ា'])){ + m=1; // Line 530 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53},'ា','ុ','ំ'])){ + m=1; // Line 532 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53},'ុ','ំ','ា'])){ + m=1; // Line 533 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ','េ','ុ','ី'])){ + m=1; // Line 541 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"អ៊ើ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ','ុ','េ','ី'])){ + m=1; // Line 542 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"អ៊ើ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ','៉','េ','ី'])){ + m=1; // Line 543 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"អ៊ើ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54},'េ','ុ','ី'])){ + m=1; // Line 545 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54},'ុ','េ','ី'])){ + m=1; // Line 546 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54},'៉','េ','ី'])){ + m=1; // Line 547 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55},'េ','ុ','ី'])){ + m=1; // Line 549 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55},'ុ','េ','ី'])){ + m=1; // Line 550 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55},'៉','េ','ី'])){ + m=1; // Line 551 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(6,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56},'េ','ុ','ី'])){ + m=1; // Line 553 + k.KDC(6,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(6,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56},'ុ','េ','ី'])){ + m=1; // Line 554 + k.KDC(6,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(6,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56},'៉','េ','ី'])){ + m=1; // Line 555 + k.KDC(6,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ','េ','ុ','ី'])){ + m=1; // Line 557 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"ហ៊ើ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ','ុ','េ','ី'])){ + m=1; // Line 558 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"ហ៊ើ"); + } + else if(k.KFCM(6,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ','៉','េ','ី'])){ + m=1; // Line 559 + k.KDC(6,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"ហ៊ើ"); + } + else if(k.KFCM(6,t,['អ','្','ង','េ','ុ','ី'])){ + m=1; // Line 561 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ង៊ើ"); + } + else if(k.KFCM(6,t,['អ','្','ង','ុ','េ','ី'])){ + m=1; // Line 562 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ង៊ើ"); + } + else if(k.KFCM(6,t,['អ','្','ង','៉','េ','ី'])){ + m=1; // Line 563 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ង៊ើ"); + } + else if(k.KFCM(6,t,['អ','្','វ','េ','ុ','ី'])){ + m=1; // Line 565 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វ៊ើ"); + } + else if(k.KFCM(6,t,['អ','្','វ','ុ','េ','ី'])){ + m=1; // Line 566 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វ៊ើ"); + } + else if(k.KFCM(6,t,['អ','្','វ','៉','េ','ី'])){ + m=1; // Line 567 + k.KDC(6,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វ៊ើ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52},'េ','ុ','ី'])){ + m=1; // Line 575 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៉ើ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52},'ុ','េ','ី'])){ + m=1; // Line 576 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៉ើ"); + } + else if(k.KFCM(6,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52},'៊','េ','ី'])){ + m=1; // Line 577 + k.KDC(6,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៉ើ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53},'េ','ុ','ី'])){ + m=1; // Line 579 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៉ើ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53},'ុ','េ','ី'])){ + m=1; // Line 580 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៉ើ"); + } + else if(k.KFCM(6,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53},'៊','េ','ី'])){ + m=1; // Line 581 + k.KDC(6,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៉ើ"); + } + else if(k.KFCM(6,t,['្','យ','្',{t:'a',a:this.s_c_out_12},'េ','ឺ'])){ + m=1; // Line 631 + k.KDC(6,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_out_12,4,t); + k.KO(-1,t,"ឿ"); + } + else if(k.KFCM(6,t,['្','យ','្',{t:'a',a:this.s_c_out_12},'េ','ឹ'])){ + m=1; // Line 632 + k.KDC(6,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_out_12,4,t); + k.KO(-1,t,"ឿ"); + } + else if(k.KFCM(6,t,['្','យ','្',{t:'a',a:this.s_c_out_12},'េ','ី'])){ + m=1; // Line 633 + k.KDC(6,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_out_12,4,t); + k.KO(-1,t,"ឿ"); + } + else if(k.KFCM(5,t,[{t:'a',a:this.s_c_shifter_28},{t:'a',a:this.s_v_combo_N_21},'ំ','្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 331 + k.KDC(5,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,5,t); + k.KIO(-1,this.s_c_shifter_28,1,t); + k.KIO(-1,this.s_v_combo_N_21,2,t); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(5,t,[{t:'a',a:this.s_c_shifter_28},{t:'a',a:this.s_v_combo_R_20},'ះ','្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 332 + k.KDC(5,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,5,t); + k.KIO(-1,this.s_c_shifter_28,1,t); + k.KIO(-1,this.s_v_combo_R_20,2,t); + k.KO(-1,t,"ះ"); + } + else if(k.KFCM(5,t,['្','ដ',{t:'a',a:this.s_v_any_19},'្','រ'])){ + m=1; // Line 339 + k.KDC(5,t); + k.KO(-1,t,"្ត"); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + k.KIO(-1,this.s_v_any_19,3,t); + } + else if(k.KFCM(5,t,['្','រ',{t:'a',a:this.s_v_any_19},'្','ដ'])){ + m=1; // Line 343 + k.KDC(5,t); + k.KO(-1,t,"្ត"); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + k.KIO(-1,this.s_v_any_19,3,t); + } + else if(k.KFCM(5,t,['្','រ',{t:'a',a:this.s_c_shifter_28},'្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 347 + k.KDC(5,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,5,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + k.KIO(-1,this.s_c_shifter_28,3,t); + } + else if(k.KFCM(5,t,['្','រ',{t:'a',a:this.s_v_any_19},'្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 349 + k.KDC(5,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,5,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + k.KIO(-1,this.s_v_any_19,3,t); + } + else if(k.KFCM(5,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ','ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 373 + k.KDC(5,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"អ៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ',{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 375 + k.KDC(5,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"អ៊"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(5,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54},'ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 378 + k.KDC(5,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54},{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 380 + k.KDC(5,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(5,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55},'ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 383 + k.KDC(5,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55},{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 385 + k.KDC(5,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,4,t); + } + if(m) {} + else if(k.KFCM(5,t,['ស','្','ប','ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 388 + k.KDC(5,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ប៉"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ស','្','ប',{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 390 + k.KDC(5,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ប៉"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(5,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56},'ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 393 + k.KDC(5,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56},{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 395 + k.KDC(5,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(5,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ','ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 398 + k.KDC(5,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"ហ៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ',{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 400 + k.KDC(5,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"ហ៊"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(5,t,['អ','្','ង','ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 403 + k.KDC(5,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ង៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['អ','្','ង',{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 405 + k.KDC(5,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ង៊"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(5,t,['អ','្','វ','ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 408 + k.KDC(5,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វ៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['អ','្','វ',{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 410 + k.KDC(5,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វ៊"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(5,t,['ហ','្','ប','ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 413 + k.KDC(5,t); + k.KO(-1,t,"ហ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ប៉"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ហ','្','ប',{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 415 + k.KDC(5,t); + k.KO(-1,t,"ហ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ប៉"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(5,t,['ហ','្',{t:'a',a:this.s_shiftable_c_2nd_with_BA_51},'ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 419 + k.KDC(5,t); + k.KO(-1,t,"ហ"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ហ','្',{t:'a',a:this.s_shiftable_c_2nd_with_BA_51},{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 421 + k.KDC(5,t); + k.KO(-1,t,"ហ"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(5,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52},'ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 426 + k.KDC(5,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52},{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 428 + k.KDC(5,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(5,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53},'ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 431 + k.KDC(5,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53},{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 433 + k.KDC(5,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(5,t,[{t:'a',a:this.s_shiftable_c_1st_48},'ុ','ំ','ា','ំ'])){ + m=1; // Line 441 + k.KDC(5,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(5,t,[{t:'a',a:this.s_shiftable_c_2nd_with_BA_51},'ុ','ំ','ា','ំ'])){ + m=1; // Line 448 + k.KDC(5,t); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,1,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(5,t,['ល','្',{t:'a',a:this.s_c_2nd_combo_LO_52},'៊',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 452 + k.KDC(5,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_LO_52,3,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ម','្',{t:'a',a:this.s_c_2nd_combo_MO_53},'៊',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 453 + k.KDC(5,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_2nd_combo_MO_53,3,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['្',{t:'a',a:this.s_shiftable_c_2nd_50},'៊',{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16}])){ + m=1; // Line 463 + k.KDC(5,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_shiftable_c_2nd_50,2,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_gen_14,4,t); + k.KIO(-1,this.s_v_pseudo_16,5,t); + } + else if(k.KFCM(5,t,[{t:'a',a:this.s_c_combo_QA_57},'្','អ','៉',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 481 + k.KDC(5,t); + k.KIO(-1,this.s_c_combo_QA_57,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"អ៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ល','្',{t:'a',a:this.s_c_1st_combo_LO_54},'៉',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 482 + k.KDC(5,t); + k.KO(-1,t,"ល"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_LO_54,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ម','្',{t:'a',a:this.s_c_1st_combo_MO_55},'៉',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 483 + k.KDC(5,t); + k.KO(-1,t,"ម"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_1st_combo_MO_55,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ស','្','ប','៉',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 484 + k.KDC(5,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ប៉"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ស','្',{t:'a',a:this.s_c_combo_SA_56},'៉',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 485 + k.KDC(5,t); + k.KO(-1,t,"ស"); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_c_combo_SA_56,3,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,[{t:'a',a:this.s_c_combo_HA_58},'្','ហ','៉',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 486 + k.KDC(5,t); + k.KIO(-1,this.s_c_combo_HA_58,1,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"ហ៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['អ','្','ង','៉',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 487 + k.KDC(5,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"ង៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['អ','្','វ','៉',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 488 + k.KDC(5,t); + k.KO(-1,t,"អ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វ៊"); + k.KIO(-1,this.s_v_above_47,5,t); + } + else if(k.KFCM(5,t,['ព','័','ន','្','ឋ'])){ + m=1; // Line 619 + k.KDC(5,t); + k.KO(-1,t,"ព"); + k.KO(-1,t,"័"); + k.KO(-1,t,"ន"); + k.KO(-1,t,"្ធ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16},{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16}])){ + m=1; // Line 312 + k.KDC(4,t); + k.KIO(-1,this.s_v_gen_14,3,t); + k.KIO(-1,this.s_v_pseudo_16,4,t); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_v_combo_N_21},'ំ','្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 325 + k.KDC(4,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,4,t); + k.KIO(-1,this.s_v_combo_N_21,1,t); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_v_combo_R_20},'ះ','្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 326 + k.KDC(4,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,4,t); + k.KIO(-1,this.s_v_combo_R_20,1,t); + k.KO(-1,t,"ះ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_c_shifter_28},{t:'a',a:this.s_v_any_19},'្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 330 + k.KDC(4,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,4,t); + k.KIO(-1,this.s_c_shifter_28,1,t); + k.KIO(-1,this.s_v_any_19,2,t); + } + else if(k.KFCM(4,t,['្','ដ','្','រ'])){ + m=1; // Line 336 + k.KDC(4,t); + k.KO(-1,t,"្ត"); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + } + else if(k.KFCM(4,t,['្','រ','្','ដ'])){ + m=1; // Line 337 + k.KDC(4,t); + k.KO(-1,t,"្ត"); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + } + else if(k.KFCM(4,t,['្','រ','្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 348 + k.KDC(4,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,4,t); + k.KO(-1,t,"្"); + k.KO(-1,t,"រ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_c_shifter_28},{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16},{t:'a',a:this.s_c_shifter_28}])){ + m=1; // Line 362 + k.KDC(4,t); + k.KIO(-1,this.s_v_gen_14,2,t); + k.KIO(-1,this.s_v_pseudo_16,3,t); + k.KIO(-1,this.s_c_shifter_28,4,t); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_1st_48},'ុ','ា','ំ'])){ + m=1; // Line 439 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_2nd_with_BA_51},'ុ','ា','ំ'])){ + m=1; // Line 446 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,1,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_2nd_50},'៊',{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16}])){ + m=1; // Line 460 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_2nd_50,1,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_gen_14,3,t); + k.KIO(-1,this.s_v_pseudo_16,4,t); + } + else if(k.KFCM(4,t,['្',{t:'a',a:this.s_shiftable_c_2nd_50},'៊',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 462 + k.KDC(4,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_shiftable_c_2nd_50,2,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,4,t); + } + else if(k.KFCM(4,t,['ប','្','យ',{t:'a',a:this.s_c_shifter_28}])){ + m=1; // Line 467 + k.KDC(4,t); + k.KO(-1,t,"ប្យ"); + k.KIO(-1,this.s_c_shifter_28,4,t); + } + else if(k.KFCM(4,t,['ស','្','ប',{t:'a',a:this.s_c_shifter_28}])){ + m=1; // Line 468 + k.KDC(4,t); + k.KO(-1,t,"ស្ប"); + k.KIO(-1,this.s_c_shifter_28,4,t); + } + else if(k.KFCM(4,t,['ឆ','្','ប',{t:'a',a:this.s_c_shifter_28}])){ + m=1; // Line 469 + k.KDC(4,t); + k.KO(-1,t,"ឆ្ប"); + k.KIO(-1,this.s_c_shifter_28,4,t); + } + else if(k.KFCM(4,t,['ប','្','យ',{t:'a',a:this.s_c_shifter_28}])){ + m=1; // Line 470 + k.KDC(4,t); + k.KO(-1,t,"ប្យ"); + k.KIO(-1,this.s_c_shifter_28,4,t); + } + else if(k.KFCM(4,t,['ស','្','ប',{t:'a',a:this.s_c_shifter_28}])){ + m=1; // Line 471 + k.KDC(4,t); + k.KO(-1,t,"ស្ប"); + k.KIO(-1,this.s_c_shifter_28,4,t); + } + else if(k.KFCM(4,t,['ឆ','្','ប',{t:'a',a:this.s_c_shifter_28}])){ + m=1; // Line 472 + k.KDC(4,t); + k.KO(-1,t,"ឆ្ប"); + k.KIO(-1,this.s_c_shifter_28,4,t); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_1st_48},'៉',{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16}])){ + m=1; // Line 477 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_gen_14,3,t); + k.KIO(-1,this.s_v_pseudo_16,4,t); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_1st_48},'ា','ុ','ំ'])){ + m=1; // Line 500 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_1st_48},'ុ','ំ','ា'])){ + m=1; // Line 501 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_2nd_with_BA_51},'ា','ុ','ំ'])){ + m=1; // Line 526 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,1,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_2nd_with_BA_51},'ុ','ំ','ា'])){ + m=1; // Line 527 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,1,t); + k.KO(-1,t,"៉"); + k.KO(-1,t,"ា"); + k.KO(-1,t,"ំ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_1st_48},'េ','ុ','ី'])){ + m=1; // Line 537 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_1st_48},'ុ','េ','ី'])){ + m=1; // Line 538 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_1st_48},'៉','េ','ី'])){ + m=1; // Line 539 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊ើ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_2nd_50},'េ','ុ','ី'])){ + m=1; // Line 571 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_2nd_50,1,t); + k.KO(-1,t,"៉ើ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_2nd_50},'ុ','េ','ី'])){ + m=1; // Line 572 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_2nd_50,1,t); + k.KO(-1,t,"៉ើ"); + } + else if(k.KFCM(4,t,[{t:'a',a:this.s_shiftable_c_2nd_50},'៊','េ','ី'])){ + m=1; // Line 573 + k.KDC(4,t); + k.KIO(-1,this.s_shiftable_c_2nd_50,1,t); + k.KO(-1,t,"៉ើ"); + } + else if(k.KFCM(4,t,['ព','ន','្','ឋ'])){ + m=1; // Line 618 + k.KDC(4,t); + k.KO(-1,t,"ព"); + k.KO(-1,t,"ន"); + k.KO(-1,t,"្ធ"); + } + else if(k.KFCM(4,t,['្','យ','េ','ឺ'])){ + m=1; // Line 627 + k.KDC(4,t); + k.KO(-1,t,"ឿ"); + } + else if(k.KFCM(4,t,['្','យ','េ','ឹ'])){ + m=1; // Line 628 + k.KDC(4,t); + k.KO(-1,t,"ឿ"); + } + else if(k.KFCM(4,t,['្','យ','េ','ី'])){ + m=1; // Line 629 + k.KDC(4,t); + k.KO(-1,t,"ឿ"); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16},{t:'a',a:this.s_v_gen_14}])){ + m=1; // Line 308 + k.KDC(3,t); + k.KIO(-1,this.s_v_gen_14,3,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16},{t:'a',a:this.s_v_pseudo_16}])){ + m=1; // Line 309 + k.KDC(3,t); + k.KIO(-1,this.s_v_pseudo_16,3,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16}])){ + m=1; // Line 310 + k.KDC(3,t); + k.KIO(-1,this.s_v_gen_14,2,t); + k.KIO(-1,this.s_v_pseudo_16,3,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_v_pseudo_16},{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16}])){ + m=1; // Line 311 + k.KDC(3,t); + k.KIO(-1,this.s_v_gen_14,2,t); + k.KIO(-1,this.s_v_pseudo_16,3,t); + } + else if(k.KFCM(3,t,['្',{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16}])){ + m=1; // Line 320 + k.KDC(3,t); + k.KIO(-1,this.s_v_gen_14,2,t); + k.KIO(-1,this.s_v_pseudo_16,3,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_v_any_19},'្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 324 + k.KDC(3,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,3,t); + k.KIO(-1,this.s_v_any_19,1,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_c_shifter_28},'្',{t:'a',a:this.s_subcons_44}])){ + m=1; // Line 355 + k.KDC(3,t); + k.KO(-1,t,"្"); + k.KIO(-1,this.s_subcons_44,3,t); + k.KIO(-1,this.s_c_shifter_28,1,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_pseudo_16},{t:'a',a:this.s_c_shifter_28}])){ + m=1; // Line 360 + k.KDC(3,t); + k.KIO(-1,this.s_c_shifter_28,3,t); + k.KIO(-1,this.s_v_gen_14,1,t); + k.KIO(-1,this.s_v_pseudo_16,2,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_c_shifter_28},{t:'a',a:this.s_v_any_19},{t:'a',a:this.s_c_shifter_28}])){ + m=1; // Line 361 + k.KDC(3,t); + k.KIO(-1,this.s_c_shifter_28,3,t); + k.KIO(-1,this.s_v_any_19,2,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_shiftable_c_1st_48},'ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 438 + k.KDC(3,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,3,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_shiftable_c_1st_48},{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 440 + k.KDC(3,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,2,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_shiftable_c_2nd_with_BA_51},'ុ',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 445 + k.KDC(3,t); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,1,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_above_47,3,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_shiftable_c_2nd_with_BA_51},{t:'a',a:this.s_v_above_47},'ុ'])){ + m=1; // Line 447 + k.KDC(3,t); + k.KIO(-1,this.s_shiftable_c_2nd_with_BA_51,1,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_above_47,2,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_shiftable_c_2nd_50},'៊',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 459 + k.KDC(3,t); + k.KIO(-1,this.s_shiftable_c_2nd_50,1,t); + k.KO(-1,t,"៉"); + k.KIO(-1,this.s_v_above_47,3,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_shiftable_c_1st_48},'៉',{t:'a',a:this.s_v_above_47}])){ + m=1; // Line 476 + k.KDC(3,t); + k.KIO(-1,this.s_shiftable_c_1st_48,1,t); + k.KO(-1,t,"៊"); + k.KIO(-1,this.s_v_above_47,3,t); + } + else if(k.KFCM(3,t,[{t:'a',a:this.s_c_out_12},{t:'a',a:this.s_v_gen_14},'៌'])){ + m=1; // Line 585 + k.KDC(3,t); + k.KIO(-1,this.s_c_out_12,1,t); + k.KO(-1,t,"៌"); + k.KIO(-1,this.s_v_gen_14,2,t); + } + else if(k.KFCM(3,t,['ណ','្','ត'])){ + m=1; // Line 589 + k.KDC(3,t); + k.KO(-1,t,"ណ"); + k.KO(-1,t,"្ដ"); + } + else if(k.KFCM(3,t,['ន','្','ដ'])){ + m=1; // Line 590 + k.KDC(3,t); + k.KO(-1,t,"ន"); + k.KO(-1,t,"្ត"); + } + else if(k.KFCM(3,t,['ទ','្','ប'])){ + m=1; // Line 594 + k.KDC(3,t); + k.KO(-1,t,"ឡ"); + } + else if(k.KFCM(3,t,['ប','្','ញ'])){ + m=1; // Line 596 + k.KDC(3,t); + k.KO(-1,t,"ឫ"); + } + else if(k.KFCM(3,t,['ព','្','ញ'])){ + m=1; // Line 602 + k.KDC(3,t); + k.KO(-1,t,"ឭ"); + } + else if(k.KFCM(3,t,['ព','្','ឋ'])){ + m=1; // Line 605 + k.KDC(3,t); + k.KO(-1,t,"ឰ"); + } + else if(k.KFCM(3,t,['ដ','្','ធ'])){ + m=1; // Line 613 + k.KDC(3,t); + k.KO(-1,t,"ដ្ឋ"); + } + else if(k.KFCM(3,t,['ទ','្','ឋ'])){ + m=1; // Line 614 + k.KDC(3,t); + k.KO(-1,t,"ទ្ធ"); + } + else if(k.KFCM(3,t,['ឪ','្','យ'])){ + m=1; // Line 622 + k.KDC(3,t); + k.KO(-1,t,"ឱ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"យ"); + } + else if(k.KFCM(3,t,['ឳ','្','យ'])){ + m=1; // Line 623 + k.KDC(3,t); + k.KO(-1,t,"ឱ"); + k.KO(-1,t,"្"); + k.KO(-1,t,"យ"); + } + else if(k.KFCM(3,t,['ញ','្','វ'])){ + m=1; // Line 625 + k.KDC(3,t); + k.KO(-1,t,"ព"); + k.KO(-1,t,"្"); + k.KO(-1,t,"វា"); + } + else if(k.KFCM(2,t,['េ','ា'])){ + m=1; // Line 291 + k.KDC(2,t); + k.KO(-1,t,"ោ"); + } + else if(k.KFCM(2,t,['ា','េ'])){ + m=1; // Line 292 + k.KDC(2,t); + k.KO(-1,t,"ោ"); + } + else if(k.KFCM(2,t,['េ','ី'])){ + m=1; // Line 293 + k.KDC(2,t); + k.KO(-1,t,"ើ"); + } + else if(k.KFCM(2,t,['ី','េ'])){ + m=1; // Line 294 + k.KDC(2,t); + k.KO(-1,t,"ើ"); + } + else if(k.KFCM(2,t,['ំ','ុ'])){ + m=1; // Line 298 + k.KDC(2,t); + k.KO(-1,t,"ុំ"); + } + else if(k.KFCM(2,t,['ំ','ា'])){ + m=1; // Line 299 + k.KDC(2,t); + k.KO(-1,t,"ាំ"); + } + else if(k.KFCM(2,t,[{t:'a',a:this.s_v_gen_14},{t:'a',a:this.s_v_gen_14}])){ + m=1; // Line 304 + k.KDC(2,t); + k.KIO(-1,this.s_v_gen_14,2,t); + } + else if(k.KFCM(2,t,[{t:'a',a:this.s_v_pseudo_16},{t:'a',a:this.s_v_pseudo_16}])){ + m=1; // Line 313 + k.KDC(2,t); + k.KIO(-1,this.s_v_pseudo_16,2,t); + } + if(m) {} + else if(k.KFCM(2,t,['្','្'])){ + m=1; // Line 318 + k.KDC(2,t); + k.KO(-1,t,"្"); + } + else if(k.KFCM(2,t,['្',{t:'a',a:this.s_v_any_19}])){ + m=1; // Line 319 + k.KDC(2,t); + k.KIO(-1,this.s_v_any_19,2,t); + } + else if(k.KFCM(2,t,[{t:'a',a:this.s_v_any_19},{t:'a',a:this.s_c_shifter_28}])){ + m=1; // Line 359 + k.KDC(2,t); + k.KIO(-1,this.s_c_shifter_28,2,t); + k.KIO(-1,this.s_v_any_19,1,t); + } + else if(k.KFCM(2,t,['ឫ','ុ'])){ + m=1; // Line 597 + k.KDC(2,t); + k.KO(-1,t,"ឬ"); + } + else if(k.KFCM(2,t,['ឭ','ា'])){ + m=1; // Line 599 + k.KDC(2,t); + k.KO(-1,t,"ញ"); + } + else if(k.KFCM(2,t,['ឮ','ា'])){ + m=1; // Line 600 + k.KDC(2,t); + k.KO(-1,t,"ញ"); + } + else if(k.KFCM(2,t,['ឭ','ុ'])){ + m=1; // Line 603 + k.KDC(2,t); + k.KO(-1,t,"ឮ"); + } + else if(k.KFCM(2,t,['ឧ','ិ'])){ + m=1; // Line 607 + k.KDC(2,t); + k.KO(-1,t,"ឱ"); + } + else if(k.KFCM(2,t,['ឧ','៌'])){ + m=1; // Line 608 + k.KDC(2,t); + k.KO(-1,t,"ឱ"); + } + else if(k.KFCM(2,t,['ឧ','៍'])){ + m=1; // Line 609 + k.KDC(2,t); + k.KO(-1,t,"ឱ"); + } + return r; + }; +} diff --git a/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.kmp b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.kmp new file mode 100644 index 00000000000..c3449523e39 Binary files /dev/null and b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.kmp differ diff --git a/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.kmx b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.kmx new file mode 100644 index 00000000000..d8495d187c4 Binary files /dev/null and b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.kmx differ diff --git a/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/khmer_angkor.kpj b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/khmer_angkor.kpj new file mode 100644 index 00000000000..4b239df20ce --- /dev/null +++ b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/khmer_angkor.kpj @@ -0,0 +1,187 @@ + + + + $PROJECTPATH\build + True + True + False + keyboard + + + + id_f347675c33d2e6b1c705c787fad4941a + khmer_angkor.kmn + source\khmer_angkor.kmn + 1.3 + .kmn +
+ Khmer Angkor + © 2015-2022 SIL International + More than just a Khmer Unicode keyboard. +
+
+ + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + khmer_angkor.kps + source\khmer_angkor.kps + + .kps +
+ Khmer Angkor + © 2015-2022 SIL International +
+
+ + id_8a1efc7c4ab7cfece8aedd847679ca27 + khmer_angkor.ico + source\khmer_angkor.ico + + .ico + id_f347675c33d2e6b1c705c787fad4941a + + + id_8dc195db32d1fd0514de0ad51fff5df0 + khmer_angkor.js + source\..\build\khmer_angkor.js + + .js + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_10596632fcbf4138d24bcccf53e6ae01 + khmer_angkor.kvk + source\..\build\khmer_angkor.kvk + + .kvk + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_0a851f95ce553ecd62cbee6c32ced68f + khmer_angkor.kmx + source\..\build\khmer_angkor.kmx + + .kmx + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_d8b6eb05f4b7e2945c10e04c1f49e4c8 + keyboard_layout.png + source\welcome\keyboard_layout.png + + .png + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_724e5b4c63f10bc0abf7077f7c3172fc + welcome.htm + source\welcome\welcome.htm + + .htm + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_35857cb2b54f123612735ec948400082 + FONTLOG.txt + source\..\..\..\shared\fonts\khmer\mondulkiri\FONTLOG.txt + + .txt + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_7e3afe5bb59b888b08b48cd5817d8de4 + Mondulkiri-B.ttf + source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-B.ttf + + .ttf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_b9734e80f86c69ea5ae4dfa9f0083d09 + Mondulkiri-BI.ttf + source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-BI.ttf + + .ttf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_25abe4d2b0abc03a5be5b666a8de776e + Mondulkiri-I.ttf + source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-I.ttf + + .ttf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_b766568498108eee46ed1601ff69c47d + Mondulkiri-R.ttf + source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-R.ttf + + .ttf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_84544d04133cab3dbfc86b91ad1a4e17 + OFL.txt + source\..\..\..\shared\fonts\khmer\mondulkiri\OFL.txt + + .txt + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_0c33fbefd1c20f487b1bea2343b3bb2c + OFL-FAQ.txt + source\..\..\..\shared\fonts\khmer\mondulkiri\OFL-FAQ.txt + + .txt + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_a59d89fca36a310147645fa2604e521b + KAK_Documentation_EN.pdf + source\welcome\KAK_Documentation_EN.pdf + + .pdf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_5643c4cd3933b3ada0b4af6579305ec4 + KAK_Documentation_KH.pdf + source\welcome\KAK_Documentation_KH.pdf + + .pdf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_8da344c4cea6f467013357fe099006f5 + readme.htm + source\readme.htm + + .htm + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_acb0dd94c60e345d999670e999cbd159 + image002.png + source\welcome\image002.png + + .png + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_4edf70bc019f05b5ad39a2ea727ad547 + khmer_busra_kbd.ttf + source\..\..\..\shared\fonts\khmer\busrakbd\khmer_busra_kbd.ttf + + .ttf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_bc823844e4399751e1867016801f7327 + splash.gif + source\splash.gif + + .gif + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + +
+
diff --git a/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/source/khmer_angkor.kps b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/source/khmer_angkor.kps new file mode 100644 index 00000000000..1278aa479c1 --- /dev/null +++ b/developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/source/khmer_angkor.kps @@ -0,0 +1,49 @@ + + + + 15.0.266.0 + 7.0 + + + ..\LICENSE.md + + + + Khmer Angkor + © 2015-2022 SIL International + Multiple Authors + + https://keyman.com/keyboards/khmer_angkor + Khmer Unicode keyboard layout based on the NiDA keyboard layout. Automatically corrects many common keying errors. + + + + ..\LICENSE.md + File LICENSE.md + 0 + .md + + + ..\build\khmer_angkor.kmx + Keyboard Khmer Angkor + 0 + .kmx + + + ..\build\khmer_angkor.js + Keyboard Khmer Angkor + 0 + .js + + + + + Khmer Angkor + khmer_angkor + 1.3 + + Central Khmer (Khmer, Cambodia) + + + + diff --git a/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler-messages.ts b/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler-messages.ts index 8a9c2652ba0..2bf73b7dd31 100644 --- a/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler-messages.ts +++ b/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler-messages.ts @@ -2,7 +2,8 @@ import { assert } from 'chai'; import 'mocha'; import { KeyboardInfoCompilerMessages } from '../src/keyboard-info-compiler-messages.js'; import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; -import { CompilerErrorNamespace, KmpJsonFile, KeymanFileTypes } from '@keymanapp/common-types'; +import { KmpJsonFile, KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; import { makePathToFixture } from './helpers/index.js'; import { KeyboardInfoCompiler, KeyboardInfoCompilerResult } from '../src/keyboard-info-compiler.js'; import { KeyboardInfoFile } from '../src/keyboard-info-file.js'; @@ -56,7 +57,7 @@ describe('KeyboardInfoCompilerMessages', function () { assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_FileDoesNotExist).includes(KeymanFileTypes.Binary.WebKeyboard), KeymanFileTypes.Binary.WebKeyboard+' not found in the message'); }); - + // ERROR_FileDoesNotExist (.kmp fileSize) it('should generate ERROR_FileDoesNotExist error if .kmp file does not exist', async function() { @@ -141,7 +142,7 @@ describe('KeyboardInfoCompilerMessages', function () { assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_LicenseFileIsMissing).includes(licenseFilename), licenseFilename+' not found in the message'); }); - + // ERROR_LicenseFileIsDamaged (error on decode) it('should generate ERROR_LicenseFileIsDamaged error if license file throws error on decode', async function() { @@ -197,7 +198,7 @@ describe('KeyboardInfoCompilerMessages', function () { assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_LicenseIsNotValid).includes(licenseFilename), licenseFilename+' not found in the message'); }); - + // ERROR_CannotBuildWithoutKmpFile it('should generate ERROR_CannotBuildWithoutKmpFile error if .kmp file is not in sources', async function() { @@ -227,7 +228,7 @@ describe('KeyboardInfoCompilerMessages', function () { assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.ERROR_CannotBuildWithoutKmpFile), `ERROR_CannotBuildWithoutKmpFile not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); }); - + // ERROR_NoLicenseFound it('should generate ERROR_NoLicenseFound error if licence file is not in .kps options', async function() { @@ -257,7 +258,7 @@ describe('KeyboardInfoCompilerMessages', function () { assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.ERROR_NoLicenseFound), `ERROR_NoLicenseFound not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); }); - + // ERROR_FontFileMetaDataIsInvalid it('should generate ERROR_FontFileMetaDataIsInvalid error if font file meta data throws an error', async function() { @@ -285,7 +286,38 @@ describe('KeyboardInfoCompilerMessages', function () { `ERROR_FontFileMetaDataIsInvalid not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_FontFileMetaDataIsInvalid).includes(kmpJsonData.files[0].name), kmpJsonData.files[0].name+' not found in the message'); - }); + }); + + // ERROR_InvalidAuthorEmail + + it('should generate ERROR_InvalidAuthorEmail error if multiple email addresses are listed in .kps', async function() { + const jsFilename = makePathToFixture('multiple-email-addresses', 'build', 'khmer_angkor.js'); + const kpsFilename = makePathToFixture('multiple-email-addresses', 'source', 'khmer_angkor.kps'); + const kmpFilename = makePathToFixture('multiple-email-addresses', 'build', 'khmer_angkor.kmp'); + + const sources = { + kmpFilename, + sourcePath: 'release/k/multiple-email-addresses', + kpsFilename, + jsFilename: jsFilename, + forPublishing: true, + }; + + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + let result: KeyboardInfoCompilerResult = null; + try { + result = await compiler.run(kmpFilename, null); + } catch(e) { + callbacks.printMessages(); + throw e; + } + assert.isNull(result); + + assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.ERROR_InvalidAuthorEmail), + `ERROR_InvalidAuthorEmail not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); + }); + }); function nodeCompilerMessage(ncb: TestCompilerCallbacks, code: number): string { diff --git a/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts index c25a1eab1fc..99c244be2dd 100644 --- a/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts +++ b/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts @@ -6,7 +6,8 @@ import { makePathToFixture } from './helpers/index.js'; import { KeyboardInfoCompiler, KeyboardInfoCompilerResult, unitTestEndpoints } from '../src/keyboard-info-compiler.js'; import langtags from "../src/imports/langtags.js"; import { KmpCompiler, KmpCompilerOptions } from '@keymanapp/kmc-package'; -import { CompilerCallbacks, KMX, KeymanFileTypes, KeymanTargets, KmpJsonFile } from '@keymanapp/common-types'; +import { KMX, KeymanFileTypes, KeymanTargets, KmpJsonFile } from '@keymanapp/common-types'; +import { CompilerCallbacks } from '@keymanapp/developer-utils'; import { KeyboardInfoFile, KeyboardInfoFileLanguage, KeyboardInfoFilePlatform } from './keyboard-info-file.js'; const callbacks = new TestCompilerCallbacks(); @@ -125,7 +126,7 @@ describe('keyboard-info-compiler', function () { assert.isTrue(await compiler.init(callbacks, {sources})); assert.deepEqual(compiler['callbacks'], callbacks); assert.deepEqual(compiler['options'], {sources}); - }); + }); it('check run returns null if KmpCompiler.init fails', async function() { const kpjFilename = KHMER_ANGKOR_KPJ; @@ -144,7 +145,7 @@ describe('keyboard-info-compiler', function () { } assert.isNull(result); }); - + it('check run returns null if KmpCompiler.transformKpsToKmpObject fails', async function() { const sources = KHMER_ANGKOR_SOURCES; const compiler = new KeyboardInfoCompiler(); @@ -161,7 +162,7 @@ describe('keyboard-info-compiler', function () { } assert.isNull(result); }); - + it('check run returns null if loadJsFile fails', async function() { const sources = KHMER_ANGKOR_SOURCES const compiler = new KeyboardInfoCompiler(); @@ -169,7 +170,7 @@ describe('keyboard-info-compiler', function () { compiler['loadJsFile'] = (_filename: string): string => null; const result = await compiler.run(KHMER_ANGKOR_KPJ, null); assert.isNull(result); - }); + }); it('check run returns null if license is not MIT', async function() { const sources = KHMER_ANGKOR_SOURCES; @@ -251,7 +252,7 @@ describe('keyboard-info-compiler', function () { compiler['fillLanguages'] = async (_kpsFilename: string, _keyboard_info: KeyboardInfoFile, _kmpJsonData: KmpJsonFile.KmpJsonFile): Promise => false; const result = await compiler.run(KHMER_ANGKOR_KPJ, null); assert.isNull(result); - }); + }); const packageIncludesTestCases = [ { @@ -441,23 +442,23 @@ describe('keyboard-info-compiler', function () { it('check mapKeymanTargetToPlatform returns correct platforms', async function() { const compiler = new KeyboardInfoCompiler(); const map: {[index in KeymanTargets.KeymanTarget]: KeyboardInfoFilePlatform[]} = { - any: [], + any: [], androidphone: ['android'], androidtablet: ['android'], - desktop: [], + desktop: [], ipad: ['ios'], iphone: ['ios'], linux: ['linux'], macosx: ['macos'], - mobile: [], - tablet: [], - web: ['desktopWeb'], + mobile: [], + tablet: [], + web: ['desktopWeb'], windows: ['windows'] } for (const [target, platform] of Object.entries(map)) { assert.deepEqual(compiler['mapKeymanTargetToPlatform'](target), platform); } - }); + }); it('check kmxFileVersionToString returns correct strings', async function() { const compiler = new KeyboardInfoCompiler(); @@ -472,7 +473,7 @@ describe('keyboard-info-compiler', function () { assert.equal(compiler['kmxFileVersionToString'](testCase.num), testCase.str); }); }); - + it('check loadKmxFiles returns empty array if .kmx file is missing from .kmp', async function() { const compiler = new KeyboardInfoCompiler(); const kmpCompiler = new KmpCompiler(); @@ -498,7 +499,7 @@ describe('keyboard-info-compiler', function () { const kmpIndex = kmpJsonData.files.findIndex(file => KeymanFileTypes.filenameIs(file.name, KeymanFileTypes.Binary.Keyboard)); kmpJsonData.files[kmpIndex].name = '../build/throw_error.kmx'; assert.throws(() => compiler['loadKmxFiles'](KHMER_ANGKOR_KPS, kmpJsonData)); - }); + }); it('check loadKmxFiles can handle two .kmx files', async function() { const jsFilename = makePathToFixture('two-kmx', 'build', 'two_kmx.js'); @@ -515,7 +516,7 @@ describe('keyboard-info-compiler', function () { const kmx_filename_001 = 'k_001___basic_input_unicodei.kmx'; const kmx_filename_002 = 'k_002___basic_input_unicode.kmx'; - + const compiler = new KeyboardInfoCompiler(); assert.isTrue(await compiler.init(callbacks, {sources})); const kmpJsonData: KmpJsonFile.KmpJsonFile = { @@ -535,7 +536,7 @@ describe('keyboard-info-compiler', function () { assert.deepEqual(kmxFiles[1].filename, kmx_filename_002); assert.isNotNull(kmxFiles[0].data); assert.isNotNull(kmxFiles[1].data); - }); + }); it('check loadJsFile throws error if .js file is invalid', async function() { const jsFilename = makePathToFixture('invalid-js-file', 'build', 'invalid_js_file.js'); @@ -767,7 +768,7 @@ describe('keyboard-info-compiler', function () { } finally { Intl.DisplayNames.prototype.of = origIntlDisplayNamesOf; } - }); + }); it('check fillLanguages returns false if fontSourceToKeyboardInfoFont fails for display font', async function() { const kmpJsonData: KmpJsonFile.KmpJsonFile = { @@ -833,5 +834,5 @@ describe('keyboard-info-compiler', function () { const fonts = [KHMER_ANGKOR_DISPLAY_FONT]; const result = await compiler['fontSourceToKeyboardInfoFont'](KHMER_ANGKOR_KPS, kmpJsonData, fonts); assert.deepEqual(result, KHMER_ANGKOR_DISPLAY_FONT_INFO); - }); + }); }); diff --git a/developer/src/kmc-keyboard-info/test/tsconfig.json b/developer/src/kmc-keyboard-info/test/tsconfig.json index 6d48ad864d2..e16b935aa47 100644 --- a/developer/src/kmc-keyboard-info/test/tsconfig.json +++ b/developer/src/kmc-keyboard-info/test/tsconfig.json @@ -8,10 +8,6 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "baseUrl": ".", - "paths": { - "@keymanapp/developer-test-helpers": ["../../common/web/test-helpers/index"], - "@keymanapp/kmc-package": ["../../kmc-package/source"], - }, }, "include": [ "**/test-*.ts", diff --git a/developer/src/kmc-keyboard-info/tsconfig.json b/developer/src/kmc-keyboard-info/tsconfig.json index 414ff1f122b..d09b25c5604 100644 --- a/developer/src/kmc-keyboard-info/tsconfig.json +++ b/developer/src/kmc-keyboard-info/tsconfig.json @@ -10,11 +10,6 @@ */ "allowJs": true, "noImplicitAny": false, - "paths": { - "@keymanapp/common-types": ["../../../common/web/types/src/main"], - "@keymanapp/kmc-package": ["../kmc-package/source"], - "@keymanapp/developer-utils": ["../common/web/utils/index"], - } }, "include": [ "src/**/*.ts", diff --git a/developer/src/kmc-kmn/.eslintrc.cjs b/developer/src/kmc-kmn/.eslintrc.cjs index 156e7b5cf07..9fb322a1e46 100644 --- a/developer/src/kmc-kmn/.eslintrc.cjs +++ b/developer/src/kmc-kmn/.eslintrc.cjs @@ -5,7 +5,7 @@ module.exports = { overrides: [ { files:"src/**/*.ts", - extends: ["../../../common/web/eslint/eslintNoNodeImports.js"], + extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } ], ignorePatterns: ["test/fixtures/*"], diff --git a/developer/src/kmc-kmn/build.sh b/developer/src/kmc-kmn/build.sh index be6a2fcff5e..a0732414758 100755 --- a/developer/src/kmc-kmn/build.sh +++ b/developer/src/kmc-kmn/build.sh @@ -15,6 +15,7 @@ builder_describe "Keyman Developer Compiler Module for .kmn to .kmx" \ "@/common/web/keyman-version" \ "@/common/web/types" \ "@/developer/src/common/web/test-helpers" \ + "@/developer/src/common/web/utils" \ "@/developer/src/kmcmplib:wasm" \ "configure" \ "build" \ @@ -69,7 +70,7 @@ if builder_start_action test; then copy_deps tsc --build test/ npm run lint - readonly C8_THRESHOLD=74 + readonly C8_THRESHOLD=80 c8 --reporter=lcov --reporter=text --lines $C8_THRESHOLD --statements $C8_THRESHOLD --branches $C8_THRESHOLD --functions $C8_THRESHOLD mocha builder_echo warning "Coverage thresholds are currently $C8_THRESHOLD%, which is lower than ideal." builder_echo warning "Please increase threshold in build.sh as test coverage improves." diff --git a/developer/src/kmc-kmn/package.json b/developer/src/kmc-kmn/package.json index db1794badd3..fc9470a6bce 100644 --- a/developer/src/kmc-kmn/package.json +++ b/developer/src/kmc-kmn/package.json @@ -26,9 +26,9 @@ "url": "https://github.com/keymanapp/keyman/issues" }, "dependencies": { + "@keymanapp/common-types": "*", "@keymanapp/keyman-version": "*", - "@keymanapp/developer-utils": "*", - "@keymanapp/kmc-kmn": "*" + "@keymanapp/developer-utils": "*" }, "devDependencies": { "@keymanapp/developer-test-helpers": "*", @@ -41,8 +41,7 @@ "chalk": "^2.4.2", "mocha": "^8.4.0", "sinon-chai": "^3.7.0", - "ts-node": "^9.1.1", - "typescript": "^4.9.5" + "typescript": "^5.4.5" }, "mocha": { "spec": "build/test/**/test-*.js", diff --git a/developer/src/kmc-kmn/src/compiler/compiler.ts b/developer/src/kmc-kmn/src/compiler/compiler.ts index a7b9a73c280..afb980e2fc7 100644 --- a/developer/src/kmc-kmn/src/compiler/compiler.ts +++ b/developer/src/kmc-kmn/src/compiler/compiler.ts @@ -6,11 +6,14 @@ TODO: implement additional interfaces: */ // TODO: rename wasm-host? -import { UnicodeSetParser, UnicodeSet, VisualKeyboard, KvkFileReader, KeymanCompiler, KeymanCompilerArtifacts, KeymanCompilerArtifactOptional, KeymanCompilerResult, KeymanCompilerArtifact } from '@keymanapp/common-types'; -import { CompilerCallbacks, CompilerEvent, CompilerOptions, KeymanFileTypes, KvkFileWriter, KvksFileReader } from '@keymanapp/common-types'; +import { VisualKeyboard, KvkFileReader, UnicodeSetParser, UnicodeSet, KeymanFileTypes, KvkFileWriter } from '@keymanapp/common-types'; +import { + CompilerCallbacks, CompilerEvent, CompilerOptions, KeymanCompiler, KeymanCompilerArtifacts, + KeymanCompilerArtifactOptional, KeymanCompilerResult, KeymanCompilerArtifact, KvksFileReader +} from '@keymanapp/developer-utils'; import * as Osk from './osk.js'; import loadWasmHost from '../import/kmcmplib/wasm-host.js'; -import { CompilerMessages, mapErrorFromKmcmplib } from './kmn-compiler-messages.js'; +import { KmnCompilerMessages } from './kmn-compiler-messages.js'; import { WriteCompiledKeyboard } from '../kmw-compiler/kmw-compiler.js'; // @@ -57,6 +60,15 @@ export interface KmnCompilerResultExtra { groups: CompilerResultExtraGroup[]; }; +/** @internal */ +export interface KmnCompilerResultMessage { + errorCode: number; + lineNumber: number; + columnNumber: number; + filename: string; + parameters: string[]; +} + /** * @public * Internal in-memory build artifacts from a successful compilation @@ -114,16 +126,6 @@ const baseOptions: KmnCompilerOptions = { warnDeprecatedCode: true, }; -/** - * Allows multiple instances of the Compiler class, by ensuring that the - * 'unique' kmnCompilerCallback global will be correlated with a specific - * instance of the Compiler class - */ -let callbackProcIdentifier = 0; - -const - callbackPrefix = 'kmnCompilerCallbacks_'; - interface MallocAndFree { malloc(sz: number) : number; free(p: number) : null; @@ -139,16 +141,10 @@ let * external IO. */ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { - private readonly callbackID: string; // a unique numeric id added to globals with prefixed names private callbacks: CompilerCallbacks; private wasmExports: MallocAndFree; private options: KmnCompilerOptions; - constructor() { - this.callbackID = callbackPrefix + callbackProcIdentifier.toString(); - callbackProcIdentifier++; - } - /** * Initialize the compiler, including loading the WASM host for kmcmplib. * Copies options. @@ -165,7 +161,7 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { Module = await loadWasmHost(); } catch(e: any) { /* c8 ignore next 3 */ - this.callbacks.reportMessage(CompilerMessages.Fatal_MissingWasmModule({e})); + this.callbacks.reportMessage(KmnCompilerMessages.Fatal_MissingWasmModule({e})); return false; } } @@ -185,7 +181,7 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { if(!Module) { /* c8 ignore next 4 */ // fail if wasm not loaded or function not found - this.callbacks.reportMessage(CompilerMessages.Fatal_MissingWasmModule({})); + this.callbacks.reportMessage(KmnCompilerMessages.Fatal_MissingWasmModule({})); return false; } return true; @@ -220,46 +216,6 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { return true; } - private compilerMessageCallback = (line: number, code: number, msg: string): number => { - this.callbacks.reportMessage(mapErrorFromKmcmplib(line, code, msg)); - return 1; - } - - private cachedFile: {filename: string; data: Uint8Array} = { - filename: null, - data: null - }; - - private loadFileCallback = (filename: string, baseFilename: string, buffer: number, bufferSize: number): number => { - let resolvedFilename = this.callbacks.resolveFilename(baseFilename, filename); - let data: Uint8Array; - if(this.cachedFile.filename == resolvedFilename) { - data = this.cachedFile.data; - } - else { - data = this.callbacks.loadFile(resolvedFilename); - if(!data) { - return -1; - } - this.cachedFile.filename = resolvedFilename; - this.cachedFile.data = data; - } - - if(buffer == 0) { - /* We need to return buffer size required */ - return data.byteLength; - } - - if(bufferSize != data.byteLength) { - /* c8 ignore next 2 */ - throw new Error(`loadFileCallback: second call, expected file size ${bufferSize} == ${data.byteLength}`); - } - - Module.HEAP8.set(data, buffer); - - return 1; - } - private copyWasmResult(wasm_result: any): KmnCompilerResult { let result: KmnCompilerResult = { // We cannot Object.assign or {...} on a wasm-defined object, so... @@ -299,6 +255,31 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { return new Uint8Array(new Uint8Array(Module.HEAP8.buffer, offset, size)); } + private toTitleCase = (s: string) => s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + + private generateKmcmpLibMessage(filename: string, line: number, code: number, parameters: string[]) { + const keys = Object.keys(KmnCompilerMessages); + const m = KmnCompilerMessages as Record; + const key = keys.find(key => m[key] === code); + if(!key) { + return `Unknown message ${code.toString(16)} -- message identifier not found`; + } + + const o = /^(INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key); + if(!o) { + return `Unknown message ${code.toString(16)} -- message identifier is not a valid format`; + } + + const generator = this.toTitleCase(o[1])+'_'+o[2]; + if(!generator || typeof m[generator] != 'function') { + return `Unknown message ${code.toString(16)} -- generator function not found`; + } + const result = m[generator]({p:parameters}); + result.filename = filename; + result.line = line; + return result; + } + /** * Compiles a .kmn file to .kmx, .kvk, and/or .js files. Returns an object * containing binary artifacts on success. The files are passed in by name, @@ -322,12 +303,17 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { outfile = outfile ?? infile.replace(/\.kmn$/i, '.kmx'); - (globalThis as any)[this.callbackID] = { - message: this.compilerMessageCallback, - loadFile: this.loadFileCallback - }; + const compiler = this; + const wasm_callbacks = Module.WasmCallbackInterface.implement({ + message: function(message: KmnCompilerResultMessage) { + compiler.callbacks.reportMessage(compiler.generateKmcmpLibMessage(message.filename, message.lineNumber, message.errorCode, message.parameters)); + }, + loadFile: function(filename: string, baseFilename: string): number[] { + const data: Uint8Array = compiler.callbacks.loadFile(compiler.callbacks.resolveFilename(baseFilename, filename)); + return data ? Array.from(data) : null; + } + }); - let wasm_interface = new Module.CompilerInterface(); let wasm_options = new Module.CompilerOptions(); let wasm_result = null; try { @@ -336,8 +322,8 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { wasm_options.warnDeprecatedCode = options.warnDeprecatedCode; wasm_options.shouldAddCompilerVersion = options.shouldAddCompilerVersion; wasm_options.target = 0; // CKF_KEYMAN; TODO use COMPILETARGETS_KMX - wasm_interface.callbacksKey = this.callbackID; // key of object on globalThis - wasm_result = Module.kmcmp_compile(infile, wasm_options, wasm_interface); + + wasm_result = Module.kmcmp_compile(infile, wasm_options, wasm_callbacks); if(!wasm_result.result) { return null; } @@ -382,7 +368,7 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { // should have no impact on the final .js if options.debug is false wasm_options.saveDebug = true; - wasm_result = Module.kmcmp_compile(infile, wasm_options, wasm_interface); + wasm_result = Module.kmcmp_compile(infile, wasm_options, wasm_callbacks); if(!wasm_result.result) { return null; } @@ -399,15 +385,14 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { return result; } catch(e) { /* c8 ignore next 3 */ - this.callbacks.reportMessage(CompilerMessages.Fatal_UnexpectedException({e:e})); + this.callbacks.reportMessage(KmnCompilerMessages.Fatal_UnexpectedException({e:e})); return null; } finally { if(wasm_result) { wasm_result.delete(); } - wasm_interface.delete(); + wasm_callbacks.delete(); wasm_options.delete(); - delete (globalThis as any)[this.callbackID]; } } @@ -440,7 +425,7 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { kvksFilename = this.callbacks.resolveFilename(kmnFilename, kvksFilename); const data = this.callbacks.loadFile(kvksFilename); if(!data) { - this.callbacks.reportMessage(CompilerMessages.Error_FileNotFound({filename: kvksFilename})); + this.callbacks.reportMessage(KmnCompilerMessages.Error_FileNotFound({filename: kvksFilename})); return null; } @@ -455,7 +440,7 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { try { vk = reader.read(data); } catch(e) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidKvkFile({filename, e})); + this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvkFile({filename, e})); return null; } } else { @@ -466,13 +451,13 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { kvks = reader.read(data); reader.validate(kvks); } catch(e) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidKvksFile({filename, e})); + this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e})); return null; } let invalidVkeys: string[] = []; vk = reader.transform(kvks, invalidVkeys); for(let invalidVkey of invalidVkeys) { - this.callbacks.reportMessage(CompilerMessages.Warn_InvalidVkeyInKvksFile({filename, invalidVkey})); + this.callbacks.reportMessage(KmnCompilerMessages.Warn_InvalidVkeyInKvksFile({filename, invalidVkey})); } } @@ -500,13 +485,13 @@ export class KmnCompiler implements KeymanCompiler, UnicodeSetParser { // Expected file format: displaymap.schema.json const data = this.callbacks.loadFile(displayMapFilename); if(!data) { - this.callbacks.reportMessage(CompilerMessages.Error_FileNotFound({filename: displayMapFilename})); + this.callbacks.reportMessage(KmnCompilerMessages.Error_FileNotFound({filename: displayMapFilename})); return null; } const mapping = JSON.parse(new TextDecoder().decode(data)); return Osk.parseMapping(mapping); } catch(e) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidDisplayMapFile({filename: displayMapFilename, e})); + this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidDisplayMapFile({filename: displayMapFilename, e})); return null; } } @@ -616,16 +601,16 @@ function getUnicodeSetError(rc: number) : CompilerEvent { const KMCMP_FATAL_OUT_OF_RANGE = -4; switch(rc) { case KMCMP_ERROR_SYNTAX_ERR: - return CompilerMessages.Error_UnicodeSetSyntaxError(); + return KmnCompilerMessages.Error_UnicodeSetSyntaxError(); case KMCMP_ERROR_HAS_STRINGS: - return CompilerMessages.Error_UnicodeSetHasStrings(); + return KmnCompilerMessages.Error_UnicodeSetHasStrings(); case KMCMP_ERROR_UNSUPPORTED_PROPERTY: - return CompilerMessages.Error_UnicodeSetHasProperties(); + return KmnCompilerMessages.Error_UnicodeSetHasProperties(); case KMCMP_FATAL_OUT_OF_RANGE: - return CompilerMessages.Fatal_UnicodeSetOutOfRange(); + return KmnCompilerMessages.Fatal_UnicodeSetOutOfRange(); default: /* c8 ignore next */ - return CompilerMessages.Fatal_UnexpectedException({e: `Unexpected UnicodeSet error code ${rc}`}); + return KmnCompilerMessages.Fatal_UnexpectedException({e: `Unexpected UnicodeSet error code ${rc}`}); } } diff --git a/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts b/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts index 32dac9fb661..8d2d4c88ead 100644 --- a/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts +++ b/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts @@ -1,6 +1,5 @@ -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/common-types"; import { kmnfile } from "../kmw-compiler/compiler-globals.js"; -import { KeymanUrls } from "@keymanapp/developer-utils"; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException, KeymanUrls } from "@keymanapp/developer-utils"; const Namespace = CompilerErrorNamespace.KmnCompiler; const SevInfo = CompilerErrorSeverity.Info | Namespace; @@ -18,30 +17,6 @@ const mw = (code: number, message: string, o?: {filename?: string, line?: number line: o?.line, }); -/** - * LogLevel comes from kmn_compiler_errors.h, for legacy compiler error messages - */ -const enum LogLevel { - LEVEL_MASK = 0xF000, - CODE_MASK = 0x0FFF, - CERR_FATAL = 0x8000, - CERR_ERROR = 0x4000, - CERR_WARNING = 0x2000, - CERR_HINT = 0x1000, - CERR_INFO = 0 -}; - -/** - * Translate the legacy compiler error messages to Severity codes - */ -const LogLevelToSeverity: Record = { - [LogLevel.CERR_FATAL]: SevFatal, - [LogLevel.CERR_ERROR]: SevError, - [LogLevel.CERR_WARNING]: SevWarn, - [LogLevel.CERR_HINT]: SevHint, - [LogLevel.CERR_INFO]: SevInfo -} - export const enum KmnCompilerMessageRanges { RANGE_KMN_COMPILER_MIN = 0x001, // from kmn_compiler_errors.h RANGE_KMN_COMPILER_MAX = 0x7FF, // from kmn_compiler_errors.h @@ -51,6 +26,8 @@ export const enum KmnCompilerMessageRanges { RANGE_CompilerMessage_Max = 0xFFF, // Highest available error code for kmc-kmn } +type KmcmpLibMessageParameters = {p:string[]}; + /** * @internal * @@ -211,10 +188,8 @@ export class KmnCompilerMessages { to the file.` ); - // static INFO_None = SevInfo | 0x000; - - static INFO_EndOfFile = SevInfo | 0x001; - static Info_EndOfFile = () => m(this.INFO_EndOfFile, `(no error - reserved code)`); + // static STATUS_None = 0x000; // This is not a real error + // static STATUS_EndOfFile = 0x001; // This is not a real error static FATAL_BadCallParams = SevFatal | 0x002; static Fatal_BadCallParams = () => m(this.FATAL_BadCallParams, `CompileKeyboardFile was called with bad parameters`); @@ -228,8 +203,8 @@ export class KmnCompilerMessages { // static ERROR_CannotCreateOutfile = SevError | 0x006; // #10678: reduced from fatal to error in 17.0, but unused // static Error_CannotCreateOutfile = () => m(this.ERROR_CannotCreateOutfile, `Cannot open output file for writing`); unused - static FATAL_UnableToWriteFully = SevFatal | 0x007; - static Fatal_UnableToWriteFully = () => m(this.FATAL_UnableToWriteFully, `Unable to write the file completely`); + // static FATAL_UnableToWriteFully = SevFatal | 0x007; + // static Fatal_UnableToWriteFully = () => m(this.FATAL_UnableToWriteFully, `Unable to write the file completely`); static ERROR_CannotReadInfile = SevError | 0x008; // #10678: reduced from fatal to error in 17.0 static Error_CannotReadInfile = () => m(this.ERROR_CannotReadInfile, `Cannot read the input file`); @@ -484,10 +459,10 @@ export class KmnCompilerMessages { `Invalid key identifier "${def(o.keyId)}"`); static ERROR_90FeatureOnlyLayoutFile = SevError | 0x05C; - static Error_90FeatureOnlyLayoutFile = () => m(this.ERROR_90FeatureOnlyLayoutFile, `Touch layout file reference requires store(version) '9.0'or higher`); + static Error_90FeatureOnlyLayoutFile = () => m(this.ERROR_90FeatureOnlyLayoutFile, `Touch layout file reference requires store(version) '9.0' or higher`); static ERROR_90FeatureOnlyKeyboardVersion = SevError | 0x05D; - static Error_90FeatureOnlyKeyboardVersion = () => m(this.ERROR_90FeatureOnlyKeyboardVersion, `KeyboardVersion system store requires store(version) '9.0'or higher`); + static Error_90FeatureOnlyKeyboardVersion = () => m(this.ERROR_90FeatureOnlyKeyboardVersion, `KeyboardVersion system store requires store(version) '9.0' or higher`); static ERROR_KeyboardVersionFormatInvalid = SevError | 0x05E; static Error_KeyboardVersionFormatInvalid = () => m(this.ERROR_KeyboardVersionFormatInvalid, `KeyboardVersion format is invalid, expecting dot-separated integers`); @@ -496,16 +471,16 @@ export class KmnCompilerMessages { static Error_ContextExHasInvalidOffset = () => m(this.ERROR_ContextExHasInvalidOffset, `context() statement has offset out of range`); static ERROR_90FeatureOnlyEmbedCSS = SevError | 0x060; - static Error_90FeatureOnlyEmbedCSS = () => m(this.ERROR_90FeatureOnlyEmbedCSS, `Embedding CSS requires store(version) '9.0'or higher`); + static Error_90FeatureOnlyEmbedCSS = () => m(this.ERROR_90FeatureOnlyEmbedCSS, `Embedding CSS requires store(version) '9.0' or higher`); static ERROR_90FeatureOnlyTargets = SevError | 0x061; - static Error_90FeatureOnlyTargets = () => m(this.ERROR_90FeatureOnlyTargets, `TARGETS system store requires store(version) '9.0'or higher`); + static Error_90FeatureOnlyTargets = () => m(this.ERROR_90FeatureOnlyTargets, `TARGETS system store requires store(version) '9.0' or higher`); static ERROR_ContextAndIndexInvalidInMatchNomatch = SevError | 0x062; static Error_ContextAndIndexInvalidInMatchNomatch = () => m(this.ERROR_ContextAndIndexInvalidInMatchNomatch, `context and index statements cannot be used in a match or nomatch statement`); static ERROR_140FeatureOnlyContextAndNotAnyWeb = SevError | 0x063; - static Error_140FeatureOnlyContextAndNotAnyWeb = () => m(this.ERROR_140FeatureOnlyContextAndNotAnyWeb, `For web and touch platforms, context() statement referring to notany() requires store(version) '14.0'or higher`); + static Error_140FeatureOnlyContextAndNotAnyWeb = () => m(this.ERROR_140FeatureOnlyContextAndNotAnyWeb, `For web and touch platforms, context() statement referring to notany() requires store(version) '14.0' or higher`); static ERROR_ExpansionMustFollowCharacterOrVKey = SevError | 0x064; @@ -548,10 +523,16 @@ export class KmnCompilerMessages { static Error_PostKeystrokeGroupMustBeReadonly = () => m(this.ERROR_PostKeystrokeGroupMustBeReadonly, `Group used in begin postKeystroke must be readonly`); static ERROR_DuplicateGroup = SevError | 0x071; - static Error_DuplicateGroup = () => m(this.ERROR_DuplicateGroup, `A group with this name has already been defined.`); + static Error_DuplicateGroup = (o:KmcmpLibMessageParameters) => m( + this.ERROR_DuplicateGroup, + `A group with the name '${o.p?.[0]}' has already been defined on line ${o.p?.[1]}.` + ); static ERROR_DuplicateStore = SevError | 0x072; - static Error_DuplicateStore = () => m(this.ERROR_DuplicateStore, `A store with this name has already been defined.`); + static Error_DuplicateStore = (o:KmcmpLibMessageParameters) => m( + this.ERROR_DuplicateStore, + `A store with the name '${o.p?.[0]}' has already been defined on line ${o.p?.[1]}.` + ); static ERROR_RepeatedBegin = SevError | 0x073; static Error_RepeatedBegin = () => m(this.ERROR_RepeatedBegin, `Begin has already been set`); @@ -571,6 +552,17 @@ export class KmnCompilerMessages { static ERROR_CharacterRangeTooLong = SevError | 0x078; static Error_CharacterRangeTooLong = () => m(this.ERROR_CharacterRangeTooLong, `Character range is too large and cannot be expanded`); + static ERROR_NonBMPCharactersNotSupportedInKeySection = SevError | 0x079; + static Error_NonBMPCharactersNotSupportedInKeySection = () => m(this.ERROR_NonBMPCharactersNotSupportedInKeySection, `Characters with codepoints over U+FFFF are not supported in the key part of the rule`); + + static ERROR_InvalidTarget = SevError | 0x07A; + static Error_InvalidTarget = (o:KmcmpLibMessageParameters) => m( + this.ERROR_InvalidTarget, + `Unrecognized compile target '${o.p?.[0]}'` + ); + + static ERROR_NoTargetsSpecified = SevError | 0x07B; + static Error_NoTargetsSpecified = () => m(this.ERROR_NoTargetsSpecified, `At least one compile target must be specified`); static WARN_TooManyWarnings = SevWarn | 0x080; static Warn_TooManyWarnings = () => m(this.WARN_TooManyWarnings, `Too many warnings or errors`); @@ -602,10 +594,11 @@ export class KmnCompilerMessages { static WARN_ReservedCharacter = SevWarn | 0x089; static Warn_ReservedCharacter = () => m(this.WARN_ReservedCharacter, `A Unicode character was found that should not be used`); - // Note: INFO_Info is called CWARN_Info in kmn_compiler_errors.h, but should have an "info" severity - static INFO_Info = SevInfo | 0x08A; - static Info_Info = () => m(this.INFO_Info, `Information`); - + static INFO_MinimumCoreEngineVersion = SevInfo | 0x08A; + static Info_MinimumCoreEngineVersion = (o:KmcmpLibMessageParameters) => m( + this.INFO_MinimumCoreEngineVersion, + `The compiler has assigned a minimum engine version of ${o.p?.[0]}.${o.p?.[1]} based on features used in this keyboard` + ); static WARN_VirtualKeyWithMnemonicLayout = SevWarn | 0x08B; static Warn_VirtualKeyWithMnemonicLayout = () => m(this.WARN_VirtualKeyWithMnemonicLayout, `Virtual key used instead of virtual character key with a mnemonic layout`); @@ -722,33 +715,23 @@ export class KmnCompilerMessages { static Warn_KeyShouldIncludeNCaps = () => m(this.WARN_KeyShouldIncludeNCaps, `Other rules which reference this key include CAPS or NCAPS modifiers, so this rule must include NCAPS modifier to avoid inconsistent matches`); static HINT_UnreachableRule = SevHint | 0x0AE; - static Hint_UnreachableRule = () => m(this.HINT_UnreachableRule, `This rule will never be matched as another rule takes precedence`); + static Hint_UnreachableRule = (o:KmcmpLibMessageParameters) => m( + this.HINT_UnreachableRule, + `This rule will never be matched as the rule on line ${o.p?.[0]} takes precedence` + ); static WARN_VirtualKeyInOutput = SevWarn | 0x0AF; static Warn_VirtualKeyInOutput = () => m(this.WARN_VirtualKeyInOutput, `Virtual keys are not supported in output`); - static FATAL_BufferOverflow = SevFatal | 0x0C0; - static Fatal_BufferOverflow = () => m(this.FATAL_BufferOverflow, `The compiler memory buffer overflowed`); - - static FATAL_Break = SevFatal | 0x0C1; - static Fatal_Break = () => m(this.FATAL_Break, `Compiler interrupted by user`); -}; + static HINT_IndexStoreLong = SevHint | 0x0B0; + static Hint_IndexStoreLong = () => m( + this.HINT_IndexStoreLong, + `The store referenced in index() is longer than the store referenced in any()` + ); -/** - * @internal - * TODO: This class is here as a stopgap as we merged it with - * KmnCompilerMessages. It should be removed in v18.0. - */ -export class CompilerMessages extends KmnCompilerMessages { -} + static FATAL_BufferOverflow = SevFatal | 0x0C0; + static Fatal_BufferOverflow = () => m(this.FATAL_BufferOverflow, `The compiler memory buffer overflowed`); -export function mapErrorFromKmcmplib(line: number, code: number, msg: string): CompilerEvent { - const severity = LogLevelToSeverity[code & LogLevel.LEVEL_MASK]; - const baseCode = code & LogLevel.CODE_MASK; - const event: CompilerEvent = { - line: line, - code: severity | CompilerErrorNamespace.KmnCompiler | baseCode, - message: msg - }; - return event; + // static FATAL_Break = SevFatal | 0x0C1; + // static Fatal_Break = () => m(this.FATAL_Break, `Compiler interrupted by user`); }; diff --git a/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts b/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts index 3b09a18f479..a8c1e1b72c2 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts @@ -1,4 +1,5 @@ -import { KMX, CompilerCallbacks, CompilerOptions } from "@keymanapp/common-types"; +import { KMX } from "@keymanapp/common-types"; +import { CompilerCallbacks, CompilerOptions } from "@keymanapp/developer-utils"; import { KmnCompilerResult } from "../compiler/compiler.js"; export let FTabStop: string; @@ -14,6 +15,18 @@ export let FUnreachableKeys: KMX.KEY[]; export let FCallFunctions: string[]; export let FFix183_LadderLength: number; +let _minimumKeymanVersion: number; + +export function minimumKeymanVersion() { + return _minimumKeymanVersion; +} + +export function minimumKeymanVersionToString() { + const major = (_minimumKeymanVersion & KMX.KMXFile.VERSION_MASK_MAJOR) >> 8; + const minor = _minimumKeymanVersion & KMX.KMXFile.VERSION_MASK_MINOR; + return `${major}.${minor}`; +} + export function setupGlobals( _callbacks: CompilerCallbacks, _options: CompilerOptions, @@ -33,20 +46,93 @@ export function setupGlobals( FUnreachableKeys = []; FCallFunctions = []; FFix183_LadderLength = 100; // TODO: option + _minimumKeymanVersion = fk.fileVersion; } -export function IsKeyboardVersion10OrLater(): boolean { - return fk.fileVersion >= KMX.KMXFile.VERSION_100; +function isKeyboardVersionAutomaticallyDetermined() { + return (fk.flags & KMX.KMXFile.KF_AUTOMATICVERSION) == KMX.KMXFile.KF_AUTOMATICVERSION; } -export function IsKeyboardVersion14OrLater(): boolean { - return fk.fileVersion >= KMX.KMXFile.VERSION_140; +/** + * @see `verifyAndSetMinimumRequiredKeymanVersion10` for usage + */ +function verifyAndSetMinimumRequiredKeymanVersion(version: KMX.KMX_Version) { + if(isKeyboardVersionAutomaticallyDetermined()) { + _minimumKeymanVersion = Math.max(version, _minimumKeymanVersion); + return true; + } + + return _minimumKeymanVersion >= version; +} + +/** + * Verify that minimum supported Keyman version in the keyboard is version 10.0, + * and upgrade to that version if possible and necessary. + * + * Will upgrade the minimum version to 10.0 if `KF_AUTOMATICVERSION` flag is set + * for the keyboard, which correlates to having no `store(&version)` line in the + * .kmn source file. + * + * @returns `true` if the version is now 10.0 or higher, `false` if a lower + * version has been specified in the source file `store(&version)` line. + */ +export function verifyAndSetMinimumRequiredKeymanVersion10(): boolean { + return verifyAndSetMinimumRequiredKeymanVersion(KMX.KMX_Version.VERSION_100); } -export function IsKeyboardVersion15OrLater(): boolean { - return fk.fileVersion >= KMX.KMXFile.VERSION_150; +/** + * Verify that minimum supported Keyman version in the keyboard is version 14.0, + * and upgrade to that version if possible and necessary. + * + * Will upgrade the minimum version to 14.0 if `KF_AUTOMATICVERSION` flag is set + * for the keyboard, which correlates to having no `store(&version)` line in the + * .kmn source file. + * + * @returns `true` if the version is now 14.0 or higher, `false` if a lower + * version has been specified in the source file `store(&version)` line. + */ +export function verifyAndSetMinimumRequiredKeymanVersion14(): boolean { + return verifyAndSetMinimumRequiredKeymanVersion(KMX.KMX_Version.VERSION_140); +} + +/** + * Verify that minimum supported Keyman version in the keyboard is version 15.0, + * and upgrade to that version if possible and necessary. + * + * Will upgrade the minimum version to 15.0 if `KF_AUTOMATICVERSION` flag is set + * for the keyboard, which correlates to having no `store(&version)` line in the + * .kmn source file. + * + * @returns `true` if the version is now 15.0 or higher, `false` if a lower + * version has been specified in the source file `store(&version)` line. + */ +export function verifyAndSetMinimumRequiredKeymanVersion15(): boolean { + return verifyAndSetMinimumRequiredKeymanVersion(KMX.KMX_Version.VERSION_150); +} + +/** + * Verify that minimum supported Keyman version in the keyboard is version 17.0, + * and upgrade to that version if possible and necessary. + * + * Will upgrade the minimum version to 17.0 if `KF_AUTOMATICVERSION` flag is set + * for the keyboard, which correlates to having no `store(&version)` line in the + * .kmn source file. + * + * @returns `true` if the version is now 17.0 or higher, `false` if a lower + * version has been specified in the source file `store(&version)` line. + */ +export function verifyAndSetMinimumRequiredKeymanVersion17(): boolean { + return verifyAndSetMinimumRequiredKeymanVersion(KMX.KMX_Version.VERSION_170); +} + +export function isKeyboardVersion10OrLater(): boolean { + return fk.fileVersion >= KMX.KMXFile.VERSION_100; +} + +export function isKeyboardVersion14OrLater(): boolean { + return fk.fileVersion >= KMX.KMXFile.VERSION_140; } -export function IsKeyboardVersion17OrLater(): boolean { +export function isKeyboardVersion17OrLater(): boolean { return fk.fileVersion >= KMX.KMXFile.VERSION_170; } diff --git a/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts b/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts index 55eca7791fe..9cdb28a3d93 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts @@ -1,10 +1,13 @@ import { TSentinelRecord, GetSuppChar, ExpandSentinel, incxstr, xstrlen, xstrlen_printing } from "./util.js"; import { KMX } from "@keymanapp/common-types"; -import { callbacks, FCallFunctions, FFix183_LadderLength, FMnemonic, FTabStop, FUnreachableKeys, IsKeyboardVersion10OrLater, IsKeyboardVersion14OrLater, kmxResult, nl, options } from "./compiler-globals.js"; +import { callbacks, FCallFunctions, FFix183_LadderLength, FMnemonic, FTabStop, FUnreachableKeys, + kmxResult, nl, options, isKeyboardVersion10OrLater, isKeyboardVersion14OrLater, + verifyAndSetMinimumRequiredKeymanVersion10 } from "./compiler-globals.js"; import { KmwCompilerMessages } from "./kmw-compiler-messages.js"; import { FormatModifierAsBitflags, RuleIsExcludedByPlatform } from "./kmw-compiler.js"; -import { KMXCodeNames, SValidIdentifierCharSet, UnreachableKeyCodes, USEnglishShift, USEnglishUnshift, USEnglishValues } from "./constants.js"; +import { KMXCodeNames, SValidIdentifierCharSet, UnreachableKeyCodes, USEnglishShift, + USEnglishUnshift, USEnglishValues } from "./constants.js"; import { KMWVKeyNames, TKeymanWebTouchStandardKey, VKeyNames } from "./keymanweb-key-codes.js"; export function JavaScript_Name(i: number, pwszName: string, KeepNameForPersistentStorage: boolean = false): string { // I3659 @@ -49,7 +52,7 @@ export function JavaScript_Store(fk: KMX.KEYBOARD, line: number, pwsz: string): let n = pwsz.indexOf(wcsentinel); // Start: plain text store. Always use for < 10.0, conditionally for >= 10.0. - if(n < 0 || !IsKeyboardVersion10OrLater()) { + if(n < 0 || !isKeyboardVersion10OrLater()) { result = '"'; while(pwsz.length) { if(pwsz.charCodeAt(0) == KMX.KMXFile.UC_SENTINEL) { @@ -247,20 +250,29 @@ export function JavaScript_Shift(fkp: KMX.KEY, FMnemonic: boolean): number { } if (fkp.ShiftFlags & KMX.KMXFile.ISVIRTUALKEY) { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { + // don't attempt to upgrade to v10 at this point, only if we are already v10+ // Full chiral modifier and state key support starts with KeymanWeb 10.0 return fkp.ShiftFlags; } // Non-chiral support only and no support for state keys if (fkp.ShiftFlags & (KMX.KMXFile.LCTRLFLAG | KMX.KMXFile.RCTRLFLAG | KMX.KMXFile.LALTFLAG | KMX.KMXFile.RALTFLAG)) { // I4118 + if(verifyAndSetMinimumRequiredKeymanVersion10()) { + // upgrade to v10 if possible + return fkp.ShiftFlags; + } callbacks.reportMessage(KmwCompilerMessages.Warn_ExtendedShiftFlagsNotSupportedInKeymanWeb({line:fkp.Line, flags: 'LALT, RALT, LCTRL, RCTRL'})); } if (fkp.ShiftFlags & ( KMX.KMXFile.CAPITALFLAG | KMX.KMXFile.NOTCAPITALFLAG | KMX.KMXFile.NUMLOCKFLAG | KMX.KMXFile.NOTNUMLOCKFLAG | KMX.KMXFile.SCROLLFLAG | KMX.KMXFile.NOTSCROLLFLAG)) { // I4118 - callbacks.reportMessage(KmwCompilerMessages.Warn_ExtendedShiftFlagsNotSupportedInKeymanWeb({line:fkp.Line, flags: 'CAPS and NCAPS'})); + if(verifyAndSetMinimumRequiredKeymanVersion10()) { + // upgrade to v10 if possible + return fkp.ShiftFlags; + } + callbacks.reportMessage(KmwCompilerMessages.Warn_ExtendedShiftFlagsNotSupportedInKeymanWeb({line:fkp.Line, flags: 'CAPS and NCAPS'})); } return KMX.KMXFile.ISVIRTUALKEY | (fkp.ShiftFlags & (KMX.KMXFile.K_SHIFTFLAG | KMX.KMXFile.K_CTRLFLAG | KMX.KMXFile.K_ALTFLAG)); @@ -403,7 +415,7 @@ export function JavaScript_KeyAsString(fkp: KMX.KEY, FMnemonic: boolean): string } export function JavaScript_ContextMatch(fk: KMX.KEYBOARD, fkp: KMX.KEY, context: string): string { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { return JavaScript_FullContextValue(fk, fkp, context); } else { @@ -429,7 +441,7 @@ function CheckStoreForInvalidFunctions(fk: KMX.KEYBOARD, key: KMX.KEY, store: KM n = store.dpString.indexOf(wcsentinel); // Disable the check with versions >= 10.0, since we now support deadkeys in stores. - if (n >= 0 && !IsKeyboardVersion10OrLater) { + if (n >= 0 && !isKeyboardVersion10OrLater()) { rec = ExpandSentinel(fk, store.dpString, n); callbacks.reportMessage(KmwCompilerMessages.Error_NotSupportedInKeymanWebStore({code:GetCodeName(rec.Code), store:store.dpName})); } @@ -630,7 +642,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp for(let I = 1; I < Index; I++) { let recContext = ExpandSentinel(fk, pwszContext, x); - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { if(recContext.IsSentinel && [KMX.KMXFile.CODE_NUL, KMX.KMXFile.CODE_IFOPT, KMX.KMXFile.CODE_IFSYSTEMSTORE].includes(recContext.Code)) { Result--; } @@ -665,9 +677,9 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp break; case KMX.KMXFile.CODE_NOTANY: // #917: Minimum version required is 14.0: the KCXO function was only added for 14.0 - // Note that this is checked in compiler.cpp as well, so this error can probably never occur - if(!IsKeyboardVersion14OrLater()) { - callbacks.reportMessage(KmwCompilerMessages.Error_NotAnyRequiresVersion14({line:fkp.Line})); + if(!isKeyboardVersion14OrLater()) { + // 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});`; break; @@ -701,7 +713,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp let pwsz = pwszOutput; if(fkp != null) { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { // KMW >= 10.0 use the full, sentinel-based length for context deletions. len = xstrlen(fkp.dpContext); let n = len; @@ -726,7 +738,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp } let x = 0; - if(IsKeyboardVersion10OrLater() && pwsz.length > 0) { + if(isKeyboardVersion10OrLater() && pwsz.length > 0) { if(!isGroupReadOnly(fk, fgp)) { Result += nlt+`k.KDC(${len},t);`; // I3681 } @@ -932,7 +944,7 @@ export function zeroPadHex(n: number, len: number): string { * @return string of JavaScript code, e.g. 'keyCodes.K_A /* 0x41 * /' */ function FormatKeyAsString(key: number): string { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { // Depends on flags defined in KeymanWeb 10.0 if (key <= 255 && KMWVKeyNames[key] != '') { return 'keyCodes.'+KMWVKeyNames[key]+ ' /* 0x' + zeroPadHex(key, 2) + ' */'; diff --git a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts index 19d58d64ba9..5170a8d6787 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts @@ -1,9 +1,9 @@ import { KmnCompilerMessages } from "../compiler/kmn-compiler-messages.js"; -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageDef as def, CompilerMessageSpec } from "@keymanapp/common-types"; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageDef as def, CompilerMessageSpec } from "@keymanapp/developer-utils"; import { kmnfile } from "./compiler-globals.js"; const Namespace = CompilerErrorNamespace.KmwCompiler; -// const SevInfo = CompilerErrorSeverity.Info | Namespace; +const SevInfo = CompilerErrorSeverity.Info | Namespace; const SevHint = CompilerErrorSeverity.Hint | Namespace; // const SevWarn = CompilerErrorSeverity.Warn | Namespace; const SevError = CompilerErrorSeverity.Error | Namespace; @@ -30,9 +30,8 @@ export class KmwCompilerMessages extends KmnCompilerMessages { // Following messages are kmw-compiler only, so use KmwCompiler error namespace - static ERROR_NotAnyRequiresVersion14 = SevError | 0x0001; - static Error_NotAnyRequiresVersion14 = (o:{line: number}) => m(this.ERROR_NotAnyRequiresVersion14, - `Statement notany in context() match requires version 14.0+ of KeymanWeb`, o); + // 0x0001 - Reserved for removed identifier ERROR_NotAnyRequiresVersion14 = + // SevError | 0x0001, added in 17.0, removed in 18.0 static ERROR_TouchLayoutIdentifierRequires15 = SevError | 0x0002; static Error_TouchLayoutIdentifierRequires15 = (o:{keyId:string, platformName:string, layerId:string}) => m(this.ERROR_TouchLayoutIdentifierRequires15, @@ -49,4 +48,10 @@ export class KmwCompilerMessages extends KmnCompilerMessages { static HINT_TouchLayoutUsesUnsupportedGesturesDownlevel = SevHint | 0x0005; static Hint_TouchLayoutUsesUnsupportedGesturesDownlevel = (o:{keyId:string}) => m(this.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel, `The touch layout uses a flick or multi-tap gesture on key ${def(o.keyId)}, which is only available on version 17.0+ of Keyman`); + + static INFO_MinimumWebEngineVersion = SevInfo | 0x0006; + static Info_MinimumWebEngineVersion = (o:{version:string}) => m( + this.INFO_MinimumWebEngineVersion, + `The compiler has assigned a minimum web engine version of ${o.version} based on features used in this keyboard` + ); }; diff --git a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler.ts b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler.ts index 974e4fd485d..5dcde400ead 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler.ts @@ -1,6 +1,7 @@ -import { KMX, CompilerOptions, CompilerCallbacks, KvkFileReader, VisualKeyboard, KmxFileReader, KvkFile } from "@keymanapp/common-types"; +import { KMX, KvkFileReader, VisualKeyboard, KmxFileReader, KvkFile } from "@keymanapp/common-types"; +import { CompilerOptions, CompilerCallbacks } from "@keymanapp/developer-utils"; import { ExpandSentinel, incxstr, xstrlen } from "./util.js"; -import { options, nl, FTabStop, setupGlobals, IsKeyboardVersion10OrLater, callbacks, FFix183_LadderLength, FCallFunctions, fk, IsKeyboardVersion17OrLater } from "./compiler-globals.js"; +import { options, nl, FTabStop, setupGlobals, callbacks, FFix183_LadderLength, FCallFunctions, fk, minimumKeymanVersionToString, isKeyboardVersion10OrLater, isKeyboardVersion17OrLater } from "./compiler-globals.js"; import { JavaScript_ContextMatch, JavaScript_KeyAsString, JavaScript_Name, JavaScript_OutputString, JavaScript_Rules, JavaScript_Shift, JavaScript_ShiftAsString, JavaScript_Store, zeroPadHex } from './javascript-strings.js'; import { KmwCompilerMessages } from "./kmw-compiler-messages.js"; import { ValidateLayoutFile } from "./validate-layout-file.js"; @@ -206,8 +207,13 @@ export function WriteCompiledKeyboard( // Following line caches the Keyman major version `${FTabStop}this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9;${nl}` + `${FTabStop}this.KI="${sName}";${nl}` + - `${FTabStop}this.KN="${RequotedString(sFullName)}";${nl}` + - `${FTabStop}this.KMINVER="${(keyboard.fileVersion & KMX.KMXFile.VERSION_MASK_MAJOR) >> 8}.${keyboard.fileVersion & KMX.KMXFile.VERSION_MASK_MINOR}";${nl}` + + `${FTabStop}this.KN="${RequotedString(sFullName)}";${nl}`; + + // We split the result text here to allow for setting the minimum required + // version at the very end of this function + const resultPrefix = result; + + result = `${FTabStop}this.KV=${sVisualKeyboard};${nl}` + `${FTabStop}this.KDU=${fDisplayUnderlying?'1':'0'};${nl}` + `${FTabStop}this.KH=${sHelp};${nl}` + @@ -400,7 +406,17 @@ export function WriteCompiledKeyboard( } result += sEmbedJS + '}' + nl; // I3681 - return result; + + const minVer = minimumKeymanVersionToString(); + const resultMinVer = `${FTabStop}this.KMINVER="${minVer}";${nl}`; + + if((fk.flags & KMX.KMXFile.KF_AUTOMATICVERSION) == KMX.KMXFile.KF_AUTOMATICVERSION) { + // Note: the KeymanWeb compiler is responsible for reporting minimum + // version for the web targets + callbacks.reportMessage(KmwCompilerMessages.Info_MinimumWebEngineVersion({version:minVer})); + } + + return resultPrefix + resultMinVer + result; } /// @@ -440,9 +456,9 @@ function GetKeyboardModifierBitmask(keyboard: KMX.KEYBOARD, fMnemonic: boolean): /// @return string of JavaScript code /// function JavaScript_SetupDebug() { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { if(options.saveDebug) { - if(IsKeyboardVersion17OrLater()) { + if(isKeyboardVersion17OrLater()) { return 'var modCodes = KeymanWeb.Codes.modifierCodes;'+nl+ FTabStop+'var keyCodes = KeymanWeb.Codes.keyCodes;'+nl; } else { @@ -455,7 +471,7 @@ function JavaScript_SetupDebug() { } function JavaScript_SetupProlog() { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { return 'if(typeof keyman === \'undefined\') {'+nl+ FTabStop+'console.log(\'Keyboard requires KeymanWeb 10.0 or later\');'+nl+ FTabStop+'if(typeof tavultesoft !== \'undefined\') tavultesoft.keymanweb.util.alert("This keyboard requires KeymanWeb 10.0 or later");'+nl+ @@ -465,7 +481,7 @@ function JavaScript_SetupProlog() { } function JavaScript_SetupEpilog() { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { return '}'; } return ''; @@ -560,7 +576,7 @@ export function FormatModifierAsBitflags(FBitMask: number): string { //TODO: We need to think about mnemonic layouts which are incompletely supported at present //tavultesoft.keymanweb.osk. - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { // This depends on flags defined in KeymanWeb 10.0 result = ''; diff --git a/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts b/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts index e2f0d23a73d..61d0985f87c 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts @@ -1,7 +1,12 @@ -import { KMX, TouchLayout, TouchLayoutFileReader, TouchLayoutFileWriter } from "@keymanapp/common-types"; -import { callbacks, fk, IsKeyboardVersion14OrLater, IsKeyboardVersion15OrLater, IsKeyboardVersion17OrLater } from "./compiler-globals.js"; +import { KMX, TouchLayout } from "@keymanapp/common-types"; +import { TouchLayoutFileReader, TouchLayoutFileWriter } from "@keymanapp/developer-utils"; +import { callbacks, minimumKeymanVersion, verifyAndSetMinimumRequiredKeymanVersion15, + isKeyboardVersion14OrLater, isKeyboardVersion17OrLater, + verifyAndSetMinimumRequiredKeymanVersion14, + verifyAndSetMinimumRequiredKeymanVersion17} from "./compiler-globals.js"; import { JavaScript_Key } from "./javascript-strings.js"; -import { TRequiredKey, CRequiredKeys, CSpecialText, CSpecialText14Map, CSpecialText17Map, CSpecialTextMinVer, CSpecialTextMaxVer } from "./constants.js"; +import { TRequiredKey, CRequiredKeys, CSpecialText, CSpecialText14Map, CSpecialText17Map, + CSpecialTextMinVer, CSpecialTextMaxVer } from "./constants.js"; import { KeymanWebTouchStandardKeyNames, KMWAdditionalKeyNames, VKeyNames } from "./keymanweb-key-codes.js"; import { KmwCompilerMessages } from "./kmw-compiler-messages.js"; import * as Osk from '../compiler/osk.js'; @@ -121,7 +126,7 @@ function CheckKey( callbacks.reportMessage(KmwCompilerMessages.Error_TouchLayoutInvalidIdentifier({keyId: FId, platformName: platformId, layerId: layer.id})); return false; } - else if (FValid == TKeyIdType.Key_Unicode_Multi && !IsKeyboardVersion15OrLater()) { + else if (FValid == TKeyIdType.Key_Unicode_Multi && !verifyAndSetMinimumRequiredKeymanVersion15()) { callbacks.reportMessage(KmwCompilerMessages.Error_TouchLayoutIdentifierRequires15({keyId: FId, platformName: platformId, layerId: layer.id})); return false; } @@ -144,10 +149,10 @@ function CheckKey( // Keyman versions before 14 do not support '*special*' labels on non-special keys. // ZWNJ use, however, is safe because it will be transformed in function // TransformSpecialKeys14 to '<|>', which does not require the custom OSK font. - const mapVersion = Math.max(Math.min(fk.fileVersion, CSpecialTextMaxVer), CSpecialTextMinVer); + const mapVersion = Math.max(Math.min(minimumKeymanVersion(), CSpecialTextMaxVer), CSpecialTextMinVer); const specialText = CSpecialText.get(mapVersion); if(specialText.includes(FText) && - !IsKeyboardVersion14OrLater() && + !verifyAndSetMinimumRequiredKeymanVersion14() && !([TouchLayout.TouchLayoutKeySp.special, TouchLayout.TouchLayoutKeySp.specialActive].includes(FKeyType))) { callbacks.reportMessage(KmwCompilerMessages.Warn_TouchLayoutSpecialLabelOnNormalKey({ keyId: FId, @@ -187,7 +192,7 @@ function CheckDictionaryKeyValidity(fk: KMX.KEYBOARD, FDictionary: string[]) { function TransformSpecialKeys14(FDebug: boolean, sLayoutFile: string): string { // Rewrite Special key labels that are only supported in Keyman 14+ // This code is a little ugly but effective. - if(!IsKeyboardVersion14OrLater()) { + if(!isKeyboardVersion14OrLater()) { for(let i = 0; i < CSpecialText14Map.length; i++) { // Assumes the JSON output format will not change if(FDebug) { @@ -203,7 +208,7 @@ function TransformSpecialKeys14(FDebug: boolean, sLayoutFile: string): string { function TransformSpecialKeys17(FDebug: boolean, sLayoutFile: string): string { // Rewrite Special key labels that are only supported in Keyman 17+ // This code is a little ugly but effective. - if(!IsKeyboardVersion17OrLater()) { + if(!isKeyboardVersion17OrLater()) { for(let i = 0; i < CSpecialText17Map.length; i++) { // Assumes the JSON output format will not change if(FDebug) { @@ -239,7 +244,7 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil let hasWarnedOfGestureUseDownlevel = false; const warnGesturesIfNeeded = function(keyId: string) { - if(!hasWarnedOfGestureUseDownlevel && !IsKeyboardVersion17OrLater()) { + if(!hasWarnedOfGestureUseDownlevel && !verifyAndSetMinimumRequiredKeymanVersion17()) { hasWarnedOfGestureUseDownlevel = true; callbacks.reportMessage(KmwCompilerMessages.Hint_TouchLayoutUsesUnsupportedGesturesDownlevel({keyId})); } diff --git a/developer/src/kmc-kmn/src/main.ts b/developer/src/kmc-kmn/src/main.ts index a7290970acb..9ec214b6111 100644 --- a/developer/src/kmc-kmn/src/main.ts +++ b/developer/src/kmc-kmn/src/main.ts @@ -4,7 +4,7 @@ */ export { KmnCompiler, KmnCompilerOptions, KmnCompilerResult, KmnCompilerArtifacts, KmnCompilerResultExtra, CompilerResultExtraStore, CompilerResultExtraGroup } from './compiler/compiler.js'; -export { KmnCompilerMessages, CompilerMessages } from './compiler/kmn-compiler-messages.js'; +export { KmnCompilerMessages } from './compiler/kmn-compiler-messages.js'; export { KmwCompilerMessages } from './kmw-compiler/kmw-compiler-messages.js'; import * as Osk from './compiler/osk.js'; diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_duplicate_group.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_duplicate_group.kmn index 8382b5ea7f4..9f5be31f17e 100644 --- a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_duplicate_group.kmn +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_duplicate_group.kmn @@ -1,7 +1,7 @@ c Description: Verifies that kmcmplib picks up on duplicated groups with Unicode names, c and that the Unicode names are correctly reported in error messages in UTF-8 -store(&NAME) 'cerr_duplicate_group' +store(&NAME) 'error_duplicate_group' store(&VERSION) '9.0' begin unicode > use(ខ្មែរ) diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_duplicate_store.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_duplicate_store.kmn index 14d27d5be60..0f520a3d745 100644 --- a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_duplicate_store.kmn +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_duplicate_store.kmn @@ -1,7 +1,7 @@ c Description: Verifies that kmcmplib picks up on duplicated stores with Unicode names, c and that the Unicode names are correctly reported in error messages in UTF-8 -store(&NAME) 'cerr_duplicate_store' +store(&NAME) 'error_duplicate_store' store(&VERSION) '9.0' begin unicode > use(ខ្មែរ) diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/hint_index_store_long.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/hint_index_store_long.kmn new file mode 100644 index 00000000000..fb95fd93b13 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/hint_index_store_long.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'hint_index_store_long' +store(&VERSION) '10.0' + +begin unicode > use(main) + +group(main) using keys + +store(abc) 'abc' +store(defg) 'defg' + +any(abc) + 'x' > index(defg, 1) diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/hint_index_store_long_key.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/hint_index_store_long_key.kmn new file mode 100644 index 00000000000..510f61bd3ee --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/hint_index_store_long_key.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'hint_index_store_long_key' +store(&VERSION) '10.0' + +begin unicode > use(main) + +group(main) using keys + +store(abc) 'abc' +store(defg) 'defg' + ++ any(abc) > index(defg, 1) diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_index_store_short.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_index_store_short.kmn new file mode 100644 index 00000000000..bbe208eb683 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_index_store_short.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'warn_index_store_short' +store(&VERSION) '10.0' + +begin unicode > use(main) + +group(main) using keys + +store(abc) 'abc' +store(defg) 'defg' + +any(defg) + 'x' > index(abc, 1) diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/warn_index_store_short_key.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_index_store_short_key.kmn new file mode 100644 index 00000000000..1131d595e3a --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/warn_index_store_short_key.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'warn_index_store_short_key' +store(&VERSION) '10.0' + +begin unicode > use(main) + +group(main) using keys + +store(abc) 'abc' +store(defg) 'defg' + ++ any(defg) > index(abc, 1) diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_9_caps_lock.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_9_caps_lock.kmn new file mode 100644 index 00000000000..7894a982714 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_9_caps_lock.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'version_9_caps_lock' +store(&TARGETS) 'web' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +c Use of CAPS should make compiler generate warning Warn_ExtendedShiftFlagsNotSupportedInKeymanWeb + ++ [CAPS K_A] > 'a' diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_9_chiral_modifiers.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_9_chiral_modifiers.kmn new file mode 100644 index 00000000000..83f36f98b06 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_9_chiral_modifiers.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'version_9_chiral_modifiers' +store(&TARGETS) 'web' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +c Use of LCTRL should make compiler generate warning Warn_ExtendedShiftFlagsNotSupportedInKeymanWeb + ++ [LCTRL K_A] > 'a' diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_auto_caps_lock.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_auto_caps_lock.kmn new file mode 100644 index 00000000000..03ed9b369ce --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_auto_caps_lock.kmn @@ -0,0 +1,10 @@ +store(&NAME) 'version_auto_caps_lock' +store(&TARGETS) 'web' + +begin Unicode > use(main) + +group(main) using keys + +c Use of CAPS should make compiler select version 10 + ++ [CAPS K_A] > 'a' diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_auto_chiral_modifiers.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_auto_chiral_modifiers.kmn new file mode 100644 index 00000000000..08417229a41 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_auto_chiral_modifiers.kmn @@ -0,0 +1,10 @@ +store(&NAME) 'version_auto_chiral_modifiers' +store(&TARGETS) 'web' + +begin Unicode > use(main) + +group(main) using keys + +c Use of LCTRL should make compiler select version 10 + ++ [LCTRL K_A] > 'a' diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_gestures.keyman-touch-layout b/developer/src/kmc-kmn/test/fixtures/kmw/version_gestures.keyman-touch-layout new file mode 100644 index 00000000000..0db1b7504d4 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_gestures.keyman-touch-layout @@ -0,0 +1,51 @@ +{ + "tablet": { + "font": "Tahoma", + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "text": "q", + "flick": { + "n": { + "id": "K_Q", + "text": "Q" + } + } + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": 90, + "sp": 1 + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 120, + "sp": 1 + }, + { + "id": "K_SPACE", + "text": "", + "width": 630, + "sp": 0 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 140, + "sp": 0 + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_gestures.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_gestures.kmn new file mode 100644 index 00000000000..412f559ae2e --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_gestures.kmn @@ -0,0 +1,9 @@ +store(&NAME) 'version_gestures' +store(&TARGETS) 'mobile' +store(&LAYOUTFILE) 'version_gestures.keyman-touch-layout' + +c Use of gestures should make compiler select version 17 + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_gestures_16.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_gestures_16.kmn new file mode 100644 index 00000000000..06a56f69834 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_gestures_16.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'version_gestures_16' +store(&TARGETS) 'mobile' +store(&LAYOUTFILE) 'version_gestures.keyman-touch-layout' +store(&VERSION) '16.0' + +c Use of gestures should make compiler generate warning +c HINT_TouchLayoutUsesUnsupportedGesturesDownlevel due to version < 17.0 + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_notany.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_notany.kmn new file mode 100644 index 00000000000..596cbef94b9 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_notany.kmn @@ -0,0 +1,12 @@ +store(&NAME) 'version_notany' +store(&TARGETS) 'web' + +begin Unicode > use(main) + +group(main) using keys + +store(abc) 'abc' + +c Use of notany with corresponding context() should make compiler select version 14 + +notany(abc) + 'a' > context(1) \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_notany_10.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_notany_10.kmn new file mode 100644 index 00000000000..bbec1509dc2 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_notany_10.kmn @@ -0,0 +1,13 @@ +store(&NAME) 'version_notany' +store(&TARGETS) 'web' +store(&VERSION) '10.0' + +begin Unicode > use(main) + +group(main) using keys + +store(abc) 'abc' + +c Use of notany with corresponding context() should make compiler generate ERROR_NotAnyRequiresVersion14 + +notany(abc) + 'a' > context(1) \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_special_key_caps.keyman-touch-layout b/developer/src/kmc-kmn/test/fixtures/kmw/version_special_key_caps.keyman-touch-layout new file mode 100644 index 00000000000..10593d518f9 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_special_key_caps.keyman-touch-layout @@ -0,0 +1,45 @@ +{ + "tablet": { + "font": "Tahoma", + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "text": "q" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": 90, + "sp": 1 + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 120, + "sp": 1 + }, + { + "id": "K_SPACE", + "text": "", + "width": 630, + "sp": 0 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 140, + "sp": 0 + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_special_key_caps.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_special_key_caps.kmn new file mode 100644 index 00000000000..6024466630c --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_special_key_caps.kmn @@ -0,0 +1,9 @@ +store(&NAME) 'version_special_key_caps' +store(&TARGETS) 'mobile' +store(&LAYOUTFILE) 'version_special_key_caps.keyman-touch-layout' + +c Use of special key caps on nomral keys should make compiler select version 14 + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_special_key_caps_14.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_special_key_caps_14.kmn new file mode 100644 index 00000000000..0fad6a72f0d --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_special_key_caps_14.kmn @@ -0,0 +1,12 @@ +store(&NAME) 'version_special_key_caps_14' +store(&TARGETS) 'mobile' +store(&LAYOUTFILE) 'version_special_key_caps.keyman-touch-layout' +store(&VERSION) '10.0' + +c Use of special key caps on normal keys should make compiler +c generate warning WARN_TouchLayoutSpecialLabelOnNormalKey due +c to version < 14.0 + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.keyman-touch-layout b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.keyman-touch-layout new file mode 100644 index 00000000000..6e32c688eb8 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.keyman-touch-layout @@ -0,0 +1,53 @@ +{ + "tablet": { + "font": "Tahoma", + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "text": "q" + }, + { + "id": "U_0041_0300", + "text": "A'" + }, + { + "id": "U_0042_0300", + "text": "B'" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": 90, + "sp": 1 + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 120, + "sp": 1 + }, + { + "id": "K_SPACE", + "text": "", + "width": 630, + "sp": 0 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 140, + "sp": 1 + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.kmn new file mode 100644 index 00000000000..a56940c6080 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.kmn @@ -0,0 +1,7 @@ +store(&NAME) 'version_u_xxxx_yyyy' +store(&TARGETS) 'mobile' +store(&LAYOUTFILE) 'version_u_xxxx_yyyy.keyman-touch-layout' + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy_14.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy_14.kmn new file mode 100644 index 00000000000..06635955e5e --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy_14.kmn @@ -0,0 +1,8 @@ +store(&NAME) 'version_u_xxxx_yyyy' +store(&TARGETS) 'mobile' +store(&LAYOUTFILE) 'version_u_xxxx_yyyy.keyman-touch-layout' +store(&VERSION) '14.0' + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-kmn/test/kmw/test-kmw-compiler.ts b/developer/src/kmc-kmn/test/kmw/test-kmw-compiler.ts index 67323515767..37a58384b2f 100644 --- a/developer/src/kmc-kmn/test/kmw/test-kmw-compiler.ts +++ b/developer/src/kmc-kmn/test/kmw/test-kmw-compiler.ts @@ -6,8 +6,10 @@ import { fileURLToPath } from 'url'; import fs from 'fs'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { KmnCompilerResult, KmnCompiler } from '../../src/compiler/compiler.js'; -import { ETLResult, extractTouchLayout as parseWebTestResult } from './util.js'; +import { ETLResult, extractTouchLayout } from './util.js'; import { KeymanFileTypes } from '@keymanapp/common-types'; +import { KmnCompilerMessages } from '../../src/compiler/kmn-compiler-messages.js'; +import { KmwCompilerMessages } from '../../src/kmw-compiler/kmw-compiler-messages.js'; const __dirname = dirname(fileURLToPath(import.meta.url)).replace(/\\/g, '/'); const fixturesDir = __dirname + '/../../../test/fixtures/kmw/'; @@ -38,49 +40,170 @@ describe('KeymanWeb Compiler', function() { callbacks.clear(); }); - it('should compile a complex keyboard', function() { - run_test_keyboard(kmnCompiler, 'khmer_angkor'); + it('should compile a complex keyboard', async function() { + await run_test_keyboard(kmnCompiler, 'khmer_angkor'); }); - it('should handle option stores', function() { + it('should handle option stores', async function() { // // This is enough to verify that the option store is set appropriately with // KLOAD because the fixture has that code present: // // this.s_foo_6=KeymanWeb.KLOAD(this.KI,"foo","0"); // - run_test_keyboard(kmnCompiler, 'test_options'); + await run_test_keyboard(kmnCompiler, 'test_options'); }); - it('should translate every "character style" key correctly', function() { + it('should translate every "character style" key correctly', async function() { // // This is enough to verify that every character style key is encoded in the // same way as the fixture. // - run_test_keyboard(kmnCompiler, 'test_keychars'); + await run_test_keyboard(kmnCompiler, 'test_keychars'); }); - it('should handle readonly groups', function() { - run_test_keyboard(kmnCompiler, 'test_readonly_groups'); + it('should handle readonly groups', async function() { + await run_test_keyboard(kmnCompiler, 'test_readonly_groups'); }); - it('should handle context(n) in output of rule, v10.0 generation', function() { - run_test_keyboard(kmnCompiler, 'test_contextn_in_output'); + it('should handle context(n) in output of rule, v10.0 generation', async function() { + await run_test_keyboard(kmnCompiler, 'test_contextn_in_output'); }); - it('should handle context(n) in output of rule, v9.0 generation', function() { - run_test_keyboard(kmnCompiler, 'test_contextn_in_output_9'); + it('should handle context(n) in output of rule, v9.0 generation', async function() { + await run_test_keyboard(kmnCompiler, 'test_contextn_in_output_9'); }); - it('should handle context(n) in context part of rule, v9.0 generation', function() { - run_test_keyboard(kmnCompiler, 'test_context_in_context_9'); + it('should handle context(n) in context part of rule, v9.0 generation', async function() { + await run_test_keyboard(kmnCompiler, 'test_context_in_context_9'); }); - it('should handle context(n) in context part of rule, v10.0 generation', function() { - run_test_keyboard(kmnCompiler, 'test_context_in_context'); + it('should handle context(n) in context part of rule, v10.0 generation', async function() { + await run_test_keyboard(kmnCompiler, 'test_context_in_context'); + }); + + it('should determine the minimum version correctly with U_xxxx_yyyy touch ids', async function() { + const filenames = generateTestFilenames('version_u_xxxx_yyyy'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNotNull(result); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); + // we expect only 1 of the info messages -- for the .kmx target (not 2) + assert.equal(callbacks.messages.filter(item => item.code == KmnCompilerMessages.INFO_MinimumCoreEngineVersion).length, 1); + + const data = new TextDecoder('utf-8').decode(result.artifacts.js.data); + assert.match(data, /KMINVER="15.0"/, `Could not find expected 'KMINVER="15.0"'`); + }); + + it('should give an error if the minimum version specified in the keyboard does not support U_xxxx_yyyy touch ids', async function() { + const filenames = generateTestFilenames('version_u_xxxx_yyyy_14'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNull(result); + assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); + assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.ERROR_TouchLayoutIdentifierRequires15)); + }); + + ['caps_lock', 'chiral_modifiers'].forEach((mode) => { + it(`should give warning WARN_ExtendedShiftFlagsNotSupportedInKeymanWeb for v9 keyboards if ${mode} found`, async function() { + const filenames = generateTestFilenames(`version_9_${mode}`); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNotNull(result); // only a warning, so output is generated + assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); + assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.WARN_ExtendedShiftFlagsNotSupportedInKeymanWeb)); + }); + + it(`should select version 10 automatically if ${mode} found`, async function() { + const filenames = generateTestFilenames(`version_auto_${mode}`); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNotNull(result); + + // we expect only 1 of the info messages -- for the .kmx target (not 2) + assert.equal(callbacks.messages.filter(item => item.code == KmnCompilerMessages.INFO_MinimumCoreEngineVersion).length, 1); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); + assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.WARN_ExtendedShiftFlagsNotSupportedInKeymanWeb)); + + const data = new TextDecoder('utf-8').decode(result.artifacts.js.data); + assert.match(data, /KMINVER="10.0"/, `Could not find expected 'KMINVER="10.0"'`); + }); + }); + + it('should determine the minimum version correctly with `notany`', async function() { + // Note that the logic being tested here is in kmx compiler.cpp, not kmw compiler + const filenames = generateTestFilenames('version_notany'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNotNull(result); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); + // we expect only 1 of the info messages -- for the .kmx target (not 2) + assert.equal(callbacks.messages.filter(item => item.code == KmnCompilerMessages.INFO_MinimumCoreEngineVersion).length, 1); + + const data = new TextDecoder('utf-8').decode(result.artifacts.js.data); + assert.match(data, /KMINVER="14.0"/, `Could not find expected 'KMINVER="14.0"'`); }); -}); + it('should give an error if the minimum version specified in the keyboard does not support `notany`', async function() { + // Note that the logic being tested here is in kmx compiler.cpp, not kmw compiler + const filenames = generateTestFilenames('version_notany_10'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNull(result); + assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); + assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.ERROR_140FeatureOnlyContextAndNotAnyWeb)); + }); + + it('should determine the minimum version correctly with special key caps on normal keys', async function() { + const filenames = generateTestFilenames('version_special_key_caps'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNotNull(result); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); + // we expect only 1 of the info messages -- for the .kmx target (not 2) + assert.equal(callbacks.messages.filter(item => item.code == KmnCompilerMessages.INFO_MinimumCoreEngineVersion).length, 1); + + const data = new TextDecoder('utf-8').decode(result.artifacts.js.data); + assert.match(data, /KMINVER="14.0"/, `Could not find expected 'KMINVER="14.0"'`); + }); + + it('should give warning WARN_TouchLayoutSpecialLabelOnNormalKey if the minimum version specified in the keyboard does not support special key caps on normal keys', async function() { + const filenames = generateTestFilenames('version_special_key_caps_14'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNotNull(result); + assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); + assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.WARN_TouchLayoutSpecialLabelOnNormalKey)); + }); + + it('should determine the minimum version correctly with v17 gestures', async function() { + const filenames = generateTestFilenames('version_gestures'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNotNull(result); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); + // we expect only 1 of the info messages -- for the .kmx target (not 2) + assert.equal(callbacks.messages.filter(item => item.code == KmnCompilerMessages.INFO_MinimumCoreEngineVersion).length, 1); + + const data = new TextDecoder('utf-8').decode(result.artifacts.js.data); + assert.match(data, /KMINVER="17.0"/, `Could not find expected 'KMINVER="17.0"'`); + }); + + it('should give warning HINT_TouchLayoutUsesUnsupportedGesturesDownlevel if the minimum version specified in the keyboard does not support special key caps on normal keys', async function() { + const filenames = generateTestFilenames('version_gestures_16'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNotNull(result); + assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); + assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel)); + }); + +}); async function run_test_keyboard(kmnCompiler: KmnCompiler, id: string): Promise<{ result: KmnCompilerResult, actualCode: string, actual: ETLResult, expectedCode: string, expected: ETLResult }> { @@ -96,17 +219,17 @@ async function run_test_keyboard(kmnCompiler: KmnCompiler, id: string): expected: null, actual: null, }; - value.actual = parseWebTestResult(value.actualCode); - value.expected = parseWebTestResult(value.expectedCode); + value.actual = extractTouchLayout(value.actualCode); + value.expected = extractTouchLayout(value.expectedCode); if(debug) { - // This is mostly to allow us to verify that parseWebTestResult is doing what we want + // This is mostly to allow us to verify that extractTouchLayout is doing what we want // fs.writeFileSync(filenames.binary + '.strip.js', value.actual.js); // fs.writeFileSync(filenames.fixture + '.strip.js', value.expected.js); fs.writeFileSync(filenames.binary, value.actualCode); } - assert.deepEqual(value.actual.js, value.expected.js); + assert.deepEqual(value.actual.js.replaceAll(/\r\n/g, '\n'), value.expected.js.replaceAll(/\r\n/g, '\n')); assert.deepEqual(JSON.parse(value.actual.touchLayout), JSON.parse(value.expected.touchLayout)); return value; diff --git a/developer/src/kmc-kmn/test/kmw/test-kmw-messages.ts b/developer/src/kmc-kmn/test/kmw/test-kmw-messages.ts index 4ab741a4cc9..20a1a5c83b3 100644 --- a/developer/src/kmc-kmn/test/kmw/test-kmw-messages.ts +++ b/developer/src/kmc-kmn/test/kmw/test-kmw-messages.ts @@ -4,7 +4,7 @@ import { KmwCompilerMessages } from '../../src/kmw-compiler/kmw-compiler-message import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; import { makePathToFixture } from '../helpers/index.js'; import { KmnCompiler } from '../../src/main.js'; -import { CompilerErrorNamespace } from '@keymanapp/common-types'; +import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; describe('KmwCompilerMessages', function () { const callbacks = new TestCompilerCallbacks(); @@ -47,6 +47,21 @@ describe('KmwCompilerMessages', function () { // TODO: other messages + // WARN_ExtendedShiftFlagsNotSupportedInKeymanWeb: + // * Implemented in test-kmw-compiler.ts: 'should give warning + // WARN_ExtendedShiftFlagsNotSupportedInKeymanWeb for v9 keyboards if + // ${mode} found' + + // WARN_TouchLayoutSpecialLabelOnNormalKey + // * Implemented in test-kmw-compiler.ts: 'should give warning + // WARN_TouchLayoutSpecialLabelOnNormalKey if the minimum version specified + // in the keyboard does not support special key caps on normal keys' + + // HINT_TouchLayoutUsesUnsupportedGesturesDownlevel + // * Implemented in test-kmw-compiler.ts: 'should give warning + // HINT_TouchLayoutUsesUnsupportedGesturesDownlevel if the minimum version + // specified in the keyboard does not support gestures' + // ERROR_NotAnyRequiresVersion14 // it('should generate ERROR_NotAnyRequiresVersion14 if ...', async function() { diff --git a/developer/src/kmc-kmn/test/test-features.ts b/developer/src/kmc-kmn/test/test-features.ts index a030ba984ca..1f82a03ed52 100644 --- a/developer/src/kmc-kmn/test/test-features.ts +++ b/developer/src/kmc-kmn/test/test-features.ts @@ -24,21 +24,21 @@ describe('Keyboard compiler features', function() { const versions = [ // TODO(lowpri): we should add a test for each version + also test automatic feature detection - ['16.0', '160', KMX.KMXFile.VERSION_160], - ['17.0', '170', KMX.KMXFile.VERSION_170], + {major: '16.0', vstr: '160', vernum: KMX.KMXFile.VERSION_160}, + {major: '17.0', vstr: '170', vernum: KMX.KMXFile.VERSION_170}, ]; for(const v of versions) { - it(`should build a version ${v[0]} keyboard`, async function() { - const fixtureName = makePathToFixture('features', `version_${v[1]}.kmn`); + it(`should build a version ${v.major} keyboard`, async function() { + const fixtureName = makePathToFixture('features', `version_${v.vstr}.kmn`); - const result = await compiler.run(fixtureName, `version_${v[1]}.kmx`); + const result = await compiler.run(fixtureName, `version_${v.vstr}.kmx`); if(result === null) callbacks.printMessages(); assert.isNotNull(result); const reader = new KmxFileReader(); const keyboard = reader.read(result.artifacts.kmx.data); - assert.equal(keyboard.fileVersion, v[2]); + assert.equal(keyboard.fileVersion, v.vernum); }); } }); diff --git a/developer/src/kmc-kmn/test/test-messages.ts b/developer/src/kmc-kmn/test/test-messages.ts index 1ba46033d8c..0c3f17a58c2 100644 --- a/developer/src/kmc-kmn/test/test-messages.ts +++ b/developer/src/kmc-kmn/test/test-messages.ts @@ -1,10 +1,10 @@ import 'mocha'; import { assert } from 'chai'; -import { CompilerMessages, KmnCompilerMessages } from '../src/compiler/kmn-compiler-messages.js'; +import { KmnCompilerMessageRanges, KmnCompilerMessages } from '../src/compiler/kmn-compiler-messages.js'; import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; import { makePathToFixture } from './helpers/index.js'; import { KmnCompiler } from '../src/main.js'; -import { CompilerErrorNamespace } from '@keymanapp/common-types'; +import { CompilerErrorMask, CompilerErrorNamespace } from '@keymanapp/developer-utils'; describe('KmnCompilerMessages', function () { const callbacks = new TestCompilerCallbacks(); @@ -13,6 +13,69 @@ describe('KmnCompilerMessages', function () { return verifyCompilerMessagesObject(KmnCompilerMessages, CompilerErrorNamespace.KmnCompiler); }); + it('should have a 1:1 correspondence with kmn_compiler_errors.h', function() { + + const headerFilename = 'kmn_compiler_errors.h'; + const tsFilename = 'kmn-compiler-messages.ts'; + + // For each message declared in KmnCompilerMessages, look for a + // corresponding message in kmn_compiler_errors.h + const headerPath = makePathToFixture(`../../../common/include/${headerFilename}`); + + // Matching the following line pattern from kmn_compiler_errors.h: + // + // FATAL_BadCallParams = SevFatal | 0x002, + const messageRegex = /^\s+((FATAL|ERROR|WARN|HINT|INFO)_[a-z0-9_]+)\s+=\s+Sev(Fatal|Error|Warn|Hint|Info)\s+\|\s+0x([0-9a-f]+)/i; + + // Convert matching lines into array of message code data + const cppMessages = callbacks.fs.readFileSync(headerPath, 'utf-8') + .replace(/\r/g, '') + .split('\n') + .filter(line => line.match(messageRegex)) // filter first, run regex twice, ;-) + .map(line => { + const m = line.match(messageRegex); + return { name: m[1], nameSeverity: m[2], maskSeverity: m[3], code: parseInt(m[4],16), found: false } + }); + + const cppMessagesByName: {[index:string]: {name: string, nameSeverity: string, maskSeveity: string, code: number}} = cppMessages + .reduce((obj, m) => {obj[m.name] = m; return obj}, {} as any); + + // We validate that the nameSeverity and maskSeverity line up, as a sanity + // check. This conceptually belongs in kmcmplib tests, but much easier to + // achieve here + cppMessages.forEach(m => { + assert.strictEqual(m.maskSeverity.toUpperCase(), m.nameSeverity, `${headerFilename} has mismatching severity for ${m.name} and ${m.maskSeverity}`) + }); + + // Use same pattern as verifyCompilerMessagesObject to get list of messages + // from KmnCompilerMessages + const keys = Object.keys(KmnCompilerMessages); + const m = KmnCompilerMessages as Record; + + for(const key of keys) { + // Verify each object member matches the pattern we expect + if(typeof m[key] == 'number') { + + if((m[key] & CompilerErrorMask.BaseError) < KmnCompilerMessageRanges.RANGE_KMN_COMPILER_MIN || + (m[key] & CompilerErrorMask.BaseError) > KmnCompilerMessageRanges.RANGE_KMN_COMPILER_MAX) { + // Message is not from kmcmplib + continue; + } + + const cppMessage = cppMessagesByName[key]; + assert.isDefined(cppMessage, `${key} in ${tsFilename} not found in ${headerFilename}`); + assert.equal(cppMessage.name, key, `${key} name in ${tsFilename} does not precisely match ${headerFilename}`); + assert.equal(cppMessage.code, m[key] & CompilerErrorMask.BaseError, `${key} in ${tsFilename} has a different value than ${headerFilename}`); + + // Remove the entry so we can spot unpaired messages afterwards + delete cppMessagesByName[key]; + } + } + + // Ensure there are no remaining messages in kmn_compiler_errors.h + assert.isEmpty(Object.keys(cppMessagesByName), `${headerFilename} has entries not found in ${tsFilename}: ${JSON.stringify(cppMessagesByName)}`); + }); + // // Message tests // @@ -33,7 +96,7 @@ describe('KmnCompilerMessages', function () { if(messageId) { assert.isTrue(callbacks.hasMessage(messageId), `messageId ${messageId.toString(16)} not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); - assert.lengthOf(callbacks.messages, 1); + assert.lengthOf(callbacks.messages, 1, `messages should have 1 entry, instead has: `+JSON.stringify(callbacks.messages,null,2)); } else { assert.lengthOf(callbacks.messages, 0, `messages should be empty, but instead got: `+JSON.stringify(callbacks.messages,null,2)); } @@ -42,13 +105,13 @@ describe('KmnCompilerMessages', function () { // ERROR_InvalidKvksFile it('should generate ERROR_InvalidKvksFile if the kvks is not valid XML', async function() { - await testForMessage(this, ['invalid-keyboards', 'error_invalid_kvks_file.kmn'], CompilerMessages.ERROR_InvalidKvksFile); + await testForMessage(this, ['invalid-keyboards', 'error_invalid_kvks_file.kmn'], KmnCompilerMessages.ERROR_InvalidKvksFile); }); // WARN_InvalidVkeyInKvksFile it('should generate WARN_InvalidVkeyInKvksFile if the kvks contains an invalid virtual key', async function() { - await testForMessage(this, ['invalid-keyboards', 'warn_invalid_vkey_in_kvks_file.kmn'], CompilerMessages.WARN_InvalidVkeyInKvksFile); + await testForMessage(this, ['invalid-keyboards', 'warn_invalid_vkey_in_kvks_file.kmn'], KmnCompilerMessages.WARN_InvalidVkeyInKvksFile); }); // ERROR_DuplicateGroup @@ -112,4 +175,20 @@ describe('KmnCompilerMessages', function () { await testForMessage(this, ['invalid-keyboards', 'error_character_range_too_long.kmn'], KmnCompilerMessages.ERROR_CharacterRangeTooLong); }); + // WARN_IndexStoreShort + + it('should generate WARN_IndexStoreShort if a store referenced in index() is shorter than the corresponding any() store', async function() { + await testForMessage(this, ['keyboards', 'warn_index_store_short.kmn'], KmnCompilerMessages.WARN_IndexStoreShort); + callbacks.clear(); + await testForMessage(this, ['keyboards', 'warn_index_store_short_key.kmn'], KmnCompilerMessages.WARN_IndexStoreShort); + }); + + // HINT_IndexStoreLong + + it('should generate HINT_IndexStoreLong if a store referenced in index() is longer than the corresponding any() store', async function() { + await testForMessage(this, ['keyboards', 'hint_index_store_long.kmn'], KmnCompilerMessages.HINT_IndexStoreLong); + callbacks.clear(); + await testForMessage(this, ['keyboards', 'hint_index_store_long_key.kmn'], KmnCompilerMessages.HINT_IndexStoreLong); + }); + }); diff --git a/developer/src/kmc-kmn/test/test-wasm-uset.ts b/developer/src/kmc-kmn/test/test-wasm-uset.ts index 13288e10ca8..ad67b352469 100644 --- a/developer/src/kmc-kmn/test/test-wasm-uset.ts +++ b/developer/src/kmc-kmn/test/test-wasm-uset.ts @@ -2,8 +2,8 @@ import 'mocha'; import { assert } from 'chai'; import { KmnCompiler } from '../src/main.js'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; -import { CompilerMessages } from '../src/compiler/kmn-compiler-messages.js'; -import { compilerErrorFormatCode } from '@keymanapp/common-types'; +import { KmnCompilerMessages } from '../src/compiler/kmn-compiler-messages.js'; +import { compilerErrorFormatCode } from '@keymanapp/developer-utils'; describe('Compiler UnicodeSet function', function() { it('should fixup "short" \\u{} escapes', function () { @@ -101,11 +101,11 @@ describe('Compiler UnicodeSet function', function() { assert(compiler.verifyInitialized()); // map from string to failing error const failures = { - '[:Adlm:]': CompilerMessages.ERROR_UnicodeSetHasProperties, // what it saye - '[acegik]': CompilerMessages.FATAL_UnicodeSetOutOfRange, // 6 ranges, allocated 1 - '[[\\p{Mn}]&[A-Z]]': CompilerMessages.ERROR_UnicodeSetHasProperties, - '[abc{def}]': CompilerMessages.ERROR_UnicodeSetHasStrings, - '[[]': CompilerMessages.ERROR_UnicodeSetSyntaxError, + '[:Adlm:]': KmnCompilerMessages.ERROR_UnicodeSetHasProperties, // what it saye + '[acegik]': KmnCompilerMessages.FATAL_UnicodeSetOutOfRange, // 6 ranges, allocated 1 + '[[\\p{Mn}]&[A-Z]]': KmnCompilerMessages.ERROR_UnicodeSetHasProperties, + '[abc{def}]': KmnCompilerMessages.ERROR_UnicodeSetHasStrings, + '[[]': KmnCompilerMessages.ERROR_UnicodeSetSyntaxError, }; for(const [pat, expected] of Object.entries(failures)) { { @@ -118,7 +118,7 @@ describe('Compiler UnicodeSet function', function() { assert.equal(code, expected, `${compilerErrorFormatCode(code)}≠${compilerErrorFormatCode(expected)} got ${firstMessage.message} for parsing ${pat}`); } // skip 'out of range' because that one won't fail during sizing. - if (expected !== CompilerMessages.FATAL_UnicodeSetOutOfRange) { + if (expected !== KmnCompilerMessages.FATAL_UnicodeSetOutOfRange) { // verify fails size callbacks.clear(); assert.equal(compiler.sizeUnicodeSet(pat), -1, `sizing ${pat}`); diff --git a/developer/src/kmc-kmn/test/tsconfig.json b/developer/src/kmc-kmn/test/tsconfig.json index e794de49202..3737f8d2ac6 100644 --- a/developer/src/kmc-kmn/test/tsconfig.json +++ b/developer/src/kmc-kmn/test/tsconfig.json @@ -7,10 +7,6 @@ "outDir": "../build/test", "baseUrl": ".", "allowSyntheticDefaultImports": true, // for chai - "paths": { - "@keymanapp/common-types": ["../../../../common/web/types/src/main"], - "@keymanapp/developer-test-helpers": ["../../common/web/test-helpers/index"], - }, }, "include": [ "**/test-*.ts", diff --git a/developer/src/kmc-kmn/tsconfig.json b/developer/src/kmc-kmn/tsconfig.json index 0aa22586108..2095229bff9 100644 --- a/developer/src/kmc-kmn/tsconfig.json +++ b/developer/src/kmc-kmn/tsconfig.json @@ -7,19 +7,14 @@ "baseUrl": ".", "allowJs": true, "preserveConstEnums": true, - "paths": { - "@keymanapp/common-types": ["../../../common/web/types/src/main"], - "@keymanapp/developer-utils": ["../common/web/utils/index"], - }, - }, "include": [ "src/**/*.ts", "src/import/kmcmplib/wasm-host.js" ], "references": [ - { "path": "../../../common/web/keyman-version/tsconfig.json" }, { "path": "../../../common/web/types/" }, + { "path": "../../../common/web/keyman-version/" }, { "path": "../common/web/utils/" }, ] } diff --git a/developer/src/kmc-ldml/.eslintrc.cjs b/developer/src/kmc-ldml/.eslintrc.cjs index 09038ae9291..41a487fe41d 100644 --- a/developer/src/kmc-ldml/.eslintrc.cjs +++ b/developer/src/kmc-ldml/.eslintrc.cjs @@ -6,7 +6,7 @@ module.exports = { overrides: [ { files:"src/**/*.ts", - extends: ["../../../common/web/eslint/eslintNoNodeImports.js"], + extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } ], rules: { diff --git a/developer/src/kmc-ldml/build.sh b/developer/src/kmc-ldml/build.sh index 41b5e494789..73a40c79d75 100755 --- a/developer/src/kmc-ldml/build.sh +++ b/developer/src/kmc-ldml/build.sh @@ -14,6 +14,7 @@ builder_describe "Keyman kmc Keyboard Compiler module" \ "@/common/web/keyman-version" \ "@/common/web/types" \ "@/developer/src/kmc-kmn" \ + "@/developer/src/common/web/utils" \ "@/developer/src/common/web/test-helpers" \ "configure" \ "build" \ diff --git a/developer/src/kmc-ldml/package.json b/developer/src/kmc-ldml/package.json index 0cdddcb8c9f..11d7c07b880 100644 --- a/developer/src/kmc-ldml/package.json +++ b/developer/src/kmc-ldml/package.json @@ -25,22 +25,24 @@ "url": "https://github.com/keymanapp/keyman/issues" }, "dependencies": { + "@keymanapp/developer-utils": "*", "@keymanapp/keyman-version": "*", "@keymanapp/kmc-kmn": "*", "@keymanapp/ldml-keyboard-constants": "*", - "semver": "^7.5.2" + "semver": "^7.5.4" }, "devDependencies": { "@keymanapp/developer-test-helpers": "*", "@keymanapp/resources-gosh": "*", + "@types/common-tags": "^1.8.4", "@types/mocha": "^5.2.7", "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "c8": "^7.12.0", "chalk": "^2.4.2", + "common-tags": "^1.8.2", "mocha": "^8.4.0", - "ts-node": "^9.1.1", - "typescript": "^4.9.5" + "typescript": "^5.4.5" }, "mocha": { "spec": "build/test/**/test-*.js", diff --git a/developer/src/kmc-ldml/src/compiler/compiler.ts b/developer/src/kmc-ldml/src/compiler/compiler.ts index b972baf7509..388a64de69b 100644 --- a/developer/src/kmc-ldml/src/compiler/compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/compiler.ts @@ -1,6 +1,17 @@ -import { LDMLKeyboardXMLSourceFileReader, LDMLKeyboard, KMXPlus, CompilerCallbacks, LDMLKeyboardTestDataXMLSourceFile, UnicodeSetParser, KeymanCompiler, KeymanCompilerResult, KeymanCompilerArtifacts, defaultCompilerOptions, KMXBuilder, KvkFileWriter, KeymanCompilerArtifactOptional } from '@keymanapp/common-types'; +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Compiles a LDML XML keyboard file into a Keyman KMXPlus file + */ +import { KMXPlus, UnicodeSetParser, KvkFileWriter, KMX } from '@keymanapp/common-types'; +import { + CompilerCallbacks, KeymanCompiler, KeymanCompilerResult, KeymanCompilerArtifacts, + defaultCompilerOptions, LDMLKeyboardXMLSourceFileReader, LDMLKeyboard, + LDMLKeyboardTestDataXMLSourceFile, KMXBuilder, + KeymanCompilerArtifactOptional +} from "@keymanapp/developer-utils"; import { LdmlCompilerOptions } from './ldml-compiler-options.js'; -import { CompilerMessages } from './messages.js'; +import { LdmlCompilerMessages } from './ldml-compiler-messages.js'; import { BkspCompiler, TranCompiler } from './tran.js'; import { DispCompiler } from './disp.js'; import { KeysCompiler } from './keys.js'; @@ -18,7 +29,7 @@ import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; import { KmnCompiler } from '@keymanapp/kmc-kmn'; import { KMXPlusMetadataCompiler } from './metadata-compiler.js'; import { LdmlKeyboardVisualKeyboardCompiler } from './visual-keyboard-compiler.js'; -//KMW17.0: import { LdmlKeyboardKeymanWebCompiler } from './keymanweb-compiler.js'; +import { LinterKeycaps } from './linter-keycaps.js'; export const SECTION_COMPILERS = [ // These are in dependency order. @@ -41,6 +52,11 @@ export const SECTION_COMPILERS = [ TranCompiler, ]; +/** list of linters, in order. */ +const LINTERS = [ + LinterKeycaps, +]; + /** * @public * Internal in-memory build artifacts from a successful compilation @@ -56,11 +72,6 @@ export interface LdmlKeyboardCompilerArtifacts extends KeymanCompilerArtifacts { * desktop projects alongside .kmx */ kvk?: KeymanCompilerArtifactOptional; - /** - * Javascript keyboard filedata and filename - installable into KeymanWeb, - * Keyman mobile products - */ - js?: KeymanCompilerArtifactOptional; }; export interface LdmlKeyboardCompilerResult extends KeymanCompilerResult { @@ -73,7 +84,7 @@ export interface LdmlKeyboardCompilerResult extends KeymanCompilerResult { /** * @public - * Compiles a LDML keyboard .xml file to a .kmx (KMXPlus), .kvk, and/or .js. The + * Compiles a LDML keyboard .xml file to a .kmx (KMXPlus), .kvk. The * compiler does not read or write from filesystem or network directly, but * relies on callbacks for all external IO. */ @@ -93,13 +104,13 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { * @returns false if initialization fails */ async init(callbacks: CompilerCallbacks, options: LdmlCompilerOptions): Promise { - this.options = {...options}; + this.options = { ...options }; this.callbacks = callbacks; return true; } /** - * Compiles a LDML keyboard .xml file to .kmx, .kvk, and/or .js files. Returns + * Compiles a LDML keyboard .xml file to .kmx, .kvk files. Returns * an object containing binary artifacts on success. The files are passed in * by name, and the compiler will use callbacks as passed to the * {@link LdmlKeyboardCompiler.init} function to read any input files by disk. @@ -125,18 +136,34 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { return null; } + outputFilename = outputFilename ?? inputFilename.replace(/\.xml$/, '.kmx'); + // In order for the KMX file to be loaded by non-KMXPlus components, it is helpful // to duplicate some of the metadata KMXPlusMetadataCompiler.addKmxMetadata(kmx.kmxplus, kmx.keyboard, compilerOptions); // Use the builder to generate the binary output file - const builder = new KMXBuilder(kmx, compilerOptions.saveDebug); - const kmx_binary = builder.compile(); + const kmxBuilder = new KMXBuilder(kmx, compilerOptions.saveDebug); + const keyboardId = this.callbacks.path.basename(outputFilename, '.kmx'); + const vkCompiler = new LdmlKeyboardVisualKeyboardCompiler(this.callbacks); + const vkCompilerResult = vkCompiler.compile(kmx.kmxplus, keyboardId); + if(vkCompilerResult === null) { + return null; + } + const vkData = typeof vkCompilerResult == 'object' ? vkCompilerResult : null; - const vkcompiler = new LdmlKeyboardVisualKeyboardCompiler(this.callbacks); - const vk = vkcompiler.compile(source); - const writer = new KvkFileWriter(); - const kvk_binary = writer.write(vk); + if(vkData) { + kmx.keyboard.stores.push({ + dpName: '', + dpString: keyboardId + '.kvk', + dwSystemID: KMX.KMXFile.TSS_VISUALKEYBOARD + }); + } + + const kmxBinary = kmxBuilder.compile(); + + const kvkWriter = new KvkFileWriter(); + const kvkBinary = vkData ? kvkWriter.write(vkData) : null; // Note: we could have a step of generating source files here // KvksFileWriter()... @@ -149,13 +176,10 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { //KMW17.0: const encoder = new TextEncoder(); //KMW17.0: const kmw_binary = encoder.encode(kmw_string); - outputFilename = outputFilename ?? inputFilename.replace(/\.xml$/, '.kmx'); - return { artifacts: { - kmx: { data: kmx_binary, filename: outputFilename }, - kvk: { data: kvk_binary, filename: outputFilename.replace(/\.kmx$/, '.kvk') }, - //KMW17.0: js: { data: kmw_binary, filename: outputFilename.replace(/\.kmx$/, '.js') }, + kmx: { data: kmxBinary, filename: outputFilename }, + kvk: kvkBinary ? { data: kvkBinary, filename: outputFilename.replace(/\.kmx$/, '.kvk') } : null, } }; } @@ -166,24 +190,19 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { * * - .kmx file - binary keyboard used by Keyman on desktop platforms * - .kvk file - binary on screen keyboard used by Keyman on desktop platforms - * - .js file - Javascript keyboard for web and touch platforms * * @param artifacts - object containing artifact binary data to write out * @returns true on success */ async write(artifacts: LdmlKeyboardCompilerArtifacts): Promise { - if(artifacts.kmx) { + if (artifacts.kmx) { this.callbacks.fs.writeFileSync(artifacts.kmx.filename, artifacts.kmx.data); } - if(artifacts.kvk) { + if (artifacts.kvk) { this.callbacks.fs.writeFileSync(artifacts.kvk.filename, artifacts.kvk.data); } - if(artifacts.js) { - this.callbacks.fs.writeFileSync(artifacts.js.filename, artifacts.js.data); - } - return true; } @@ -192,7 +211,7 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { * Construct or return a UnicodeSetParser, aka KmnCompiler * @returns the held UnicodeSetParser */ - async getUsetParser() : Promise { + async getUsetParser(): Promise { if (this.usetparser === undefined) { // initialize const compiler = new KmnCompiler(); @@ -222,14 +241,14 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { const reader = new LDMLKeyboardXMLSourceFileReader(this.options.readerOptions, this.callbacks); // load the file from disk into a string const data = this.callbacks.loadFile(filename); - if(!data) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidFile({errorText: 'Unable to read XML file'})); + if (!data) { + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidFile({ errorText: 'Unable to read XML file' })); return null; } // parse (load) the string into an object tree const source = reader.load(data); - if(!source) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidFile({errorText: 'Unable to load XML file'})); + if (!source) { + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidFile({ errorText: 'Unable to load XML file' })); return null; } try { @@ -237,8 +256,8 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { if (!reader.validate(source)) { return null; } - } catch(e) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidFile({errorText: e.toString()})); + } catch (e) { + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidFile({ errorText: e.toString() })); return null; } @@ -255,14 +274,14 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { public loadTestData(filename: string): LDMLKeyboardTestDataXMLSourceFile | null { const reader = new LDMLKeyboardXMLSourceFileReader(this.options.readerOptions, this.callbacks); const data = this.callbacks.loadFile(filename); - if(!data) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidFile({errorText: 'Unable to read XML file'})); + if (!data) { + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidFile({ errorText: 'Unable to read XML file' })); return null; } const source = reader.loadTestData(data); /* c8 ignore next 4 */ - if(!source) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidFile({errorText: 'Unable to load XML file'})); + if (!source) { + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidFile({ errorText: 'Unable to load XML file' })); return null; } // TODO-LDML: The unboxed data doesn't match the schema anymore. Skipping validation, for now. @@ -272,23 +291,52 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { // return null; // } // } catch(e) { - // this.callbacks.reportMessage(CompilerMessages.Error_InvalidFile({errorText: e.toString()})); + // this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidFile({errorText: e.toString()})); // return null; // } return source; } + /** Materialize the linters against the built datafile */ + private buildLinters(source: LDMLKeyboardXMLSourceFile, kmx: KMXPlus.KMXPlusFile) { + return LINTERS.map(c => new c(source, kmx, this.callbacks)); + } + + /** + * Runs any linter steps, adding hints to the callbacks as needed + * @internal + * @returns true unless there was a linter failure. + */ + private async lint(source: LDMLKeyboardXMLSourceFile, kmx: KMXPlus.KMXPlusFile): Promise { + if (!kmx || !source) { + return false; + } + // run each of the linters + for (const linter of this.buildLinters(source, kmx)) { + if (!await linter.lint()) { + return false; + } + } + return true; + } + /** * @internal - * Validates that the LDML keyboard source file and lints. Actually just - * compiles the keyboard and returns `true` if everything is good... + * Validates that the LDML keyboard source file and lints. * @param source - in-memory representation of LDML keyboard xml file * @returns true if the file validates */ public async validate(source: LDMLKeyboardXMLSourceFile): Promise { - return !!(await this.compile(source, true)); + // We need to compile in order to validate. + const kmx = await this.compile(source, true); + if (!kmx) { + return false; + } + + // Run the linters + return (await this.lint(source, kmx)); } /** @@ -304,8 +352,8 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { const kmx = new KMXPlusFile(); - for(let section of sections) { - if(!section.validate()) { + for (let section of sections) { + if (!section.validate()) { // TODO-LDML: coverage passed = false; // We'll keep validating other sections anyway, so we get a full set of @@ -313,13 +361,13 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { continue; } // clone - const globalSections : DependencySections = Object.assign({}, kmx.kmxplus); + const globalSections: DependencySections = Object.assign({}, kmx.kmxplus); // pre-initialize the usetparser globalSections.usetparser = await this.getUsetParser(); const dependencies = section.dependencies; let dependencyProblem = false; - Object.keys(constants.section).forEach((sectstr : string) => { - const sectid : SectionIdent = constants.section[sectstr]; + Object.keys(constants.section).forEach((sectstr: string) => { + const sectid: SectionIdent = constants.section[sectstr]; if (dependencies.has(sectid)) { /* c8 ignore next 4 */ if (!kmx.kmxplus[sectid]) { @@ -343,12 +391,12 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { } const sect = section.compile(globalSections); - if(!sect) { + if (!sect) { passed = false; continue; } /* c8 ignore next 4 */ - if(kmx.kmxplus[section.id]) { + if (kmx.kmxplus[section.id]) { // Internal error useful during section bring-up throw new Error(`Internal error: section ${section.id} would be assigned twice`); } @@ -357,8 +405,8 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { // give all sections a chance to postValidate if (postValidate) { - for(let section of sections) { - if(!section.postValidate(kmx.kmxplus[section.id])) { + for (let section of sections) { + if (!section.postValidate(kmx.kmxplus[section.id])) { passed = false; } } diff --git a/developer/src/kmc-ldml/src/compiler/disp.ts b/developer/src/kmc-ldml/src/compiler/disp.ts index 2c614def8da..03c411ce8d2 100644 --- a/developer/src/kmc-ldml/src/compiler/disp.ts +++ b/developer/src/kmc-ldml/src/compiler/disp.ts @@ -1,7 +1,8 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { KMXPlus, LDMLKeyboard } from '@keymanapp/common-types'; +import { LDMLKeyboard } from '@keymanapp/developer-utils'; +import { KMXPlus } from "@keymanapp/common-types"; -import { CompilerMessages } from "./messages.js"; +import { LdmlCompilerMessages } from "./ldml-compiler-messages.js"; import { SectionCompiler } from "./section-compiler.js"; import DependencySections = KMXPlus.DependencySections; @@ -32,18 +33,18 @@ export class DispCompiler extends SectionCompiler { if (this.keyboard3.displays?.display) { for (const { output, keyId } of this.keyboard3.displays?.display) { if ((output && keyId) || (!output && !keyId)) { - this.callbacks.reportMessage(CompilerMessages.Error_DisplayNeedsToOrId({ output, keyId })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayNeedsToOrId({ output, keyId })); return false; } else if (output) { if (tos.has(output)) { - this.callbacks.reportMessage(CompilerMessages.Error_DisplayIsRepeated({ output })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ output })); return false; } else { tos.add(output); } } else if (keyId) { if (ids.has(keyId)) { - this.callbacks.reportMessage(CompilerMessages.Error_DisplayIsRepeated({ keyId })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ keyId })); return false; } else { ids.add(keyId); diff --git a/developer/src/kmc-ldml/src/compiler/empty-compiler.ts b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts index 911e56d8961..44b04182a3f 100644 --- a/developer/src/kmc-ldml/src/compiler/empty-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts @@ -1,11 +1,12 @@ import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; import { SectionCompiler } from "./section-compiler.js"; -import { LDMLKeyboard, KMXPlus, CompilerCallbacks, util, MarkerParser } from "@keymanapp/common-types"; +import { util, KMXPlus, MarkerParser } from "@keymanapp/common-types"; +import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils"; import { VarsCompiler } from './vars.js'; -import { CompilerMessages } from './messages.js'; +import { LdmlCompilerMessages } from './ldml-compiler-messages.js'; /** - * Compiler for typrs that don't actually consume input XML + * Compiler for types that don't actually consume input XML */ export abstract class EmptyCompiler extends SectionCompiler { private _id: SectionIdent; @@ -58,16 +59,16 @@ export class StrsCompiler extends EmptyCompiler { const illegals = m.get(util.BadStringType.illegal); if (puas) { const [count, lowestCh] = [puas.size, Array.from(puas.values()).sort((a, b) => a - b)[0]]; - this.callbacks.reportMessage(CompilerMessages.Hint_PUACharacters({ count, lowestCh })) + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_PUACharacters({ count, lowestCh })) } if (unassigneds) { const [count, lowestCh] = [unassigneds.size, Array.from(unassigneds.values()).sort((a, b) => a - b)[0]]; - this.callbacks.reportMessage(CompilerMessages.Warn_UnassignedCharacters({ count, lowestCh })) + this.callbacks.reportMessage(LdmlCompilerMessages.Warn_UnassignedCharacters({ count, lowestCh })) } if (illegals) { // do this last, because we will return false. const [count, lowestCh] = [illegals.size, Array.from(illegals.values()).sort((a, b) => a - b)[0]]; - this.callbacks.reportMessage(CompilerMessages.Error_IllegalCharacters({ count, lowestCh })) + this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalCharacters({ count, lowestCh })) return false; } } diff --git a/developer/src/kmc-ldml/src/compiler/keymanweb-compiler.ts b/developer/src/kmc-ldml/src/compiler/keymanweb-compiler.ts deleted file mode 100644 index a35739247b4..00000000000 --- a/developer/src/kmc-ldml/src/compiler/keymanweb-compiler.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { CompilerCallbacks, VisualKeyboard, LDMLKeyboard, TouchLayoutFileWriter, KeymanFileTypes } from "@keymanapp/common-types"; -import { LdmlCompilerOptions } from "./ldml-compiler-options.js"; -import { TouchLayoutCompiler } from "./touch-layout-compiler.js"; -import { LdmlKeyboardVisualKeyboardCompiler } from "./visual-keyboard-compiler.js"; - -const MINIMUM_KMW_VERSION = '16.0'; - -export class LdmlKeyboardKeymanWebCompiler { - private readonly options: LdmlCompilerOptions; - private readonly nl: string; - private readonly tab: string; - constructor(private callbacks: CompilerCallbacks, options?: LdmlCompilerOptions) { - this.options = { ...options }; - this.nl = this.options.saveDebug ? "\n" : ''; - this.tab = this.options.saveDebug ? " " : ''; - } - - public compileVisualKeyboard(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile) { - const nl = this.nl, tab = this.tab; - const vkc = new LdmlKeyboardVisualKeyboardCompiler(this.callbacks); - const vk: VisualKeyboard.VisualKeyboard = vkc.compile(source); - - let result = - `{F: ' 1em ${JSON.stringify(vk.header.unicodeFont.name)}', `+ - `K102: ${vk.header.flags & VisualKeyboard.VisualKeyboardHeaderFlags.kvkh102 ? 1 : 0}};${nl}` + // TODO-LDML: escape ' and " in font name correctly - `${tab}this.KV.KLS={${nl}` + - `${tab}${tab}TODO_LDML: ${vk.keys.length}${nl}` + - // TODO-LDML: fill in KLS - `${tab}}`; - - return result; - } - - public compileTouchLayout(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile) { - const tlcompiler = new TouchLayoutCompiler(); - const layout = tlcompiler.compileToJavascript(source); - const writer = new TouchLayoutFileWriter({formatted: this.options.saveDebug}); - return writer.compile(layout); - } - - private cleanName(name: string): string { - let result = this.callbacks.path.basename(name, KeymanFileTypes.Source.LdmlKeyboard); - if(!result) { - throw new Error(`Invalid file name ${name}`); - } - - result = result.replaceAll(/[^a-z0-9]/g, '_'); - if(result.match(/^[0-9]/)) { - // Can't have a digit as initial - result = '_' + result; - } - return result; - } - - public compile(name: string, source: LDMLKeyboard.LDMLKeyboardXMLSourceFile): string { - const nl = this.nl, tab = this.tab; - - const sName = 'Keyboard_'+this.cleanName(name); - const displayUnderlying = true; // TODO-LDML - const modifierBitmask = 0; // TODO-LDML: define the modifiers used by this keyboard - const vkDictionary = ''; // TODO-LDML: vk dictionary for touch keys - const hasSupplementaryPlaneChars = false; // TODO-LDML - const isRTL = false; // TODO-LDML - - let result = - `if(typeof keyman === 'undefined') {${nl}` + - `${tab}console.error('Keyboard requires KeymanWeb ${MINIMUM_KMW_VERSION} or later');${nl}` + - `} else {${nl}` + - `${tab}KeymanWeb.KR(new ${sName}());${nl}` + - `}${nl}` + - `function ${sName}() {${nl}` + - // `${tab}${this.setupDebug()}${nl}` + ? we may use this for modifierBitmask in future - // `${tab}this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9;${nl}` + ? we probably don't need this, it's for back-compat - `${tab}this.KI="${sName}";${nl}` + - `${tab}this.KN=${JSON.stringify(source.keyboard3.info.name)};${nl}` + - `${tab}this.KMINVER=${JSON.stringify(MINIMUM_KMW_VERSION)};${nl}` + - `${tab}this.KV=${this.compileVisualKeyboard(source)};${nl}` + - `${tab}this.KDU=${displayUnderlying ? '1' : '0'};${nl}` + - `${tab}this.KH="";${nl}` + // TODO-LDML: help text not supported - `${tab}this.KM=0;${nl}` + // TODO-LDML: mnemonic layout not supported for LDML keyboards - `${tab}this.KBVER=${JSON.stringify(source.keyboard3.version?.number || '0.0')};${nl}` + - `${tab}this.KMBM=${modifierBitmask};${nl}`; - - if(isRTL) { - result += `${tab}this.KRTL=1;${nl}`; - } - - if(hasSupplementaryPlaneChars) { - result += `${tab}this.KS=1;${nl}`; - } - - if(vkDictionary != '') { - result += `${tab}this.KVKD=${JSON.stringify(vkDictionary)};${nl}`; - } - - let layoutFile = this.compileTouchLayout(source); - if(layoutFile != '') { - result += `${tab}this.KVKL=${layoutFile};${nl}`; - } - // TODO-LDML: KCSS not supported - - // TODO-LDML: embed binary keyboard for loading into Core - - // A LDML keyboard has a no-op for its gs() (begin Unicode) function, - // because the functionality is embedded in Keyman Core - result += `${tab}this.gs=function(t,e){${nl}`+ - `${tab}${tab}return 0;${nl}`+ // TODO-LDML: we will start by embedding call into Keyman Core here - `${tab}};${nl}`; - - result += `}${nl}`; - return result; - } -} diff --git a/developer/src/kmc-ldml/src/compiler/keys.ts b/developer/src/kmc-ldml/src/compiler/keys.ts index 7d97df41265..e341f3ac876 100644 --- a/developer/src/kmc-ldml/src/compiler/keys.ts +++ b/developer/src/kmc-ldml/src/compiler/keys.ts @@ -1,6 +1,7 @@ import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; -import { LDMLKeyboard, KMXPlus, Constants } from '@keymanapp/common-types'; -import { CompilerMessages } from './messages.js'; +import { KMXPlus, Constants } from '@keymanapp/common-types'; +import { LDMLKeyboard } from '@keymanapp/developer-utils'; +import { LdmlCompilerMessages } from './ldml-compiler-messages.js'; import { SectionCompiler } from "./section-compiler.js"; import DependencySections = KMXPlus.DependencySections; @@ -71,7 +72,7 @@ export class KeysCompiler extends SectionCompiler { this.keyboard3.forms?.form?.forEach((form) => { if (!LDMLKeyboard.ImportStatus.isImpliedImport(form)) { // If it's not an implied import, give a warning. - this.callbacks.reportMessage(CompilerMessages.Warn_CustomForm({ id: form.id })); + this.callbacks.reportMessage(LdmlCompilerMessages.Warn_CustomForm({ id: form.id })); } }); @@ -98,7 +99,7 @@ export class KeysCompiler extends SectionCompiler { if (!flickHash.has(flickId)) { valid = false; this.callbacks.reportMessage( - CompilerMessages.Error_MissingFlicks({ flickId, id: keyId }) + LdmlCompilerMessages.Error_MissingFlicks({ flickId, id: keyId }) ); } } @@ -110,7 +111,7 @@ export class KeysCompiler extends SectionCompiler { // TODO-LDML: could keep track of already missing keys so we don't warn multiple times on gesture keys valid = false; this.callbacks.reportMessage( - CompilerMessages.Error_GestureKeyNotFoundInKeyBag({keyId: gestureKeyId, parentKeyId: keyId, attribute: attrs.join(',')}) + LdmlCompilerMessages.Error_GestureKeyNotFoundInKeyBag({keyId: gestureKeyId, parentKeyId: keyId, attribute: attrs.join(',')}) ); } else { usedKeys.add(gestureKeyId); @@ -407,7 +408,7 @@ export class KeysCompiler extends SectionCompiler { const { modifiers } = layer; if (!validModifier(modifiers)) { this.callbacks.reportMessage( - CompilerMessages.Error_InvalidModifier({ modifiers, layer: layer.id }) + LdmlCompilerMessages.Error_InvalidModifier({ modifiers, layer: layer.id }) ); valid = false; } @@ -416,14 +417,14 @@ export class KeysCompiler extends SectionCompiler { const keymap = this.getKeymapFromForm(hardware, badScans); if (!keymap) { this.callbacks.reportMessage( - CompilerMessages.Error_InvalidHardware({ formId: hardware }) + LdmlCompilerMessages.Error_InvalidHardware({ formId: hardware }) ); valid = false; return valid; } else if (badScans.size !== 0) { const codes = Array.from(badScans.values()).map(n => Number(n).toString(16)).sort(); this.callbacks.reportMessage( - CompilerMessages.Error_InvalidScanCode({ form: hardware, codes }) + LdmlCompilerMessages.Error_InvalidScanCode({ form: hardware, codes }) ); valid = false; return valid; @@ -431,7 +432,7 @@ export class KeysCompiler extends SectionCompiler { if (layer.row.length > keymap.length) { this.callbacks.reportMessage( - CompilerMessages.Error_HardwareLayerHasTooManyRows() + LdmlCompilerMessages.Error_HardwareLayerHasTooManyRows() ); valid = false; } @@ -441,7 +442,7 @@ export class KeysCompiler extends SectionCompiler { if (keys.length > keymap[y].length) { this.callbacks.reportMessage( - CompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({ + LdmlCompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({ row: y + 1, hardware, modifiers, @@ -457,7 +458,7 @@ export class KeysCompiler extends SectionCompiler { let keydef = keyHash.get(key); if (!keydef) { this.callbacks.reportMessage( - CompilerMessages.Error_KeyNotFoundInKeyBag({ + LdmlCompilerMessages.Error_KeyNotFoundInKeyBag({ keyId: key, col: x + 1, row: y + 1, @@ -470,7 +471,7 @@ export class KeysCompiler extends SectionCompiler { } if (!keydef.output && !keydef.gap && !keydef.layerId) { this.callbacks.reportMessage( - CompilerMessages.Error_KeyMissingToGapOrSwitch({ keyId: key }) + LdmlCompilerMessages.Error_KeyMissingToGapOrSwitch({ keyId: key }) ); valid = false; continue; diff --git a/developer/src/kmc-ldml/src/compiler/layr.ts b/developer/src/kmc-ldml/src/compiler/layr.ts index 8056144b25b..62f8b3360d0 100644 --- a/developer/src/kmc-ldml/src/compiler/layr.ts +++ b/developer/src/kmc-ldml/src/compiler/layr.ts @@ -1,6 +1,6 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; import { KMXPlus } from '@keymanapp/common-types'; -import { CompilerMessages } from './messages.js'; +import { LdmlCompilerMessages } from './ldml-compiler-messages.js'; import { SectionCompiler } from "./section-compiler.js"; import { translateLayerAttrToModifier, validModifier } from '../util/util.js'; @@ -33,14 +33,14 @@ export class LayrCompiler extends SectionCompiler { hardwareLayers++; if (hardwareLayers > 1) { valid = false; - this.callbacks.reportMessage(CompilerMessages.Error_ExcessHardware({formId})); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_ExcessHardware({formId})); } } layers.layer.forEach((layer) => { const { modifiers, id } = layer; totalLayerCount++; if (!validModifier(modifiers)) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidModifier({ modifiers, layer: id || '' })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidModifier({ modifiers, layer: id || '' })); valid = false; } }); @@ -48,7 +48,7 @@ export class LayrCompiler extends SectionCompiler { if (totalLayerCount === 0) { // TODO-LDML: does not validate touch layers yet // no layers seen anywhere valid = false; - this.callbacks.reportMessage(CompilerMessages.Error_MustBeAtLeastOneLayerElement()); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement()); } return valid; } diff --git a/developer/src/kmc-ldml/src/compiler/messages.ts b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts similarity index 79% rename from developer/src/kmc-ldml/src/compiler/messages.ts rename to developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts index 7ebf6bddc77..901f4b00227 100644 --- a/developer/src/kmc-ldml/src/compiler/messages.ts +++ b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts @@ -1,4 +1,5 @@ -import { util, CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def } from "@keymanapp/common-types"; +import { util } from "@keymanapp/common-types"; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def } from '@keymanapp/developer-utils'; // const SevInfo = CompilerErrorSeverity.Info | CompilerErrorNamespace.LdmlKeyboardCompiler; const SevHint = CompilerErrorSeverity.Hint | CompilerErrorNamespace.LdmlKeyboardCompiler; const SevWarn = CompilerErrorSeverity.Warn | CompilerErrorNamespace.LdmlKeyboardCompiler; @@ -11,7 +12,7 @@ const SevErrorTransform = SevError | 0xF00; /** * @internal */ -export class CompilerMessages { +export class LdmlCompilerMessages { static HINT_NormalizationDisabled = SevHint | 0x0001; static Hint_NormalizationDisabled = () => m(this.HINT_NormalizationDisabled, `normalization=disabled is not recommended.`); @@ -26,19 +27,19 @@ export class CompilerMessages { static ERROR_KeyNotFoundInKeyBag = SevError | 0x0005; static Error_KeyNotFoundInKeyBag = (o:{keyId: string, col: number, row: number, layer: string, form: string}) => - m(this.ERROR_KeyNotFoundInKeyBag, `Key '${def(o.keyId)}' in position #${def(o.col)} on row #${def(o.row)} of layer ${def(o.layer)}, form '${def(o.form)}' not found in key bag`); + m(this.ERROR_KeyNotFoundInKeyBag, `Key '${def(o.keyId)}' in position #${def(o.col)} on row #${def(o.row)} of layer ${def(o.layer)}, form '${def(o.form)}' not found in key bag`); static HINT_OneOrMoreRepeatedLocales = SevHint | 0x0006; static Hint_OneOrMoreRepeatedLocales = () => - m(this.HINT_OneOrMoreRepeatedLocales, `After minimization, one or more locales is repeated and has been removed`); + m(this.HINT_OneOrMoreRepeatedLocales, `After minimization, one or more locales is repeated and has been removed`); static ERROR_InvalidFile = SevError | 0x0007; static Error_InvalidFile = (o:{errorText: string}) => - m(this.ERROR_InvalidFile, `The source file has an invalid structure: ${def(o.errorText)}`); + m(this.ERROR_InvalidFile, `The source file has an invalid structure: ${def(o.errorText)}`); static HINT_LocaleIsNotMinimalAndClean = SevHint | 0x0008; static Hint_LocaleIsNotMinimalAndClean = (o:{sourceLocale: string, locale: string}) => - m(this.HINT_LocaleIsNotMinimalAndClean, `Locale '${def(o.sourceLocale)}' is not minimal or correctly formatted and should be '${def(o.locale)}'`); + m(this.HINT_LocaleIsNotMinimalAndClean, `Locale '${def(o.sourceLocale)}' is not minimal or correctly formatted and should be '${def(o.locale)}'`); static ERROR_InvalidScanCode = SevError | 0x0009; static Error_InvalidScanCode = (o:{form?: string, codes?: string[]}) => @@ -52,17 +53,21 @@ export class CompilerMessages { static Error_GestureKeyNotFoundInKeyBag = (o:{keyId: string, parentKeyId: string, attribute: string}) => m(this.ERROR_GestureKeyNotFoundInKeyBag, `Key '${def(o.keyId)}' not found in key bag, referenced from other '${def(o.parentKeyId)}' in ${def(o.attribute)}`); - // 0x000C - available + static HINT_NoDisplayForMarker = SevHint | 0x000C; + static Hint_NoDisplayForMarker = (o: { id: string }) => + m(this.HINT_NoDisplayForMarker, `Key element with id "${def(o.id)}" has only marker output, but there is no matching display element by output or keyId. Keycap may be blank.`); static ERROR_InvalidVersion = SevError | 0x000D; static Error_InvalidVersion = (o:{version: string}) => - m(this.ERROR_InvalidVersion, `Version number '${def(o.version)}' must be a semantic version format string.`); + m(this.ERROR_InvalidVersion, `Version number '${def(o.version)}' must be a semantic version format string.`); static ERROR_MustBeAtLeastOneLayerElement = SevError | 0x000E; static Error_MustBeAtLeastOneLayerElement = () => - m(this.ERROR_MustBeAtLeastOneLayerElement, `The source file must contain at least one layer element.`); + m(this.ERROR_MustBeAtLeastOneLayerElement, `The source file must contain at least one layer element.`); - // 0x000F - available + static HINT_NoDisplayForSwitch = SevHint | 0x000F; + static Hint_NoDisplayForSwitch = (o: { id: string }) => + m(this.HINT_NoDisplayForSwitch, `Key element with id "${def(o.id)}" is a layer switch key, but there is no matching display element by keyId. Keycap may be blank.`); /** annotate the to= or id= entry */ private static outputOrKeyId(o:{output?: string, keyId?: string}) { @@ -79,7 +84,7 @@ export class CompilerMessages { static ERROR_DisplayIsRepeated = SevError | 0x0010; static Error_DisplayIsRepeated = (o:{output?: string, keyId?: string}) => - m(this.ERROR_DisplayIsRepeated, `display ${CompilerMessages.outputOrKeyId(o)} has more than one display entry.`); + m(this.ERROR_DisplayIsRepeated, `display ${LdmlCompilerMessages.outputOrKeyId(o)} has more than one display entry.`); static ERROR_KeyMissingToGapOrSwitch = SevError | 0x0011; static Error_KeyMissingToGapOrSwitch = (o:{keyId: string}) => @@ -93,9 +98,17 @@ export class CompilerMessages { static Error_InvalidHardware = (o:{formId: string}) => m(this.ERROR_InvalidHardware, `layers has invalid value formId=${def(o.formId)}`); + private static layerIdOrEmpty(layer : string) { + if (layer) { + return ` on layer id=${def(layer)}`; + } else { + return ''; + } + } + static ERROR_InvalidModifier = SevError | 0x0014; static Error_InvalidModifier = (o:{layer: string, modifiers: string}) => m(this.ERROR_InvalidModifier, - `layer has invalid modifiers='${def(o.modifiers)}' on layer id=${def(o.layer)}`); + `layer has invalid modifiers='${def(o.modifiers)}'` + LdmlCompilerMessages.layerIdOrEmpty(o.layer)); static ERROR_MissingFlicks = SevError | 0x0015; static Error_MissingFlicks = (o:{flickId: string, id: string}) => m(this.ERROR_MissingFlicks, @@ -148,7 +161,7 @@ export class CompilerMessages { static ERROR_DisplayNeedsToOrId = SevError | 0x0022; static Error_DisplayNeedsToOrId = (o:{output?: string, keyId?: string}) => - m(this.ERROR_DisplayNeedsToOrId, `display ${CompilerMessages.outputOrKeyId(o)} needs output= or keyId=, but not both`); + m(this.ERROR_DisplayNeedsToOrId, `display ${LdmlCompilerMessages.outputOrKeyId(o)} needs output= or keyId=, but not both`); static HINT_PUACharacters = SevHint | 0x0023; static Hint_PUACharacters = (o: { count: number, lowestCh: number }) => @@ -174,7 +187,13 @@ export class CompilerMessages { static Error_UnparseableReorderSet = (o: { from: string, set: string }) => m(this.ERROR_UnparseableReorderSet, `Illegal UnicodeSet "${def(o.set)}" in reorder "${def(o.from)}`); - // Available: 0x029 + static ERROR_InvalidVariableIdentifer = SevError | 0x0029; + static Error_InvalidVariableIdentifer = (o: { id: string }) => m( + this.ERROR_InvalidVariableIdentifer, + `Invalid variable identifier "\\u${def(o.id)}". Identifiers must be between 1 and 32 characters, and can use A-Z, a-z, 0-9, and _.`, + ); + + // Available: 0x02A-0x2F static ERROR_InvalidQuadEscape = SevError | 0x0030; static Error_InvalidQuadEscape = (o: { cp: number }) => @@ -189,9 +208,12 @@ export class CompilerMessages { m(this.ERROR_UnparseableTransformFrom, `Invalid transform from="${def(o.from)}": "${def(o.message)}"`); static ERROR_IllegalTransformDollarsign = SevErrorTransform | 0x01; - static Error_IllegalTransformDollarsign = (o: { from: string }) => - m(this.ERROR_IllegalTransformDollarsign, `Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, - '**Hint**: Use `\\$` to match a literal dollar-sign.'); + static Error_IllegalTransformDollarsign = (o: { from: string }) => m( + this.ERROR_IllegalTransformDollarsign, + `Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, + `**Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a variable name, `+ + `the variable name may not be valid (A-Z, a-z, 0-9, _, 32 character maximum).` + ); static ERROR_TransformFromMatchesNothing = SevErrorTransform | 0x02; static Error_TransformFromMatchesNothing = (o: { from: string }) => diff --git a/developer/src/kmc-ldml/src/compiler/ldml-compiler-options.ts b/developer/src/kmc-ldml/src/compiler/ldml-compiler-options.ts index 9ea0f043bb1..1b4f72113c1 100644 --- a/developer/src/kmc-ldml/src/compiler/ldml-compiler-options.ts +++ b/developer/src/kmc-ldml/src/compiler/ldml-compiler-options.ts @@ -1,4 +1,4 @@ -import { CompilerOptions, LDMLKeyboardXMLSourceFileReaderOptions } from "@keymanapp/common-types"; +import { CompilerOptions, LDMLKeyboardXMLSourceFileReaderOptions } from "@keymanapp/developer-utils"; /** * @public diff --git a/developer/src/kmc-ldml/src/compiler/linter-keycaps.ts b/developer/src/kmc-ldml/src/compiler/linter-keycaps.ts new file mode 100644 index 00000000000..eb6710952f6 --- /dev/null +++ b/developer/src/kmc-ldml/src/compiler/linter-keycaps.ts @@ -0,0 +1,51 @@ +import { MarkerParser } from "@keymanapp/common-types"; +import { Linter } from "./linter.js"; +import { LdmlCompilerMessages } from "./ldml-compiler-messages.js"; + +/** A linter concerned with the display of keycaps */ +export class LinterKeycaps extends Linter { + + private findDisp(keyId: string, output: string) { + for (const disp of this.kmx.kmxplus.disp.disps) { + const {id, to} = disp; + if (id.value !== '' && id.value === keyId) { + return disp; + } + + if (to.value !== '' && to.value === output) { + return disp; + } + } + return null; + } + + public async lint(): Promise { + const ANY_MARKER_REGEX = new RegExp(MarkerParser.ANY_MARKER_MATCH,'g'); + // Are there any keys which: + // 0. Are present in the layout, flicks or gestures, AND + // 1. Consist entirely of marker output, AND + // 2. Are not matched by any display id= OR display output= + + for (const key of this.kmx.kmxplus.keys.keys) { + const { id, to } = key; + const nonMarkerOutput = to.value.replaceAll(ANY_MARKER_REGEX, ''); + + if (key['switch'].value !== '' && to.value === '') { + // switch with no output + const disp = this.findDisp(id.value, to.value); + + if (!disp) { + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForSwitch({ id: id.value })); + } + } else if (to.value !== '' && nonMarkerOutput === '') { + // has output, but only markers + const disp = this.findDisp(id.value, to.value); + if (!disp) { + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForMarker({ id: id.value })); + } + } + } + + return true; + } +} diff --git a/developer/src/kmc-ldml/src/compiler/linter.ts b/developer/src/kmc-ldml/src/compiler/linter.ts new file mode 100644 index 00000000000..f01d4bd8415 --- /dev/null +++ b/developer/src/kmc-ldml/src/compiler/linter.ts @@ -0,0 +1,24 @@ +import { KMXPlus } from "@keymanapp/common-types"; +import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils"; + +/** newable interface to Linter c'tor */ +export type LinterNew = new (source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, kmx: KMXPlus.KMXPlusFile, callbacks: CompilerCallbacks) => Linter; +/** Abstract interface for a class which provides additional hints against a compiled keyboard file */ +export abstract class Linter { + protected readonly keyboard3: LDMLKeyboard.LKKeyboard; + protected readonly callbacks: CompilerCallbacks; + protected readonly kmx: KMXPlus.KMXPlusFile; + + constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, kmx: KMXPlus.KMXPlusFile, callbacks: CompilerCallbacks) { + this.keyboard3 = source.keyboard3; + this.callbacks = callbacks; + this.kmx = kmx; + } + + /** + * Check the file for any additional needed hints. + * Add the hints to the callbacks. + * @returns false on catastrophic failure of the linter. + */ + public abstract lint() : Promise; +} diff --git a/developer/src/kmc-ldml/src/compiler/loca.ts b/developer/src/kmc-ldml/src/compiler/loca.ts index 84be9e0bbf4..fea8cc43384 100644 --- a/developer/src/kmc-ldml/src/compiler/loca.ts +++ b/developer/src/kmc-ldml/src/compiler/loca.ts @@ -1,7 +1,8 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { LDMLKeyboard, KMXPlus } from '@keymanapp/common-types'; +import { KMXPlus } from '@keymanapp/common-types'; +import { LDMLKeyboard } from '@keymanapp/developer-utils'; import { SectionCompiler } from "./section-compiler.js"; -import { CompilerMessages } from "./messages.js"; +import { LdmlCompilerMessages } from "./ldml-compiler-messages.js"; import DependencySections = KMXPlus.DependencySections; import Loca = KMXPlus.Loca; @@ -30,7 +31,7 @@ export class LocaCompiler extends SectionCompiler { new Intl.Locale(tag); } catch(e) { if(e instanceof RangeError) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidLocale({tag})); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidLocale({tag})); valid = false; } else { /* c8 ignore next 2 */ @@ -50,7 +51,7 @@ export class LocaCompiler extends SectionCompiler { const locales = sourceLocales.map((sourceLocale: string) => { const locale = new Intl.Locale(sourceLocale).minimize().toString(); if(locale != sourceLocale) { - this.callbacks.reportMessage(CompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale, locale})); + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale, locale})); } return locale; }); @@ -62,7 +63,7 @@ export class LocaCompiler extends SectionCompiler { result.locales = canonicalLocales.map(locale => sections.strs.allocString(locale)); if(result.locales.length < locales.length) { - this.callbacks.reportMessage(CompilerMessages.Hint_OneOrMoreRepeatedLocales()); + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales()); } return result; diff --git a/developer/src/kmc-ldml/src/compiler/meta.ts b/developer/src/kmc-ldml/src/compiler/meta.ts index 7c86f6331b3..66cc7cf20d8 100644 --- a/developer/src/kmc-ldml/src/compiler/meta.ts +++ b/developer/src/kmc-ldml/src/compiler/meta.ts @@ -1,7 +1,7 @@ import { SectionIdent, constants } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlus } from '@keymanapp/common-types'; -import { CompilerMessages } from "./messages.js"; +import { LdmlCompilerMessages } from "./ldml-compiler-messages.js"; import { SectionCompiler } from "./section-compiler.js"; import semver from "semver"; @@ -28,11 +28,11 @@ export class MetaCompiler extends SectionCompiler { if(versionNumber !== undefined) { if(versionNumber.match(/^[=v]/i)) { // semver ignores a preceding '=' or 'v' - this.callbacks.reportMessage(CompilerMessages.Error_InvalidVersion({ version: versionNumber })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVersion({ version: versionNumber })); return false; } if(!semver.parse(versionNumber, {loose: false})) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidVersion({ version: versionNumber })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVersion({ version: versionNumber })); return false; } } @@ -41,7 +41,7 @@ export class MetaCompiler extends SectionCompiler { private validateNormalization(normalization?: string) { if (normalization === 'disabled') { - this.callbacks.reportMessage(CompilerMessages.Hint_NormalizationDisabled()); + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NormalizationDisabled()); } return true; } diff --git a/developer/src/kmc-ldml/src/compiler/section-compiler.ts b/developer/src/kmc-ldml/src/compiler/section-compiler.ts index f2b14e56a38..e33eda1086b 100644 --- a/developer/src/kmc-ldml/src/compiler/section-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/section-compiler.ts @@ -1,4 +1,5 @@ -import { LDMLKeyboard, KMXPlus, CompilerCallbacks } from "@keymanapp/common-types"; +import { KMXPlus } from "@keymanapp/common-types"; +import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils"; import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; /** newable interface to SectionCompiler c'tor */ diff --git a/developer/src/kmc-ldml/src/compiler/touch-layout-compiler.ts b/developer/src/kmc-ldml/src/compiler/touch-layout-compiler.ts deleted file mode 100644 index 5c5c7eb194e..00000000000 --- a/developer/src/kmc-ldml/src/compiler/touch-layout-compiler.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { TouchLayout, LDMLKeyboard } from "@keymanapp/common-types"; - -export class TouchLayoutCompiler { - public compileToJavascript(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile): TouchLayout.TouchLayoutFile { - let result: TouchLayout.TouchLayoutFile = {}; - - // start with desktop to mimic vk emit - result.desktop = { - defaultHint: "none", // TODO-LDML this should be optional - layer: [] - }; - - for(let layers of source.keyboard3.layers) { - for(let layer of layers.layer) { - const resultLayer = this.compileHardwareLayer(source, result, layer); - result.desktop.layer.push(resultLayer); - } - } - return result; - } - - private compileHardwareLayer( - source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, - file: TouchLayout.TouchLayoutFile, - layer: LDMLKeyboard.LKLayer - ) { - // TODO-LDML: consider consolidation with keys.ts? - - let fileLayer: TouchLayout.TouchLayoutLayer = { - id: this.translateLayerIdToTouchLayoutLayerId(layer.id, layer.modifiers), - row: [] - }; - - let y = -1; - - for(let row of layer.row) { - y++; - - let fileRow: TouchLayout.TouchLayoutRow = {id: y, key: []}; - fileLayer.row.push(fileRow); - - const keys = row.keys.split(' '); - for(let key of keys) { - const keydef = source.keyboard3.keys?.key?.find(x => x.id == key); - if(keydef) { - const fileKey: TouchLayout.TouchLayoutKey = { - id: this.translateKeyIdentifierToTouch(keydef.id) as TouchLayout.TouchLayoutKeyId, - text: keydef.output || '', - // TODO-LDML: additional properties - }; - fileRow.key.push(fileKey); - } else { - // TODO-LDML: consider logging missing keys - } - } - } - - return fileLayer; - } - - private translateLayerIdToTouchLayoutLayerId(id: string, modifier: string): string { - // Touch layout layers have a set of reserved names that correspond to - // hardware modifiers. We want to map these identifiers first before falling - // back to the layer ids - - // The set of recognized layer identifiers is: - // - // touch | LDML - // ---------------+------------- - // default | none - // shift | shift - // caps | caps - // rightalt | altR - // rightalt-shift | altR shift - // - const map = { - none: 'default', - shift: 'shift', - caps: 'caps', - altR: 'rightalt', - "altR shift": 'rightalt-shift' - }; - - // canonicalize modifier string, alphabetical - // TODO-LDML: need to support multiple here - if (modifier && modifier.indexOf(',') !== -1) { - throw Error(`Internal error: TODO-LDML: multiple modifiers ${modifier} not yet supported.`); - } - modifier = (modifier||'').split(/\b/).sort().join(' ').trim(); - - if(Object.hasOwn(map, modifier)) { - return (map as any)[modifier]; - } - - // TODO-LDML: Other layer names will be used unmodified, is this sufficient? - return id; - } - - private translateKeyIdentifierToTouch(id: string): string { - // Note: keys identifiers in kmx were traditionally case-insensitive, but we - // are going to use them as case-insensitive for LDML keyboards. The set of - // standard key identifiers a-z, A-Z, 0-9 will be used, where possible, and - // all other keys will be mapped to `T_key`. - - if(id.match(/^[0-9a-zA-Z]$/)) { - return 'K_'+id; - } - - // Not a standard key - return 'T_'+id; - } -} diff --git a/developer/src/kmc-ldml/src/compiler/tran.ts b/developer/src/kmc-ldml/src/compiler/tran.ts index 7ddffd47471..a25cc2cc089 100644 --- a/developer/src/kmc-ldml/src/compiler/tran.ts +++ b/developer/src/kmc-ldml/src/compiler/tran.ts @@ -1,5 +1,6 @@ import { constants, SectionIdent } from "@keymanapp/ldml-keyboard-constants"; -import { KMXPlus, LDMLKeyboard, CompilerCallbacks, VariableParser, MarkerParser, util } from '@keymanapp/common-types'; +import { KMXPlus, VariableParser, MarkerParser, util } from '@keymanapp/common-types'; +import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils"; import { SectionCompiler } from "./section-compiler.js"; import Bksp = KMXPlus.Bksp; @@ -14,7 +15,7 @@ import LKReorder = LDMLKeyboard.LKReorder; import LKTransform = LDMLKeyboard.LKTransform; import LKTransforms = LDMLKeyboard.LKTransforms; import { verifyValidAndUnique } from "../util/util.js"; -import { CompilerMessages } from "./messages.js"; +import { LdmlCompilerMessages } from "./ldml-compiler-messages.js"; import { Substitutions, SubstitutionUse } from "./substitution-tracker.js"; type TransformCompilerType = 'simple' | 'backspace'; @@ -59,9 +60,9 @@ export abstract class TransformCompiler type); if (!verifyValidAndUnique(types, - types => reportMessage(CompilerMessages.Error_DuplicateTransformsType({ types })), + types => reportMessage(LdmlCompilerMessages.Error_DuplicateTransformsType({ types })), new Set(['simple', 'backspace']), - types => reportMessage(CompilerMessages.Error_InvalidTransformsType({ types })))) { + types => reportMessage(LdmlCompilerMessages.Error_InvalidTransformsType({ types })))) { valid = false; } @@ -78,11 +79,11 @@ export abstract class TransformCompiler DTD + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVariableIdentifer({id})); + return false; + } + return true; + } + private validateVars(st: Substitutions): boolean { let valid = true; const variables = this.keyboard3?.variables; @@ -67,13 +75,28 @@ export class VarsCompiler extends SectionCompiler { if (variables) { // Strings for (const { id, value } of variables.string) { + if(!this.validateIdentifier(id)) { + valid = false; + continue; + } addId(id); - allStrings.add(id); const stringrefs = VariableParser.allStringReferences(value); + for(const ref of stringrefs) { + if(!allStrings.has(ref)) { + valid = false; + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingStringVariable({id: ref})); + allStrings.add(ref); // avoids multiple reports of same missing variable + } + } st.string.add(SubstitutionUse.variable, stringrefs); + allStrings.add(id); } // Sets for (const { id, value } of variables.set) { + if(!this.validateIdentifier(id)) { + valid = false; + continue; + } addId(id); allSets.add(id); // check for illegal references, here. @@ -87,7 +110,7 @@ export class VarsCompiler extends SectionCompiler { if (setrefs.length > 1) { // this is the form $[seta]$[setb] valid = false; - this.callbacks.reportMessage(CompilerMessages.Error_NeedSpacesBetweenSetVariables({ item })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_NeedSpacesBetweenSetVariables({ item })); } else { st.set.add(SubstitutionUse.variable, setrefs); // the reference to a 'map' } @@ -96,6 +119,10 @@ export class VarsCompiler extends SectionCompiler { } // UnicodeSets for (const { id, value } of variables.uset) { + if(!this.validateIdentifier(id)) { + valid = false; + continue; + } addId(id); allUnicodeSets.add(id); const stringrefs = VariableParser.allStringReferences(value); @@ -106,7 +133,7 @@ export class VarsCompiler extends SectionCompiler { valid = false; if (allSets.has(id2)) { // $[set] in a UnicodeSet must be another UnicodeSet. - this.callbacks.reportMessage(CompilerMessages.Error_CantReferenceSetFromUnicodeSet({ id: id2 })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_CantReferenceSetFromUnicodeSet({ id: id2 })); } else { st.uset.add(SubstitutionUse.variable, [id2]); } @@ -116,7 +143,7 @@ export class VarsCompiler extends SectionCompiler { } // one report if any dups if (dups.size > 0) { - this.callbacks.reportMessage(CompilerMessages.Error_DuplicateVariable({ + this.callbacks.reportMessage(LdmlCompilerMessages.Error_DuplicateVariable({ ids: Array.from(dups.values()).sort().join(', ') })); valid = false; @@ -128,19 +155,19 @@ export class VarsCompiler extends SectionCompiler { // intended. collisions are handled separately. if (!allSets.has(id) && !allUnicodeSets.has(id)) { valid = false; - this.callbacks.reportMessage(CompilerMessages.Error_MissingSetVariable({ id })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingSetVariable({ id })); } } for (const id of st.string.all) { if (!allStrings.has(id)) { valid = false; - this.callbacks.reportMessage(CompilerMessages.Error_MissingStringVariable({ id })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingStringVariable({ id })); } } for (const id of st.uset.all) { if (!allUnicodeSets.has(id)) { valid = false; - this.callbacks.reportMessage(CompilerMessages.Error_MissingUnicodeSetVariable({ id })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingUnicodeSetVariable({ id })); } } @@ -179,7 +206,7 @@ export class VarsCompiler extends SectionCompiler { // report once if (matchedNotEmitted.size > 0) { - this.callbacks.reportMessage(CompilerMessages.Error_MissingMarkers({ ids: Array.from(matchedNotEmitted.values()).sort() })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingMarkers({ ids: Array.from(matchedNotEmitted.values()).sort() })); valid = false; } diff --git a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts index bbb2d83abf9..7783d42405c 100644 --- a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts @@ -1,84 +1,164 @@ -import { VisualKeyboard, LDMLKeyboard, CompilerCallbacks } from "@keymanapp/common-types"; -import { KeysCompiler } from "./keys.js"; -import { CompilerMessages } from "./messages.js"; +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Export LDML data (https://www.unicode.org/reports/tr35/tr35-keyboards.html) + * to .kvk format. This is an interim solution until Keyman Core supports + * interrogation of the KMX+ data for OSK. + */ +import { ModifierKeyConstants, KMXPlus, VisualKeyboard } from "@keymanapp/common-types"; +import { CompilerCallbacks } from "@keymanapp/developer-utils"; +import { LdmlCompilerMessages } from "./ldml-compiler-messages.js"; + +// This is a partial polyfill for findLast, so not polluting Array.prototype +// https://medium.com/@stheodorejohn/findlast-method-polyfill-in-javascript-bridging-browser-gaps-c3baf6aabae1 +// TODO: remove and replace with Array.prototype.findLast when it is +// well-supported +function findLast(arr: any, callback: any) { + if (!arr) { + return undefined; + } + const len = arr.length >>> 0; + for (let i = len - 1; i >= 0; i--) { + if (callback(arr[i], i, arr)) { + return arr[i]; + } + } + return undefined; +} + + +const LDML_MODIFIER_TO_KVK_MODIFIER = new Map(); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.LCTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_LCTRL); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.RCTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_RCTRL); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.LALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_LALT); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.RALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_SHIFTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_CTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_CTRL); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_ALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_ALT); export class LdmlKeyboardVisualKeyboardCompiler { public constructor(private callbacks: CompilerCallbacks) { } - public compile(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile): VisualKeyboard.VisualKeyboard { + /** + * Generate a visual keyboard + * @param source Compiled KMX+ data; note that this is modified to add + * &VISUALKEYBOARD system store on success + * @param keyboardId Basename of keyboard, without file extension + * @returns Visual keyboard data on success, null on failure, or + * false if no VK was generated for this keyboard + */ + public compile(source: KMXPlus.KMXPlusData, keyboardId: string): VisualKeyboard.VisualKeyboard | boolean | null { let result = new VisualKeyboard.VisualKeyboard(); /* TODO-LDML: consider VisualKeyboardHeaderFlags.kvkhUseUnderlying kvkhDisplayUnderlying kvkhAltGr kvkh102 */ result.header.flags = 0; result.header.version = 0x0600; - - /* TODO-LDML: consider associatedKeyboard: this _must_ be set to id (aka basename sans ext) of keyboard .kmx file */ - result.header.associatedKeyboard = ''; + result.header.associatedKeyboard = keyboardId; result.header.ansiFont = {...VisualKeyboard.DEFAULT_KVK_FONT}; result.header.unicodeFont = {...VisualKeyboard.DEFAULT_KVK_FONT}; - for(let layers of source.keyboard3.layers) { - const { formId } = layers; - for(let layer of layers.layer) { - this.compileHardwareLayer(source, result, layer, formId); + let hasVisualKeyboard = false; + + for(let layersList of source.layr.lists) { + const formId = layersList.hardware.value; + if(formId == 'touch') { + continue; + } + + for(let layer of layersList.layers) { + const res = this.compileHardwareLayer(source, result, layer, formId); + if(res === false) { + // failed to compile the layer + return null; + } + if(res === null) { + // not a supported layer type, but not an error + continue; + } + hasVisualKeyboard = true; } } + + if(!hasVisualKeyboard) { + return false; + } + return result; } private compileHardwareLayer( - source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, + source: KMXPlus.KMXPlusData, vk: VisualKeyboard.VisualKeyboard, - layer: LDMLKeyboard.LKLayer, + layer: KMXPlus.LayrEntry, hardware: string, ) { - const layerId = layer.id; - if (hardware === 'touch') { - hardware = 'us'; // TODO-LDML: US Only. Do something different here? - } - const keymap = KeysCompiler.getKeymapFromForms(source.keyboard3?.forms?.form, hardware); - if (!keymap) { - this.callbacks.reportMessage( - CompilerMessages.Error_InvalidHardware({ formId: hardware }) - ); - return; + const layerId = layer.id.value; + + hardware = 'us'; // TODO-LDML: US Only. We need to clean this up for other hardware forms + + const shift = this.translateLayerModifiersToVisualKeyboardShift(layer.mod); + if(shift === null) { + // Caps (num, scroll) is not a supported shift state in .kvk + return null; } - const shift = this.translateLayerIdToVisualKeyboardShift(layer.id); + let result = true; let y = -1; - for(let row of layer.row) { + for(let row of layer.rows) { y++; - - const keys = row.keys.split(' '); let x = -1; - for(let key of keys) { - const keyId = key; + for(let key of row.keys) { x++; - let keydef = source.keyboard3.keys?.key?.find(x => x.id == key); + const keydef: KMXPlus.KeysKeys = findLast(source.keys?.keys, (kd: KMXPlus.KeysKeys) => kd.id.value == key.value); + const kmap = source.keys.kmap.find(k => k.key == keydef.id.value && k.mod == layer.mod); + const text = this.getDisplayFromKey(keydef, source) ?? null; - if (!keydef) { + if (!keydef || !kmap || text === null) { this.callbacks.reportMessage( - CompilerMessages.Error_KeyNotFoundInKeyBag({ keyId, layer: layerId, row: y, col: x, form: hardware }) + LdmlCompilerMessages.Error_KeyNotFoundInKeyBag({ keyId: key.value, layer: layerId, row: y, col: x, form: hardware }) ); + result = false; } else { vk.keys.push({ flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, - shift: shift, - text: keydef.output, // TODO-LDML: displays - vkey: keymap[y][x], + shift, + text, + vkey: kmap.vkey }); } } } + return result; + } + + private getDisplayFromKey(keydef: KMXPlus.KeysKeys, source: KMXPlus.KMXPlusData) { + const display = source.disp?.disps?.find(d => d.id.value == keydef.id.value || d.to.value == keydef.to.value); + return display?.display.value ?? keydef.to.value; } - private translateLayerIdToVisualKeyboardShift(id: string) { - if(id == 'base') { - return 0; + private translateLayerModifiersToVisualKeyboardShift(modifiers: number): VisualKeyboard.VisualKeyboardShiftState { + + if(modifiers == 0) { + return VisualKeyboard.VisualKeyboardShiftState.KVKS_NORMAL; } - // TODO-LDML: other modifiers - return 0; + + if(modifiers & + (ModifierKeyConstants.CAPITALFLAG | ModifierKeyConstants.NUMLOCKFLAG | ModifierKeyConstants.SCROLLFLAG) + ) { + // Caps/Num/Scroll are not supported in .kvk, in combination or alone + return null; + } + + let shift: VisualKeyboard.VisualKeyboardShiftState = 0; + + for(const mod of LDML_MODIFIER_TO_KVK_MODIFIER.keys()) { + if(modifiers & mod) { + shift |= LDML_MODIFIER_TO_KVK_MODIFIER.get(mod); + } + } + + return shift; } } diff --git a/developer/src/kmc-ldml/src/main.ts b/developer/src/kmc-ldml/src/main.ts index c2e0f190cbb..53d218db308 100644 --- a/developer/src/kmc-ldml/src/main.ts +++ b/developer/src/kmc-ldml/src/main.ts @@ -1,4 +1,4 @@ export { LdmlKeyboardCompiler } from './compiler/compiler.js'; export { LdmlCompilerOptions } from './compiler/ldml-compiler-options.js'; -export { CompilerMessages as LdmlKeyboardCompilerMessages } from './compiler/messages.js'; // TODO: rename messages.js and CompilerMessages +export { LdmlCompilerMessages } from './compiler/ldml-compiler-messages.js'; diff --git a/developer/src/kmc-ldml/src/util/util.ts b/developer/src/kmc-ldml/src/util/util.ts index 21409e72990..dbb5dc05319 100644 --- a/developer/src/kmc-ldml/src/util/util.ts +++ b/developer/src/kmc-ldml/src/util/util.ts @@ -1,4 +1,4 @@ -import { LDMLKeyboard } from "@keymanapp/common-types"; +import { LDMLKeyboard } from "@keymanapp/developer-utils"; import { constants } from "@keymanapp/ldml-keyboard-constants"; /** * Verifies that value is an item in the enumeration. diff --git a/developer/src/kmc-ldml/test/fixtures/basic-kvk.txt b/developer/src/kmc-ldml/test/fixtures/basic-kvk.txt index 68666feacf3..da8f5e6011a 100644 --- a/developer/src/kmc-ldml/test/fixtures/basic-kvk.txt +++ b/developer/src/kmc-ldml/test/fixtures/basic-kvk.txt @@ -1,4 +1,6 @@ # +# Keyman is copyright (C) SIL International. MIT License. +# # basic-kvk.txt describes the expected output of running kmc against basic.xml to generate # a .kvk file. It is used in the end-to-end test test-visual-keyboard-compiler-e2e.ts. # @@ -10,8 +12,8 @@ block(kvkheader) 00 06 00 00 # version 0x0600 00 # flags block(associated_keyboard) - 01 00 # string len in UTF-16 code units incl zterm - 00 00 # '\0' + 06 00 # string len in UTF-16 code units incl zterm + 62 00 61 00 73 00 69 00 63 00 00 00 # 'basic\0' block(ansi_font) 06 00 # string len in UTF-16 code units incl zterm diff --git a/developer/src/kmc-ldml/test/fixtures/basic-no-debug.js b/developer/src/kmc-ldml/test/fixtures/basic-no-debug.js deleted file mode 100644 index 84168414cf8..00000000000 --- a/developer/src/kmc-ldml/test/fixtures/basic-no-debug.js +++ /dev/null @@ -1 +0,0 @@ -if(typeof keyman === 'undefined') {console.error('Keyboard requires KeymanWeb 16.0 or later');} else {KeymanWeb.KR(new Keyboard_basic());}function Keyboard_basic() {this.KI="Keyboard_basic";this.KN="TestKbd";this.KMINVER="16.0";this.KV={F: ' 1em "Arial"', K102: 0};this.KV.KLS={TODO_LDML: 2};this.KDU=1;this.KH="";this.KM=0;this.KBVER="1.0.0";this.KMBM=0;this.KVKL={"desktop":{"defaultHint":"none","layer":[{"id":"base","row":[{"id":"0","key":[{"id":"T_hmaqtugha","text":"ħ"},{"id":"T_that","text":"ថា"}]}]}],"displayUnderlying":false}};this.gs=function(t,e){return 0;};} diff --git a/developer/src/kmc-ldml/test/fixtures/basic.js b/developer/src/kmc-ldml/test/fixtures/basic.js deleted file mode 100644 index c74798cfac5..00000000000 --- a/developer/src/kmc-ldml/test/fixtures/basic.js +++ /dev/null @@ -1,48 +0,0 @@ -if(typeof keyman === 'undefined') { - console.error('Keyboard requires KeymanWeb 16.0 or later'); -} else { - KeymanWeb.KR(new Keyboard_basic()); -} -function Keyboard_basic() { - this.KI="Keyboard_basic"; - this.KN="TestKbd"; - this.KMINVER="16.0"; - this.KV={F: ' 1em "Arial"', K102: 0}; - this.KV.KLS={ - TODO_LDML: 2 - }; - this.KDU=1; - this.KH=""; - this.KM=0; - this.KBVER="1.0.0"; - this.KMBM=0; - this.KVKL={ - "desktop": { - "defaultHint": "none", - "layer": [ - { - "id": "base", - "row": [ - { - "id": "0", - "key": [ - { - "id": "T_hmaqtugha", - "text": "ħ" - }, - { - "id": "T_that", - "text": "ថា" - } - ] - } - ] - } - ], - "displayUnderlying": false - } -}; - this.gs=function(t,e){ - return 0; - }; -} diff --git a/developer/src/kmc-ldml/test/fixtures/basic.txt b/developer/src/kmc-ldml/test/fixtures/basic.txt index b471228a52c..6302a5eb276 100644 --- a/developer/src/kmc-ldml/test/fixtures/basic.txt +++ b/developer/src/kmc-ldml/test/fixtures/basic.txt @@ -1,4 +1,6 @@ # +# Keyman is copyright (C) SIL International. MIT License. +# # basic.txt describes the expected output of running kmc against basic.xml. It is used in # the end-to-end test test-compiler-e2e.ts. # @@ -402,7 +404,7 @@ block(layr) # struct COMP_KMXPLUS_LAYR { 01 00 00 00 # count 7B 00 00 00 # KMX_DWORD minDeviceWidth; // 123 # layers 0 - index(strNull,strBase,2) # KMXPLUS_STR id; + 00 00 00 00 # KMXPLUS_STR id; 00 00 00 00 # KMX_DWORD mod 00 00 00 00 # KMX_DWORD row index 01 00 00 00 # KMX_DWORD count @@ -502,7 +504,6 @@ block(strs) # struct COMP_KMXPLUS_STRS { diff(strs,strSet) sizeof(strSet,2) diff(strs,strSet2) sizeof(strSet2,2) diff(strs,strTranTo) sizeof(strTranTo,2) - diff(strs,strBase) sizeof(strBase,2) diff(strs,strElemBkspFrom2) sizeof(strElemBkspFrom2,2) diff(strs,strGapReserved) sizeof(strGapReserved,2) diff(strs,strHmaqtugha) sizeof(strHmaqtugha,2) @@ -538,7 +539,6 @@ block(strs) # struct COMP_KMXPLUS_STRS { #str #0A block(strSet2) 61 00 62 00 63 00 block(x) 00 00 # 'abc' block(strTranTo) 61 00 02 03 block(x) 00 00 # 'â' (U+0061 U+0302) - block(strBase) 62 00 61 00 73 00 65 00 block(x) 00 00 # 'base' block(strElemBkspFrom2) 65 00 block(x) 00 00 # 'e' block(strGapReserved) 67 00 61 00 70 00 20 00 28 00 72 00 65 00 73 00 65 00 72 00 76 00 65 00 64 00 29 00 block(x) 00 00 # 'gap (reserved)' block(strHmaqtugha) 68 00 6d 00 61 00 71 00 74 00 75 00 67 00 68 00 61 00 block(x) 00 00 # 'hmaqtugha' diff --git a/developer/src/kmc-ldml/test/fixtures/basic.xml b/developer/src/kmc-ldml/test/fixtures/basic.xml index c8565271ed1..168ee0ef9a0 100644 --- a/developer/src/kmc-ldml/test/fixtures/basic.xml +++ b/developer/src/kmc-ldml/test/fixtures/basic.xml @@ -1,4 +1,5 @@ + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/keys/warn-no-keycap.xml b/developer/src/kmc-ldml/test/fixtures/sections/keys/warn-no-keycap.xml new file mode 100644 index 00000000000..6d0cf23f5b9 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/keys/warn-no-keycap.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bogus-modifiers.xml b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bogus-modifiers.xml new file mode 100644 index 00000000000..85ef41bd47b --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bogus-modifiers.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-7.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-7.xml new file mode 100644 index 00000000000..25e7c46a1d4 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-7.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-invalid-identifiers.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-invalid-identifiers.xml new file mode 100644 index 00000000000..880864fd4e6 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-invalid-identifiers.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index 2e9e16ff6af..e1463e81169 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -1,16 +1,18 @@ -/** +/* + * Keyman is copyright (C) SIL International. MIT License. + * * Helpers and utilities for the Mocha tests. */ import 'mocha'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { SectionCompiler, SectionCompilerNew } from '../../src/compiler/section-compiler.js'; -import { util, KMXPlus, LDMLKeyboardXMLSourceFileReader, VisualKeyboard, CompilerEvent, LDMLKeyboardTestDataXMLSourceFile, compilerEventFormat, LDMLKeyboard, UnicodeSetParser, CompilerCallbacks } from '@keymanapp/common-types'; +import { util, KMXPlus, UnicodeSetParser } from '@keymanapp/common-types'; +import { CompilerEvent, compilerEventFormat, CompilerCallbacks, LDMLKeyboardXMLSourceFileReader, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboard, } from "@keymanapp/developer-utils"; import { LdmlKeyboardCompiler } from '../../src/main.js'; // make sure main.js compiles import { assert } from 'chai'; import { KMXPlusMetadataCompiler } from '../../src/compiler/metadata-compiler.js'; import { LdmlCompilerOptions } from '../../src/compiler/ldml-compiler-options.js'; -import { LdmlKeyboardVisualKeyboardCompiler } from '../../src/compiler/visual-keyboard-compiler.js'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import KMXPlusFile = KMXPlus.KMXPlusFile; @@ -20,7 +22,6 @@ import Section = KMXPlus.Section; import { ElemCompiler, ListCompiler, StrsCompiler } from '../../src/compiler/empty-compiler.js'; import { KmnCompiler } from '@keymanapp/kmc-kmn'; import { VarsCompiler } from '../../src/compiler/vars.js'; -// import Vars = KMXPlus.Vars; /** * Builds a path to the fixture with the given path components. @@ -47,7 +48,7 @@ beforeEach(function() { afterEach(function() { if (this.currentTest.state !== 'passed') { - compilerTestCallbacks.messages.forEach(message => console.log(message.message)); + compilerTestCallbacks.printMessages(); } }); @@ -160,28 +161,7 @@ export async function compileKeyboard(inputFilename: string, options: LdmlCompil return kmx; } -export async function compileVisualKeyboard(inputFilename: string, options: LdmlCompilerOptions): Promise { - const k = new LdmlKeyboardCompiler(); - assert.isTrue(await k.init(compilerTestCallbacks, options)); - const source = k.load(inputFilename); - checkMessages(); - assert.isNotNull(source, 'k.load should not have returned null'); - - const valid = await k.validate(source); - checkMessages(); - assert.isTrue(valid, 'k.validate should not have failed'); - - const vk = (new LdmlKeyboardVisualKeyboardCompiler(compilerTestCallbacks)).compile(source); - checkMessages(); - assert.isNotNull(vk, 'LdmlKeyboardVisualKeyboardCompiler.compile should not have returned null'); - - return vk; -} - export function checkMessages() { - if(compilerTestCallbacks.messages.length > 0) { - console.log(compilerTestCallbacks.messages); - } assert.isEmpty(compilerTestCallbacks.messages, compilerEventFormat(compilerTestCallbacks.messages)); } diff --git a/developer/src/kmc-ldml/test/test-compiler-e2e.ts b/developer/src/kmc-ldml/test/test-compiler-e2e.ts index a66b67369fb..1f87e55a22a 100644 --- a/developer/src/kmc-ldml/test/test-compiler-e2e.ts +++ b/developer/src/kmc-ldml/test/test-compiler-e2e.ts @@ -1,11 +1,11 @@ import 'mocha'; import {assert} from 'chai'; import hextobin from '@keymanapp/hextobin'; -import { KMXBuilder } from '@keymanapp/common-types'; +import { KMXBuilder } from '@keymanapp/developer-utils'; import {checkMessages, compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture} from './helpers/index.js'; import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js'; -import { CompilerMessages } from '../src/compiler/messages.js'; +/** Overall compiler tests */ describe('compiler-tests', function() { this.slow(500); // 0.5 sec -- json schema validation takes a while @@ -72,62 +72,4 @@ describe('compiler-tests', function() { const source = k.load(filename); assert.notOk(source, `Trying to loadTestData(${filename})`); }); - it('should fail on illegal chars - sections/strs/invalid-illegal.xml', async function() { - const inputFilename = makePathToFixture('sections/strs/invalid-illegal.xml'); - const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, - [ - // validation messages - CompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: 0xFDD0 }), - CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), - ], - true, // validation should fail - [ - // compiler messages (not reached, we've already failed) - ]); - assert.isNull(kmx); // should fail post-validate - }); - it('should hint on pua chars', async function() { - const inputFilename = makePathToFixture('sections/strs/hint-pua.xml'); - // Compile the keyboard - const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, - [ - // validation messages - CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), - ], - false, // validation should pass - [ - // same messages - CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), - ]); - assert.isNotNull(kmx); - }); - it.skip('should warn on unassigned chars', async function() { - // unassigned not implemented yet - const inputFilename = makePathToFixture('sections/strs/warn-unassigned.xml'); - const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, - [ - // validation messages - CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), - CompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }), - ], - false, // validation should pass - [ - // same messages - CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), - CompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }), - ]); - assert.isNotNull(kmx); - }); - it('should fail on extra escapes - sections/tran/fail-bad-tran-2.xml', async function() { - const inputFilename = makePathToFixture('sections/tran/fail-bad-tran-2.xml'); - const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, - [ - CompilerMessages.Error_InvalidQuadEscape({ cp: 295 }), - ], - true, // validation should fail - [ - // compiler messages (not reached, we've already failed) - ]); - assert.isNull(kmx); // should fail post-validate - }); }); diff --git a/developer/src/kmc-ldml/test/test-disp.ts b/developer/src/kmc-ldml/test/test-disp.ts index 678728d3e52..36f08f2780a 100644 --- a/developer/src/kmc-ldml/test/test-disp.ts +++ b/developer/src/kmc-ldml/test/test-disp.ts @@ -3,7 +3,7 @@ import {assert} from 'chai'; import { DispCompiler } from '../src/compiler/disp.js'; import { compilerTestCallbacks, loadSectionFixture, testCompilationCases } from './helpers/index.js'; import { KMXPlus } from '@keymanapp/common-types'; -import { CompilerMessages } from '../src/compiler/messages.js'; +import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; import Disp = KMXPlus.Disp; @@ -61,32 +61,32 @@ describe('disp', function () { let disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupto.xml', compilerTestCallbacks) as Disp; assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_DisplayIsRepeated({ output: 'e' })); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ output: 'e' })); }); it('should reject duplicate ids', async function() { let disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupid.xml', compilerTestCallbacks) as Disp;1 assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_DisplayIsRepeated({ keyId: 'e' })); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ keyId: 'e' })); }); it('should reject if neither to nor id', async function() { let disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-none.xml', compilerTestCallbacks) as Disp; assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_DisplayNeedsToOrId({})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({})); }); it('should reject if both to and id', async function() { let disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-both.xml', compilerTestCallbacks) as Disp; assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_DisplayNeedsToOrId({ output: 'e', keyId: 'e' })); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ output: 'e', keyId: 'e' })); }); testCompilationCases(DispCompiler, [ { subpath: 'sections/disp/invalid-var.xml', errors: [ - CompilerMessages.Error_MissingStringVariable({id: "missingdisplay"}), - CompilerMessages.Error_MissingStringVariable({id: "missingoutput"}), + LdmlCompilerMessages.Error_MissingStringVariable({id: "missingdisplay"}), + LdmlCompilerMessages.Error_MissingStringVariable({id: "missingoutput"}), ], }, ]); diff --git a/developer/src/kmc-ldml/test/test-helpers.ts b/developer/src/kmc-ldml/test/test-helpers.ts index 30ab974721e..8a7a6ba5429 100644 --- a/developer/src/kmc-ldml/test/test-helpers.ts +++ b/developer/src/kmc-ldml/test/test-helpers.ts @@ -1,21 +1,21 @@ -import { CompilerEvent } from '@keymanapp/common-types'; +import { CompilerEvent } from '@keymanapp/developer-utils'; import 'mocha'; import {assert} from 'chai'; import { CompilerEventOrMatch, matchCompilerEvents } from './helpers/index.js'; -import { CompilerMessages } from '../src/compiler/messages.js'; +import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; describe('test of test/helpers/CompilerMatch', () => { /** the message(s) to test */ const messages : CompilerEvent[] = [ - CompilerMessages.Error_InvalidHardware({ formId: 'outdated' }), - CompilerMessages.Hint_NormalizationDisabled(), + LdmlCompilerMessages.Error_InvalidHardware({ formId: 'outdated' }), + LdmlCompilerMessages.Hint_NormalizationDisabled(), ]; it('should work for exact matches', () => { const spec : CompilerEventOrMatch[] = [ - CompilerMessages.Hint_NormalizationDisabled(), - CompilerMessages.Error_InvalidHardware({ formId: 'outdated' }), + LdmlCompilerMessages.Hint_NormalizationDisabled(), + LdmlCompilerMessages.Error_InvalidHardware({ formId: 'outdated' }), ]; const matched = matchCompilerEvents(messages, spec); assert.sameDeepMembers(messages, spec); // exact match works here @@ -24,9 +24,9 @@ describe('test of test/helpers/CompilerMatch', () => { it('should work for a fuzzy matches', () => { const spec : CompilerEventOrMatch[] = [ // mixed messages here - this one is a CompilerEvent - CompilerMessages.Hint_NormalizationDisabled(), + LdmlCompilerMessages.Hint_NormalizationDisabled(), // fuzzy match on this one - CompilerMatch - { code: CompilerMessages.ERROR_InvalidHardware, matchMessage: /(out|up|inun)dated/ }, + { code: LdmlCompilerMessages.ERROR_InvalidHardware, matchMessage: /(out|up|inun)dated/ }, ]; const matched = matchCompilerEvents(messages, spec); assert.sameDeepMembers(messages, matched); diff --git a/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts b/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts deleted file mode 100644 index 9a14b38c33d..00000000000 --- a/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts +++ /dev/null @@ -1,50 +0,0 @@ -import 'mocha'; -import { assert } from 'chai'; -import { checkMessages, compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './helpers/index.js'; -import { LdmlKeyboardKeymanWebCompiler } from '../src/compiler/keymanweb-compiler.js'; -import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js'; -import * as fs from 'fs'; - -describe('LdmlKeyboardKeymanWebCompiler', function() { - - it('should build a .js file', async function() { - // Let's build basic.xml - // It should generate content identical to basic.js - const inputFilename = makePathToFixture('basic.xml'); - const outputFilename = makePathToFixture('basic.js'); - const outputFilenameNoDebug = makePathToFixture('basic-no-debug.js'); - - // Load input data; we'll use the LDML keyboard compiler loader to save us - // effort here - const k = new LdmlKeyboardCompiler(); - await k.init(compilerTestCallbacks, {...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false}); - const source = k.load(inputFilename); - checkMessages(); - assert.isNotNull(source, 'k.load should not have returned null'); - - // Sanity check ... this is also checked in other tests - const valid = await k.validate(source); - checkMessages(); - assert.isTrue(valid, 'k.validate should not have failed'); - - // Actual test: compile to javascript - const jsCompiler = new LdmlKeyboardKeymanWebCompiler(compilerTestCallbacks, {...compilerTestOptions, saveDebug: true}); - const output = jsCompiler.compile('basic.xml', source); - assert.isNotNull(output); - - // Does the emitted js match? - const outputFixture = fs.readFileSync(outputFilename, 'utf-8').replaceAll(/\r\n/g, '\n'); - assert.strictEqual(output, outputFixture); - - // Second test: compile to javascript without debug formatting - const jsCompilerNoDebug = new LdmlKeyboardKeymanWebCompiler(compilerTestCallbacks, {...compilerTestOptions, saveDebug: false}); - const outputNoDebug = jsCompilerNoDebug.compile('basic.xml', source); - assert.isNotNull(outputNoDebug); - - // Does the emitted js match? The nodebug has no newline at end, but allow one in the fixture - const outputFixtureNoDebug = fs.readFileSync(outputFilenameNoDebug, 'utf-8').replaceAll(/\r\n/g, '\n').trim(); - assert.strictEqual(outputNoDebug, outputFixtureNoDebug); - - // TODO(lowpri): consider using Typescript parser to generate AST for further validation - }); -}); diff --git a/developer/src/kmc-ldml/test/test-keys.ts b/developer/src/kmc-ldml/test/test-keys.ts index 7d37dcfd362..8c0a47bebb3 100644 --- a/developer/src/kmc-ldml/test/test-keys.ts +++ b/developer/src/kmc-ldml/test/test-keys.ts @@ -3,7 +3,7 @@ import { assert } from 'chai'; import { KeysCompiler } from '../src/compiler/keys.js'; import { assertCodePoints, compilerTestCallbacks, loadSectionFixture, testCompilationCases } from './helpers/index.js'; import { KMXPlus, Constants, MarkerParser } from '@keymanapp/common-types'; -import { CompilerMessages } from '../src/compiler/messages.js'; +import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; import { constants } from '@keymanapp/ldml-keyboard-constants'; import { MetaCompiler } from '../src/compiler/meta.js'; const keysDependencies = [ ...BASIC_DEPENDENCIES, MetaCompiler ]; @@ -120,7 +120,7 @@ describe('keys', function () { }, warnings: [ - CompilerMessages.Hint_NormalizationDisabled() + LdmlCompilerMessages.Hint_NormalizationDisabled() ], }, { @@ -298,18 +298,18 @@ describe('keys.kmap', function () { { subpath: 'sections/keys/invalid-bad-modifier.xml', errors: [ - CompilerMessages.Error_InvalidModifier({layer:'base',modifiers:'altR-shift'}), + LdmlCompilerMessages.Error_InvalidModifier({layer:'base',modifiers:'altR-shift'}), ] }, { subpath: 'sections/keys/invalid-missing-flick.xml', errors: [ - CompilerMessages.Error_MissingFlicks({flickId:'an-undefined-flick-id',id:'Q'}), + LdmlCompilerMessages.Error_MissingFlicks({flickId:'an-undefined-flick-id',id:'Q'}), ] }, { subpath: 'sections/layr/invalid-invalid-form.xml', - errors: [CompilerMessages.Error_InvalidHardware({ + errors: [LdmlCompilerMessages.Error_InvalidHardware({ formId: 'holographic', }),], }, @@ -317,7 +317,7 @@ describe('keys.kmap', function () { // warning on custom form subpath: 'sections/layr/warn-custom-us-form.xml', warnings: [ - CompilerMessages.Warn_CustomForm({id: "us"}), + LdmlCompilerMessages.Warn_CustomForm({id: "us"}), ], callback: (sect, subpath, callbacks) => { const keys = sect as Keys; @@ -347,7 +347,7 @@ describe('keys.kmap', function () { // warning on a custom unknown form - but no error! subpath: 'sections/layr/warn-custom-zzz-form.xml', warnings: [ - CompilerMessages.Warn_CustomForm({id: "zzz"}), + LdmlCompilerMessages.Warn_CustomForm({id: "zzz"}), ], callback: (sect, subpath, callbacks) => { const keys = sect as Keys; @@ -376,31 +376,31 @@ describe('keys.kmap', function () { { subpath: 'sections/layr/error-custom-us-form.xml', warnings: [ - CompilerMessages.Warn_CustomForm({id: "us"}), + LdmlCompilerMessages.Warn_CustomForm({id: "us"}), ], errors: [ - CompilerMessages.Error_InvalidScanCode({ form: "us", codes: ['ff'] }), + LdmlCompilerMessages.Error_InvalidScanCode({ form: "us", codes: ['ff'] }), ], }, { subpath: 'sections/layr/error-custom-zzz-form.xml', warnings: [ - CompilerMessages.Warn_CustomForm({id: "zzz"}), + LdmlCompilerMessages.Warn_CustomForm({id: "zzz"}), ], errors: [ - CompilerMessages.Error_InvalidScanCode({ form: "zzz", codes: ['ff'] }), + LdmlCompilerMessages.Error_InvalidScanCode({ form: "zzz", codes: ['ff'] }), ], }, { subpath: 'sections/keys/invalid-undefined-var-1.xml', errors: [ - CompilerMessages.Error_MissingStringVariable({id: "varsok"}), + LdmlCompilerMessages.Error_MissingStringVariable({id: "varsok"}), ], }, { subpath: 'sections/keys/invalid-undefined-var-1b.xml', errors: [ - CompilerMessages.Error_MissingStringVariable({id: "varsok"}), + LdmlCompilerMessages.Error_MissingStringVariable({id: "varsok"}), ], }, // modifiers test @@ -429,7 +429,7 @@ describe('keys.kmap', function () { assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_HardwareLayerHasTooManyRows()); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_HardwareLayerHasTooManyRows()); }); it('should reject layouts with too many hardware keys', async function() { @@ -437,7 +437,7 @@ describe('keys.kmap', function () { assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({row: 1, hardware: 'us', modifiers: 'none'})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({row: 1, hardware: 'us', modifiers: 'none'})); }); it('should reject layouts with undefined keys', async function() { @@ -445,13 +445,13 @@ describe('keys.kmap', function () { assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_KeyNotFoundInKeyBag({col: 1, form: 'hardware', keyId: 'foo', layer: 'base', row: 1})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_KeyNotFoundInKeyBag({col: 1, form: 'hardware', keyId: 'foo', layer: 'base', row: 1})); }); it('should reject layouts with invalid keys', async function() { let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-key-missing-attrs.xml', compilerTestCallbacks, keysDependencies) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_KeyMissingToGapOrSwitch({keyId: 'Q'})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_KeyMissingToGapOrSwitch({keyId: 'Q'})); }); it('should accept layouts with gap/switch keys', async function() { let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/gap-switch.xml', compilerTestCallbacks, keysDependencies) as Keys; diff --git a/developer/src/kmc-ldml/test/test-layr.ts b/developer/src/kmc-ldml/test/test-layr.ts index 32d212a0096..3cf0701986a 100644 --- a/developer/src/kmc-ldml/test/test-layr.ts +++ b/developer/src/kmc-ldml/test/test-layr.ts @@ -1,7 +1,7 @@ import 'mocha'; import { assert } from 'chai'; import { LayrCompiler } from '../src/compiler/layr.js'; -import { CompilerMessages } from '../src/compiler/messages.js'; +import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; import { compilerTestCallbacks, testCompilationCases } from './helpers/index.js'; import { KMXPlus } from '@keymanapp/common-types'; import { constants } from '@keymanapp/ldml-keyboard-constants'; @@ -95,7 +95,7 @@ describe('layr', function () { { subpath: 'sections/keys/invalid-bad-modifier.xml', errors: [ - CompilerMessages.Error_InvalidModifier({ + LdmlCompilerMessages.Error_InvalidModifier({ layer: 'base', modifiers: 'altR-shift' }), @@ -103,12 +103,12 @@ describe('layr', function () { }, { subpath: 'sections/layr/invalid-multi-hardware.xml', - errors: [CompilerMessages.Error_ExcessHardware({ formId: 'iso' })], + errors: [LdmlCompilerMessages.Error_ExcessHardware({ formId: 'iso' })], }, { // missing layer element subpath: 'sections/layr/invalid-missing-layer.xml', - errors: [CompilerMessages.Error_MustBeAtLeastOneLayerElement()], + errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement()], }, { // keep in sync with similar test in test-keys.ts @@ -129,5 +129,11 @@ describe('layr', function () { ]); }, }, + { + subpath: 'sections/layr/error-bogus-modifiers.xml', + errors: [ + LdmlCompilerMessages.Error_InvalidModifier({ layer: '', modifiers: 'caps bogus'}), + ] + }, ]); }); diff --git a/developer/src/kmc-ldml/test/test-linter.ts b/developer/src/kmc-ldml/test/test-linter.ts new file mode 100644 index 00000000000..cd122840e87 --- /dev/null +++ b/developer/src/kmc-ldml/test/test-linter.ts @@ -0,0 +1,30 @@ +import 'mocha'; +import {assert} from 'chai'; +import {compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture} from './helpers/index.js'; +import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; + +/** Overall tests that have to do with cross-section linting or hinting */ +describe('linter-tests', function() { + this.slow(500); // 0.5 sec -- json schema validation takes a while + + before(function() { + compilerTestCallbacks.clear(); + }); + + it('should warn on mark keys with no display', async function () { + // unassigned not implemented yet + const inputFilename = makePathToFixture('sections/keys/warn-no-keycap.xml'); + const msgs = [ + LdmlCompilerMessages.Hint_NoDisplayForMarker({ id: 'mark-dotbelow' }), + LdmlCompilerMessages.Hint_NoDisplayForMarker({ id: 'mark-dotbelowabove' }), + LdmlCompilerMessages.Hint_NoDisplayForSwitch({ id: 'rebase' }), + ]; + const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, + msgs, + false, // validation should pass + msgs, + ); + assert.isNotNull(kmx); + }); + + }); diff --git a/developer/src/kmc-ldml/test/test-loca.ts b/developer/src/kmc-ldml/test/test-loca.ts index bfc975fb1b0..4dedc2bfd43 100644 --- a/developer/src/kmc-ldml/test/test-loca.ts +++ b/developer/src/kmc-ldml/test/test-loca.ts @@ -3,7 +3,7 @@ import { assert } from 'chai'; import { LocaCompiler } from '../src/compiler/loca.js'; import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js'; import { KMXPlus } from '@keymanapp/common-types'; -import { CompilerMessages } from '../src/compiler/messages.js'; +import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; import Loca = KMXPlus.Loca; @@ -26,10 +26,10 @@ describe('loca', function () { // Note: multiple.xml includes fr-FR twice, with differing case, which should be canonicalized assert.equal(compilerTestCallbacks.messages.length, 4); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-FR', locale: 'fr'})); - assert.deepEqual(compilerTestCallbacks.messages[1], CompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'km-khmr-kh', locale: 'km'})); - assert.deepEqual(compilerTestCallbacks.messages[2], CompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-fr', locale: 'fr'})); - assert.deepEqual(compilerTestCallbacks.messages[3], CompilerMessages.Hint_OneOrMoreRepeatedLocales()); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-FR', locale: 'fr'})); + assert.deepEqual(compilerTestCallbacks.messages[1], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'km-khmr-kh', locale: 'km'})); + assert.deepEqual(compilerTestCallbacks.messages[2], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-fr', locale: 'fr'})); + assert.deepEqual(compilerTestCallbacks.messages[3], LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales()); // Original is 6 locales, now five minimized in the results assert.equal(loca.locales.length, 5); @@ -46,6 +46,6 @@ describe('loca', function () { assert.equal(compilerTestCallbacks.messages.length, 1); // We'll only test one invalid BCP 47 tag to verify that we are properly calling BCP 47 validation routines. // Furthermore, we are testing BCP 47 structure, not the validity of each subtag -- we must assume the author knows of new subtags! - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidLocale({tag:'en-*'})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_InvalidLocale({tag:'en-*'})); }) }); diff --git a/developer/src/kmc-ldml/test/test-messages.ts b/developer/src/kmc-ldml/test/test-messages.ts index a88da8bbbfe..041e1ed214e 100644 --- a/developer/src/kmc-ldml/test/test-messages.ts +++ b/developer/src/kmc-ldml/test/test-messages.ts @@ -1,10 +1,10 @@ import 'mocha'; -import { CompilerMessages } from '../src/compiler/messages.js'; +import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; -import { CompilerErrorNamespace } from '@keymanapp/common-types'; +import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; -describe('CompilerMessages', function () { - it('should have a valid CompilerMessages object', function() { - return verifyCompilerMessagesObject(CompilerMessages, CompilerErrorNamespace.LdmlKeyboardCompiler); +describe('LdmlCompilerMessages', function () { + it('should have a valid LdmlCompilerMessages object', function() { + return verifyCompilerMessagesObject(LdmlCompilerMessages, CompilerErrorNamespace.LdmlKeyboardCompiler); }); }); diff --git a/developer/src/kmc-ldml/test/test-meta.ts b/developer/src/kmc-ldml/test/test-meta.ts index 1e5a5a1aeb6..908f66667ec 100644 --- a/developer/src/kmc-ldml/test/test-meta.ts +++ b/developer/src/kmc-ldml/test/test-meta.ts @@ -3,7 +3,7 @@ import {assert} from 'chai'; import { MetaCompiler } from '../src/compiler/meta.js'; import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js'; import { KMXPlus } from '@keymanapp/common-types'; -import { CompilerMessages } from '../src/compiler/messages.js'; +import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; import KeyboardSettings = KMXPlus.KeyboardSettings; import Meta = KMXPlus.Meta; @@ -39,19 +39,19 @@ describe('meta', function () { let meta = await loadSectionFixture(MetaCompiler, 'sections/meta/hint-normalization.xml', compilerTestCallbacks) as Meta; assert.isNotNull(meta); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Hint_NormalizationDisabled()); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Hint_NormalizationDisabled()); }); it('should reject invalid version', async function() { let meta = await loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-1.0.xml', compilerTestCallbacks) as Meta; assert.isNull(meta); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidVersion({version:'1.0'})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_InvalidVersion({version:'1.0'})); meta = await loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-v1.0.3.xml', compilerTestCallbacks) as Meta; assert.isNull(meta); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidVersion({version:'v1.0.3'})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_InvalidVersion({version:'v1.0.3'})); }); }); diff --git a/developer/src/kmc-ldml/test/test-strs.ts b/developer/src/kmc-ldml/test/test-strs.ts new file mode 100644 index 00000000000..a5f7e89e35c --- /dev/null +++ b/developer/src/kmc-ldml/test/test-strs.ts @@ -0,0 +1,74 @@ +import 'mocha'; +import { assert } from 'chai'; +import { compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './helpers/index.js'; +import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; + + +/** strs tests */ +describe('strs', function () { + this.slow(500); // 0.5 sec -- json schema validation takes a while + + before(function () { + compilerTestCallbacks.clear(); + }); + + it('should fail on illegal chars - sections/strs/invalid-illegal.xml', async function () { + const inputFilename = makePathToFixture('sections/strs/invalid-illegal.xml'); + const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, + [ + // validation messages + LdmlCompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: 0xFDD0 }), + LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), + ], + true, // validation should fail + [ + // compiler messages (not reached, we've already failed) + ]); + assert.isNull(kmx); // should fail post-validate + }); + it('should hint on pua chars', async function () { + const inputFilename = makePathToFixture('sections/strs/hint-pua.xml'); + // Compile the keyboard + const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, + [ + // validation messages + LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), + ], + false, // validation should pass + [ + // same messages + LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), + ]); + assert.isNotNull(kmx); + }); + it.skip('should warn on unassigned chars', async function () { + // unassigned not implemented yet + const inputFilename = makePathToFixture('sections/strs/warn-unassigned.xml'); + const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, + [ + // validation messages + LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), + LdmlCompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }), + ], + false, // validation should pass + [ + // same messages + LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), + LdmlCompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }), + ]); + assert.isNotNull(kmx); + }); + + it('should fail on extra escapes - sections/tran/fail-bad-tran-2.xml', async function () { + const inputFilename = makePathToFixture('sections/tran/fail-bad-tran-2.xml'); + const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, + [ + LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 295 }), + ], + true, // validation should fail + [ + // compiler messages (not reached, we've already failed) + ]); + assert.isNull(kmx); // should fail post-validate + }); +}); diff --git a/developer/src/kmc-ldml/test/test-tran.ts b/developer/src/kmc-ldml/test/test-tran.ts index b52e46cb75a..06266d9a71e 100644 --- a/developer/src/kmc-ldml/test/test-tran.ts +++ b/developer/src/kmc-ldml/test/test-tran.ts @@ -2,8 +2,8 @@ import 'mocha'; import { assert } from 'chai'; import { TranCompiler, BkspCompiler } from '../src/compiler/tran.js'; import { BASIC_DEPENDENCIES, UsetCompiler } from '../src/compiler/empty-compiler.js'; -import { CompilerMessages } from '../src/compiler/messages.js'; -import { CompilerMessages as KmnOtherCompilerMessages } from '@keymanapp/kmc-kmn'; +import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; +import { KmnCompilerMessages } from '@keymanapp/kmc-kmn'; import { assertCodePoints, compilerTestCallbacks, testCompilationCases } from './helpers/index.js'; import { KMXPlus, MarkerParser } from '@keymanapp/common-types'; @@ -120,7 +120,7 @@ describe('tran', function () { `\u{1f76}${m(1,false)}\u{033c}`); }, warnings: [ - CompilerMessages.Hint_NormalizationDisabled() + LdmlCompilerMessages.Hint_NormalizationDisabled() ], }, { subpath: 'sections/tran/fail-invalid-type.xml', @@ -129,7 +129,7 @@ describe('tran', function () { { subpath: 'sections/tran/fail-duplicate-type.xml', errors: [ - CompilerMessages.Error_DuplicateTransformsType({types: ['simple']}) + LdmlCompilerMessages.Error_DuplicateTransformsType({types: ['simple']}) ] }, { @@ -139,13 +139,13 @@ describe('tran', function () { { subpath: 'sections/tran/fail-mixed.xml', errors: [ - CompilerMessages.Error_MixedTransformGroup(), + LdmlCompilerMessages.Error_MixedTransformGroup(), ], }, { subpath: 'sections/tran/fail-empty.xml', errors: [ - CompilerMessages.Error_EmptyTransformGroup(), + LdmlCompilerMessages.Error_EmptyTransformGroup(), ], }, // reorder test @@ -249,44 +249,44 @@ describe('tran', function () { { subpath: 'sections/tran/tran-warn-range.xml', warnings: [ - CompilerMessages.Warn_CharClassExplicitDenorm({lowestCh: 0xE1}), + LdmlCompilerMessages.Warn_CharClassExplicitDenorm({lowestCh: 0xE1}), ], }, { subpath: 'sections/tran/tran-hint-range.xml', warnings: [ - CompilerMessages.Hint_CharClassImplicitDenorm({lowestCh: 0xc0}), + LdmlCompilerMessages.Hint_CharClassImplicitDenorm({lowestCh: 0xc0}), ], }, { subpath: 'sections/tran/tran-hint-range2.xml', warnings: [ - CompilerMessages.Hint_CharClassImplicitDenorm({lowestCh: 0xC0}), + LdmlCompilerMessages.Hint_CharClassImplicitDenorm({lowestCh: 0xC0}), ], }, { subpath: 'sections/tran/fail-bad-reorder-1.xml', errors: [ - KmnOtherCompilerMessages.Error_UnicodeSetSyntaxError() + KmnCompilerMessages.Error_UnicodeSetSyntaxError() ], }, { subpath: 'sections/tran/fail-bad-reorder-2.xml', errors: [ - CompilerMessages.Error_InvalidQuadEscape({ cp: 0x1a6b }), + LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 0x1a6b }), ], }, { subpath: 'sections/tran/fail-bad-reorder-3.xml', errors: [ - CompilerMessages.Error_InvalidQuadEscape({ cp: 0x1a60 }), + LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 0x1a60 }), ], }, // error due to bad regex { subpath: `sections/tran/fail-bad-tran-1.xml`, errors: [ - { code: CompilerMessages.ERROR_UnparseableTransformFrom, + { code: LdmlCompilerMessages.ERROR_UnparseableTransformFrom, matchMessage: /Invalid regular expression.*Unterminated group/, } ], @@ -295,43 +295,43 @@ describe('tran', function () { // also used in test-compiler-e2e.ts subpath: `sections/tran/fail-bad-tran-2.xml`, errors: [ - CompilerMessages.Error_InvalidQuadEscape({ cp: 295 }), + LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 295 }), ], }, { subpath: `sections/tran/fail-missing-var-1.xml`, errors: [ - CompilerMessages.Error_MissingStringVariable({ id: "missingfrom" }), + LdmlCompilerMessages.Error_MissingStringVariable({ id: "missingfrom" }), ], }, { subpath: `sections/tran/fail-missing-var-2.xml`, errors: [ - CompilerMessages.Error_MissingStringVariable({ id: "missingto" }), + LdmlCompilerMessages.Error_MissingStringVariable({ id: "missingto" }), ], }, { subpath: `sections/tran/fail-missing-var-3.xml`, errors: [ - CompilerMessages.Error_MissingSetVariable({ id: "missingset" }), + LdmlCompilerMessages.Error_MissingSetVariable({ id: "missingset" }), ], }, { subpath: `sections/tran/fail-missing-var-4.xml`, errors: [ - CompilerMessages.Error_MissingSetVariable({ id: "missingset" }), + LdmlCompilerMessages.Error_MissingSetVariable({ id: "missingset" }), ], }, { subpath: `sections/tran/fail-missing-var-5.xml`, errors: [ - CompilerMessages.Error_MissingSetVariable({ id: "missingset" }), + LdmlCompilerMessages.Error_MissingSetVariable({ id: "missingset" }), ], }, { subpath: `sections/tran/fail-missing-var-6.xml`, errors: [ - CompilerMessages.Error_MissingStringVariable({ id: "missingstr" }), + LdmlCompilerMessages.Error_MissingStringVariable({ id: "missingstr" }), ], }, // cases that share the same error code @@ -339,7 +339,7 @@ describe('tran', function () { subpath: `sections/tran/fail-IllegalTransformDollarsign-${n}.xml`, errors: [ { - code: CompilerMessages.ERROR_IllegalTransformDollarsign, + code: LdmlCompilerMessages.ERROR_IllegalTransformDollarsign, matchMessage: /.*/, } ], @@ -348,7 +348,7 @@ describe('tran', function () { subpath: `sections/tran/fail-IllegalTransformAsterisk-${n}.xml`, errors: [ { - code: CompilerMessages.ERROR_IllegalTransformAsterisk, + code: LdmlCompilerMessages.ERROR_IllegalTransformAsterisk, matchMessage: /.*/, } ], @@ -357,7 +357,7 @@ describe('tran', function () { subpath: `sections/tran/fail-IllegalTransformPlus-${n}.xml`, errors: [ { - code: CompilerMessages.ERROR_IllegalTransformPlus, + code: LdmlCompilerMessages.ERROR_IllegalTransformPlus, matchMessage: /.*/, } ], @@ -372,7 +372,7 @@ describe('tran', function () { subpath: `sections/tran/fail-matches-nothing-${n}.xml`, errors: [ { - code: CompilerMessages.ERROR_TransformFromMatchesNothing, + code: LdmlCompilerMessages.ERROR_TransformFromMatchesNothing, matchMessage: /.*/, } ], @@ -411,7 +411,7 @@ describe('bksp', function () { subpath: 'sections/vars/fail-markers-badref-0.xml', strictErrors: true, errors: [ - CompilerMessages.Error_MissingMarkers({ + LdmlCompilerMessages.Error_MissingMarkers({ ids: [ 'doesnt_exist_1', 'doesnt_exist_2', diff --git a/developer/src/kmc-ldml/test/test-utils.ts b/developer/src/kmc-ldml/test/test-utils.ts index fcf1f786839..3b89e3c2afe 100644 --- a/developer/src/kmc-ldml/test/test-utils.ts +++ b/developer/src/kmc-ldml/test/test-utils.ts @@ -5,7 +5,7 @@ import { isValidEnumValue, calculateUniqueKeys, allUsedKeyIdsInLayers, translateLayerAttrToModifier, validModifier, verifyValidAndUnique } from '../src/util/util.js'; import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { LDMLKeyboard } from '@keymanapp/common-types'; +import { LDMLKeyboard } from '@keymanapp/developer-utils'; describe('test of util/util.ts', () => { describe('isValidEnumValue()', () => { diff --git a/developer/src/kmc-ldml/test/test-vars.ts b/developer/src/kmc-ldml/test/test-vars.ts index e3ac53c4a92..8eef17af25c 100644 --- a/developer/src/kmc-ldml/test/test-vars.ts +++ b/developer/src/kmc-ldml/test/test-vars.ts @@ -1,8 +1,8 @@ import 'mocha'; import { assert } from 'chai'; import { VarsCompiler } from '../src/compiler/vars.js'; -import { CompilerMessages } from '../src/compiler/messages.js'; -import { CompilerMessages as KmnCompilerMessages } from '@keymanapp/kmc-kmn'; +import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; +import { KmnCompilerMessages } from '@keymanapp/kmc-kmn'; import { testCompilationCases } from './helpers/index.js'; import { KMXPlus, KMX } from '@keymanapp/common-types'; import { BASIC_DEPENDENCIES } from '../src/compiler/empty-compiler.js'; @@ -112,13 +112,23 @@ describe('vars', function () { { subpath: 'sections/vars/dup0.xml', errors: [ - CompilerMessages.Error_DuplicateVariable({ids: 'y'}) + LdmlCompilerMessages.Error_DuplicateVariable({ids: 'y'}) ], }, { subpath: 'sections/vars/dup1.xml', errors: [ - CompilerMessages.Error_DuplicateVariable({ids: 'upper, y'}) + LdmlCompilerMessages.Error_DuplicateVariable({ids: 'upper, y'}) + ], + }, + { + subpath: 'sections/vars/fail-invalid-identifiers.xml', + errors: [ + LdmlCompilerMessages.Error_InvalidVariableIdentifer({id: 'invalid-string'}), + LdmlCompilerMessages.Error_InvalidVariableIdentifer({id: 'invalid-set'}), + LdmlCompilerMessages.Error_InvalidVariableIdentifer({id: 'invalid-uset'}), + LdmlCompilerMessages.Error_InvalidVariableIdentifer({id: 'a_marker_name_more_than_32_chars_long'}), + LdmlCompilerMessages.Error_InvalidVariableIdentifer({id: '😡'}), ], }, { @@ -148,46 +158,53 @@ describe('vars', function () { { subpath: 'sections/vars/fail-badref-0.xml', errors: [ - CompilerMessages.Error_MissingStringVariable({id: "yes"}), + LdmlCompilerMessages.Error_MissingStringVariable({id: "yes"}), ], }, { subpath: 'sections/vars/fail-badref-1.xml', errors: [ - CompilerMessages.Error_MissingSetVariable({id: "lower"}), + LdmlCompilerMessages.Error_MissingSetVariable({id: "lower"}), ], }, { subpath: 'sections/vars/fail-badref-2.xml', errors: [ - CompilerMessages.Error_MissingStringVariable({ id: 'doesnotexist' }) + LdmlCompilerMessages.Error_MissingStringVariable({ id: 'doesnotexist' }) ], }, { subpath: 'sections/vars/fail-badref-3.xml', errors: [ - CompilerMessages.Error_MissingUnicodeSetVariable({id: 'doesnotexist'}) + LdmlCompilerMessages.Error_MissingUnicodeSetVariable({id: 'doesnotexist'}) ], }, { subpath: 'sections/vars/fail-badref-4.xml', errors: [ - CompilerMessages.Error_NeedSpacesBetweenSetVariables({item: '$[vowels]$[consonants]'}) + LdmlCompilerMessages.Error_NeedSpacesBetweenSetVariables({item: '$[vowels]$[consonants]'}) ], }, { subpath: 'sections/vars/fail-badref-5.xml', errors: [ - CompilerMessages.Error_CantReferenceSetFromUnicodeSet({id: 'nonUnicodeSet'}) + LdmlCompilerMessages.Error_CantReferenceSetFromUnicodeSet({id: 'nonUnicodeSet'}) ], }, { subpath: 'sections/vars/fail-badref-6.xml', errors: [ - CompilerMessages.Error_MissingStringVariable({id: 'missingStringInSet'}) + LdmlCompilerMessages.Error_MissingStringVariable({id: 'missingStringInSet'}) + ], + }, + { + subpath: 'sections/vars/fail-badref-7.xml', + errors: [ + LdmlCompilerMessages.Error_MissingStringVariable({id: 'usedBeforeDefinition'}) ], + strictErrors: true }, - ], varsDependencies); +], varsDependencies); describe('should match some marker constants', () => { // neither of these live here, but, common/web/types does not import ldml-keyboard-constants otherwise. @@ -211,7 +228,7 @@ describe('vars', function () { { subpath: 'sections/vars/fail-markers-badref-0.xml', errors: [ - CompilerMessages.Error_MissingMarkers({ + LdmlCompilerMessages.Error_MissingMarkers({ ids: [ 'doesnt_exist_1', 'doesnt_exist_2', diff --git a/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts b/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts deleted file mode 100644 index df0aea03918..00000000000 --- a/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts +++ /dev/null @@ -1,31 +0,0 @@ -import 'mocha'; -import {assert} from 'chai'; -import hextobin from '@keymanapp/hextobin'; -import { KvkFileWriter } from '@keymanapp/common-types'; -import {checkMessages, compilerTestOptions, compileVisualKeyboard, makePathToFixture} from './helpers/index.js'; - -describe('visual-keyboard-compiler', function() { - this.slow(500); // 0.5 sec -- json schema validation takes a while - - it('should build fixtures', async function() { - // Let's build basic.xml - // It should match basic.kvk (built from basic-kvk.txt) - - const inputFilename = makePathToFixture('basic.xml'); - const binaryFilename = makePathToFixture('basic-kvk.txt'); - - // Compile the visual keyboard - const vk = await compileVisualKeyboard(inputFilename, {...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false}); - assert.isNotNull(vk); - - // Use the builder to generate the binary output file - const writer = new KvkFileWriter(); - const code = writer.write(vk); - checkMessages(); - assert.isNotNull(code); - - // Compare output - let expected = await hextobin(binaryFilename, undefined, {silent:true}); - assert.deepEqual(code, expected); - }); -}); diff --git a/developer/src/kmc-ldml/test/test-visual-keyboard-compiler.ts b/developer/src/kmc-ldml/test/test-visual-keyboard-compiler.ts new file mode 100644 index 00000000000..25e3abb47bb --- /dev/null +++ b/developer/src/kmc-ldml/test/test-visual-keyboard-compiler.ts @@ -0,0 +1,236 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ +import 'mocha'; +import * as path from 'path'; +import {assert} from 'chai'; +import { stripIndent } from 'common-tags'; + +import { KMX, KvkFileWriter, VisualKeyboard } from '@keymanapp/common-types'; +import hextobin from '@keymanapp/hextobin'; + +import { checkMessages, compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './helpers/index.js'; + +import { LdmlKeyboardVisualKeyboardCompiler } from '../src/compiler/visual-keyboard-compiler.js'; +import { LDMLKeyboardXMLSourceFileReader } from '@keymanapp/developer-utils'; +import { LdmlKeyboardCompiler } from '../src/main.js'; + +describe('visual-keyboard-compiler', function() { + this.slow(500); // 0.5 sec -- json schema validation takes a while + + it('should build fixtures', async function() { + // Let's build basic.xml + + // It should match basic.kvk (built from basic-kvk.txt) + const inputFilename = makePathToFixture('basic.xml'); + const binaryFilename = makePathToFixture('basic-kvk.txt'); + + // Compile the visual keyboard + const k = new LdmlKeyboardCompiler(); + assert.isTrue(await k.init(compilerTestCallbacks, {...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false})); + const source = k.load(inputFilename); + checkMessages(); + assert.isNotNull(source, 'k.load should not have returned null'); + + const valid = await k.validate(source); + checkMessages(); + assert.isTrue(valid, 'k.validate should not have failed'); + + let kmx = await k.compile(source); + assert(kmx, 'k.compile should not have failed'); + + const keyboardId = path.basename(inputFilename, '.xml'); + + const vk = (new LdmlKeyboardVisualKeyboardCompiler(compilerTestCallbacks)).compile(kmx.kmxplus, keyboardId); + checkMessages(); + assert.isNotNull(vk, 'LdmlKeyboardVisualKeyboardCompiler.compile should not have returned null'); + assert.isNotNull(kmx.keyboard.stores.find(store => + store.dwSystemID == KMX.KMXFile.TSS_VISUALKEYBOARD && + store.dpString == keyboardId + '.kvk' + )); + assert(typeof vk == 'object'); + + // Use the builder to generate the binary output file + const writer = new KvkFileWriter(); + const code = writer.write(vk); + assert.isEmpty(compilerTestCallbacks.messages); + assert.isNotNull(code); + + // Compare output + let expected = await hextobin(binaryFilename, undefined, {silent:true}); + + assert.deepEqual(code, expected); + }); + + it('should support various modifiers', async function() { + const xml = stripIndent` + + + + + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.keys.length, 5); + assert.equal(vk.keys[0].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_NORMAL); + assert.equal(vk.keys[1].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT); + assert.equal(vk.keys[2].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT); + assert.equal(vk.keys[3].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_RCTRL | VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT); + assert.equal(vk.keys[4].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT | VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT); + }); + + it('should emit the correct associated keyboard id', async function() { + const xml = stripIndent` + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.header.associatedKeyboard, 'test'); + }); + + it('should support ', async function() { + const xml = stripIndent` + + + + + + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.keys.length, 2); + assert.equal(vk.keys[0].text, 'C'); + assert.equal(vk.keys[1].text, 'D'); + }); + + it('should correctly decode \\u{xxxx}', async function() { + const xml = stripIndent` + + + + + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.keys.length, 2); + assert.equal(vk.keys[0].text, '\u{0e80}'); + assert.equal(vk.keys[1].text, '\u{0e81}'); + }); + + it('should read string variables in key.output', async function() { + const xml = stripIndent` + + + + + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.keys.length, 1); + assert.equal(vk.keys[0].text, '2'); + }); + + it('should read string variables in display.display', async function() { + const xml = stripIndent` + + + + + + + + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.keys.length, 1); + assert.equal(vk.keys[0].text, '2'); + }); +}); + +async function loadVisualKeyboardFromXml(xml: string, id: string) { + const data = new TextEncoder().encode(xml); + assert.isOk(data); + + const reader = new LDMLKeyboardXMLSourceFileReader(compilerTestOptions.readerOptions, compilerTestCallbacks); + const source = reader.load(data); + assert.isEmpty(compilerTestCallbacks.messages); + assert.isOk(source); + + const k = new LdmlKeyboardCompiler(); + assert.isTrue(await k.init(compilerTestCallbacks, compilerTestOptions)); + + const kmx = await k.compile(source); + assert(kmx, 'k.compile should not have failed'); + + const vk = (new LdmlKeyboardVisualKeyboardCompiler(compilerTestCallbacks)).compile(kmx.kmxplus, id); + assert(typeof vk == 'object'); + assert.isEmpty(compilerTestCallbacks.messages); + assert.isOk(vk); + + return vk; +} diff --git a/developer/src/kmc-ldml/test/tsconfig.json b/developer/src/kmc-ldml/test/tsconfig.json index f47c5c5f71f..cda71853604 100644 --- a/developer/src/kmc-ldml/test/tsconfig.json +++ b/developer/src/kmc-ldml/test/tsconfig.json @@ -7,12 +7,6 @@ "outDir": "../build/test", "baseUrl": ".", "strictNullChecks": false, // TODO: get rid of this as some point - "paths": { - // "@keymanapp/keyman-version": ["../../../common/web/keyman-version/keyman-version.mts"], - "@keymanapp/common-types": ["../../../../common/web/types/src/main"], - "@keymanapp/developer-test-helpers": ["../../common/web/test-helpers/index"], - // "@keymanapp/": ["core/include/ldml/ldml-keyboard-constants"], - }, }, "include": [ "**/test-*.ts", diff --git a/developer/src/kmc-ldml/tsconfig.json b/developer/src/kmc-ldml/tsconfig.json index 09c2df59b7e..36d915f0749 100644 --- a/developer/src/kmc-ldml/tsconfig.json +++ b/developer/src/kmc-ldml/tsconfig.json @@ -5,14 +5,6 @@ "outDir": "build/src/", "rootDir": "src/", "baseUrl": ".", - - "paths": { - // "@keymanapp/keyman-version": ["../../../common/web/keyman-version/keyman-version.mts"], - "@keymanapp/common-types": ["../../../common/web/types/src/main"], - "@keymanapp/kmc-kmn": ["../kmc-kmn/src/main"], - // "@keymanapp/": ["core/include/ldml/ldml-keyboard-constants"], - }, - }, "include": [ "src/**/*.ts" diff --git a/developer/src/kmc-model-info/.eslintrc.cjs b/developer/src/kmc-model-info/.eslintrc.cjs index d37a4610738..494ae6b00ab 100644 --- a/developer/src/kmc-model-info/.eslintrc.cjs +++ b/developer/src/kmc-model-info/.eslintrc.cjs @@ -8,7 +8,7 @@ module.exports = { overrides: [ { files: "src/**/*.ts", - extends: ["../../../common/web/eslint/eslintNoNodeImports.js"], + extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } ], rules: { diff --git a/developer/src/kmc-model-info/build.sh b/developer/src/kmc-model-info/build.sh index 334bfbd07bd..b1ba95c34b8 100755 --- a/developer/src/kmc-model-info/build.sh +++ b/developer/src/kmc-model-info/build.sh @@ -9,7 +9,6 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" builder_describe "Build Keyman kmc Lexical Model model-info Compiler module" \ "@/common/web/types" \ - "@/common/models/types" \ "clean" \ "configure" \ "build" \ diff --git a/developer/src/kmc-model-info/package.json b/developer/src/kmc-model-info/package.json index fdc80821716..20be72d1899 100644 --- a/developer/src/kmc-model-info/package.json +++ b/developer/src/kmc-model-info/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@keymanapp/common-types": "*", - "@keymanapp/models-types": "*", + "@keymanapp/common-types": "*", "@keymanapp/developer-utils": "*" }, "devDependencies": { @@ -40,8 +40,7 @@ "c8": "^7.12.0", "chalk": "^2.4.2", "mocha": "^8.4.0", - "ts-node": "^9.1.1", - "typescript": "^4.9.5" + "typescript": "^5.4.5" }, "mocha": { "spec": "build/test/**/test-*.js", diff --git a/developer/src/kmc-model-info/src/model-info-compiler-messages.ts b/developer/src/kmc-model-info/src/model-info-compiler-messages.ts index 9a1c5fdbdcd..cbf6b57eb90 100644 --- a/developer/src/kmc-model-info/src/model-info-compiler-messages.ts +++ b/developer/src/kmc-model-info/src/model-info-compiler-messages.ts @@ -1,4 +1,4 @@ -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/common-types"; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/developer-utils"; const Namespace = CompilerErrorNamespace.ModelInfoCompiler; // const SevInfo = CompilerErrorSeverity.Info | Namespace; @@ -44,5 +44,10 @@ export class ModelInfoCompilerMessages { static ERROR_NoLicenseFound = SevError | 0x0009; static Error_NoLicenseFound = () => m(this.ERROR_NoLicenseFound, `No license for the model was found. MIT license is required for publication to Keyman lexical-models repository.`); + + static ERROR_DescriptionIsMissing = SevError | 0x000A; + static Error_DescriptionIsMissing = (o:{filename:string}) => m( + this.ERROR_DescriptionIsMissing, + `The Info.Description field in the package ${def(o.filename)} is required, but is missing or empty.`); } diff --git a/developer/src/kmc-model-info/src/model-info-compiler.ts b/developer/src/kmc-model-info/src/model-info-compiler.ts index b6a4af30643..a5fb62cbcda 100644 --- a/developer/src/kmc-model-info/src/model-info-compiler.ts +++ b/developer/src/kmc-model-info/src/model-info-compiler.ts @@ -5,9 +5,9 @@ import { minKeymanVersion } from "./min-keyman-version.js"; import { ModelInfoFile } from "./model-info-file.js"; -import { CompilerCallbacks, CompilerOptions, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KmpJsonFile } from "@keymanapp/common-types"; +import { KmpJsonFile } from "@keymanapp/common-types"; import { ModelInfoCompilerMessages } from "./model-info-compiler-messages.js"; -import { KeymanUrls, validateMITLicense } from "@keymanapp/developer-utils"; +import { CompilerCallbacks, CompilerOptions, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult,KeymanUrls, isValidEmail, validateMITLicense } from "@keymanapp/developer-utils"; /* c8 ignore start */ /** @@ -192,6 +192,11 @@ export class ModelInfoCompiler implements KeymanCompiler { return null; } + if(!isValidEmail(match[2])) { + this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_InvalidAuthorEmail({email:author.url})); + return null; + } + model_info.authorEmail = match[2]; } @@ -199,6 +204,9 @@ export class ModelInfoCompiler implements KeymanCompiler { if(sources.kmpJsonData.info.description?.description) { model_info.description = sources.kmpJsonData.info.description.description.trim(); + } else { + this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_DescriptionIsMissing({filename:sources.kpsFilename})); + return null; } // isRTL -- this is a little bit of a heuristic from a compiled .js diff --git a/developer/src/kmc-model-info/test/test-model-info-compiler-messages.ts b/developer/src/kmc-model-info/test/test-model-info-compiler-messages.ts index 4dd1de538f3..062fc9f3c01 100644 --- a/developer/src/kmc-model-info/test/test-model-info-compiler-messages.ts +++ b/developer/src/kmc-model-info/test/test-model-info-compiler-messages.ts @@ -1,7 +1,7 @@ import 'mocha'; import { ModelInfoCompilerMessages } from '../src/model-info-compiler-messages.js'; import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; -import { CompilerErrorNamespace } from '@keymanapp/common-types'; +import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; describe('ModelInfoCompilerMessages', function () { it('should have a valid ModelInfoCompilerMessages object', function() { diff --git a/developer/src/kmc-model-info/test/tsconfig.json b/developer/src/kmc-model-info/test/tsconfig.json index 6d48ad864d2..e16b935aa47 100644 --- a/developer/src/kmc-model-info/test/tsconfig.json +++ b/developer/src/kmc-model-info/test/tsconfig.json @@ -8,10 +8,6 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "baseUrl": ".", - "paths": { - "@keymanapp/developer-test-helpers": ["../../common/web/test-helpers/index"], - "@keymanapp/kmc-package": ["../../kmc-package/source"], - }, }, "include": [ "**/test-*.ts", diff --git a/developer/src/kmc-model-info/tsconfig.json b/developer/src/kmc-model-info/tsconfig.json index 6cb999c0a8d..e4d68f11bfd 100644 --- a/developer/src/kmc-model-info/tsconfig.json +++ b/developer/src/kmc-model-info/tsconfig.json @@ -4,17 +4,12 @@ "compilerOptions": { "outDir": "build/src/", "rootDir": "src/", - "paths": { - "@keymanapp/common-types": ["../../../common/web/types/src/main"], - "@keymanapp/developer-utils": ["../common/web/utils/index"], - } }, "include": [ "src/**/*.ts" ], "references": [ { "path": "../../../common/web/types" }, - { "path": "../../../common/models/types" }, { "path": "../common/web/utils" }, ] } diff --git a/developer/src/kmc-model/.eslintrc.cjs b/developer/src/kmc-model/.eslintrc.cjs index 3d339676e2e..efa3d1a96a4 100644 --- a/developer/src/kmc-model/.eslintrc.cjs +++ b/developer/src/kmc-model/.eslintrc.cjs @@ -9,7 +9,7 @@ module.exports = { overrides: [ { files: "src/**/*.ts", - extends: ["../../../common/web/eslint/eslintNoNodeImports.js"], + extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], }, ], rules: { diff --git a/developer/src/kmc-model/build.sh b/developer/src/kmc-model/build.sh index 36b9f74e3e2..372a145312a 100755 --- a/developer/src/kmc-model/build.sh +++ b/developer/src/kmc-model/build.sh @@ -8,12 +8,10 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" . "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" -# TODO: "@/common/models/types" \ - builder_describe "Keyman kmc Lexical Model Compiler module" \ "@/common/web/keyman-version" \ "@/developer/src/common/web/test-helpers" \ - "@/common/models/templates test" \ + "@/web/src/engine/predictive-text/templates/ test" \ "clean" \ "configure" \ "build" \ @@ -33,6 +31,7 @@ builder_parse "$@" #------------------------------------------------------------------------------------------------------------------- function do_build() { + tsc -b ./tools/tsconfig.json npm run build } diff --git a/developer/src/kmc-model/package.json b/developer/src/kmc-model/package.json index 831677a1ac8..9f95de51f8d 100644 --- a/developer/src/kmc-model/package.json +++ b/developer/src/kmc-model/package.json @@ -32,8 +32,8 @@ "dependencies": { "@keymanapp/common-types": "*", "@keymanapp/keyman-version": "*", - "@keymanapp/models-types": "*", - "typescript": "^4.9.5" + "@keymanapp/common-types": "*", + "typescript": "^5.4.5" }, "devDependencies": { "@keymanapp/developer-test-helpers": "*", @@ -42,9 +42,8 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "esbuild": "^0.15.7", - "mocha": "^10.0.0", - "ts-node": "^10.9.1" + "esbuild": "^0.18.9", + "mocha": "^10.0.0" }, "mocha": { "spec": "build/test/**/test-*.js", diff --git a/developer/src/kmc-model/src/compiler-callbacks.ts b/developer/src/kmc-model/src/compiler-callbacks.ts index a077035003f..401a68f5683 100644 --- a/developer/src/kmc-model/src/compiler-callbacks.ts +++ b/developer/src/kmc-model/src/compiler-callbacks.ts @@ -1,4 +1,4 @@ -import { CompilerCallbacks } from "@keymanapp/common-types"; +import { CompilerCallbacks } from "@keymanapp/developer-utils"; export let callbacks: CompilerCallbacks; diff --git a/developer/src/kmc-model/src/join-word-breaker-decorator.ts b/developer/src/kmc-model/src/join-word-breaker-decorator.ts index 6d0426938a9..dec58d583ab 100644 --- a/developer/src/kmc-model/src/join-word-breaker-decorator.ts +++ b/developer/src/kmc-model/src/join-word-breaker-decorator.ts @@ -1,4 +1,4 @@ -/// +import { Span, WordBreakingFunction } from '@keymanapp/common-types'; /** * Returns a word breaker that joins spans of an existing word breaker. diff --git a/developer/src/kmc-model/src/lexical-model-compiler.ts b/developer/src/kmc-model/src/lexical-model-compiler.ts index 9add61ee238..e1071512a9d 100644 --- a/developer/src/kmc-model/src/lexical-model-compiler.ts +++ b/developer/src/kmc-model/src/lexical-model-compiler.ts @@ -10,7 +10,7 @@ import {decorateWithScriptOverrides} from "./script-overrides-decorator.js"; import { LexicalModelSource, WordBreakerSpec, SimpleWordBreakerSpec } from "./lexical-model.js"; import { ModelCompilerError, ModelCompilerMessageContext, ModelCompilerMessages } from "./model-compiler-messages.js"; import { callbacks, setCompilerCallbacks } from "./compiler-callbacks.js"; -import { CompilerCallbacks, CompilerOptions, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult } from "@keymanapp/common-types"; +import { CompilerCallbacks, CompilerOptions, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult } from "@keymanapp/developer-utils"; /** * An ECMAScript module as emitted by the TypeScript compiler. diff --git a/developer/src/kmc-model/src/lexical-model.ts b/developer/src/kmc-model/src/lexical-model.ts index 32d5bf5775f..f0fd95c19e6 100644 --- a/developer/src/kmc-model/src/lexical-model.ts +++ b/developer/src/kmc-model/src/lexical-model.ts @@ -3,6 +3,8 @@ * the LMLayer's internal worker code, so we provide those definitions too. */ +import { CasingFunction, LexicalModelPunctuation, WordBreakingFunction } from '@keymanapp/common-types'; + export interface LexicalModelDeclaration { readonly format: 'trie-1.0'|'fst-foma-1.0'|'custom-1.0', //... metadata ... diff --git a/developer/src/kmc-model/src/model-compiler-messages.ts b/developer/src/kmc-model/src/model-compiler-messages.ts index 26126b5d139..51e4042ea8b 100644 --- a/developer/src/kmc-model/src/model-compiler-messages.ts +++ b/developer/src/kmc-model/src/model-compiler-messages.ts @@ -1,4 +1,4 @@ -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/common-types"; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/developer-utils"; const Namespace = CompilerErrorNamespace.ModelCompiler; // const SevInfo = CompilerErrorSeverity.Info | Namespace; diff --git a/developer/src/kmc-model/src/model-defaults.ts b/developer/src/kmc-model/src/model-defaults.ts index 541cadcb497..4d6c40d3a11 100644 --- a/developer/src/kmc-model/src/model-defaults.ts +++ b/developer/src/kmc-model/src/model-defaults.ts @@ -1,3 +1,5 @@ +import { CasingForm, CasingFunction } from '@keymanapp/common-types'; + /** * Converts wordforms into an indexable form. It does this by * normalizing the letter case of characters INDIVIDUALLY (to disregard @@ -104,4 +106,4 @@ export function defaultApplyCasing(casing: CasingForm, text: string): string { return text.substring(0, headUnitLength).toUpperCase() // head - uppercased .concat(text.substring(headUnitLength)); // tail - lowercased } -} \ No newline at end of file +} diff --git a/developer/src/kmc-model/src/model-definitions.ts b/developer/src/kmc-model/src/model-definitions.ts index d05a8254ce5..ccef469c91e 100644 --- a/developer/src/kmc-model/src/model-definitions.ts +++ b/developer/src/kmc-model/src/model-definitions.ts @@ -5,6 +5,7 @@ import { defaultApplyCasing, import KEYMAN_VERSION from "@keymanapp/keyman-version"; import { LexicalModelSource, WordformToKeySpec } from "./lexical-model.js"; +import { CasingForm, CasingFunction } from '@keymanapp/common-types'; /** * Processes certain defined model behaviors in such a way that the needed closures @@ -219,4 +220,4 @@ export class ModelDefinitions { } // Because it references the class field, this line must come afterward. -const PSEUDOCLOSURE = ModelDefinitions.COMPILED_NAME; \ No newline at end of file +const PSEUDOCLOSURE = ModelDefinitions.COMPILED_NAME; diff --git a/developer/src/kmc-model/src/script-overrides-decorator.ts b/developer/src/kmc-model/src/script-overrides-decorator.ts index 544f1b03869..ebf08b819e2 100644 --- a/developer/src/kmc-model/src/script-overrides-decorator.ts +++ b/developer/src/kmc-model/src/script-overrides-decorator.ts @@ -1,3 +1,4 @@ +import { Span, WordBreakingFunction } from '@keymanapp/common-types'; import { OverrideScriptDefaults } from "./lexical-model.js"; import { ModelCompilerError, ModelCompilerMessages } from "./model-compiler-messages.js"; diff --git a/developer/src/kmc-model/test/helpers/index.ts b/developer/src/kmc-model/test/helpers/index.ts index e7d274f5712..d58e0834d11 100644 --- a/developer/src/kmc-model/test/helpers/index.ts +++ b/developer/src/kmc-model/test/helpers/index.ts @@ -1,5 +1,3 @@ -/// - /** * Helpers and utilities for the Mocha tests. */ diff --git a/developer/src/kmc-model/test/test-compile-model-with-pseudoclosure.ts b/developer/src/kmc-model/test/test-compile-model-with-pseudoclosure.ts index 08247e17c1d..049776ec201 100644 --- a/developer/src/kmc-model/test/test-compile-model-with-pseudoclosure.ts +++ b/developer/src/kmc-model/test/test-compile-model-with-pseudoclosure.ts @@ -4,6 +4,7 @@ import 'mocha'; import { makePathToFixture, compileModelSourceCode } from './helpers/index.js'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; +import { CasingForm, CasingFunction } from '@keymanapp/common-types'; describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { const callbacks = new TestCompilerCallbacks(); diff --git a/developer/src/kmc-model/test/test-compile-model.ts b/developer/src/kmc-model/test/test-compile-model.ts index e3074d697ac..7de9d6d5dac 100644 --- a/developer/src/kmc-model/test/test-compile-model.ts +++ b/developer/src/kmc-model/test/test-compile-model.ts @@ -9,6 +9,8 @@ import { KeymanFileTypes } from '@keymanapp/common-types'; describe('LexicalModelCompiler', function () { let callbacks = new TestCompilerCallbacks(); + this.timeout(5000); + // Try to compile ALL of the correct models. const MODELS = [ 'example.qaa.sencoten', diff --git a/developer/src/kmc-model/test/test-compile-trie.ts b/developer/src/kmc-model/test/test-compile-trie.ts index a9829568370..845022cef5c 100644 --- a/developer/src/kmc-model/test/test-compile-trie.ts +++ b/developer/src/kmc-model/test/test-compile-trie.ts @@ -8,6 +8,7 @@ import { createTrieDataStructure } from '../src/build-trie.js'; import { ModelCompilerError } from '../src/model-compiler-messages.js'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { TrieModel } from '@keymanapp/models-templates'; +import { Span } from '@keymanapp/common-types'; describe('LexicalModelCompiler', function () { const callbacks = new TestCompilerCallbacks(); diff --git a/developer/src/kmc-model/test/test-default-search-term-to-key.ts b/developer/src/kmc-model/test/test-default-search-term-to-key.ts index b777c7c79c3..0f8120e6bae 100644 --- a/developer/src/kmc-model/test/test-default-search-term-to-key.ts +++ b/developer/src/kmc-model/test/test-default-search-term-to-key.ts @@ -4,6 +4,7 @@ import {assert} from 'chai'; import { defaultSearchTermToKey, defaultCasedSearchTermToKey, defaultApplyCasing } from '../src/model-defaults.js'; +import { CasingForm, CasingFunction } from '@keymanapp/common-types'; describe('The default searchTermToKey() function', function () { diff --git a/developer/src/kmc-model/test/test-join-word-breaker.ts b/developer/src/kmc-model/test/test-join-word-breaker.ts index b08411cb561..ff220088f5c 100644 --- a/developer/src/kmc-model/test/test-join-word-breaker.ts +++ b/developer/src/kmc-model/test/test-join-word-breaker.ts @@ -1,6 +1,7 @@ import { assert } from "chai"; import defaultWordBreaker from '@keymanapp/models-wordbreakers'; import {decorateWithJoin} from '../src/join-word-breaker-decorator.js'; +import { Span } from '@keymanapp/common-types'; describe('The join word breaker decorator', function () { it('should decorate an existing word breaker', function () { diff --git a/developer/src/kmc-model/test/test-messages.ts b/developer/src/kmc-model/test/test-messages.ts index df584d80d1e..2f87dfd7181 100644 --- a/developer/src/kmc-model/test/test-messages.ts +++ b/developer/src/kmc-model/test/test-messages.ts @@ -1,7 +1,7 @@ import 'mocha'; import { ModelCompilerMessages } from '../src/model-compiler-messages.js'; import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; -import { CompilerErrorNamespace } from '@keymanapp/common-types'; +import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; describe('ModelCompilerMessages', function () { it('should have a valid ModelCompilerMessages object', function() { diff --git a/developer/src/kmc-model/test/test-model-definitions.ts b/developer/src/kmc-model/test/test-model-definitions.ts index 297bb1a995b..ee569a01b1a 100644 --- a/developer/src/kmc-model/test/test-model-definitions.ts +++ b/developer/src/kmc-model/test/test-model-definitions.ts @@ -2,6 +2,7 @@ import 'mocha'; import { assert } from 'chai'; import { ModelDefinitions } from '../src/model-definitions.js'; import { LexicalModelSource } from '../src/lexical-model.js'; +import { CasingForm, CasingFunction } from '@keymanapp/common-types'; describe('Model definition pseudoclosures', function () { describe('14.0 defaults', function() { @@ -195,4 +196,4 @@ describe('Model definition pseudoclosures', function () { }); } }); -}); \ No newline at end of file +}); diff --git a/developer/src/kmc-model/test/test-override-script-defaults.ts b/developer/src/kmc-model/test/test-override-script-defaults.ts index a0500320fd9..b8afb18900b 100644 --- a/developer/src/kmc-model/test/test-override-script-defaults.ts +++ b/developer/src/kmc-model/test/test-override-script-defaults.ts @@ -1,6 +1,7 @@ import { assert } from "chai"; import defaultWordBreaker from '@keymanapp/models-wordbreakers'; import {decorateWithScriptOverrides} from '../src/script-overrides-decorator.js'; +import { Span } from '@keymanapp/common-types'; const THIN_SPACE = "\u2009"; diff --git a/developer/src/kmc-model/test/tsconfig.json b/developer/src/kmc-model/test/tsconfig.json index 0917ea186ae..1b1ca1668f6 100644 --- a/developer/src/kmc-model/test/tsconfig.json +++ b/developer/src/kmc-model/test/tsconfig.json @@ -8,9 +8,6 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "baseUrl": ".", - "paths": { - "@keymanapp/developer-test-helpers": ["../../common/web/test-helpers/index"], - }, }, "include": [ "**/test-*.ts", diff --git a/developer/src/kmc-model/tools/create-override-script-regexp.ts b/developer/src/kmc-model/tools/create-override-script-regexp.ts index 38604320583..38f6ade0a65 100644 --- a/developer/src/kmc-model/tools/create-override-script-regexp.ts +++ b/developer/src/kmc-model/tools/create-override-script-regexp.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env npx ts-node - /** * Prints a JavaScript regular expression suitable for use in the * **overrideScriptDefaults** word breaker decorator. @@ -67,7 +65,7 @@ export const HAS_SOUTHEAST_ASIAN_LETTER = /[${characterClasses}]/;`); ////////////////////////////////// Helpers /////////////////////////////////// function* unicodeData() { - let unicodeDataFile = readFileSync(path.join(UCD_DIR, "UnicodeData.txt"), "UTF-8"); + let unicodeDataFile = readFileSync(path.join(UCD_DIR, "UnicodeData.txt"), "utf8"); for (let line of unicodeDataFile.split("\n")) { if (line.trim() == "") { continue; @@ -82,7 +80,7 @@ function* unicodeData() { } function* blocks() { - let blocksFile = readFileSync(path.join(UCD_DIR, "Blocks.txt"), "UTF-8"); + let blocksFile = readFileSync(path.join(UCD_DIR, "Blocks.txt"), "utf8"); for (let line of blocksFile.split("\n")) { if (line.trim() === "") { continue; diff --git a/developer/src/kmc-model/tools/tsconfig.json b/developer/src/kmc-model/tools/tsconfig.json new file mode 100644 index 00000000000..e01c780bc40 --- /dev/null +++ b/developer/src/kmc-model/tools/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + + "compilerOptions": { + "outDir": "../build/tools/", + "rootDir": ".", + "baseUrl": "../.", + "allowSyntheticDefaultImports": true, + }, + "include": [ + "./*.ts" + ] +} diff --git a/developer/src/kmc-model/tsconfig.json b/developer/src/kmc-model/tsconfig.json index 724ebd74d0b..2400856d265 100644 --- a/developer/src/kmc-model/tsconfig.json +++ b/developer/src/kmc-model/tsconfig.json @@ -12,7 +12,6 @@ ], "references": [ { "path": "../../../common/web/keyman-version" }, - { "path": "../../../common/models/types" }, { "path": "../../../common/web/types" }, ] } diff --git a/developer/src/kmc-package/.eslintrc.cjs b/developer/src/kmc-package/.eslintrc.cjs index 09038ae9291..41a487fe41d 100644 --- a/developer/src/kmc-package/.eslintrc.cjs +++ b/developer/src/kmc-package/.eslintrc.cjs @@ -6,7 +6,7 @@ module.exports = { overrides: [ { files:"src/**/*.ts", - extends: ["../../../common/web/eslint/eslintNoNodeImports.js"], + extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } ], rules: { diff --git a/developer/src/kmc-package/build.sh b/developer/src/kmc-package/build.sh index 27c725a9633..0b2d7bda9b9 100755 --- a/developer/src/kmc-package/build.sh +++ b/developer/src/kmc-package/build.sh @@ -14,6 +14,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" builder_describe "Build Keyman kmc Package Compiler module" \ "@/common/web/keyman-version" \ "@/developer/src/common/web/test-helpers" \ + "@/developer/src/common/web/utils" \ "configure" \ "build" \ "api analyze API and prepare API documentation" \ diff --git a/developer/src/kmc-package/package.json b/developer/src/kmc-package/package.json index dd821e3008e..521ae02e44b 100644 --- a/developer/src/kmc-package/package.json +++ b/developer/src/kmc-package/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@keymanapp/common-types": "*", + "@keymanapp/developer-utils": "*", "jszip": "^3.7.0", "marked": "^7.0.0" }, @@ -40,8 +41,7 @@ "c8": "^7.12.0", "chalk": "^2.4.2", "mocha": "^8.4.0", - "ts-node": "^9.1.1", - "typescript": "^4.9.5" + "typescript": "^5.4.5" }, "mocha": { "spec": "build/test/**/test-*.js", diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index 0d4740de9e0..04688d0e5a2 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -1,9 +1,10 @@ -import { xml2js } from '@keymanapp/common-types'; +import { xml2js } from '@keymanapp/developer-utils'; import JSZip from 'jszip'; import KEYMAN_VERSION from "@keymanapp/keyman-version"; -import { KmpJsonFile, KpsFile, SchemaValidators, CompilerCallbacks, KeymanFileTypes, KvkFile, KeymanCompiler, CompilerOptions, KeymanCompilerResult, KeymanCompilerArtifacts, KeymanCompilerArtifact } from '@keymanapp/common-types'; -import { CompilerMessages } from './package-compiler-messages.js'; +import { KmpJsonFile, SchemaValidators, KeymanFileTypes, KvkFile } from '@keymanapp/common-types'; +import { CompilerCallbacks, KpsFile, KeymanCompiler, CompilerOptions, KeymanCompilerResult, KeymanCompilerArtifacts, KeymanCompilerArtifact } from '@keymanapp/developer-utils'; +import { PackageCompilerMessages } from './package-compiler-messages.js'; import { PackageMetadataCollector } from './package-metadata-collector.js'; import { KmpInfWriter } from './kmp-inf-writer.js'; import { transcodeToCP1252 } from './cp1252.js'; @@ -172,7 +173,7 @@ export class KmpCompiler implements KeymanCompiler { // Load the KPS data from XML as JS structured data. const buffer = this.callbacks.loadFile(kpsFilename); if(!buffer) { - this.callbacks.reportMessage(CompilerMessages.Error_FileDoesNotExist({filename: kpsFilename})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: kpsFilename})); return null; } const data = new TextDecoder().decode(buffer); @@ -186,7 +187,7 @@ export class KmpCompiler implements KeymanCompiler { try { parser.parseString(data, (e: unknown, r: unknown) => { if(e) throw e; a = r as KpsFile.KpsPackage }); } catch(e) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidPackageFile({e})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_InvalidPackageFile({e})); } return a; })(); @@ -277,7 +278,7 @@ export class KmpCompiler implements KeymanCompiler { if(!kmp.files.reduce((result: boolean, file) => { if(!file.name) { // as the filename field is missing or blank, we'll try with the description instead - this.callbacks.reportMessage(CompilerMessages.Error_FileRecordIsMissingName({description: file.description ?? '(no description)'})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_FileRecordIsMissingName({description: file.description ?? '(no description)'})); return false; } return result; @@ -509,14 +510,14 @@ export class KmpCompiler implements KeymanCompiler { if(this.callbacks.path.isAbsolute(filename)) { // absolute paths are not portable to other computers - this.callbacks.reportMessage(CompilerMessages.Warn_AbsolutePath({filename: filename})); + this.callbacks.reportMessage(PackageCompilerMessages.Warn_AbsolutePath({filename: filename})); } filename = this.callbacks.resolveFilename(kpsFilename, filename); const basename = this.callbacks.path.basename(filename); if(!this.callbacks.fs.existsSync(filename)) { - this.callbacks.reportMessage(CompilerMessages.Error_FileDoesNotExist({filename: filename})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: filename})); failed = true; return; } @@ -525,7 +526,7 @@ export class KmpCompiler implements KeymanCompiler { try { memberFileData = this.callbacks.loadFile(filename); } catch(e) { - this.callbacks.reportMessage(CompilerMessages.Error_FileCouldNotBeRead({filename: filename, e: e})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_FileCouldNotBeRead({filename: filename, e: e})); failed = true; return; } @@ -605,7 +606,7 @@ export class KmpCompiler implements KeymanCompiler { 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(CompilerMessages.Warn_FileIsNotABinaryKvkFile({filename: filename})); + this.callbacks.reportMessage(PackageCompilerMessages.Warn_FileIsNotABinaryKvkFile({filename: filename})); } } } diff --git a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts index 1b286686ed9..00d8a380b5e 100644 --- a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts +++ b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts @@ -1,4 +1,4 @@ -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/common-types"; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/developer-utils"; const Namespace = CompilerErrorNamespace.PackageCompiler; const SevInfo = CompilerErrorSeverity.Info | Namespace; @@ -10,7 +10,7 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace; /** * @internal */ -export class CompilerMessages { +export class PackageCompilerMessages { static FATAL_UnexpectedException = SevFatal | 0x0001; static Fatal_UnexpectedException = (o:{e: any}) => CompilerMessageSpecWithException(this.FATAL_UnexpectedException, null, o.e ?? 'unknown error'); @@ -134,5 +134,15 @@ export class CompilerMessages { static ERROR_FileRecordIsMissingName = SevError | 0x001F; static Error_FileRecordIsMissingName = (o:{description:string}) => m(this.ERROR_FileRecordIsMissingName, `File record in the package with description '${o.description}' is missing a filename.`); + + static ERROR_InvalidAuthorEmail = SevError | 0x0020; + static Error_InvalidAuthorEmail = (o:{email:string}) => m(this.ERROR_InvalidAuthorEmail, + `Invalid author email: ${def(o.email)}`); + + static ERROR_PackageFileHasEmptyVersion = SevError | 0x0021; + static Error_PackageFileHasEmptyVersion = () => m( + this.ERROR_PackageFileHasEmptyVersion, + `Package version is not following keyboard version, but the package version field is blank.` + ); } diff --git a/developer/src/kmc-package/src/compiler/package-keyboard-target-validator.ts b/developer/src/kmc-package/src/compiler/package-keyboard-target-validator.ts index 01d2d3ca011..06f36c77275 100644 --- a/developer/src/kmc-package/src/compiler/package-keyboard-target-validator.ts +++ b/developer/src/kmc-package/src/compiler/package-keyboard-target-validator.ts @@ -1,5 +1,6 @@ -import { KeymanTargets, CompilerCallbacks, KmpJsonFile } from "@keymanapp/common-types"; -import { CompilerMessages } from "./package-compiler-messages.js"; +import { KeymanTargets, KmpJsonFile } from "@keymanapp/common-types"; +import { CompilerCallbacks } from "@keymanapp/developer-utils"; +import { PackageCompilerMessages } from "./package-compiler-messages.js"; import { KeyboardMetadataCollection } from "./package-metadata-collector.js"; @@ -15,7 +16,7 @@ export class PackageKeyboardTargetValidator { hasJS = this.verifyTargets(metadata[keyboard].keyboard, metadata[keyboard].data.targets, kmp); } if(hasJS && !metadata[keyboard].data.hasTouchLayout) { - this.callbacks.reportMessage(CompilerMessages.Hint_JsKeyboardFileHasNoTouchTargets({id: metadata[keyboard].keyboard.id})); + this.callbacks.reportMessage(PackageCompilerMessages.Hint_JsKeyboardFileHasNoTouchTargets({id: metadata[keyboard].keyboard.id})); } } } @@ -38,7 +39,7 @@ export class PackageKeyboardTargetValidator { if(targets.some(target => KeymanTargets.TouchKeymanTargets.includes(target))) { if(!kmp.files.find(file => this.callbacks.path.basename(file.name ?? '', '.js') == keyboard.id)) { // .js version of the keyboard is not found, warn - this.callbacks.reportMessage(CompilerMessages.Warn_JsKeyboardFileIsMissing({id: keyboard.id})); + this.callbacks.reportMessage(PackageCompilerMessages.Warn_JsKeyboardFileIsMissing({id: keyboard.id})); return false; } // A js file is included and targeted diff --git a/developer/src/kmc-package/src/compiler/package-metadata-collector.ts b/developer/src/kmc-package/src/compiler/package-metadata-collector.ts index 346e23a7278..89a8f35b09b 100644 --- a/developer/src/kmc-package/src/compiler/package-metadata-collector.ts +++ b/developer/src/kmc-package/src/compiler/package-metadata-collector.ts @@ -1,6 +1,7 @@ -import { KmpJsonFile, CompilerCallbacks, KmxFileReader, KmxFileReaderError, KMX, KeymanFileTypes } from '@keymanapp/common-types'; +import { KmpJsonFile, KmxFileReader, KmxFileReaderError, KMX, KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerCallbacks } from "@keymanapp/developer-utils"; import { getCompiledKmxKeyboardMetadata } from './kmx-keyboard-metadata.js'; -import { CompilerMessages } from './package-compiler-messages.js'; +import { PackageCompilerMessages } from './package-compiler-messages.js'; import { getCompiledWebKeyboardMetadata, WebKeyboardMetadata } from './web-keyboard-metadata.js'; export type KeyboardMetadata = { @@ -45,19 +46,19 @@ export class PackageMetadataCollector { isJavascript = true; file = kmp.files.find(file => this.callbacks.path.basename(file.name ?? '', KeymanFileTypes.Binary.WebKeyboard) == keyboard.id); if(!file) { - this.callbacks.reportMessage(CompilerMessages.Error_KeyboardContentFileNotFound({id:keyboard.id})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_KeyboardContentFileNotFound({id:keyboard.id})); return null; } } if(!file.name) { - this.callbacks.reportMessage(CompilerMessages.Error_FileRecordIsMissingName({description: file.description ?? '(no description)'})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_FileRecordIsMissingName({description: file.description ?? '(no description)'})); return null; } const filename = this.callbacks.resolveFilename(kpsFilename, file.name); if(!this.callbacks.fs.existsSync(filename)) { - this.callbacks.reportMessage(CompilerMessages.Error_KeyboardFileNotFound({filename})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_KeyboardFileNotFound({filename})); return null; } @@ -68,7 +69,7 @@ export class PackageMetadataCollector { try { fileData = this.callbacks.loadFile(filename); } catch(e) { - this.callbacks.reportMessage(CompilerMessages.Error_FileCouldNotBeRead({filename, e})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_FileCouldNotBeRead({filename, e})); return null; } @@ -83,7 +84,7 @@ export class PackageMetadataCollector { } catch(e) { if(e instanceof KmxFileReaderError) { // The file couldn't be read, it might not be a .kmx file - this.callbacks.reportMessage(CompilerMessages.Error_KeyboardFileNotValid({filename, e})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_KeyboardFileNotValid({filename, e})); return null; } else { // Unknown error, bubble it up diff --git a/developer/src/kmc-package/src/compiler/package-validation.ts b/developer/src/kmc-package/src/compiler/package-validation.ts index ce919d4a64c..a55610adad3 100644 --- a/developer/src/kmc-package/src/compiler/package-validation.ts +++ b/developer/src/kmc-package/src/compiler/package-validation.ts @@ -1,6 +1,7 @@ -import { KmpJsonFile, CompilerCallbacks, CompilerOptions, KeymanFileTypes } from '@keymanapp/common-types'; -import { CompilerMessages } from './package-compiler-messages.js'; +import { KmpJsonFile, KeymanFileTypes } from '@keymanapp/common-types'; +import { PackageCompilerMessages } from './package-compiler-messages.js'; import { keymanEngineForWindowsFiles, keymanForWindowsInstallerFiles, keymanForWindowsRedistFiles } from './redist-files.js'; +import { isValidEmail, CompilerCallbacks, CompilerOptions } from '@keymanapp/developer-utils'; // The keyboard ID SHOULD adhere to this pattern: const KEYBOARD_ID_PATTERN_PACKAGE = /^[a-z_][a-z0-9_]*\.(kps|kmp)$/; @@ -48,9 +49,9 @@ export class PackageValidation { if(languages.length == 0) { if(resourceType == 'keyboard') { - this.callbacks.reportMessage(CompilerMessages.Warn_KeyboardShouldHaveAtLeastOneLanguage({id})); + this.callbacks.reportMessage(PackageCompilerMessages.Warn_KeyboardShouldHaveAtLeastOneLanguage({id})); } else { - this.callbacks.reportMessage(CompilerMessages.Error_ModelMustHaveAtLeastOneLanguage({id})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_ModelMustHaveAtLeastOneLanguage({id})); return false; } } @@ -60,18 +61,18 @@ export class PackageValidation { try { locale = new Intl.Locale(lang.id); } catch(e: any) { - this.callbacks.reportMessage(CompilerMessages.Error_LanguageTagIsNotValid({resourceType, id, lang: lang.id, e})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_LanguageTagIsNotValid({resourceType, id, lang: lang.id, e})); return false; } const minimalTag = locale.minimize().toString(); if(minimalTag.toLowerCase() !== lang.id.toLowerCase()) { - this.callbacks.reportMessage(CompilerMessages.Hint_LanguageTagIsNotMinimal({resourceType, id, actual: lang.id, expected: minimalTag})); + this.callbacks.reportMessage(PackageCompilerMessages.Hint_LanguageTagIsNotMinimal({resourceType, id, actual: lang.id, expected: minimalTag})); } if(minimalTags[minimalTag]) { - this.callbacks.reportMessage(CompilerMessages.Hint_PackageShouldNotRepeatLanguages({resourceType, id, minimalTag, firstTag: lang.id, secondTag: minimalTags[minimalTag]})); + this.callbacks.reportMessage(PackageCompilerMessages.Hint_PackageShouldNotRepeatLanguages({resourceType, id, minimalTag, firstTag: lang.id, secondTag: minimalTags[minimalTag]})); } else { minimalTags[minimalTag] = lang.id; @@ -83,7 +84,7 @@ export class PackageValidation { private checkForModelsAndKeyboardsInSamePackage(kmpJson: KmpJsonFile.KmpJsonFile): boolean { if(kmpJson.lexicalModels?.length > 0 && kmpJson.keyboards?.length > 0) { - this.callbacks.reportMessage(CompilerMessages.Error_PackageCannotContainBothModelsAndKeyboards()); + this.callbacks.reportMessage(PackageCompilerMessages.Error_PackageCannotContainBothModelsAndKeyboards()); return false; } @@ -91,7 +92,7 @@ export class PackageValidation { // Note: we require at least 1 keyboard or model in the package. This may // change in the future if we start to use packages to distribute, e.g. // localizations or themes. - this.callbacks.reportMessage(CompilerMessages.Error_PackageMustContainAModelOrAKeyboard()); + this.callbacks.reportMessage(PackageCompilerMessages.Error_PackageMustContainAModelOrAKeyboard()); return false; } @@ -108,7 +109,7 @@ export class PackageValidation { filename = this.callbacks.path.basename(filename); if(!MODEL_ID_PATTERN_PACKAGE.test(filename)) { - this.callbacks.reportMessage(CompilerMessages.Warn_PackageNameDoesNotFollowLexicalModelConventions({filename})); + this.callbacks.reportMessage(PackageCompilerMessages.Warn_PackageNameDoesNotFollowLexicalModelConventions({filename})); } for(let model of kmpJson.lexicalModels) { @@ -128,7 +129,7 @@ export class PackageValidation { filename = this.callbacks.path.basename(filename); if(!KEYBOARD_ID_PATTERN_PACKAGE.test(filename)) { - this.callbacks.reportMessage(CompilerMessages.Warn_PackageNameDoesNotFollowKeyboardConventions({filename})); + this.callbacks.reportMessage(PackageCompilerMessages.Warn_PackageNameDoesNotFollowKeyboardConventions({filename})); } for(let keyboard of kmpJson.keyboards) { @@ -158,7 +159,7 @@ export class PackageValidation { const base = filename.substring(0, filename.length-ext.length); if(this.options.checkFilenameConventions) { if(!CONTENT_FILE_BASENAME_PATTERN.test(base) || !CONTENT_FILE_EXTENSION_PATTERN.test(ext)) { - this.callbacks.reportMessage(CompilerMessages.Warn_FileInPackageDoesNotFollowFilenameConventions({filename})); + this.callbacks.reportMessage(PackageCompilerMessages.Warn_FileInPackageDoesNotFollowFilenameConventions({filename})); } } @@ -177,13 +178,13 @@ export class PackageValidation { if(keymanForWindowsInstallerFiles.includes(filename) || keymanForWindowsRedistFiles.includes(filename) || keymanEngineForWindowsFiles.includes(filename)) { - this.callbacks.reportMessage(CompilerMessages.Warn_RedistFileShouldNotBeInPackage({filename})); + this.callbacks.reportMessage(PackageCompilerMessages.Warn_RedistFileShouldNotBeInPackage({filename})); } // # Test for inclusion of .doc or .docx files if(filename.match(/\.doc(x?)$/)) { - this.callbacks.reportMessage(CompilerMessages.Warn_DocFileDangerous({filename})); + this.callbacks.reportMessage(PackageCompilerMessages.Warn_DocFileDangerous({filename})); } // # Test for inclusion of keyboard source files @@ -196,7 +197,7 @@ export class PackageValidation { // which may be valid, not just LDML keyboards const fileType = KeymanFileTypes.sourceTypeFromFilename(file.name); if(fileType !== null && fileType !== KeymanFileTypes.Source.LdmlKeyboard) { - this.callbacks.reportMessage(CompilerMessages.Hint_PackageContainsSourceFile({filename: file.name})); + this.callbacks.reportMessage(PackageCompilerMessages.Hint_PackageContainsSourceFile({filename: file.name})); } return true; @@ -204,10 +205,25 @@ export class PackageValidation { private checkPackageInfo(file: KmpJsonFile.KmpJsonFile) { if(!file.info || !file.info.name || !file.info.name.description.trim()) { - this.callbacks.reportMessage(CompilerMessages.Error_PackageNameCannotBeBlank()); + this.callbacks.reportMessage(PackageCompilerMessages.Error_PackageNameCannotBeBlank()); return false; } + if(file.info?.author?.url) { + // we strip the mailto: from the .kps file for the .model_info + const match = file.info.author.url.match(/^(mailto\:)?(.+)$/); + /* c8 ignore next 3 */ + if (match === null) { + this.callbacks.reportMessage(PackageCompilerMessages.Error_InvalidAuthorEmail({email:file.info.author.url})); + return null; + } + if(!isValidEmail(match[2])) { + this.callbacks.reportMessage(PackageCompilerMessages.Error_InvalidAuthorEmail({email:file.info.author.url})); + return null; + } + + } + return true; } } diff --git a/developer/src/kmc-package/src/compiler/package-version-validator.ts b/developer/src/kmc-package/src/compiler/package-version-validator.ts index 430755434a1..b20e086aec7 100644 --- a/developer/src/kmc-package/src/compiler/package-version-validator.ts +++ b/developer/src/kmc-package/src/compiler/package-version-validator.ts @@ -1,6 +1,7 @@ -import { KmpJsonFile, CompilerCallbacks, KpsFile } from '@keymanapp/common-types'; -import { CompilerMessages } from './package-compiler-messages.js'; +import { KmpJsonFile } from '@keymanapp/common-types'; +import { PackageCompilerMessages } from './package-compiler-messages.js'; import { KeyboardMetadataCollection } from './package-metadata-collector.js'; +import { KpsFile, CompilerCallbacks } from '@keymanapp/developer-utils'; export const DEFAULT_KEYBOARD_VERSION = '1.0'; export const MIN_LM_FILEVERSION_KMP_JSON = '12.0'; @@ -41,6 +42,11 @@ export class PackageVersionValidator { if(!this.checkFollowKeyboardVersion(kmp)) { return false; } + } else { + if(!kmp.info.version) { + this.callbacks.reportMessage(PackageCompilerMessages.Error_PackageFileHasEmptyVersion()); + return false; + } } if(!kmp.keyboards) { @@ -65,13 +71,13 @@ export class PackageVersionValidator { // most up-to-date keyboard version data here, from the compiled keyboard. if(followKeyboardVersion && data.data.keyboardVersion === null) { - this.callbacks.reportMessage(CompilerMessages.Info_KeyboardFileHasNoKeyboardVersion({filename: keyboard.id})); + this.callbacks.reportMessage(PackageCompilerMessages.Info_KeyboardFileHasNoKeyboardVersion({filename: keyboard.id})); } keyboard.version = data.data.keyboardVersion ?? DEFAULT_KEYBOARD_VERSION; if(result && followKeyboardVersion) { if(kmp.keyboards[0].version !== keyboard.version) { - this.callbacks.reportMessage(CompilerMessages.Warn_KeyboardVersionsDoNotMatch({ + this.callbacks.reportMessage(PackageCompilerMessages.Warn_KeyboardVersionsDoNotMatch({ keyboard: keyboard.id, version: keyboard.version, firstKeyboard: kmp.keyboards[0].id, @@ -91,12 +97,12 @@ export class PackageVersionValidator { private checkFollowKeyboardVersion(kmp: KmpJsonFile.KmpJsonFile) { // Lexical model packages do not allow FollowKeyboardVersion if(kmp.lexicalModels && kmp.lexicalModels.length) { - this.callbacks.reportMessage(CompilerMessages.Error_FollowKeyboardVersionNotAllowedForModelPackages()); + this.callbacks.reportMessage(PackageCompilerMessages.Error_FollowKeyboardVersionNotAllowedForModelPackages()); return false; } if(!kmp.keyboards || !kmp.keyboards.length) { - this.callbacks.reportMessage(CompilerMessages.Error_FollowKeyboardVersionButNoKeyboards()); + this.callbacks.reportMessage(PackageCompilerMessages.Error_FollowKeyboardVersionButNoKeyboards()); return false; } diff --git a/developer/src/kmc-package/src/compiler/windows-package-installer-compiler.ts b/developer/src/kmc-package/src/compiler/windows-package-installer-compiler.ts index 49e0ca25f9a..3eefada8988 100644 --- a/developer/src/kmc-package/src/compiler/windows-package-installer-compiler.ts +++ b/developer/src/kmc-package/src/compiler/windows-package-installer-compiler.ts @@ -11,10 +11,11 @@ */ import JSZip from 'jszip'; -import { CompilerCallbacks, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanFileTypes, KmpJsonFile, KpsFile } from "@keymanapp/common-types"; +import { KeymanFileTypes, KmpJsonFile } from "@keymanapp/common-types"; +import { CompilerCallbacks, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KpsFile } from '@keymanapp/developer-utils'; import KEYMAN_VERSION from "@keymanapp/keyman-version"; import { KmpCompiler, KmpCompilerOptions } from "./kmp-compiler.js"; -import { CompilerMessages } from "./package-compiler-messages.js"; +import { PackageCompilerMessages } from "./package-compiler-messages.js"; const SETUP_INF_FILENAME = 'setup.inf'; const PRODUCT_NAME = 'Keyman'; @@ -120,7 +121,7 @@ export class WindowsPackageInstallerCompiler implements KeymanCompiler { // Check existence of required files for(const filename of [sources.licenseFilename, sources.msiFilename, sources.setupExeFilename]) { if(!this.callbacks.fs.existsSync(filename)) { - this.callbacks.reportMessage(CompilerMessages.Error_FileDoesNotExist({filename})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename})); return null; } } @@ -128,7 +129,7 @@ export class WindowsPackageInstallerCompiler implements KeymanCompiler { // Check existence of optional files for(const filename of [sources.titleImageFilename]) { if(filename && !this.callbacks.fs.existsSync(filename)) { - this.callbacks.reportMessage(CompilerMessages.Error_FileDoesNotExist({filename})); + this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename})); return null; } } @@ -175,7 +176,7 @@ export class WindowsPackageInstallerCompiler implements KeymanCompiler { private async buildZip(kps: KpsFile.KpsFile, kpsFilename: string, sources: WindowsPackageInstallerSources): Promise { const kmpJson: KmpJsonFile.KmpJsonFile = this.kmpCompiler.transformKpsFileToKmpObject(kpsFilename, kps); if(!kmpJson.info?.name?.description) { - this.callbacks.reportMessage(CompilerMessages.Error_PackageNameCannotBeBlank()); + this.callbacks.reportMessage(PackageCompilerMessages.Error_PackageNameCannotBeBlank()); return null; } diff --git a/developer/src/kmc-package/src/main.ts b/developer/src/kmc-package/src/main.ts index e8979d3d8e0..f191c6fe9e7 100644 --- a/developer/src/kmc-package/src/main.ts +++ b/developer/src/kmc-package/src/main.ts @@ -7,4 +7,4 @@ export { WindowsPackageInstallerCompilerResult, WindowsPackageInstallerCompilerArtifacts, } from "./compiler/windows-package-installer-compiler.js"; -export { CompilerMessages as PackageCompilerMessages } from './compiler/package-compiler-messages.js'; \ No newline at end of file +export { PackageCompilerMessages } from './compiler/package-compiler-messages.js'; \ No newline at end of file diff --git a/developer/src/kmc-package/test/fixtures/absolute_path/source/absolute_path.kps b/developer/src/kmc-package/test/fixtures/absolute_path/source/absolute_path.kps index e7c4a8b8512..b5a74c06ee1 100644 --- a/developer/src/kmc-package/test/fixtures/absolute_path/source/absolute_path.kps +++ b/developer/src/kmc-package/test/fixtures/absolute_path/source/absolute_path.kps @@ -17,6 +17,7 @@ Absolute Path + 1.0 diff --git a/developer/src/kmc-package/test/fixtures/invalid/error_invalid_author_email.kps b/developer/src/kmc-package/test/fixtures/invalid/error_invalid_author_email.kps new file mode 100644 index 00000000000..8d483052868 --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/invalid/error_invalid_author_email.kps @@ -0,0 +1,32 @@ + + + + 15.0.266.0 + 7.0 + + + + Invalid Email Address + © 2019 National Research Council Canada + Eddie Antonio Santos + 1.0 + + + + basic.kmx + Keyboard Basic + 0 + .kmx + + + + + Basic + basic + 1.0 + + Central Khmer (Khmer, Cambodia) + + + + diff --git a/developer/src/kmc-package/test/fixtures/invalid/error_invalid_author_email_multiple.kps b/developer/src/kmc-package/test/fixtures/invalid/error_invalid_author_email_multiple.kps new file mode 100644 index 00000000000..4e854025591 --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/invalid/error_invalid_author_email_multiple.kps @@ -0,0 +1,32 @@ + + + + 15.0.266.0 + 7.0 + + + + Invalid Email Address + © 2019 National Research Council Canada + Eddie Antonio Santos + 1.0 + + + + basic.kmx + Keyboard Basic + 0 + .kmx + + + + + Basic + basic + 1.0 + + Central Khmer (Khmer, Cambodia) + + + + diff --git a/developer/src/kmc-package/test/fixtures/invalid/error_package_file_has_empty_version.kps b/developer/src/kmc-package/test/fixtures/invalid/error_package_file_has_empty_version.kps new file mode 100644 index 00000000000..386b4e2e03e --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/invalid/error_package_file_has_empty_version.kps @@ -0,0 +1,32 @@ + + + + 15.0.266.0 + 7.0 + + + + Invalid Email Address + © 2019 National Research Council Canada + Eddie Antonio Santos + + + + + basic.kmx + Keyboard Basic + 0 + .kmx + + + + + Basic + basic + 1.0 + + Central Khmer (Khmer, Cambodia) + + + + diff --git a/developer/src/kmc-package/test/test-kmp-inf-writer.ts b/developer/src/kmc-package/test/test-kmp-inf-writer.ts index b49f39311e9..f852fc69b81 100644 --- a/developer/src/kmc-package/test/test-kmp-inf-writer.ts +++ b/developer/src/kmc-package/test/test-kmp-inf-writer.ts @@ -36,7 +36,7 @@ type InfFile = {[name:string]: Section}; * which never repeats */ function parseInf(inf: string): InfFile { - let items = inf.split('\r\n'); + let items = inf.replaceAll(/\r\n/g, '\n').split('\n'); let sections: InfFile = {}, newSection: Section = {}; sections[0] = newSection; for(let item of items) { diff --git a/developer/src/kmc-package/test/test-messages.ts b/developer/src/kmc-package/test/test-messages.ts index 082576eff04..2d8df44eb50 100644 --- a/developer/src/kmc-package/test/test-messages.ts +++ b/developer/src/kmc-package/test/test-messages.ts @@ -1,17 +1,17 @@ import 'mocha'; import { assert } from 'chai'; import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; -import { CompilerMessages } from '../src/compiler/package-compiler-messages.js'; +import { PackageCompilerMessages } from '../src/compiler/package-compiler-messages.js'; import { makePathToFixture } from './helpers/index.js'; import { KmpCompiler } from '../src/compiler/kmp-compiler.js'; -import { CompilerErrorNamespace, CompilerOptions } from '@keymanapp/common-types'; +import { CompilerErrorNamespace, CompilerOptions } from '@keymanapp/developer-utils'; const debug = false; const callbacks = new TestCompilerCallbacks(); -describe('CompilerMessages', function () { - it('should have a valid CompilerMessages object', function() { - return verifyCompilerMessagesObject(CompilerMessages, CompilerErrorNamespace.PackageCompiler); +describe('PackageCompilerMessages', function () { + it('should have a valid PackageCompilerMessages object', function() { + return verifyCompilerMessagesObject(PackageCompilerMessages, CompilerErrorNamespace.PackageCompiler); }); @@ -43,7 +43,7 @@ describe('CompilerMessages', function () { // WARN_FileIsNotABinaryKvkFile it('should generate WARN_FileIsNotABinaryKvkFile if a non-binary kvk file is included', async function() { - await testForMessage(this, ['xml_kvk_file', 'source', 'xml_kvk_file.kps'], CompilerMessages.WARN_FileIsNotABinaryKvkFile); + await testForMessage(this, ['xml_kvk_file', 'source', 'xml_kvk_file.kps'], PackageCompilerMessages.WARN_FileIsNotABinaryKvkFile); }); it('should not warn if a binary kvk file is included', async function() { @@ -53,70 +53,70 @@ describe('CompilerMessages', function () { // ERROR_FollowKeyboardVersionNotAllowedForModelPackages it('should generate ERROR_FollowKeyboardVersionNotAllowedForModelPackages if is set for model packages', async function() { - await testForMessage(this, ['invalid', 'followkeyboardversion.qaa.sencoten.model.kps'], CompilerMessages.ERROR_FollowKeyboardVersionNotAllowedForModelPackages); + await testForMessage(this, ['invalid', 'followkeyboardversion.qaa.sencoten.model.kps'], PackageCompilerMessages.ERROR_FollowKeyboardVersionNotAllowedForModelPackages); }); // ERROR_FollowKeyboardVersionButNoKeyboards it('should generate ERROR_FollowKeyboardVersionButNoKeyboards if is set for a package with no keyboards', async function() { - await testForMessage(this, ['invalid', 'followkeyboardversion.empty.kps'], CompilerMessages.ERROR_FollowKeyboardVersionButNoKeyboards); + await testForMessage(this, ['invalid', 'followkeyboardversion.empty.kps'], PackageCompilerMessages.ERROR_FollowKeyboardVersionButNoKeyboards); }); // ERROR_KeyboardContentFileNotFound it('should generate ERROR_KeyboardContentFileNotFound if a is listed in a package but not found in ', async function() { - await testForMessage(this, ['invalid', 'keyboardcontentfilenotfound.kps'], CompilerMessages.ERROR_KeyboardContentFileNotFound); + await testForMessage(this, ['invalid', 'keyboardcontentfilenotfound.kps'], PackageCompilerMessages.ERROR_KeyboardContentFileNotFound); }); // ERROR_KeyboardFileNotValid it('should generate ERROR_KeyboardFileNotValid if a .kmx is not valid in ', async function() { - await testForMessage(this, ['invalid', 'keyboardfilenotvalid.kps'], CompilerMessages.ERROR_KeyboardFileNotValid); + await testForMessage(this, ['invalid', 'keyboardfilenotvalid.kps'], PackageCompilerMessages.ERROR_KeyboardFileNotValid); }); // INFO_KeyboardFileHasNoKeyboardVersion it('should generate INFO_KeyboardFileHasNoKeyboardVersion if is set but keyboard has no version', async function() { - await testForMessage(this, ['invalid', 'nokeyboardversion.kps'], CompilerMessages.INFO_KeyboardFileHasNoKeyboardVersion); + await testForMessage(this, ['invalid', 'nokeyboardversion.kps'], PackageCompilerMessages.INFO_KeyboardFileHasNoKeyboardVersion); }); // ERROR_PackageCannotContainBothModelsAndKeyboards it('should generate ERROR_PackageCannotContainBothModelsAndKeyboards if package has both keyboards and models', async function() { - await testForMessage(this, ['invalid', 'error_package_cannot_contain_both_models_and_keyboards.kps'], CompilerMessages.ERROR_PackageCannotContainBothModelsAndKeyboards); + await testForMessage(this, ['invalid', 'error_package_cannot_contain_both_models_and_keyboards.kps'], PackageCompilerMessages.ERROR_PackageCannotContainBothModelsAndKeyboards); }); // HINT_PackageShouldNotRepeatLanguages (models) it('should generate HINT_PackageShouldNotRepeatLanguages if model has same language repeated', async function() { - await testForMessage(this, ['invalid', 'keyman.en.hint_package_should_not_repeat_languages.model.kps'], CompilerMessages.HINT_PackageShouldNotRepeatLanguages); + await testForMessage(this, ['invalid', 'keyman.en.hint_package_should_not_repeat_languages.model.kps'], PackageCompilerMessages.HINT_PackageShouldNotRepeatLanguages); }); // HINT_PackageShouldNotRepeatLanguages (keyboards) it('should generate HINT_PackageShouldNotRepeatLanguages if keyboard has same language repeated', async function() { - await testForMessage(this, ['invalid', 'hint_package_should_not_repeat_languages.kps'], CompilerMessages.HINT_PackageShouldNotRepeatLanguages); + await testForMessage(this, ['invalid', 'hint_package_should_not_repeat_languages.kps'], PackageCompilerMessages.HINT_PackageShouldNotRepeatLanguages); }); // WARN_PackageNameDoesNotFollowLexicalModelConventions it('should generate WARN_PackageNameDoesNotFollowLexicalModelConventions if filename has wrong conventions', async function() { - await testForMessage(this, ['invalid', 'WARN_PackageNameDoesNotFollowLexicalModelConventions.kps'], CompilerMessages.WARN_PackageNameDoesNotFollowLexicalModelConventions); + await testForMessage(this, ['invalid', 'WARN_PackageNameDoesNotFollowLexicalModelConventions.kps'], PackageCompilerMessages.WARN_PackageNameDoesNotFollowLexicalModelConventions); }); // WARN_PackageNameDoesNotFollowKeyboardConventions it('should generate WARN_PackageNameDoesNotFollowKeyboardConventions if filename has wrong conventions', async function() { - await testForMessage(this, ['invalid', 'WARN_PackageNameDoesNotFollowKeyboardConventions.kps'], CompilerMessages.WARN_PackageNameDoesNotFollowKeyboardConventions); + await testForMessage(this, ['invalid', 'WARN_PackageNameDoesNotFollowKeyboardConventions.kps'], PackageCompilerMessages.WARN_PackageNameDoesNotFollowKeyboardConventions); }); // WARN_FileInPackageDoesNotFollowFilenameConventions it('should generate WARN_FileInPackageDoesNotFollowFilenameConventions if content filename has wrong conventions', async function() { await testForMessage(this, ['invalid', 'warn_file_in_package_does_not_follow_filename_conventions.kps'], - CompilerMessages.WARN_FileInPackageDoesNotFollowFilenameConventions, {checkFilenameConventions: true}); + PackageCompilerMessages.WARN_FileInPackageDoesNotFollowFilenameConventions, {checkFilenameConventions: true}); await testForMessage(this, ['invalid', 'warn_file_in_package_does_not_follow_filename_conventions_2.kps'], - CompilerMessages.WARN_FileInPackageDoesNotFollowFilenameConventions, {checkFilenameConventions: true}); + PackageCompilerMessages.WARN_FileInPackageDoesNotFollowFilenameConventions, {checkFilenameConventions: true}); }); // Test the inverse -- no warning generated if checkFilenameConventions is false @@ -129,81 +129,81 @@ describe('CompilerMessages', function () { // ERROR_PackageNameCannotBeBlank it('should generate ERROR_PackageNameCannotBeBlank if package info has empty name', async function() { - await testForMessage(this, ['invalid', 'error_package_name_cannot_be_blank.kps'], CompilerMessages.ERROR_PackageNameCannotBeBlank); // blank field - await testForMessage(this, ['invalid', 'error_package_name_cannot_be_blank_2.kps'], CompilerMessages.ERROR_PackageNameCannotBeBlank); // missing field + await testForMessage(this, ['invalid', 'error_package_name_cannot_be_blank.kps'], PackageCompilerMessages.ERROR_PackageNameCannotBeBlank); // blank field + await testForMessage(this, ['invalid', 'error_package_name_cannot_be_blank_2.kps'], PackageCompilerMessages.ERROR_PackageNameCannotBeBlank); // missing field }); // ERROR_KeyboardFileNotFound it('should generate ERROR_KeyboardFileNotFound if a is listed in a package but not found in ', async function() { - await testForMessage(this, ['invalid', 'keyboardfilenotfound.kps'], CompilerMessages.ERROR_KeyboardFileNotFound); + await testForMessage(this, ['invalid', 'keyboardfilenotfound.kps'], PackageCompilerMessages.ERROR_KeyboardFileNotFound); }); // WARN_KeyboardVersionsDoNotMatch it('should generate WARN_KeyboardVersionsDoNotMatch if two have different versions', async function() { - await testForMessage(this, ['invalid', 'warn_keyboard_versions_do_not_match.kps'], CompilerMessages.WARN_KeyboardVersionsDoNotMatch); + await testForMessage(this, ['invalid', 'warn_keyboard_versions_do_not_match.kps'], PackageCompilerMessages.WARN_KeyboardVersionsDoNotMatch); }); // ERROR_LanguageTagIsNotValid it('should generate ERROR_LanguageTagIsNotValid if keyboard has an invalid language tag', async function() { - testForMessage(this, ['invalid', 'error_language_tag_is_not_valid.kps'], CompilerMessages.ERROR_LanguageTagIsNotValid); + testForMessage(this, ['invalid', 'error_language_tag_is_not_valid.kps'], PackageCompilerMessages.ERROR_LanguageTagIsNotValid); }); // HINT_LanguageTagIsNotMinimal it('should generate HINT_LanguageTagIsNotMinimal if keyboard has a non-minimal language tag', async function() { - await testForMessage(this, ['invalid', 'hint_language_tag_is_not_minimal.kps'], CompilerMessages.HINT_LanguageTagIsNotMinimal); + await testForMessage(this, ['invalid', 'hint_language_tag_is_not_minimal.kps'], PackageCompilerMessages.HINT_LanguageTagIsNotMinimal); }); // ERROR_ModelMustHaveAtLeastOneLanguage it('should generate ERROR_MustHaveAtLeastOneLanguage if model has zero language tags', async function() { await testForMessage(this, ['invalid', 'keyman.en.error_model_must_have_at_least_one_language.model.kps'], - CompilerMessages.ERROR_ModelMustHaveAtLeastOneLanguage); + PackageCompilerMessages.ERROR_ModelMustHaveAtLeastOneLanguage); }); // WARN_RedistFileShouldNotBeInPackage it('should generate WARN_RedistFileShouldNotBeInPackage if package contains a redist file', async function() { await testForMessage(this, ['invalid', 'warn_redist_file_should_not_be_in_package.kps'], - CompilerMessages.WARN_RedistFileShouldNotBeInPackage); + PackageCompilerMessages.WARN_RedistFileShouldNotBeInPackage); }); // WARN_DocFileDangerous it('should generate WARN_DocFileDangerous if package contains a .doc file', async function() { await testForMessage(this, ['invalid', 'warn_doc_file_dangerous.kps'], - CompilerMessages.WARN_DocFileDangerous); + PackageCompilerMessages.WARN_DocFileDangerous); }); // ERROR_PackageMustContainAPackageOrAKeyboard it('should generate ERROR_PackageMustContainAModelOrAKeyboard if package contains no keyboard or model', async function() { await testForMessage(this, ['invalid', 'error_package_must_contain_a_model_or_a_keyboard.kps'], - CompilerMessages.ERROR_PackageMustContainAModelOrAKeyboard); + PackageCompilerMessages.ERROR_PackageMustContainAModelOrAKeyboard); }); // WARN_JsKeyboardFileIsMissing it('should generate WARN_JsKeyboardFileIsMissing if package is missing corresponding .js for a touch .kmx', async function() { await testForMessage(this, ['invalid', 'warn_js_keyboard_file_is_missing.kps'], - CompilerMessages.WARN_JsKeyboardFileIsMissing); + PackageCompilerMessages.WARN_JsKeyboardFileIsMissing); }); // WARN_KeyboardShouldHaveAtLeastOneLanguage it('should generate WARN_KeyboardShouldHaveAtLeastOneLanguage if keyboard has zero language tags', async function() { await testForMessage(this, ['invalid', 'warn_keyboard_should_have_at_least_one_language.kps'], - CompilerMessages.WARN_KeyboardShouldHaveAtLeastOneLanguage); + PackageCompilerMessages.WARN_KeyboardShouldHaveAtLeastOneLanguage); }); // HINT_JsKeyboardFileHasNoTouchTargets it('should generate HINT_JsKeyboardFileHasNoTouchTargets if keyboard has no touch targets', async function() { await testForMessage(this, ['invalid', 'hint_js_keyboard_file_has_no_touch_targets.kps'], - CompilerMessages.HINT_JsKeyboardFileHasNoTouchTargets); + PackageCompilerMessages.HINT_JsKeyboardFileHasNoTouchTargets); }); it('should not generate HINT_JsKeyboardFileHasNoTouchTargets if keyboard has a touch target', async function() { @@ -214,14 +214,30 @@ describe('CompilerMessages', function () { it('should generate HINT_PackageContainsSourceFile if package contains a source file', async function() { await testForMessage(this, ['invalid', 'hint_source_file_should_not_be_in_package.kps'], - CompilerMessages.HINT_PackageContainsSourceFile); + PackageCompilerMessages.HINT_PackageContainsSourceFile); }); // ERROR_InvalidPackageFile it('should generate ERROR_InvalidPackageFile if package source file contains invalid XML', async function() { await testForMessage(this, ['invalid', 'error_invalid_package_file.kps'], - CompilerMessages.ERROR_InvalidPackageFile); + PackageCompilerMessages.ERROR_InvalidPackageFile); }); + // ERROR_InvalidAuthorEmail + + it('should generate ERROR_InvalidAuthorEmail if author email address has multiple addresses', async function() { + await testForMessage(this, ['invalid', 'error_invalid_author_email_multiple.kps'], + PackageCompilerMessages.ERROR_InvalidAuthorEmail); + }); + + it('should generate ERROR_InvalidAuthorEmail if author email address is formatted incorrectly', async function() { + await testForMessage(this, ['invalid', 'error_invalid_author_email.kps'], + PackageCompilerMessages.ERROR_InvalidAuthorEmail); + }); + + it('should generate ERROR_PackageFileHasEmptyVersion if FollowKeyboardVersion is not present and Version is empty', async function() { + await testForMessage(this, ['invalid', 'error_package_file_has_empty_version.kps'], + PackageCompilerMessages.ERROR_PackageFileHasEmptyVersion); + }); }); diff --git a/developer/src/kmc-package/test/test-package-compiler.ts b/developer/src/kmc-package/test/test-package-compiler.ts index 768bab4e443..7952e2145a2 100644 --- a/developer/src/kmc-package/test/test-package-compiler.ts +++ b/developer/src/kmc-package/test/test-package-compiler.ts @@ -11,7 +11,7 @@ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { makePathToFixture } from './helpers/index.js'; import { KmpCompiler } from '../src/compiler/kmp-compiler.js'; -import { CompilerMessages } from '../src/compiler/package-compiler-messages.js'; +import { PackageCompilerMessages } from '../src/compiler/package-compiler-messages.js'; const debug = false; @@ -209,13 +209,15 @@ describe('KmpCompiler', function () { kmpJson = kmpCompiler.transformKpsToKmpObject(kpsPath); }); + assert.isNotNull(kmpJson); + await assert.isNull(kmpCompiler.buildKmpFile(kpsPath, kmpJson)); if(debug) callbacks.printMessages(); assert.lengthOf(callbacks.messages, 2); - assert.deepEqual(callbacks.messages[0].code, CompilerMessages.WARN_AbsolutePath); - assert.deepEqual(callbacks.messages[1].code, CompilerMessages.ERROR_FileDoesNotExist); + assert.deepEqual(callbacks.messages[0].code, PackageCompilerMessages.WARN_AbsolutePath); + assert.deepEqual(callbacks.messages[1].code, PackageCompilerMessages.ERROR_FileDoesNotExist); }); // Testing path normalization @@ -268,7 +270,7 @@ describe('KmpCompiler', function () { it(`should load a package with missing keyboard ID metadata`, function () { const kmpJson = kmpCompiler.transformKpsToKmpObject(makePathToFixture('invalid', 'missing_keyboard_id.kps')); assert.isNull(kmpJson); // with a missing keyboard_id, the package shouldn't load, but it shouldn't crash either - assert.deepEqual(callbacks.messages[0].code, CompilerMessages.ERROR_KeyboardContentFileNotFound); + assert.deepEqual(callbacks.messages[0].code, PackageCompilerMessages.ERROR_KeyboardContentFileNotFound); }); diff --git a/developer/src/kmc-package/test/tsconfig.json b/developer/src/kmc-package/test/tsconfig.json index 05d10505504..62904114c8b 100644 --- a/developer/src/kmc-package/test/tsconfig.json +++ b/developer/src/kmc-package/test/tsconfig.json @@ -7,9 +7,6 @@ "outDir": "../build/test", "baseUrl": ".", "allowSyntheticDefaultImports": true, // for jszip - "paths": { - "@keymanapp/developer-test-helpers": ["../../common/web/test-helpers/index"], - }, }, "include": [ "**/test-*.ts", diff --git a/developer/src/kmc-package/tsconfig.json b/developer/src/kmc-package/tsconfig.json index 0ab78a91ffb..4fae2b81ec9 100644 --- a/developer/src/kmc-package/tsconfig.json +++ b/developer/src/kmc-package/tsconfig.json @@ -10,6 +10,7 @@ "src/**/*.ts", ], "references": [ + { "path": "../common/web/utils" }, { "path": "../../../common/web/keyman-version" }, { "path": "../../../common/web/types" }, ] diff --git a/developer/src/kmc/build.sh b/developer/src/kmc/build.sh index fda34667156..a39808a546f 100755 --- a/developer/src/kmc/build.sh +++ b/developer/src/kmc/build.sh @@ -16,6 +16,7 @@ builder_describe "Build Keyman Keyboard Compiler kmc" \ "@/common/include" \ "@/common/web/keyman-version" \ "@/common/web/types" \ + "@/core/include/ldml" \ "@/developer/src/common/web/utils" \ "@/developer/src/kmc-analyze" \ "@/developer/src/kmc-keyboard-info" \ @@ -61,7 +62,7 @@ function do_api() { function do_test() { eslint . tsc --build test/ - readonly C8_THRESHOLD=48 + readonly C8_THRESHOLD=50 c8 --reporter=lcov --reporter=text --lines $C8_THRESHOLD --statements $C8_THRESHOLD --branches $C8_THRESHOLD --functions $C8_THRESHOLD mocha builder_echo warning "Coverage thresholds are currently $C8_THRESHOLD%, which is lower than ideal." builder_echo warning "Please increase threshold in build.sh as test coverage improves." @@ -104,6 +105,7 @@ function do_bundle() { builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo builder_run_action configure verify_npm_setup builder_run_action build do_build +builder_run_action test do_test builder_run_action api do_api builder_run_action bundle do_bundle builder_run_action publish builder_publish_npm diff --git a/developer/src/kmc/package.json b/developer/src/kmc/package.json index d782e0f7d99..4d36bb1e98c 100644 --- a/developer/src/kmc/package.json +++ b/developer/src/kmc/package.json @@ -44,7 +44,7 @@ "@keymanapp/kmc-model": "*", "@keymanapp/kmc-model-info": "*", "@keymanapp/kmc-package": "*", - "@keymanapp/models-types": "*", + "@keymanapp/common-types": "*", "@sentry/node": "^7.57.0", "chalk": "^2.4.2", "commander": "^10.0.0", @@ -59,10 +59,9 @@ "@types/mocha": "^5.2.7", "@types/node": "^20.4.1", "c8": "^7.12.0", - "esbuild": "^0.15.8", + "esbuild": "^0.18.9", "mocha": "^8.4.0", - "ts-node": "^9.1.1", - "typescript": "^4.9.5" + "typescript": "^5.4.5" }, "mocha": { "spec": "build/test/**/test-*.js", diff --git a/developer/src/kmc/src/commands/analyze.ts b/developer/src/kmc/src/commands/analyze.ts index 3c53609179c..5085262f07b 100644 --- a/developer/src/kmc/src/commands/analyze.ts +++ b/developer/src/kmc/src/commands/analyze.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { Command, Option } from 'commander'; import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js'; import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; -import { CompilerCallbacks, CompilerLogLevel } from '@keymanapp/common-types'; +import { CompilerCallbacks, CompilerLogLevel } from '@keymanapp/developer-utils'; import { AnalyzeOskCharacterUse, AnalyzeOskRewritePua } from '@keymanapp/kmc-analyze'; import { BaseOptions } from '../util/baseOptions.js'; import { runOnFiles } from '../util/projectRunner.js'; diff --git a/developer/src/kmc/src/commands/build.ts b/developer/src/kmc/src/commands/build.ts index 96699970553..b8d5496c5bc 100644 --- a/developer/src/kmc/src/commands/build.ts +++ b/developer/src/kmc/src/commands/build.ts @@ -5,7 +5,8 @@ import { buildActivities } from './buildClasses/buildActivities.js'; import { BuildProject } from './buildClasses/BuildProject.js'; import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js'; import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; -import { CompilerFileCallbacks, CompilerOptions, KeymanFileTypes } from '@keymanapp/common-types'; +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerOptions, CompilerFileCallbacks } from '@keymanapp/developer-utils'; import { BaseOptions } from '../util/baseOptions.js'; import { expandFileLists } from '../util/fileLists.js'; import { isProject } from '../util/projectLoader.js'; diff --git a/developer/src/kmc/src/commands/buildClasses/BuildActivity.ts b/developer/src/kmc/src/commands/buildClasses/BuildActivity.ts index 864611c26d9..a8cde224bcf 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildActivity.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildActivity.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CompilerCallbacks, CompilerOptions, KeymanCompiler, KeymanFileTypes } from "@keymanapp/common-types"; +import { KeymanFileTypes } from "@keymanapp/common-types"; +import { CompilerCallbacks, CompilerOptions, KeymanCompiler } from "@keymanapp/developer-utils"; import { InfrastructureMessages } from '../../messages/infrastructureMessages.js'; export abstract class BuildActivity { diff --git a/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts b/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts index 605870741e3..4248000c7a9 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts @@ -1,6 +1,8 @@ import * as fs from 'fs'; +import * as path from 'path'; import { BuildActivity } from './BuildActivity.js'; -import { CompilerCallbacks, KeymanFileTypes } from '@keymanapp/common-types'; +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerCallbacks } from '@keymanapp/developer-utils'; import { KeyboardInfoCompiler } from '@keymanapp/kmc-keyboard-info'; import { loadProject } from '../../util/projectLoader.js'; import { InfrastructureMessages } from '../../messages/infrastructureMessages.js'; @@ -34,7 +36,8 @@ export class BuildKeyboardInfo extends BuildActivity { const keyboard = project.files.find(file => file.fileType == KeymanFileTypes.Source.KeymanKeyboard); const jsFilename = keyboard ? project.resolveOutputFilePath(keyboard, KeymanFileTypes.Source.KeymanKeyboard, KeymanFileTypes.Binary.WebKeyboard) : null; - const lastCommitDate = getLastGitCommitDate(project.projectPath); + const historyPath = path.join(project.projectPath, KeymanFileTypes.HISTORY_MD); + const lastCommitDate = getLastGitCommitDate(fs.existsSync(historyPath) ? historyPath : project.projectPath); const sources = { kmpFilename: project.resolveOutputFilePath(kps, KeymanFileTypes.Source.Package, KeymanFileTypes.Binary.Package), kpsFilename: project.resolveInputFilePath(kps), diff --git a/developer/src/kmc/src/commands/buildClasses/BuildKmnKeyboard.ts b/developer/src/kmc/src/commands/buildClasses/BuildKmnKeyboard.ts index e540aba4513..a4d6f45b854 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildKmnKeyboard.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildKmnKeyboard.ts @@ -1,7 +1,8 @@ import * as path from 'path'; import { platform } from 'os'; import { KmnCompiler } from '@keymanapp/kmc-kmn'; -import { CompilerOptions, CompilerCallbacks, KeymanFileTypes } from '@keymanapp/common-types'; +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerOptions, CompilerCallbacks } from '@keymanapp/developer-utils'; import { BuildActivity } from './BuildActivity.js'; export class BuildKmnKeyboard extends BuildActivity { diff --git a/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts b/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts index 0e0cfd97661..6ac98c9b92e 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts @@ -1,5 +1,7 @@ import * as kmcLdml from '@keymanapp/kmc-ldml'; -import { CompilerCallbacks, LDMLKeyboardXMLSourceFileReader, CompilerOptions, KeymanFileTypes } from '@keymanapp/common-types'; +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerOptions, CompilerCallbacks } from '@keymanapp/developer-utils'; +import { LDMLKeyboardXMLSourceFileReader } from '@keymanapp/developer-utils'; import { BuildActivity } from './BuildActivity.js'; import { fileURLToPath } from 'url'; diff --git a/developer/src/kmc/src/commands/buildClasses/BuildModel.ts b/developer/src/kmc/src/commands/buildClasses/BuildModel.ts index b2afc592943..c01d1adf20c 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildModel.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildModel.ts @@ -1,6 +1,7 @@ import { BuildActivity } from './BuildActivity.js'; import { LexicalModelCompiler } from '@keymanapp/kmc-model'; -import { CompilerCallbacks, CompilerOptions, KeymanFileTypes } from '@keymanapp/common-types'; +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerOptions, CompilerCallbacks } from '@keymanapp/developer-utils'; export class BuildModel extends BuildActivity { public get name(): string { return 'Lexical model'; } diff --git a/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts b/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts index 470c90639de..3924309f93a 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts @@ -1,6 +1,8 @@ +import * as fs from 'fs'; import * as path from 'path'; import { BuildActivity } from './BuildActivity.js'; -import { CompilerCallbacks, KeymanFileTypes } from '@keymanapp/common-types'; +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerCallbacks } from '@keymanapp/developer-utils'; import { ModelInfoCompiler } from '@keymanapp/kmc-model-info'; import { KmpCompiler } from '@keymanapp/kmc-package'; import { loadProject } from '../../util/projectLoader.js'; @@ -62,7 +64,8 @@ export class BuildModelInfo extends BuildActivity { return false; } - const lastCommitDate = getLastGitCommitDate(project.projectPath); + const historyPath = path.join(project.projectPath, KeymanFileTypes.HISTORY_MD); + const lastCommitDate = getLastGitCommitDate(fs.existsSync(historyPath) ? historyPath : project.projectPath); const sources = { model_id: path.basename(project.projectPath, KeymanFileTypes.Source.Project), kmpJsonData, diff --git a/developer/src/kmc/src/commands/buildClasses/BuildPackage.ts b/developer/src/kmc/src/commands/buildClasses/BuildPackage.ts index 1bb26b3b7f8..8ecc08dace8 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildPackage.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildPackage.ts @@ -1,5 +1,6 @@ import { BuildActivity } from './BuildActivity.js'; -import { CompilerCallbacks, CompilerOptions, KeymanFileTypes } from '@keymanapp/common-types'; +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerOptions, CompilerCallbacks } from '@keymanapp/developer-utils'; import { KmpCompiler } from '@keymanapp/kmc-package'; export class BuildPackage extends BuildActivity { diff --git a/developer/src/kmc/src/commands/buildClasses/BuildProject.ts b/developer/src/kmc/src/commands/buildClasses/BuildProject.ts index 4a5f9148bc6..ee1204a7f0d 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildProject.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildProject.ts @@ -1,6 +1,8 @@ import * as path from 'path'; import * as fs from 'fs'; -import { CompilerCallbacks, CompilerFileCallbacks, KeymanDeveloperProject, KeymanDeveloperProjectFile, KeymanDeveloperProjectType, KeymanFileTypes } from '@keymanapp/common-types'; +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerFileCallbacks, CompilerCallbacks } from '@keymanapp/developer-utils'; +import { KeymanDeveloperProject, KeymanDeveloperProjectFile, KeymanDeveloperProjectType } from '@keymanapp/developer-utils'; import { BuildActivity } from './BuildActivity.js'; import { buildActivities, buildKeyboardInfoActivity, buildModelInfoActivity } from './buildActivities.js'; import { InfrastructureMessages } from '../../messages/infrastructureMessages.js'; diff --git a/developer/src/kmc/src/commands/buildTestData/index.ts b/developer/src/kmc/src/commands/buildTestData/index.ts index 6e598954c10..c958673c2fa 100644 --- a/developer/src/kmc/src/commands/buildTestData/index.ts +++ b/developer/src/kmc/src/commands/buildTestData/index.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as kmcLdml from '@keymanapp/kmc-ldml'; -import { CompilerCallbacks, defaultCompilerOptions, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboardXMLSourceFileReader } from '@keymanapp/common-types'; +import { CompilerCallbacks, defaultCompilerOptions, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboardXMLSourceFileReader } from '@keymanapp/developer-utils'; import { NodeCompilerCallbacks } from '../../util/NodeCompilerCallbacks.js'; import { fileURLToPath } from 'url'; import { CommandLineBaseOptions } from 'src/util/baseOptions.js'; diff --git a/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts b/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts index 591dfe2e02b..06076db9292 100644 --- a/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts +++ b/developer/src/kmc/src/commands/buildWindowsPackageInstaller/index.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CompilerCallbacks, defaultCompilerOptions } from '@keymanapp/common-types'; +import { CompilerCallbacks, defaultCompilerOptions } from '@keymanapp/developer-utils'; import { NodeCompilerCallbacks } from '../../util/NodeCompilerCallbacks.js'; import { WindowsPackageInstallerCompiler, WindowsPackageInstallerSources } from '@keymanapp/kmc-package'; import { CommandLineBaseOptions } from 'src/util/baseOptions.js'; diff --git a/developer/src/kmc/src/commands/messageCommand.ts b/developer/src/kmc/src/commands/messageCommand.ts index cf5e46343f9..ee568dad9c4 100644 --- a/developer/src/kmc/src/commands/messageCommand.ts +++ b/developer/src/kmc/src/commands/messageCommand.ts @@ -1,14 +1,13 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CompilerBaseOptions, CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerEvent } from '@keymanapp/common-types'; import { Command, Option } from 'commander'; +import { escapeMarkdownChar, KeymanUrls, CompilerBaseOptions, CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerEvent } from '@keymanapp/developer-utils'; import { CompilerMessageSource, messageNamespaceKeys, messageSources } from '../messages/messageNamespaces.js'; import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js'; import { exitProcess } from '../util/sysexits.js'; import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; -import { CompilerMessageDetail, findMessageDetails } from '../util/extendedCompilerOptions.js'; -import { escapeMarkdownChar, KeymanUrls } from '@keymanapp/developer-utils'; +import { CompilerMessageDetail, findMessageDetails, findMessagesById, getMessageIdentifiersSorted } from '../util/extendedCompilerOptions.js'; type MessageFormat = 'text'|'markdown'|'json'; @@ -31,6 +30,10 @@ export function declareMessage(program: Command) { program .command('message [messages...]') .description(`Describe one or more compiler messages. Note: Markdown format is always written to files on disk.`) + .addHelpText('after', ` +Message identifiers can be: + * numeric, e.g. "KM07006" or "7006", or + * [namespace.]id, substring id supported; e.g. "kmc-kmn.INFO_MinimumEngineVersion" or "kmc-kmn." or "INFO_Min"`) .addOption(new Option('-f, --format ', 'Output format').choices(['text', 'markdown', 'json']).default('text')) .option('-o, --out-path ', 'Output path for Markdown files; output filename for text and json formats') .option('-a, --all-messages', 'Emit descriptions for all messages (text, json)') @@ -57,7 +60,7 @@ async function messageCommand(messages: string[], _options: any, commander: any) } const messageDetails = messages.length - ? messages.map(message => translateMessageInputToCode(message, callbacks)) + ? messages.flatMap(message => translateMessageInputToCode(message, callbacks)) : allMessageDetails(); if(callbacks.messageCount > 0) { @@ -96,16 +99,43 @@ async function messageCommand(messages: string[], _options: any, commander: any) const helpUrl = (code:any) => KeymanUrls.COMPILER_ERROR_CODE(CompilerError.formatCode(code).toLowerCase()); const getModuleName = (ms: CompilerMessageSource) => `${ms.module}.${ms.class.name}`; -function translateMessageInputToCode(message: string, callbacks: CompilerCallbacks): CompilerMessageDetail { +function parseMessageIdentifier(message: string, callbacks: CompilerCallbacks): { namespace?: CompilerErrorNamespace, id?: string } { + const parts = message.split('.', 2); + if(parts.length == 1) { + // searching all namespaces + return { id: parts[0] }; + } + + // searching one namespace + const namespace = messageNamespaceKeys.find(ns => messageSources[ns].module == parts[0].toLowerCase()); + if(!namespace) { + return { }; + } + return { namespace, id: parts[1] }; +} + +function translateMessageInputToCode(message: string, callbacks: CompilerCallbacks): CompilerMessageDetail[] { const pattern = /^(KM)?([0-9a-f]+)$/i; const result = message.match(pattern); if(!result) { - callbacks.reportMessage(InfrastructureMessages.Error_UnrecognizedMessageCode({message})); - return null; + const { namespace, id } = parseMessageIdentifier(message.toLowerCase(), callbacks); + if(!namespace && !id) { + callbacks.reportMessage(InfrastructureMessages.Error_MessageNamespaceNameNotFound({message})); + return null; + } + + // We assume that this is a INFO, HINT, ERROR, etc message, and do a substring search + const items = findMessagesById(namespace, id); + if(!items.length) { + callbacks.reportMessage(InfrastructureMessages.Error_UnrecognizedMessageCode({message})); + return null; + } + + return items; } const code = Number.parseInt(result[2], 16); - return findMessageDetails(code, callbacks); + return [findMessageDetails(code, callbacks)]; } function initialize(options: any): MessageOptions { @@ -123,11 +153,6 @@ function initialize(options: any): MessageOptions { } } -const getMessageIdentifiersSorted = (cls: any) => - Object.keys(cls) - .filter(id => typeof cls[id] == 'number') - .sort((a,b) => CompilerError.error(cls[a])-CompilerError.error(cls[b])); - function allMessageDetails(): CompilerMessageDetail[] { let result: CompilerMessageDetail[] = []; messageNamespaceKeys.forEach((namespace: CompilerErrorNamespace) => { diff --git a/developer/src/kmc/src/messages/infrastructureMessages.ts b/developer/src/kmc/src/messages/infrastructureMessages.ts index 00299f21573..b4d8cebd2ef 100644 --- a/developer/src/kmc/src/messages/infrastructureMessages.ts +++ b/developer/src/kmc/src/messages/infrastructureMessages.ts @@ -1,4 +1,4 @@ -import { CompilerError, CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/common-types"; +import { CompilerError, CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/developer-utils"; const Namespace = CompilerErrorNamespace.Infrastructure; const SevInfo = CompilerErrorSeverity.Info | Namespace; @@ -115,7 +115,7 @@ export class InfrastructureMessages { static ERROR_UnrecognizedMessageCode = SevError | 0x001a; static Error_UnrecognizedMessageCode = (o:{message:string}) => m( this.ERROR_UnrecognizedMessageCode, - `Invalid parameter: message identifier '${def(o.message)}' must match format '[KM]#####'`); + `Invalid parameter: message identifier '${def(o.message)}' must match format '[KM]#####' or be a search for a ...`); static ERROR_MustSpecifyMessageCode = SevError | 0x001b; static Error_MustSpecifyMessageCode = () => m( @@ -136,5 +136,10 @@ export class InfrastructureMessages { static Error_OutputPathMustExistAndBeADirectory = (o:{outPath:string}) => m( this.ERROR_OutputPathMustExistAndBeADirectory, `Output path ${def(o.outPath)} must exist and must be a folder`); -} + + static ERROR_MessageNamespaceNameNotFound = SevError | 0x001f; + static Error_MessageNamespaceNameNotFound = (o:{message: string}) => m( + this.ERROR_MessageNamespaceNameNotFound, + `Invalid parameter: --message ${def(o.message)} does not have a recognized namespace`); + } diff --git a/developer/src/kmc/src/messages/messageNamespaces.ts b/developer/src/kmc/src/messages/messageNamespaces.ts index 242c83c5b70..fc0e7e4d277 100644 --- a/developer/src/kmc/src/messages/messageNamespaces.ts +++ b/developer/src/kmc/src/messages/messageNamespaces.ts @@ -1,8 +1,8 @@ -import { CommonTypesMessages, CompilerErrorNamespace } from '@keymanapp/common-types'; +import { CommonTypesMessages, CompilerErrorNamespace } from '@keymanapp/developer-utils'; import { AnalyzerMessages } from '@keymanapp/kmc-analyze'; import { KeyboardInfoCompilerMessages } from '@keymanapp/kmc-keyboard-info'; import { KmnCompilerMessages, KmwCompilerMessages } from '@keymanapp/kmc-kmn'; -import { LdmlKeyboardCompilerMessages } from '@keymanapp/kmc-ldml'; +import { LdmlCompilerMessages } from '@keymanapp/kmc-ldml'; import { ModelCompilerMessages } from '@keymanapp/kmc-model'; import { ModelInfoCompilerMessages } from '@keymanapp/kmc-model-info'; import { PackageCompilerMessages } from '@keymanapp/kmc-package'; @@ -10,7 +10,7 @@ import { InfrastructureMessages } from './infrastructureMessages.js'; // Maps every compiler error namespace to the corresponding implementation const messageNamespaces: Record = { - [CompilerErrorNamespace.LdmlKeyboardCompiler]: LdmlKeyboardCompilerMessages, + [CompilerErrorNamespace.LdmlKeyboardCompiler]: LdmlCompilerMessages, [CompilerErrorNamespace.CommonTypes]: CommonTypesMessages, [CompilerErrorNamespace.KmnCompiler]: KmnCompilerMessages, [CompilerErrorNamespace.ModelCompiler]: ModelCompilerMessages, @@ -24,7 +24,7 @@ const messageNamespaces: Record = { // This works around pain points in enumerating enum members in Typescript // ref https://www.totaltypescript.com/iterate-over-object-keys-in-typescript -export const messageNamespaceKeys = Object.keys(messageNamespaces).map(v => Number.parseInt(v)); +export const messageNamespaceKeys = Object.keys(messageNamespaces).map(v => Number.parseInt(v) as CompilerErrorNamespace); export type CompilerMessageSource = { module: string; @@ -33,7 +33,7 @@ export type CompilerMessageSource = { // TODO: consolidate with messageNamespaces above export const messageSources: Record = { - [CompilerErrorNamespace.LdmlKeyboardCompiler]: { module: 'kmc-ldml', class: LdmlKeyboardCompilerMessages }, + [CompilerErrorNamespace.LdmlKeyboardCompiler]: { module: 'kmc-ldml', class: LdmlCompilerMessages }, [CompilerErrorNamespace.CommonTypes]: { module: 'common-types', class: CommonTypesMessages }, [CompilerErrorNamespace.KmnCompiler]: { module: 'kmc-kmn', class: KmnCompilerMessages }, [CompilerErrorNamespace.ModelCompiler]: { module: 'kmc-model', class: ModelCompilerMessages }, diff --git a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts index 446924913ad..f94e06c18a7 100644 --- a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts +++ b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts @@ -6,7 +6,7 @@ import { CompilerCallbacks, CompilerEvent, compilerLogLevelToSeverity, CompilerErrorSeverity, CompilerError, CompilerCallbackOptions, - CompilerFileCallbacks} from '@keymanapp/common-types'; + CompilerFileCallbacks} from '@keymanapp/developer-utils'; import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; import chalk from 'chalk'; import supportsColor from 'supports-color'; diff --git a/developer/src/kmc/src/util/baseOptions.ts b/developer/src/kmc/src/util/baseOptions.ts index dd91f0bae67..ab07921dbd7 100644 --- a/developer/src/kmc/src/util/baseOptions.ts +++ b/developer/src/kmc/src/util/baseOptions.ts @@ -1,4 +1,4 @@ -import { ALL_COMPILER_LOG_FORMATS, ALL_COMPILER_LOG_LEVELS, CompilerLogFormat, CompilerLogLevel } from "@keymanapp/common-types"; +import { ALL_COMPILER_LOG_FORMATS, ALL_COMPILER_LOG_LEVELS, CompilerLogFormat, CompilerLogLevel } from "@keymanapp/developer-utils"; import { Command, Option } from "commander"; /** diff --git a/developer/src/kmc/src/util/extendedCompilerOptions.ts b/developer/src/kmc/src/util/extendedCompilerOptions.ts index 77fcceddd6b..0d72b5b550a 100644 --- a/developer/src/kmc/src/util/extendedCompilerOptions.ts +++ b/developer/src/kmc/src/util/extendedCompilerOptions.ts @@ -1,6 +1,6 @@ -import { CompilerCallbacks, CompilerError, CompilerErrorSeverity, CompilerMessageOverride, CompilerMessageOverrideMap, CompilerOptions } from '@keymanapp/common-types'; +import { CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageOverride, CompilerMessageOverrideMap, CompilerOptions } from '@keymanapp/developer-utils'; import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; -import { messageNamespaceKeys, messageSources } from '../messages/messageNamespaces.js'; +import { CompilerMessageSource, messageNamespaceKeys, messageSources } from '../messages/messageNamespaces.js'; export interface ExtendedCompilerOptions extends CompilerOptions { /** @@ -97,6 +97,52 @@ export function findMessageDetails(code: number, callbacks: CompilerCallbacks): return {code: m[id], id, module: source.module, class: source.class}; } +export const getMessageIdentifiersSorted = (cls: any) => + Object.keys(cls) + .filter(id => typeof cls[id] == 'number') + .sort((a,b) => CompilerError.error(cls[a])-CompilerError.error(cls[b])); + +/** + * Gets an array of compiler messages matching the search identifier. Substrings + * are supported for the id portion of the searchId + * @param searchNamespace optional namespace to search in, if omitted, searches + * all namespaces + * @param searchId a substring to match with optional namespace prefix, e.g. + * "INFO_" + */ +export function findMessagesById(searchNamespace: CompilerErrorNamespace, searchId: string): CompilerMessageDetail[] { + searchId = searchId.toLowerCase(); + + const messages: CompilerMessageDetail[] = []; + + messageNamespaceKeys.forEach((namespace: CompilerErrorNamespace) => { + if(searchNamespace && searchNamespace != namespace) { + return; + } + + const ms = messageSources[namespace] as CompilerMessageSource; + + const ids = getMessageIdentifiersSorted(ms.class); + for(const id of ids) { + const code = ms.class[id]; + const lid = id.toLowerCase(); + if(typeof code != 'number') { + continue; + } + if(lid.includes(searchId)) { + messages.push({ + code, + id, + class: ms.class, + module: ms.module + }); + } + } + }); + + return messages; +} + /** * Verifies that a given message is valid and that the severity is allowed to be * modified (Info, Hint and Warn only -- Error and Fatal cannot be modified) diff --git a/developer/src/kmc/src/util/fileLists.ts b/developer/src/kmc/src/util/fileLists.ts index ecd786d0ae2..29defb4b8d8 100644 --- a/developer/src/kmc/src/util/fileLists.ts +++ b/developer/src/kmc/src/util/fileLists.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CompilerCallbacks } from "@keymanapp/common-types"; +import { CompilerCallbacks } from "@keymanapp/developer-utils"; import { InfrastructureMessages } from "../messages/infrastructureMessages.js"; /** diff --git a/developer/src/kmc/src/util/getLastGitCommitDate.ts b/developer/src/kmc/src/util/getLastGitCommitDate.ts index 645fcc6820b..bc85edacce3 100644 --- a/developer/src/kmc/src/util/getLastGitCommitDate.ts +++ b/developer/src/kmc/src/util/getLastGitCommitDate.ts @@ -1,12 +1,12 @@ import { execFileSync } from 'child_process'; // RFC3339 pattern, UTC -export const expectedGitDateFormat = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ$/; +export const expectedGitDateFormat = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z$/; /** * Returns the date and time of the last commit from git for the passed in path * @param path Path for which to retrieve the last commit message - * @returns string, in RFC3339, 'YYYY-MM-DDThh:nn:ssZ' + * @returns string, in RFC3339, 'YYYY-MM-DDThh:nn:ss.SSSZ' */ export function getLastGitCommitDate(path: string): string { // TZ=UTC0 git log -1 --no-merges --date=format:%Y-%m-%dT%H:%M:%SZ --format=%ad @@ -17,13 +17,12 @@ export function getLastGitCommitDate(path: string): string { 'log', // git log '-1', // one commit only '--no-merges', // we're only interested in 'real' commits - '--date=format:%Y-%m-%dT%H:%M:%SZ', // format the date in our expected RFC3339 format - '--format=%ad' // emit only the commit date + '--format=%at', // emit only the commit date as a UNIX timestamp + '--', + path ], { - env: { ...process.env, TZ: 'TZ0' }, // use UTC timezone, not local encoding: 'utf-8', // force a string result rather than Buffer windowsHide: true, // on windows, we may need this to suppress a console window popup - cwd: path, // path to run git from stdio: ['pipe', 'pipe', 'pipe'] // all output via pipe, so we don't get git errors on console }); } catch (e) { @@ -36,13 +35,8 @@ export function getLastGitCommitDate(path: string): string { return null; } - result = result.trim(); + // We receive a timestamp in seconds but we need milliseconds + const msec = Number.parseInt(result.trim()) * 1000; - // We'll only return the result if it walks like a date, swims like a date, - // and quacks like a date. - if (!result.match(expectedGitDateFormat)) { - return null; - } - - return result; + return new Date(msec).toISOString(); } diff --git a/developer/src/kmc/src/util/projectLoader.ts b/developer/src/kmc/src/util/projectLoader.ts index b26399ee395..04a1af27960 100644 --- a/developer/src/kmc/src/util/projectLoader.ts +++ b/developer/src/kmc/src/util/projectLoader.ts @@ -1,7 +1,8 @@ import * as path from 'path'; import * as fs from 'fs'; -import { CompilerCallbacks, KeymanDeveloperProject, KeymanFileTypes, KPJFileReader } from "@keymanapp/common-types"; +import { KeymanFileTypes } from "@keymanapp/common-types"; +import { CompilerCallbacks, KeymanDeveloperProject, KPJFileReader } from "@keymanapp/developer-utils"; import { InfrastructureMessages } from "../messages/infrastructureMessages.js"; export const isProject = (filename: string): boolean => diff --git a/developer/src/kmc/src/util/projectRunner.ts b/developer/src/kmc/src/util/projectRunner.ts index 1f3ffdfcd31..c4186635b70 100644 --- a/developer/src/kmc/src/util/projectRunner.ts +++ b/developer/src/kmc/src/util/projectRunner.ts @@ -1,4 +1,5 @@ -import { CompilerCallbacks, KeymanFileTypes } from "@keymanapp/common-types"; +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { CompilerCallbacks } from '@keymanapp/developer-utils'; import { isProject, loadProject } from './projectLoader.js'; async function runProject(callbacks: CompilerCallbacks, filename: string, callback: (filename:string)=>Promise): Promise { diff --git a/developer/src/kmc/test/fixtures/get-last-git-commit-date/README.md b/developer/src/kmc/test/fixtures/get-last-git-commit-date/README.md new file mode 100644 index 00000000000..d192c2cd5d8 --- /dev/null +++ b/developer/src/kmc/test/fixtures/get-last-git-commit-date/README.md @@ -0,0 +1,5 @@ +# Test file for getLastGitCommitDate. + +Note that if changes to this file are committed, the corresponding test in +test-getLastGitCommitDate.ts will need to be updated to reflect the new date in +git history. \ No newline at end of file diff --git a/developer/src/kmc/test/test-extendedCompilerOptions.ts b/developer/src/kmc/test/test-extendedCompilerOptions.ts index f0a304f2f4e..adda29dfdbd 100644 --- a/developer/src/kmc/test/test-extendedCompilerOptions.ts +++ b/developer/src/kmc/test/test-extendedCompilerOptions.ts @@ -3,7 +3,7 @@ import { assert } from 'chai'; import 'mocha'; import { unitTestEndpoints } from '../src/util/extendedCompilerOptions.js'; import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js'; -import { CompilerError, CompilerMessageOverride, CompilerErrorSeverity } from '@keymanapp/common-types'; +import { CompilerError, CompilerMessageOverride, CompilerErrorSeverity } from '@keymanapp/developer-utils'; import { KmnCompilerMessages } from '@keymanapp/kmc-kmn'; interface MessageTest {input: string, result: CompilerMessageOverride}; diff --git a/developer/src/kmc/test/test-getLastGitCommitDate.ts b/developer/src/kmc/test/test-getLastGitCommitDate.ts index 3497ef309dc..917cfdc7ad0 100644 --- a/developer/src/kmc/test/test-getLastGitCommitDate.ts +++ b/developer/src/kmc/test/test-getLastGitCommitDate.ts @@ -14,4 +14,13 @@ describe('getLastGitCommitDate', function () { const date = getLastGitCommitDate('/'); assert.isNull(date); }); + + it('should return a valid date for a specific file in the repo', async function() { + const path = makePathToFixture('get-last-git-commit-date/README.md'); + const date = getLastGitCommitDate(path); + // The expected date was manually extracted using the following command, with msec appended: + // TZ=UTC git log --date=iso-strict-local -- fixtures/get-last-git-commit-date/README.md + // If the fixture modified, then this will also need to be updated. + assert.equal(date, '2024-06-17T20:43:48.000Z'); + }); }); diff --git a/developer/src/kmc/test/test-infrastructureMessages.ts b/developer/src/kmc/test/test-infrastructureMessages.ts index bd39a0a65ba..37316a8f3c2 100644 --- a/developer/src/kmc/test/test-infrastructureMessages.ts +++ b/developer/src/kmc/test/test-infrastructureMessages.ts @@ -1,15 +1,16 @@ import 'mocha'; +import * as fs from 'fs'; import { assert } from 'chai'; import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js'; import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; import { makePathToFixture } from './helpers/index.js'; import { NodeCompilerCallbacks } from '../src/util/NodeCompilerCallbacks.js'; -import { CompilerErrorNamespace, CompilerEvent, KeymanFileTypes } from '@keymanapp/common-types'; +import { KeymanFileTypes } from '@keymanapp/common-types'; import { unitTestEndpoints } from '../src/commands/build.js'; import { KmnCompilerMessages } from '@keymanapp/kmc-kmn'; import { clearOptions } from '@keymanapp/developer-utils'; import { loadProject } from '../src/util/projectLoader.js'; -import { defaultCompilerOptions, CompilerOptions} from '@keymanapp/common-types'; +import { CompilerErrorNamespace, CompilerEvent, defaultCompilerOptions, CompilerOptions} from '@keymanapp/developer-utils'; import { analyzeUnitTestEndpoints } from '../src/commands/analyze.js'; import { BuildKeyboardInfo } from '../src/commands/buildClasses/BuildKeyboardInfo.js'; import { BuildModelInfo } from '../src/commands/buildClasses/BuildModelInfo.js'; @@ -171,10 +172,19 @@ describe('InfrastructureMessages', function () { it('should generate HINT_FilenameHasDifferingCase if a referenced file has differing case', async function() { // This message is generated by NodeCompilerCallbacks, because that's where the filesystem is visible, // so we can't use our usual testForMessage pattern. - const ncb = new NodeCompilerCallbacks({logLevel: 'silent'}); - const expectedMessages = [InfrastructureMessages.HINT_FilenameHasDifferingCase]; - ncb.loadFile(makePathToFixture('invalid-keyboards', 'Hint_Filename_Has_Differing_Case.kmn')); - assertMessagesEqual(ncb.messages, expectedMessages); + const filename = makePathToFixture('invalid-keyboards', 'Hint_Filename_Has_Differing_Case.kmn'); + if(fs.existsSync(filename)) { + // The file is actually named 'hint_filename_has_differing_case.kmn', so + // if we can see it, then we know we are not on a case-sensitive + // filesystem, and so we can test + const ncb = new NodeCompilerCallbacks({logLevel: 'silent'}); + const expectedMessages = [InfrastructureMessages.HINT_FilenameHasDifferingCase]; + assert.isNotNull(ncb.loadFile(filename), `Expected to load 'hint_filename_has_differing_case.kmn' with mixed case '${filename}'`); + assertMessagesEqual(ncb.messages, expectedMessages); + } else { + // We are on a case-sensitive filesystem, so this hint can never be generated + assert.isTrue(true); + } }); // INFO_WarningsHaveFailedBuild diff --git a/developer/src/kmc/test/tsconfig.json b/developer/src/kmc/test/tsconfig.json index ac8680daf74..3d0d6eb4f6c 100644 --- a/developer/src/kmc/test/tsconfig.json +++ b/developer/src/kmc/test/tsconfig.json @@ -6,10 +6,6 @@ "rootDirs": ["./", "../src/"], "outDir": "../build/test", "baseUrl": ".", - "paths": { - "@keymanapp/common-types": ["../../../../common/web/types/src/main"], - "@keymanapp/developer-test-helpers": ["../../common/web/test-helpers/index"], - }, }, "include": [ "**/test-*.ts", diff --git a/developer/src/kmc/tsconfig.json b/developer/src/kmc/tsconfig.json index 0ba354075d5..cf78a8a7755 100644 --- a/developer/src/kmc/tsconfig.json +++ b/developer/src/kmc/tsconfig.json @@ -5,16 +5,6 @@ "outDir": "build/src/", "rootDir": "src/", "baseUrl": ".", - "paths": { - "@keymanapp/common-types": [ "../../../common/web/types/src/main" ], - "@keymanapp/kmc-analyze": [ "../kmc-analyze/src/index" ], - "@keymanapp/kmc-keyboard-info": [ "../kmc-keyboard-info/src/index" ], - "@keymanapp/kmc-kmn": [ "../kmc-kmn/src/main" ], - "@keymanapp/kmc-ldml": [ "../kmc-ldml/src/main" ], - "@keymanapp/kmc-model": [ "../kmc-model/src/main" ], - "@keymanapp/kmc-model-info": [ "../kmc-model-info/src/index" ], - "@keymanapp/kmc-package": [ "../kmc-package/src/kmp-compiler" ], - } }, "include": [ "src/**/*.ts" diff --git a/developer/src/kmcmplib/include/kmcmplibapi.h b/developer/src/kmcmplib/include/kmcmplibapi.h index eb7a9b549c4..536d4fff0d4 100644 --- a/developer/src/kmcmplib/include/kmcmplibapi.h +++ b/developer/src/kmcmplib/include/kmcmplibapi.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -53,6 +55,14 @@ struct KMCMP_COMPILER_RESULT_EXTRA_GROUP { std::string name; }; +struct KMCMP_COMPILER_RESULT_MESSAGE { + unsigned int errorCode; + int lineNumber; + int columnNumber; + std::string filename; + std::vector parameters; +}; + #define COMPILETARGETS_KMX 0x01 #define COMPILETARGETS_JS 0x02 #define COMPILETARGETS__MASK 0x03 @@ -75,19 +85,8 @@ struct KMCMP_COMPILER_RESULT { /** * @param szText UTF-8 string */ -typedef int (*kmcmp_CompilerMessageProc)(int line, uint32_t dwMsgCode, const char* szText, void* context); - -// parameters in UTF-8 -// TODO typical usage: -// if(!kmcmp_LoadFileProc("filename.ico", "/tmp/filename.kmn", nullptr, &size)) { -// return error; -// } -// buf = new unsigned char[size]; -// if(!kmcmp_LoadFileProc("filename.ico", "/tmp/filename.kmn", buf, &size)) { -// delete[] buf; -// return error; -// } -typedef bool (*kmcmp_LoadFileProc)(const char* loadFilename, const char* baseFilename, void* buffer, int* bufferSize, void* context); +typedef void (*kmcmp_CompilerMessageProc)(const KMCMP_COMPILER_RESULT_MESSAGE &message, void* context); +typedef const std::vector (*kmcmp_LoadFileProc)(const std::string& loadFilename, const std::string& baseFilename); /** * @param pszInfile UTF-8 path to file.kmn diff --git a/developer/src/kmcmplib/include/kmcompx.h b/developer/src/kmcmplib/include/kmcompx.h index a1403ad02fd..9d7ca75508b 100644 --- a/developer/src/kmcmplib/include/kmcompx.h +++ b/developer/src/kmcmplib/include/kmcompx.h @@ -3,11 +3,6 @@ // TODO: merge with kmcmplib.h -typedef wchar_t KMX_WCHART; -typedef KMX_DWORD * PKMX_DWORD; -typedef char * PKMX_STR; -typedef KMX_WCHAR* PKMX_WCHAR ; - // TODO: these defines are copied out of unicode.h, because unicode.h still // has windows-specific types. These should be remerged at a future date @@ -29,6 +24,7 @@ typedef KMX_WCHAR* PKMX_WCHAR ; #ifndef _MSC_VER #include +#include template < typename T, size_t N > size_t _countof( T ( & /*arr*/ )[ N ] ) diff --git a/developer/src/kmcmplib/meson.build b/developer/src/kmcmplib/meson.build index c996981f47a..d494e884d90 100644 --- a/developer/src/kmcmplib/meson.build +++ b/developer/src/kmcmplib/meson.build @@ -5,7 +5,7 @@ # project('kmcmplib', 'cpp', 'c', - version: run_command(find_program('getversion.bat', 'getversion.sh')).stdout().strip(), + version: run_command(find_program('getversion.bat', 'getversion.sh'), check: true).stdout().strip(), license: 'MIT', default_options : ['buildtype=release', 'cpp_std=c++14', diff --git a/developer/src/kmcmplib/src/CasedKeys.cpp b/developer/src/kmcmplib/src/CasedKeys.cpp index 759a497d148..0883c401ce3 100644 --- a/developer/src/kmcmplib/src/CasedKeys.cpp +++ b/developer/src/kmcmplib/src/CasedKeys.cpp @@ -1,13 +1,14 @@ #include "pch.h" +#include "km_u16.h" +#include "../../../../common/windows/cpp/include/vkeys.h" + #include "compfile.h" #include "kmn_compiler_errors.h" -#include "../../../../common/windows/cpp/include/vkeys.h" #include "kmcmplib.h" #include "CharToKeyConversion.h" -#include "kmx_u16.h" #include "xstring.h" namespace kmcmp { @@ -24,7 +25,7 @@ KMX_DWORD VerifyCasedKeys(PFILE_STORE sp) { if (kmcmp::FMnemonicLayout) { // The &CasedKeys system store is not supported for // mnemonic layouts in 14.0 - return CERR_CasedKeysNotSupportedWithMnemonicLayout; + return KmnCompilerMessages::ERROR_CasedKeysNotSupportedWithMnemonicLayout; } // We will rewrite this store with virtual keys @@ -37,20 +38,20 @@ KMX_DWORD VerifyCasedKeys(PFILE_STORE sp) { KMX_UINT key = 0, shift = 0; if (*p != UC_SENTINEL) { if (!kmcmp::MapUSCharToVK(*p, &key, &shift)) { - return CERR_CasedKeysMustContainOnlyVirtualKeys; + return KmnCompilerMessages::ERROR_CasedKeysMustContainOnlyVirtualKeys; } if (shift & K_SHIFTFLAG) { - return CERR_CasedKeysMustNotIncludeShiftStates; + return KmnCompilerMessages::ERROR_CasedKeysMustNotIncludeShiftStates; } } else { if (*(p + 1) != CODE_EXTENDED) { - return CERR_CasedKeysMustContainOnlyVirtualKeys; + return KmnCompilerMessages::ERROR_CasedKeysMustContainOnlyVirtualKeys; } shift = *(p + 2); key = *(p + 3); if (shift != ISVIRTUALKEY) { - return CERR_CasedKeysMustNotIncludeShiftStates; + return KmnCompilerMessages::ERROR_CasedKeysMustNotIncludeShiftStates; } } *q++ = UC_SENTINEL; @@ -66,7 +67,7 @@ KMX_DWORD VerifyCasedKeys(PFILE_STORE sp) { delete[] sp->dpString; sp->dpString = buf; - return CERR_None; + return STATUS_Success; } KMX_DWORD ExpandCapsRulesForGroup(PFILE_KEYBOARD fk, PFILE_GROUP gp) { @@ -76,14 +77,14 @@ KMX_DWORD ExpandCapsRulesForGroup(PFILE_KEYBOARD fk, PFILE_GROUP gp) { if (kmcmp::FMnemonicLayout) { // The &CasedKeys system store is not supported for // mnemonic layouts in 14.0 - return CERR_None; + return STATUS_Success; } PFILE_STORE sp = FindSystemStore(fk, TSS_CASEDKEYS); if (!sp) { // If there is no &CasedKeys system store, then we do not // process the key - return CERR_None; + return STATUS_Success; } KMX_DWORD msg; @@ -92,11 +93,11 @@ KMX_DWORD ExpandCapsRulesForGroup(PFILE_KEYBOARD fk, PFILE_GROUP gp) { // dereference the array every call int cxKeyArray = gp->cxKeyArray; for (int i = 0; i < cxKeyArray; i++) { - if ((msg = ExpandCapsRule(gp, &gp->dpKeyArray[i], sp)) != CERR_None) { + if ((msg = ExpandCapsRule(gp, &gp->dpKeyArray[i], sp)) != STATUS_Success) { return msg; } } - return CERR_None; + return STATUS_Success; } KMX_DWORD ExpandCapsRule(PFILE_GROUP gp, PFILE_KEY kpp, PFILE_STORE sp) { @@ -106,13 +107,13 @@ KMX_DWORD ExpandCapsRule(PFILE_GROUP gp, PFILE_KEY kpp, PFILE_STORE sp) { if (shift == 0) { // Convert US key cap to a virtual key if (!kmcmp::MapUSCharToVK(kpp->Key, &key, &shift)) { - return CERR_None; + return STATUS_Success; } } if (shift & (CAPITALFLAG | NOTCAPITALFLAG)) { // Don't attempt expansion if either Caps Lock flag is specified in the key rule - return CERR_None; + return STATUS_Success; } PKMX_WCHAR p = sp->dpString; @@ -125,13 +126,13 @@ KMX_DWORD ExpandCapsRule(PFILE_GROUP gp, PFILE_KEY kpp, PFILE_STORE sp) { if (!*p) { // This key is not modified by Caps Lock - return CERR_None; + return STATUS_Success; } // This key is modified by Caps Lock, so we need to duplicate this rule int offset = (int)(kpp - gp->dpKeyArray); if(!resizeKeyArray(gp)) { - return CERR_CannotAllocateMemory; + return KmnCompilerMessages::FATAL_CannotAllocateMemory; } kpp = &gp->dpKeyArray[offset]; gp->cxKeyArray++; @@ -149,5 +150,5 @@ KMX_DWORD ExpandCapsRule(PFILE_GROUP gp, PFILE_KEY kpp, PFILE_STORE sp) { kpp->Key = key; kpp->ShiftFlags = shift | NOTCAPITALFLAG; - return CERR_None; + return STATUS_Success; } diff --git a/developer/src/kmcmplib/src/CheckForDuplicates.cpp b/developer/src/kmcmplib/src/CheckForDuplicates.cpp index 56b6195fb42..e0fa0094bcb 100644 --- a/developer/src/kmcmplib/src/CheckForDuplicates.cpp +++ b/developer/src/kmcmplib/src/CheckForDuplicates.cpp @@ -1,16 +1,16 @@ #include "pch.h" +#include #include "compfile.h" #include #include "kmcmplib.h" #include -#include "kmx_u16.h" #include #include "CheckForDuplicates.h" -KMX_DWORD CheckForDuplicateGroup(PFILE_KEYBOARD fk, PFILE_GROUP gp) noexcept { +KMX_BOOL CheckForDuplicateGroup(PFILE_KEYBOARD fk, PFILE_GROUP gp) noexcept { KMX_DWORD i; PFILE_GROUP gp0 = fk->dpGroupArray; for (i = 0; i < fk->cxGroupArray; i++, gp0++) { @@ -18,18 +18,21 @@ KMX_DWORD CheckForDuplicateGroup(PFILE_KEYBOARD fk, PFILE_GROUP gp) noexcept { continue; } if (u16icmp(gp0->szName, gp->szName) == 0) { - snprintf(ErrExtraLIB, ERR_EXTRA_LIB_LEN, " Group '%s' declared on line %d", string_from_u16string(gp->szName).c_str(), gp0->Line); - return CERR_DuplicateGroup; + ReportCompilerMessage(KmnCompilerMessages::ERROR_DuplicateGroup, { + /*groupName*/ string_from_u16string(gp->szName), + /*lineNumber*/ std::to_string(gp0->Line) + }); + return FALSE; } } - return CERR_None; + return TRUE; } -KMX_DWORD CheckForDuplicateStore(PFILE_KEYBOARD fk, PFILE_STORE sp) noexcept { +KMX_BOOL CheckForDuplicateStore(PFILE_KEYBOARD fk, PFILE_STORE sp) noexcept { if (!sp->szName[0]) { // Stores with zero length names are reserved system stores. // They cannot be defined in user code. This is not an issue. - return CERR_None; + return TRUE; } KMX_DWORD i; PFILE_STORE sp0 = fk->dpStoreArray; @@ -38,9 +41,12 @@ KMX_DWORD CheckForDuplicateStore(PFILE_KEYBOARD fk, PFILE_STORE sp) noexcept { continue; } if (u16icmp(sp0->szName, sp->szName) == 0) { - snprintf(ErrExtraLIB, ERR_EXTRA_LIB_LEN, " Store '%s' declared on line %d", string_from_u16string(sp0->szName).c_str(), sp0->line); - return CERR_DuplicateStore; + ReportCompilerMessage(KmnCompilerMessages::ERROR_DuplicateStore, { + /*storeName*/ string_from_u16string(sp0->szName), + /*lineNumber*/ std::to_string(sp0->line) + }); + return FALSE; } } - return CERR_None; + return TRUE; } diff --git a/developer/src/kmcmplib/src/CheckForDuplicates.h b/developer/src/kmcmplib/src/CheckForDuplicates.h index 20789dd5e2c..e338f290f5c 100644 --- a/developer/src/kmcmplib/src/CheckForDuplicates.h +++ b/developer/src/kmcmplib/src/CheckForDuplicates.h @@ -1,4 +1,4 @@ #pragma once -KMX_DWORD CheckForDuplicateGroup(PFILE_KEYBOARD fk, PFILE_GROUP gp) noexcept; -KMX_DWORD CheckForDuplicateStore(PFILE_KEYBOARD fk, PFILE_STORE sp) noexcept; +KMX_BOOL CheckForDuplicateGroup(PFILE_KEYBOARD fk, PFILE_GROUP gp) noexcept; +KMX_BOOL CheckForDuplicateStore(PFILE_KEYBOARD fk, PFILE_STORE sp) noexcept; diff --git a/developer/src/kmcmplib/src/CheckNCapsConsistency.cpp b/developer/src/kmcmplib/src/CheckNCapsConsistency.cpp index c86c62702e7..a170419e020 100644 --- a/developer/src/kmcmplib/src/CheckNCapsConsistency.cpp +++ b/developer/src/kmcmplib/src/CheckNCapsConsistency.cpp @@ -33,7 +33,7 @@ * * @param fk Keyboard to check */ -bool CheckNCapsConsistency(PFILE_KEYBOARD fk) { +void CheckNCapsConsistency(PFILE_KEYBOARD fk) { struct CapsUsage { int ncaps_line, caps_line, neither_line; }; @@ -87,13 +87,11 @@ bool CheckNCapsConsistency(PFILE_KEYBOARD fk) { if (caps_ncaps_usage[i].neither_line && (caps_ncaps_usage[i].caps_line || caps_ncaps_usage[i].ncaps_line)) { // We set the current line to one needing work: the developer should add the NCAPS flag kmcmp::currentLine = caps_ncaps_usage[i].neither_line; - AddWarningBool(CWARN_KeyShouldIncludeNCaps); + ReportCompilerMessage(KmnCompilerMessages::WARN_KeyShouldIncludeNCaps); } } delete[] caps_ncaps_usage; kmcmp::currentLine = oldCurrentLine; - - return TRUE; } diff --git a/developer/src/kmcmplib/src/CheckNCapsConsistency.h b/developer/src/kmcmplib/src/CheckNCapsConsistency.h index 50fa744c4ac..913ab87fa4d 100644 --- a/developer/src/kmcmplib/src/CheckNCapsConsistency.h +++ b/developer/src/kmcmplib/src/CheckNCapsConsistency.h @@ -2,4 +2,4 @@ #include -bool CheckNCapsConsistency(PFILE_KEYBOARD fk); +void CheckNCapsConsistency(PFILE_KEYBOARD fk); diff --git a/developer/src/kmcmplib/src/CompMsg.cpp b/developer/src/kmcmplib/src/CompMsg.cpp deleted file mode 100644 index 86a9b4d4757..00000000000 --- a/developer/src/kmcmplib/src/CompMsg.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include - -struct CompilerError { - KMX_DWORD ErrorCode; - const KMX_CHAR* Text; - }; - -const struct CompilerError CompilerErrors[] = { - { CERR_InvalidLayoutLine , "Invalid 'layout' command"}, - { CERR_NoVersionLine , "No version line found for file"}, - { CERR_InvalidGroupLine , "Invalid 'group' command"}, - { CERR_InvalidStoreLine , "Invalid 'store' command"}, - { CERR_InvalidCodeInKeyPartOfRule , "Invalid command or code found in key part of rule"}, - { CERR_InvalidDeadkey , "Invalid 'deadkey' or 'dk' command"}, - { CERR_InvalidValue , "Invalid value in extended string"}, - { CERR_ZeroLengthString , "A string of zero characters was found"}, - { CERR_TooManyIndexToKeyRefs , "Too many index commands refering to key string"}, - { CERR_UnterminatedString , "Unterminated string in line"}, - { CERR_StringInVirtualKeySection , "extend string illegal in virtual key section"}, - { CERR_AnyInVirtualKeySection , "'any' command is illegal in virtual key section"}, - { CERR_InvalidAny , "Invalid 'any' command"}, - { CERR_StoreDoesNotExist , "Store referenced does not exist"}, - { CERR_BeepInVirtualKeySection , "'beep' command is illegal in virtual key section"}, - { CERR_IndexInVirtualKeySection , "'index' command is illegal in virtual key section"}, - { CERR_BadCallParams , "CompileKeyboardFile was called with bad parameters"}, - { CERR_InfileNotExist , "Cannot find the input file"}, - // { CERR_CannotCreateOutfile , "Cannot open output file for writing"}, unused - { CERR_UnableToWriteFully , "Unable to write the file completely"}, - { CERR_CannotReadInfile , "Cannot read the input file"}, - { CERR_SomewhereIGotItWrong , "Internal error: contact Keyman"}, - { CERR_BufferOverflow , "The compiler memory buffer overflowed"}, - { CERR_Break , "Compiler interrupted by user"}, - { CERR_CannotAllocateMemory , "Out of memory"}, - { CERR_InvalidBitmapLine , "Invalid 'bitmaps' command"}, - { CERR_CannotReadBitmapFile , "Cannot open the bitmap or icon file for reading"}, - { CERR_IndexDoesNotPointToAny , "An index() in the output does not have a corresponding any() statement"}, - { CERR_ReservedCharacter , "A reserved character was found"}, - { CERR_InvalidCharacter , "A character was found that is outside the valid Unicode range (U+0000 - U+10FFFF)"}, - { CERR_InvalidCall , "The 'call' command is invalid"}, - { CERR_CallInVirtualKeySection , "'call' command is illegal in virtual key section"}, - { CERR_CodeInvalidInKeyStore , "The command is invalid inside a store that is used in a key part of the rule"}, - { CERR_CannotLoadIncludeFile , "Cannot load the included file: it is either invalid or does not exist"}, - { CERR_60FeatureOnly_EthnologueCode , "EthnologueCode system store requires VERSION 6.0 or higher"}, - { CERR_60FeatureOnly_MnemonicLayout , "MnemonicLayout functionality requires VERSION 6.0 or higher"}, - { CERR_60FeatureOnly_OldCharPosMatching , "OldCharPosMatching system store requires VERSION 6.0 or higher"}, - { CERR_60FeatureOnly_NamedCodes , "Named character constants requires VERSION 6.0 or higher"}, - { CERR_60FeatureOnly_Contextn , "Context(n) requires VERSION 6.0 or higher"}, - { CERR_501FeatureOnly_Call , "Call() requires VERSION 5.01 or higher"}, - { CERR_InvalidNamedCode , "Invalid named code constant"}, - { CERR_InvalidSystemStore , "Invalid system store name found"}, - { CERR_60FeatureOnly_VirtualCharKey , "Virtual character keys require VERSION 6.0 or higher"}, - { CERR_VersionAlreadyIncluded , "Only one VERSION or store(version) line allowed in a source file."}, - { CERR_70FeatureOnly , "This feature requires store(version) '7.0' or higher"}, - { CERR_80FeatureOnly , "This feature requires store(version) '8.0' or higher"}, - { CERR_InvalidInVirtualKeySection , "This statement is not valid in a virtual key section"}, - { CERR_InvalidIf , "The if() statement is not valid"}, - { CERR_InvalidReset , "The reset() statement is not valid"}, - { CERR_InvalidSet , "The set() statement is not valid"}, - { CERR_InvalidSave , "The save() statement is not valid"}, - { CERR_InvalidEthnologueCode , "Invalid ethnologuecode format"}, - { CERR_90FeatureOnly_IfSystemStores , "if(store) requires store(version) '9.0' or higher"}, - { CERR_IfSystemStore_NotFound , "System store in if() not found"}, - { CERR_90FeatureOnly_SetSystemStores , "set(store) requires store(version) '9.0' or higher"}, - { CERR_SetSystemStore_NotFound , "System store in set() not found"}, - { CERR_90FeatureOnlyVirtualKeyDictionary , "Custom virtual key names require store(version) '9.0'"}, - { CERR_InvalidIndex , "Invalid 'index' command"}, - { CERR_OutsInVirtualKeySection , "'outs' command is illegal in virtual key section"}, - { CERR_InvalidOuts , "Invalid 'outs' command"}, - { CERR_ContextInVirtualKeySection , "'context' command is illegal in virtual key section"}, - { CERR_InvalidUse , "Invalid 'use' command"}, - { CERR_GroupDoesNotExist , "Group does not exist"}, - { CERR_VirtualKeyNotAllowedHere , "Virtual key is not allowed here"}, - { CERR_InvalidSwitch , "Invalid 'switch' command"}, - { CERR_NoTokensFound , "No tokens found in line"}, - { CERR_InvalidLineContinuation , "Invalid line continuation"}, - { CERR_LineTooLong , "Line too long"}, - { CERR_InvalidCopyright , "Invalid 'copyright' command"}, - { CERR_CodeInvalidInThisSection , "This line is invalid in this section of the file"}, - { CERR_InvalidMessage , "Invalid 'message' command"}, - { CERR_InvalidLanguageName , "Invalid 'languagename' command"}, - { CERR_EndOfFile , "(no error - reserved code)"}, - { CERR_InvalidToken , "Invalid token found"}, - { CERR_InvalidBegin , "Invalid 'begin' command"}, - { CERR_InvalidName , "Invalid 'name' command"}, - { CERR_InvalidVersion , "Invalid 'version' command"}, - { CERR_InvalidLanguageLine , "Invalid 'language' command"}, - { CERR_LayoutButNoLanguage , "Layout command found but no language command"}, - { CERR_CannotCreateTempfile , "Cannot create temp file"}, - { CERR_90FeatureOnlyLayoutFile , "Touch layout file reference requires store(version) '9.0'or higher"}, - { CERR_90FeatureOnlyKeyboardVersion , "KeyboardVersion system store requires store(version) '9.0'or higher"}, - { CERR_KeyboardVersionFormatInvalid , "KeyboardVersion format is invalid, expecting dot-separated integers"}, - { CERR_ContextExHasInvalidOffset , "context() statement has offset out of range"}, - { CERR_90FeatureOnlyEmbedCSS , "Embedding CSS requires store(version) '9.0'or higher"}, - { CERR_90FeatureOnlyTargets , "TARGETS system store requires store(version) '9.0'or higher"}, - { CERR_ContextAndIndexInvalidInMatchNomatch , "context and index statements cannot be used in a match or nomatch statement"}, - { CERR_140FeatureOnlyContextAndNotAnyWeb , "For web and touch platforms, context() statement referring to notany() requires store(version) '14.0'or higher"}, - { CERR_ExpansionMustFollowCharacterOrVKey , "An expansion must follow a character or a virtual key"}, - { CERR_VKeyExpansionMustBeFollowedByVKey , "A virtual key expansion must be terminated by a virtual key"}, - { CERR_CharacterExpansionMustBeFollowedByCharacter , "A character expansion must be terminated by a character key"}, - { CERR_VKeyExpansionMustUseConsistentShift , "A virtual key expansion must use the same shift state for both terminators"}, - { CERR_ExpansionMustBePositive , "An expansion must have positive difference (i.e. A-Z, not Z-A)"}, - { CERR_CasedKeysMustContainOnlyVirtualKeys , "The &CasedKeys system store must contain only virtual keys or characters found on a US English keyboard"}, - { CERR_CasedKeysMustNotIncludeShiftStates , "The &CasedKeys system store must not include shift states"}, - { CERR_CasedKeysNotSupportedWithMnemonicLayout , "The &CasedKeys system store is not supported with mnemonic layouts"}, - { CERR_CannotUseReadWriteGroupFromReadonlyGroup , "Group used from a readonly group must also be readonly"}, - { CERR_StatementNotPermittedInReadonlyGroup , "Statement is not permitted in output of readonly group"}, - { CERR_OutputInReadonlyGroup , "Output is not permitted in a readonly group"}, - { CERR_NewContextGroupMustBeReadonly , "Group used in begin newContext must be readonly"}, - { CERR_PostKeystrokeGroupMustBeReadonly , "Group used in begin postKeystroke must be readonly"}, - { CERR_DuplicateGroup , "A group with this name has already been defined."}, - { CERR_DuplicateStore , "A store with this name has already been defined."}, - { CERR_RepeatedBegin , "Begin has already been set"}, - { CERR_VirtualKeyInContext , "Virtual keys are not permitted in context"}, - { CERR_OutsTooLong , "Store cannot be inserted with outs() as it makes the extended string too long" }, - { CERR_ExtendedStringTooLong , "Extended string is too long" }, - { CERR_VirtualKeyExpansionTooLong , "Virtual key expansion is too large" }, - { CERR_CharacterRangeTooLong , "Character range is too large and cannot be expanded" }, - - { CHINT_UnreachableRule , "This rule will never be matched as another rule takes precedence"}, - { CHINT_NonUnicodeFile , "Keyman Developer has detected that the file has ANSI encoding. Consider converting this file to UTF-8"}, - - { CWARN_TooManyWarnings , "Too many warnings or errors"}, - { CWARN_OldVersion , "The keyboard file is an old version"}, - { CWARN_BitmapNotUsed , "The 'bitmaps' statement is obsolete and only the first bitmap referred to will be used, you should use 'bitmap'."}, - { CWARN_CustomLanguagesNotSupported , "Languages over 0x1FF, 0x1F are not supported correctly by Windows. You should use no LANGUAGE line instead."}, - { CWARN_KeyBadLength , "There are too many characters in the keystroke part of the rule."}, - { CWARN_IndexStoreShort , "The store referenced in index() is shorter than the store referenced in any()"}, - { CWARN_UnicodeInANSIGroup , "A Unicode character was found in an ANSI group"}, - { CWARN_ANSIInUnicodeGroup , "An ANSI character was found in a Unicode group"}, - { CWARN_UnicodeSurrogateUsed , "A Unicode surrogate character was found. You should use Unicode scalar values to represent values > U+FFFF"}, - { CWARN_ReservedCharacter , "A Unicode character was found that should not be used"}, - { CWARN_Info , "Information"}, - { CWARN_VirtualKeyWithMnemonicLayout , "Virtual key used instead of virtual character key with a mnemonic layout"}, - { CWARN_VirtualCharKeyWithPositionalLayout , "Virtual character key used with a positional layout instead of mnemonic layout"}, - { CWARN_StoreAlreadyUsedAsOptionOrCall , "Store already used as an option or in a call statement and should not be used as a normal store"}, - { CWARN_StoreAlreadyUsedAsStoreOrCall , "Store already used as a normal store or in a call statement and should not be used as an option"}, - { CWARN_StoreAlreadyUsedAsStoreOrOption , "Store already used as a normal store or as an option and should not be used in a call statement"}, - { CWARN_PunctuationInEthnologueCode , "Punctuation should not be used to separate Ethnologue codes; instead use spaces"}, - { CWARN_PlatformNotInTargets , "The specified platform is not a target platform"}, - { CWARN_HeaderStatementIsDeprecated , "Header statements are deprecated; use instead the equivalent system store"}, - { CWARN_UseNotLastStatementInRule , "A rule with use() statements in the output should not have other content following the use() statements"}, - { CWARN_KVKFileIsInSourceFormat , ".kvk file should be binary but is an XML file"}, - { CWARN_DontMixChiralAndNonChiralModifiers , "Don't mix the use of left/right modifiers with non-left/right modifiers in the same platform"}, - { CWARN_MixingLeftAndRightModifiers , "Left and right modifiers should not both be used in the same rule"}, - { CWARN_LanguageHeadersDeprecatedInKeyman10 , "This language header has been deprecated in Keyman 10. Instead, add language metadata in the package file"}, - { CWARN_HotkeyHasInvalidModifier , "Hotkey has modifiers that are not supported. Use only SHIFT, CTRL and ALT"}, - { CWARN_NulNotFirstStatementInContext , "nul must be the first statement in the context"}, - { CWARN_IfShouldBeAtStartOfContext , "if, platform and baselayout should be at start of context (after nul, if present)"}, - { CWARN_KeyShouldIncludeNCaps , "Other rules which reference this key include CAPS or NCAPS modifiers, so this rule must include NCAPS modifier to avoid inconsistent matches"}, - { CWARN_VirtualKeyInOutput , "Virtual keys are not supported in output"}, - - { 0, nullptr } - }; - -KMX_CHAR *GetCompilerErrorString(KMX_DWORD code) -{ - for(int i = 0; CompilerErrors[i].ErrorCode; i++) { - if(CompilerErrors[i].ErrorCode == code) { - return ( KMX_CHAR*) CompilerErrors[i].Text; - } - } - return nullptr; -} diff --git a/developer/src/kmcmplib/src/CompMsg.h b/developer/src/kmcmplib/src/CompMsg.h deleted file mode 100644 index 5517ea88112..00000000000 --- a/developer/src/kmcmplib/src/CompMsg.h +++ /dev/null @@ -1,5 +0,0 @@ - -#include "km_types.h" -#include - -KMX_CHAR *GetCompilerErrorString(KMX_DWORD code) ; diff --git a/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp b/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp index 1b4b23bd2e2..155b1252589 100644 --- a/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp +++ b/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp @@ -20,13 +20,13 @@ bool CompileKeyboardBuffer(KMX_BYTE* infile, int sz, PFILE_KEYBOARD fk) kmcmp::FMnemonicLayout = FALSE; if (!fk) { - AddCompileError(CERR_SomewhereIGotItWrong); + ReportCompilerMessage(KmnCompilerMessages::FATAL_SomewhereIGotItWrong); return FALSE; } str = new KMX_WCHAR[LINESIZE]; if (!str) { - AddCompileError(CERR_CannotAllocateMemory); + ReportCompilerMessage(KmnCompilerMessages::FATAL_CannotAllocateMemory); return FALSE; } @@ -66,39 +66,42 @@ bool CompileKeyboardBuffer(KMX_BYTE* infile, int sz, PFILE_KEYBOARD fk) if(kmcmp::FShouldAddCompilerVersion) { u16sprintf(str,LINESIZE, L"Created with Keyman Developer version %d.%d.%d.%d", KEYMAN_VersionMajor, KEYMAN_VersionMinor, KEYMAN_VersionPatch, 0); - AddStore(fk, TSS_KEYMANCOPYRIGHT, str); + if(!AddStore(fk, TSS_KEYMANCOPYRIGHT, str)) { + return FALSE; + } } /* Add a system store for the Keyman edition number */ - AddStore(fk, TSS_CUSTOMKEYMANEDITION, u"0"); - AddStore(fk, TSS_CUSTOMKEYMANEDITIONNAME, u"Keyman"); + if(!AddStore(fk, TSS_CUSTOMKEYMANEDITION, u"0")) { + return FALSE; + } + if(!AddStore(fk, TSS_CUSTOMKEYMANEDITIONNAME, u"Keyman")) { + return FALSE; + } int offset = 0; // must preprocess for group and store names -> this isn't really necessary, but never mind! - while ((msg = ReadLine(infile, sz, offset, str, TRUE)) == CERR_None) + while ((msg = ReadLine(infile, sz, offset, str, TRUE)) == STATUS_Success) { p = str; switch (LineTokenType(&p)) { case T_VERSION: *(p + 4) = 0; - if ((msg = AddStore(fk, TSS_VERSION, p)) != CERR_None) { - AddCompileError(msg); + if (!AddStore(fk, TSS_VERSION, p)) { return FALSE; } break; case T_GROUP: - if ((msg = ProcessGroupLine(fk, p)) != CERR_None) { - AddCompileError(msg); + if (!ProcessGroupLine(fk, p)) { return FALSE; } break; case T_STORE: - if ((msg = ProcessStoreLine(fk, p)) != CERR_None) { - AddCompileError(msg); + if (!ProcessStoreLine(fk, p)) { return FALSE; } break; @@ -108,8 +111,8 @@ bool CompileKeyboardBuffer(KMX_BYTE* infile, int sz, PFILE_KEYBOARD fk) } } - if (msg != CERR_EndOfFile) { - AddCompileError(msg); + if (msg != STATUS_EndOfFile) { + ReportCompilerMessage(msg); return FALSE; } @@ -121,32 +124,30 @@ bool CompileKeyboardBuffer(KMX_BYTE* infile, int sz, PFILE_KEYBOARD fk) kmcmp::CodeConstants->reindex(); /* ReadLine will automatically skip over $Keyman lines, and parse wrapped lines */ - while ((msg = ReadLine(infile, sz, offset, str, FALSE)) == CERR_None) + while ((msg = ReadLine(infile, sz, offset, str, FALSE)) == STATUS_Success) { - msg = ParseLine(fk, str); - if (msg != CERR_None) { - AddCompileError(msg); + if(!ParseLine(fk, str)) { return FALSE; } } - if (msg != CERR_EndOfFile) { - AddCompileError(msg); + if (msg != STATUS_EndOfFile) { + ReportCompilerMessage(msg); return FALSE; } - ProcessGroupFinish(fk); + if(!ProcessGroupFinish(fk)) { + return FALSE; + } if (kmcmp::FSaveDebug) kmcmp::RecordDeadkeyNames(fk); /* Add the compiler version as a system store */ - if ((msg = kmcmp::AddCompilerVersionStore(fk)) != CERR_None) { - AddCompileError(msg); + if (!kmcmp::AddCompilerVersionStore(fk)) { return FALSE; } - if ((msg = BuildVKDictionary(fk)) != CERR_None) { - AddCompileError(msg); + if (!BuildVKDictionary(fk)) { return FALSE; } @@ -174,7 +175,7 @@ namespace kmcmp { void CopyExtraData(PFILE_KEYBOARD fk) { /* Copy stores */ PFILE_STORE store = fk->dpStoreArray; - for(int i = 0; i < fk->cxStoreArray; i++, store++) { + for(KMX_DWORD i = 0; i < fk->cxStoreArray; i++, store++) { KMCMP_COMPILER_RESULT_EXTRA_STORE extraStore; extraStore.storeType = (store->fIsStore ? STORETYPE_STORE : 0) | @@ -188,11 +189,11 @@ namespace kmcmp { } PFILE_GROUP group = fk->dpGroupArray; - for(int i = 0; i < fk->cxGroupArray; i++, group++) { + for(KMX_DWORD i = 0; i < fk->cxGroupArray; i++, group++) { KMCMP_COMPILER_RESULT_EXTRA_GROUP extraGroup; extraGroup.isReadOnly = group->fReadOnly; extraGroup.name = string_from_u16string(group->szName); fk->extra->groups.push_back(extraGroup); } } -} \ No newline at end of file +} diff --git a/developer/src/kmcmplib/src/Compiler.cpp b/developer/src/kmcmplib/src/Compiler.cpp index a7a603c7b3e..57702ee1f0f 100644 --- a/developer/src/kmcmplib/src/Compiler.cpp +++ b/developer/src/kmcmplib/src/Compiler.cpp @@ -97,36 +97,32 @@ #include #include #include +#include +#include #include "UnreachableRules.h" #include "CheckForDuplicates.h" -#include "kmx_u16.h" -#include +#include "km_u16.h" /* These macros are adapted from winnt.h and legacy use only */ #define MAKELANGID(p, s) ((((uint16_t)(s)) << 10) | (uint16_t)(p)) #define PRIMARYLANGID(lgid) ((uint16_t)(lgid) & 0x3ff) #define SUBLANGID(lgid) ((uint16_t)(lgid) >> 10) -#define COMPILE_ERROR_MAX_LEN (SZMAX_ERRORTEXT + 1 + 280) - using namespace kmcmp; - char ErrExtraLIB[ERR_EXTRA_LIB_LEN]; // utf-8 KMX_BOOL AWarnDeprecatedCode_GLOBAL_LIB; namespace kmcmp{ KMX_BOOL FShouldAddCompilerVersion = TRUE; KMX_BOOL FSaveDebug, FCompilerWarningsAsErrors; // I4865 // I4866 - int ErrChr; - int nErrors = 0; KMX_BOOL FMnemonicLayout = FALSE; KMX_BOOL FOldCharPosMatching = FALSE; int CompileTarget; int BeginLine[4]; KMX_BOOL IsValidCallStore(PFILE_STORE fs); - KMX_BOOL CheckStoreUsage(PFILE_KEYBOARD fk, int storeIndex, KMX_BOOL fIsStore, KMX_BOOL fIsOption, KMX_BOOL fIsCall); + void CheckStoreUsage(PFILE_KEYBOARD fk, int storeIndex, KMX_BOOL fIsStore, KMX_BOOL fIsOption, KMX_BOOL fIsCall); KMX_DWORD UTF32ToUTF16(int n, int *n1, int *n2); KMX_DWORD CheckUTF16(int n); int cmpkeys(const void *key, const void *elem); @@ -134,6 +130,7 @@ namespace kmcmp{ int xatoi(PKMX_WCHAR *p); int atoiW(PKMX_WCHAR p); +bool isIntegerWstring(PKMX_WCHAR p); void safe_wcsncpy(PKMX_WCHAR out, PKMX_WCHAR in, int cbMax); int GetDeadKey(PFILE_KEYBOARD fk, PKMX_WCHAR p); @@ -141,7 +138,7 @@ KMX_BOOL IsSameToken(PKMX_WCHAR *p, KMX_WCHAR const * token); KMX_DWORD GetRHS(PFILE_KEYBOARD fk, PKMX_WCHAR p, PKMX_WCHAR buf, int bufsize, int offset, int IsUnicode); PKMX_WCHAR GetDelimitedString(PKMX_WCHAR *p, KMX_WCHAR const * Delimiters, KMX_WORD Flags); KMX_DWORD GetXString(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_WCHAR const * token, PKMX_WCHAR output, int max, int offset, PKMX_WCHAR *newp, int isVKey, int isUnicode); -int GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store); +KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets); int GetGroupNum(PFILE_KEYBOARD fk, PKMX_WCHAR p); @@ -154,7 +151,7 @@ KMX_DWORD ImportBitmapFile(PFILE_KEYBOARD fk, PKMX_WCHAR szName, PKMX_DWORD File KMX_DWORD ExpandKp(PFILE_KEYBOARD fk, PFILE_KEY kpp, KMX_DWORD storeIndex); int GetVKCode(PFILE_KEYBOARD fk, PKMX_WCHAR p); // I3438 // TODO: Consolidate GetDeadKey and GetVKCode? -KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE sp); +KMX_BOOL ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE sp); KMX_DWORD process_if(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx); KMX_DWORD process_reset(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx); @@ -236,10 +233,7 @@ enum LinePrefixType { lptNone, lptKeymanAndKeymanWeb, lptKeymanWebOnly, lptKeyma /* Compile target */ -kmcmp_CompilerMessageProc msgproc = NULL; -kmcmp_LoadFileProc loadfileproc = NULL; - -void* msgprocContext = NULL; +kmcmp_LoadFileProc kmcmp::loadfileproc = NULL; int kmcmp::currentLine = 0; @@ -267,52 +261,8 @@ PKMX_STR wstrtostr(PKMX_WCHAR in) return result; } -KMX_BOOL kmcmp::AddCompileWarning(PKMX_CHAR buf) -{ - (*msgproc)(kmcmp::currentLine + 1, CWARN_Info, buf, msgprocContext); - return FALSE; -} - -KMX_BOOL AddCompileError(KMX_DWORD msg) -{ - KMX_CHAR szText[COMPILE_ERROR_MAX_LEN]; - KMX_CHAR* szTextp = NULL; - - if (msg & CERR_FATAL) - { - szTextp = GetCompilerErrorString(msg); - (*msgproc)(kmcmp::currentLine + 1, msg, szTextp, msgprocContext); - kmcmp::nErrors++; - return TRUE; - } - - if (msg & CERR_ERROR) - kmcmp::nErrors++; - szTextp = GetCompilerErrorString(msg); - - if (szTextp) { - strcpy(szText, szTextp); - } else { - snprintf(szText, COMPILE_ERROR_MAX_LEN, "Unknown error %x", msg); - } - - if (kmcmp::ErrChr > 0) { - char *szTextNull = strchr(szText, 0); - snprintf(szTextNull, COMPILE_ERROR_MAX_LEN-(szTextNull-szText), " character offset: %d", kmcmp::ErrChr); - } - - if (*ErrExtraLIB) { - char *szTextNull = strchr(szText, 0); - snprintf(szTextNull, COMPILE_ERROR_MAX_LEN-(szTextNull-szText), "%s", ErrExtraLIB); - } - - ErrChr = 0; *ErrExtraLIB =0; - if (!(*msgproc)(kmcmp::currentLine, msg, szText, msgprocContext)) return TRUE; - return FALSE; -} -KMX_DWORD ProcessBeginLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) -{ +KMX_BOOL ProcessBeginLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) { KMX_WCHAR tstr[128]; PKMX_WCHAR q, pp; int BeginMode; @@ -320,30 +270,42 @@ KMX_DWORD ProcessBeginLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) pp = p; - q = ( PKMX_WCHAR) u16chr(p, '>'); - if (!q) return CERR_NoTokensFound; + q = (PKMX_WCHAR) u16chr(p, '>'); + if (!q) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_NoTokensFound); + return FALSE; + } while (iswspace(*p)) p++; if (u16nicmp(p, u"unicode", 7) == 0) BeginMode = BEGIN_UNICODE; else if (u16nicmp(p, u"ansi", 4) == 0) BeginMode = BEGIN_ANSI; else if (u16nicmp(p, u"newContext", 10) == 0) BeginMode = BEGIN_NEWCONTEXT; else if (u16nicmp(p, u"postKeystroke", 13) == 0) BeginMode = BEGIN_POSTKEYSTROKE; - else if (*p != '>') return CERR_InvalidToken; + else if (*p != '>') { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidToken); + return FALSE; + } else BeginMode = BEGIN_ANSI; if(kmcmp::BeginLine[BeginMode] != -1) { - return CERR_RepeatedBegin; + ReportCompilerMessage(KmnCompilerMessages::ERROR_RepeatedBegin); + return FALSE; } kmcmp::BeginLine[BeginMode] = kmcmp::currentLine; - if ((msg = GetRHS(fk, p, tstr, 80, (int)(p - pp), FALSE)) != CERR_None) return msg; + if ((msg = GetRHS(fk, p, tstr, 80, (int)(p - pp), FALSE)) != STATUS_Success) { + ReportCompilerMessage(msg); + return FALSE; + } if (tstr[0] != UC_SENTINEL || tstr[1] != CODE_USE) { - return CERR_InvalidBegin; + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidBegin); + return FALSE; } if (tstr[3] != 0) { - return CERR_InvalidToken; + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidToken); + return FALSE; } if (BeginMode == BEGIN_ANSI || BeginMode == BEGIN_UNICODE) { @@ -356,20 +318,18 @@ KMX_DWORD ProcessBeginLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) /* Record a system store for the line number of the begin statement */ AddDebugStore(fk, BeginMode == BEGIN_UNICODE ? DEBUGSTORE_BEGIN u"Unicode" : DEBUGSTORE_BEGIN u"ANSI"); } - } - else { - PFILE_GROUP gp = &fk->dpGroupArray[tstr[2] - 1]; - if (!gp->fReadOnly) { - return BeginMode == BEGIN_NEWCONTEXT ? - CERR_NewContextGroupMustBeReadonly : - CERR_PostKeystrokeGroupMustBeReadonly; - } - return AddStore(fk, BeginMode == BEGIN_NEWCONTEXT ? TSS_BEGIN_NEWCONTEXT : TSS_BEGIN_POSTKEYSTROKE, tstr, NULL); + } else { + PFILE_GROUP gp = &fk->dpGroupArray[tstr[2] - 1]; + if (!gp->fReadOnly) { + ReportCompilerMessage(BeginMode == BEGIN_NEWCONTEXT ? + KmnCompilerMessages::ERROR_NewContextGroupMustBeReadonly : + KmnCompilerMessages::ERROR_PostKeystrokeGroupMustBeReadonly); + return FALSE; } + return AddStore(fk, BeginMode == BEGIN_NEWCONTEXT ? TSS_BEGIN_NEWCONTEXT : TSS_BEGIN_POSTKEYSTROKE, tstr, NULL); + } - - - return CERR_None; + return TRUE; } KMX_DWORD ValidateMatchNomatchOutput(PKMX_WCHAR p) { @@ -379,16 +339,15 @@ KMX_DWORD ValidateMatchNomatchOutput(PKMX_WCHAR p) { case CODE_CONTEXT: case CODE_CONTEXTEX: case CODE_INDEX: - return CERR_ContextAndIndexInvalidInMatchNomatch; + return KmnCompilerMessages::ERROR_ContextAndIndexInvalidInMatchNomatch; } } p = incxstr(p); } - return CERR_None; + return STATUS_Success; } -KMX_DWORD ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) -{ +KMX_BOOL ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) { PKMX_WCHAR p, q, pp; PFILE_GROUP gp; KMX_DWORD msg; @@ -410,49 +369,72 @@ KMX_DWORD ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) case T_BEGIN: // after a begin can be "Unicode" or "ANSI" or nothing (=ANSI) - if ((msg = ProcessBeginLine(fk, p)) != CERR_None) return msg; + if (!ProcessBeginLine(fk, p)) { + return FALSE; + } break; case T_GROUP: - if (fk->currentGroup == 0xFFFFFFFF) fk->currentGroup = 0; - else - { - if ((msg = ProcessGroupFinish(fk)) != CERR_None) return msg; // finish off previous group first? + if (fk->currentGroup == 0xFFFFFFFF) { + fk->currentGroup = 0; + } else { + if (!ProcessGroupFinish(fk)) { + return FALSE; // finish off previous group first? + } fk->currentGroup++; } - // if( (err = ProcessGroupLine( fk, p )) != CERR_None ) return err; break; case T_NAME: kmcmp::WarnDeprecatedHeader(); // I4866 q = GetDelimitedString(&p, u"\"\"", 0); - if (!q) return CERR_InvalidName; + if (!q) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidName); + return FALSE; + } - if ((msg = AddStore(fk, TSS_NAME, q)) != CERR_None) return msg; + if (!AddStore(fk, TSS_NAME, q)) { + return FALSE; + } break; case T_COPYRIGHT: kmcmp::WarnDeprecatedHeader(); // I4866 q = GetDelimitedString(&p, u"\"\"", 0); - if (!q) return CERR_InvalidCopyright; + if (!q) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidCopyright); + return FALSE; + } - if ((msg = AddStore(fk, TSS_COPYRIGHT, q)) != CERR_None) return msg; + if (!AddStore(fk, TSS_COPYRIGHT, q)) { + return FALSE; + } break; case T_MESSAGE: kmcmp::WarnDeprecatedHeader(); // I4866 q = GetDelimitedString(&p, u"\"\"", 0); - if (!q) return CERR_InvalidMessage; + if (!q) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidMessage); + return FALSE; + } - if ((msg = AddStore(fk, TSS_MESSAGE, q)) != CERR_None) return msg; + if (!AddStore(fk, TSS_MESSAGE, q)) { + return FALSE; + } break; case T_LANGUAGENAME: kmcmp::WarnDeprecatedHeader(); // I4866 q = GetDelimitedString(&p, u"\"\"", 0); - if (!q) return CERR_InvalidLanguageName; + if (!q) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidLanguageName); + return FALSE; + } - if ((msg = AddStore(fk, TSS_LANGUAGENAME, q)) != CERR_None) return msg; + if (!AddStore(fk, TSS_LANGUAGENAME, q)) { + return FALSE; + } break; case T_LANGUAGE: @@ -461,7 +443,9 @@ KMX_DWORD ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) KMX_WCHAR *tokcontext = NULL; q = u16tok(p, p_sep, &tokcontext); // I3481 - if ((msg = AddStore(fk, TSS_LANGUAGE, q)) != CERR_None) return msg; + if (!AddStore(fk, TSS_LANGUAGE, q)) { + return FALSE; + } break; } case T_LAYOUT: @@ -469,79 +453,118 @@ KMX_DWORD ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) kmcmp::WarnDeprecatedHeader(); // I4866 KMX_WCHAR *tokcontext = NULL; q = u16tok(p, p_sep, &tokcontext); // I3481 - if ((msg = AddStore(fk, TSS_LAYOUT, q)) != CERR_None) return msg; + if (!AddStore(fk, TSS_LAYOUT, q)) { + return FALSE; + } break; } case T_CAPSOFF: kmcmp::WarnDeprecatedHeader(); // I4866 - if ((msg = AddStore(fk, TSS_CAPSALWAYSOFF, u"1")) != CERR_None) return msg; + if (!AddStore(fk, TSS_CAPSALWAYSOFF, u"1")) { + return FALSE; + } break; case T_CAPSON: kmcmp::WarnDeprecatedHeader(); // I4866 - if ((msg = AddStore(fk, TSS_CAPSONONLY, u"1")) != CERR_None) return msg; + if (!AddStore(fk, TSS_CAPSONONLY, u"1")) { + return FALSE; + } break; case T_SHIFT: kmcmp::WarnDeprecatedHeader(); // I4866 - if ((msg = AddStore(fk, TSS_SHIFTFREESCAPS, u"1")) != CERR_None) return msg; + if (!AddStore(fk, TSS_SHIFTFREESCAPS, u"1")) { + return FALSE; + } break; case T_HOTKEY: { kmcmp::WarnDeprecatedHeader(); // I4866 KMX_WCHAR *tokcontext = NULL; - if ((q = u16tok(p, p_sep, &tokcontext)) == NULL) return CERR_CodeInvalidInThisSection; // I3481 - if ((msg = AddStore(fk, TSS_HOTKEY, q)) != CERR_None) return msg; + if ((q = u16tok(p, p_sep, &tokcontext)) == NULL) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_CodeInvalidInThisSection); // I3481 + return FALSE; + } + if (!AddStore(fk, TSS_HOTKEY, q)) { + return FALSE; + } break; } case T_BITMAP: { kmcmp::WarnDeprecatedHeader(); // I4866 KMX_WCHAR *tokcontext = NULL; - if ((q = u16tok(p, p_sep, &tokcontext)) == NULL) return CERR_InvalidBitmapLine; // I3481 + if ((q = u16tok(p, p_sep, &tokcontext)) == NULL) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidBitmapLine); // I3481 + return FALSE; + } while (iswspace(*q)) q++; if (*q == '"') { p = q; q = GetDelimitedString(&p, u"\"\"", 0); - if (!q) return CERR_InvalidBitmapLine; + if (!q) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidBitmapLine); + return FALSE; + } } - if ((msg = AddStore(fk, TSS_BITMAP, q)) != CERR_None) return msg; + if (!AddStore(fk, TSS_BITMAP, q)) { + return FALSE; + } break; } case T_BITMAPS: { kmcmp::WarnDeprecatedHeader(); // I4866 KMX_WCHAR *tokcontext = NULL; - AddWarning(CWARN_BitmapNotUsed); + ReportCompilerMessage(KmnCompilerMessages::WARN_BitmapNotUsed); - if ((q = u16tok(p, p_sep, &tokcontext)) == NULL) return CERR_InvalidBitmapLine; // I3481 + if ((q = u16tok(p, p_sep, &tokcontext)) == NULL) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidBitmapLine); // I3481 + return FALSE; + } - if ((PKMX_WCHAR) u16chr(q, ',')) *(PKMX_WCHAR) u16chr(q, ',') = 0; - if ((msg = AddStore(fk, TSS_BITMAP, q)) != CERR_None) return msg; + if ((PKMX_WCHAR) u16chr(q, ',')) { + *(PKMX_WCHAR) u16chr(q, ',') = 0; + } + if (!AddStore(fk, TSS_BITMAP, q)) { + return FALSE; + } break; } case T_KEYTOKEY: // A rule - if (fk->currentGroup == 0xFFFFFFFF) return CERR_CodeInvalidInThisSection; - if ((msg = ProcessKeyLine(fk, p, IsUnicode)) != CERR_None) return msg; + if (fk->currentGroup == 0xFFFFFFFF) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_CodeInvalidInThisSection); + return FALSE; + } + if ((msg = ProcessKeyLine(fk, p, IsUnicode)) != STATUS_Success) { + ReportCompilerMessage(msg); + return FALSE; + } break; case T_MATCH: - if (fk->currentGroup == 0xFFFFFFFF) return CERR_CodeInvalidInThisSection; + if (fk->currentGroup == 0xFFFFFFFF) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_CodeInvalidInThisSection); + return FALSE; + } { PKMX_WCHAR buf = new KMX_WCHAR[GLOBAL_BUFSIZE]; - if ((msg = GetRHS(fk, p, buf, GLOBAL_BUFSIZE - 1, (int)(p - pp), IsUnicode)) != CERR_None) + if ((msg = GetRHS(fk, p, buf, GLOBAL_BUFSIZE - 1, (int)(p - pp), IsUnicode)) != STATUS_Success) { delete[] buf; - return msg; + ReportCompilerMessage(msg); + return FALSE; } - if ((msg = ValidateMatchNomatchOutput(buf)) != CERR_None) { + if ((msg = ValidateMatchNomatchOutput(buf)) != STATUS_Success) { delete[] buf; - return msg; + ReportCompilerMessage(msg); + return FALSE; } gp = &fk->dpGroupArray[fk->currentGroup]; @@ -557,7 +580,7 @@ KMX_DWORD ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) //swprintf(tstr, "%d", fk->currentGroup); /* Record a system store for the line number of the begin statement */ //wcscpy(tstr, DEBUGSTORE_MATCH); - u16sprintf(tstr, _countof(tstr), L"%ls%d ", u16fmt(DEBUGSTORE_MATCH).c_str(), (int) fk->currentGroup); + u16sprintf(tstr, _countof(tstr), L"%ls%d ", DEBUGSTORE_MATCH_L, (int) fk->currentGroup); u16ncat(tstr, gp->szName, _countof(tstr)); AddDebugStore(fk, tstr); @@ -566,18 +589,23 @@ KMX_DWORD ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) break; case T_NOMATCH: - if (fk->currentGroup == 0xFFFFFFFF) return CERR_CodeInvalidInThisSection; + if (fk->currentGroup == 0xFFFFFFFF) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_CodeInvalidInThisSection); + return FALSE; + } { PKMX_WCHAR buf = new KMX_WCHAR[GLOBAL_BUFSIZE]; - if ((msg = GetRHS(fk, p, buf, GLOBAL_BUFSIZE, (int)(p - pp), IsUnicode)) != CERR_None) + if ((msg = GetRHS(fk, p, buf, GLOBAL_BUFSIZE, (int)(p - pp), IsUnicode)) != STATUS_Success) { delete[] buf; - return msg; + ReportCompilerMessage(msg); + return FALSE; } - if ((msg = ValidateMatchNomatchOutput(buf)) != CERR_None) { + if ((msg = ValidateMatchNomatchOutput(buf)) != STATUS_Success) { delete[] buf; - return msg; + ReportCompilerMessage(msg); + return FALSE; } gp = &fk->dpGroupArray[fk->currentGroup]; @@ -591,7 +619,7 @@ KMX_DWORD ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) { KMX_WCHAR tstr[128]; /* Record a system store for the line number of the begin statement */ - u16sprintf(tstr, _countof(tstr), L"%ls%d ", u16fmt(DEBUGSTORE_NOMATCH).c_str(), (int) fk->currentGroup); + u16sprintf(tstr, _countof(tstr), L"%ls%d ", DEBUGSTORE_NOMATCH_L, (int) fk->currentGroup); u16ncat(tstr, gp->szName, _countof(tstr)); AddDebugStore(fk, tstr); } @@ -599,21 +627,25 @@ KMX_DWORD ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) break; default: - return CERR_InvalidToken; + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidToken); + return FALSE; } - return CERR_None; + return TRUE; } //********************************************************************************************************************** -KMX_DWORD ProcessGroupLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) +KMX_BOOL ProcessGroupLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) { PFILE_GROUP gp; PKMX_WCHAR q; gp = new FILE_GROUP[fk->cxGroupArray + 1]; - if (!gp) return CERR_CannotAllocateMemory; + if (!gp) { + ReportCompilerMessage(KmnCompilerMessages::FATAL_CannotAllocateMemory); + return FALSE; + } if (fk->dpGroupArray) { @@ -631,7 +663,10 @@ KMX_DWORD ProcessGroupLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) gp->cxKeyArray = 0; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q) return CERR_InvalidGroupLine; + if (!q) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidGroupLine); + return FALSE; + } gp->fUsingKeys = FALSE; gp->fReadOnly = IsSameToken(&p, u"readonly"); @@ -648,7 +683,7 @@ KMX_DWORD ProcessGroupLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) { KMX_WCHAR tstr[128]; /* Record a system store for the line number of the begin statement */ - u16sprintf(tstr, _countof(tstr), L"%ls%d ", u16fmt(DEBUGSTORE_GROUP).c_str(), fk->cxGroupArray - 1); + u16sprintf(tstr, _countof(tstr), L"%ls%d ", DEBUGSTORE_GROUP_L, fk->cxGroupArray - 1); u16ncat(tstr, gp->szName, _countof(tstr)); AddDebugStore(fk, tstr); } @@ -694,49 +729,61 @@ int kmcmp::cmpkeys(const void *key, const void *elem) return(char_key - char_elem); // akey->Key - aelem->Key); } -KMX_DWORD ProcessGroupFinish(PFILE_KEYBOARD fk) -{ +KMX_BOOL ProcessGroupFinish(PFILE_KEYBOARD fk) { PFILE_GROUP gp; KMX_DWORD msg; - if (fk->currentGroup == 0xFFFFFFFF) return CERR_None; - // Just got to first group - so nothing to finish yet + if (fk->currentGroup == 0xFFFFFFFF) { + // Just got to first group - so nothing to finish yet + return TRUE; + } gp = &fk->dpGroupArray[fk->currentGroup]; // Finish off the previous group stuff! - if ((msg = ExpandCapsRulesForGroup(fk, gp)) != CERR_None) return msg; + if ((msg = ExpandCapsRulesForGroup(fk, gp)) != STATUS_Success) { + ReportCompilerMessage(msg); + return FALSE; + } qsort(gp->dpKeyArray, gp->cxKeyArray, sizeof(FILE_KEY), kmcmp::cmpkeys); - return VerifyUnreachableRules(gp); + VerifyUnreachableRules(gp); + + return TRUE; } /*************************************** * Store management */ -KMX_DWORD ProcessStoreLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) -{ +KMX_BOOL ProcessStoreLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) { PKMX_WCHAR q, pp; PFILE_STORE sp; - //WCHAR temp[GLOBAL_BUFSIZE]; KMX_DWORD msg; int i = 0; pp = p; - if ((q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL)) == NULL) return CERR_InvalidStoreLine; + if ((q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL)) == NULL) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidStoreLine); + return FALSE; + } - if (*q == *SSN__PREFIX) - { - for (i = 0; StoreTokens[i]; i++) - if (!u16icmp(q, StoreTokens[i])) // I3481 + if (*q == *SSN__PREFIX) { + for (i = 0; StoreTokens[i]; i++) { + if (!u16icmp(q, StoreTokens[i])) { // I3481 break; - if (!StoreTokens[i]) return CERR_InvalidSystemStore; + } + } + if (!StoreTokens[i]) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidSystemStore); + return FALSE; + } } if(!resizeStoreArray(fk)) { - return CERR_CannotAllocateMemory; + ReportCompilerMessage(KmnCompilerMessages::FATAL_CannotAllocateMemory); + return FALSE; } sp = &fk->dpStoreArray[fk->cxStoreArray]; @@ -751,10 +798,10 @@ KMX_DWORD ProcessStoreLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) { PKMX_WCHAR temp = new KMX_WCHAR[GLOBAL_BUFSIZE]; - if ((msg = GetXString(fk, p, u"c\n", temp, GLOBAL_BUFSIZE - 1, (int)(p - pp), &p, FALSE, TRUE)) != CERR_None) - { + if ((msg = GetXString(fk, p, u"c\n", temp, GLOBAL_BUFSIZE - 1, (int)(p - pp), &p, FALSE, TRUE)) != STATUS_Success) { delete[] temp; - return msg; + ReportCompilerMessage(msg); + return FALSE; } sp->dwSystemID = i; @@ -765,23 +812,29 @@ KMX_DWORD ProcessStoreLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) } if (xstrlen(sp->dpString) == 1 && *sp->dpString != UC_SENTINEL && - sp->dwSystemID == 0 && (fk->version >= VERSION_60 || fk->version == 0)) - { + sp->dwSystemID == 0 && (fk->version >= VERSION_60 || fk->version == 0)) { // In this case, we want to change behaviour for older versioned keyboards so that // we don't mix up named character codes which weren't supported in 5.x - VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_NamedCodes); + if(!VerifyKeyboardVersion(fk, VERSION_60)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_60FeatureOnly_NamedCodes); + return FALSE; + } // Add a single char store as a defined character constant - if (Uni_IsSurrogate1(*sp->dpString)) + if (Uni_IsSurrogate1(*sp->dpString)) { kmcmp::CodeConstants->AddCode(Uni_SurrogateToUTF32(sp->dpString[0], sp->dpString[1]), sp->szName, fk->cxStoreArray); - else + } else { kmcmp::CodeConstants->AddCode(sp->dpString[0], sp->szName, fk->cxStoreArray); + } kmcmp::CodeConstants->reindex(); // has to be done after every character add due to possible use in another store. // I4982 } fk->cxStoreArray++; // increment now, because GetXString refers to stores - if (i > 0) - if ((msg = ProcessSystemStore(fk, i, sp)) != CERR_None) return msg; + if (i > 0) { + if (!ProcessSystemStore(fk, i, sp)) { + return FALSE; + } + } return CheckForDuplicateStore(fk, sp); } @@ -806,12 +859,13 @@ bool resizeStoreArray(PFILE_KEYBOARD fk) { * reallocates the key array in increments of 100 */ bool resizeKeyArray(PFILE_GROUP gp, int increment) { - if((gp->cxKeyArray + increment - 1) % 100 < increment) { - PFILE_KEY kp = new FILE_KEY[((gp->cxKeyArray + increment)/100 + 1) * 100]; + const int cxKeyArray = (int)gp->cxKeyArray; + if((cxKeyArray + increment - 1) % 100 < increment) { + PFILE_KEY kp = new FILE_KEY[((cxKeyArray + increment)/100 + 1) * 100]; if (!kp) return false; if (gp->dpKeyArray) { - memcpy(kp, gp->dpKeyArray, gp->cxKeyArray * sizeof(FILE_KEY)); + memcpy(kp, gp->dpKeyArray, cxKeyArray * sizeof(FILE_KEY)); delete[] gp->dpKeyArray; } @@ -820,11 +874,11 @@ bool resizeKeyArray(PFILE_GROUP gp, int increment) { return true; } -KMX_DWORD AddStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, const KMX_WCHAR * str, KMX_DWORD *dwStoreID) -{ +KMX_BOOL AddStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, const KMX_WCHAR * str, KMX_DWORD *dwStoreID) { PFILE_STORE sp; if(!resizeStoreArray(fk)) { - return CERR_CannotAllocateMemory; + ReportCompilerMessage(KmnCompilerMessages::FATAL_CannotAllocateMemory); + return FALSE; } sp = &fk->dpStoreArray[fk->cxStoreArray]; @@ -847,7 +901,7 @@ KMX_DWORD AddStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, const KMX_WCHAR * str, fk->cxStoreArray++; - return ProcessSystemStore( fk, SystemID, sp); + return ProcessSystemStore(fk, SystemID, sp); } KMX_DWORD AddDebugStore(PFILE_KEYBOARD fk, KMX_WCHAR const * str) @@ -857,7 +911,7 @@ KMX_DWORD AddDebugStore(PFILE_KEYBOARD fk, KMX_WCHAR const * str) u16sprintf(tstr, _countof(tstr), L"%d", kmcmp::currentLine); // I3481 if(!resizeStoreArray(fk)) { - return CERR_CannotAllocateMemory; + return KmnCompilerMessages::FATAL_CannotAllocateMemory; } sp = &fk->dpStoreArray[fk->cxStoreArray]; @@ -874,13 +928,12 @@ KMX_DWORD AddDebugStore(PFILE_KEYBOARD fk, KMX_WCHAR const * str) sp->dwSystemID = TSS_DEBUG_LINE; fk->cxStoreArray++; - return CERR_None; + return STATUS_Success; } PKMX_WCHAR pssBuf = NULL; -KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE sp) -{ +KMX_BOOL ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE sp) { //WCHAR buf[GLOBAL_BUFSIZE]; int i, j; KMX_DWORD msg; @@ -891,9 +944,14 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE switch (SystemID) { + case TSS_BASELAYOUT: + break; + case TSS_BITMAP: - if ((msg = ImportBitmapFile(fk, sp->dpString, &fk->dwBitmapSize, &fk->lpBitmap)) != CERR_None) - return msg; + if ((msg = ImportBitmapFile(fk, sp->dpString, &fk->dwBitmapSize, &fk->lpBitmap)) != STATUS_Success) { + ReportCompilerMessage(msg); + return FALSE; + } break; case TSS_CALLDEFINITION: @@ -916,16 +974,31 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE case TSS_COPYRIGHT: break; + case TSS_CUSTOMKEYMANEDITION: + break; + + case TSS_CUSTOMKEYMANEDITIONNAME: + break; + case TSS_DEBUG_LINE: break; case TSS_ETHNOLOGUECODE: - VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_EthnologueCode); - if ((msg = ProcessEthnologueStore(sp->dpString)) != CERR_None) return msg; // I2646 + if(!VerifyKeyboardVersion(fk, VERSION_60)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_60FeatureOnly_EthnologueCode); + return FALSE; + } + if ((msg = ProcessEthnologueStore(sp->dpString)) != STATUS_Success) { + ReportCompilerMessage(msg); + return FALSE; // I2646 + } break; case TSS_HOTKEY: - if ((msg = ProcessHotKey(sp->dpString, &fk->dwHotKey)) != CERR_None) return msg; + if ((msg = ProcessHotKey(sp->dpString, &fk->dwHotKey)) != STATUS_Success) { + ReportCompilerMessage(msg); + return FALSE; + } u16sprintf(buf, GLOBAL_BUFSIZE, L"%d", (int)fk->dwHotKey); // I3481 delete[] sp->dpString; sp->dpString = new KMX_WCHAR[u16len(buf) + 1]; @@ -933,20 +1006,30 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE break; case TSS_INCLUDECODES: - VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_NamedCodes); + if(!VerifyKeyboardVersion(fk, VERSION_60)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_60FeatureOnly_NamedCodes); + return FALSE; + } if (!kmcmp::CodeConstants->LoadFile(fk, sp->dpString)) { - return CERR_CannotLoadIncludeFile; + ReportCompilerMessage(KmnCompilerMessages::ERROR_CannotLoadIncludeFile); + return FALSE; } kmcmp::CodeConstants->reindex(); // I4982 break; + case TSS_KEYMANCOPYRIGHT: + break; + case TSS_LANGUAGE: { KMX_WCHAR *context = NULL; KMX_WCHAR sep_c[3] = u", "; PKMX_WCHAR p_sep_c = sep_c; q = u16tok(sp->dpString, p_sep_c, &context); // I3481 - if (!q) return CERR_InvalidLanguageLine; + if (!q) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidLanguageLine); + return FALSE; + } i = xatoi(&q); KMX_WCHAR sep_n[4] = u" c\n"; @@ -954,15 +1037,23 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE q = u16tok(NULL, p_sep_n, &context); // I3481 if (!q) { - VERIFY_KEYBOARD_VERSION(fk, VERSION_70, CERR_InvalidLanguageLine); + if(!VerifyKeyboardVersion(fk, VERSION_70)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidLanguageLine); + return FALSE; + } j = SUBLANGID(i); i = PRIMARYLANGID(i); } else j = xatoi(&q); - if (i < 1 || j < 1 || i > 0x3FF || j > 0x3F) return CERR_InvalidLanguageLine; - if (i >= 0x200 || j >= 0x20) AddWarning(CWARN_CustomLanguagesNotSupported); + if (i < 1 || j < 1 || i > 0x3FF || j > 0x3F) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidLanguageLine); + return FALSE; + } + if (i >= 0x200 || j >= 0x20) { + ReportCompilerMessage(KmnCompilerMessages::WARN_CustomLanguagesNotSupported); + } fk->KeyboardID = (KMX_DWORD)MAKELANGID(i, j); @@ -976,8 +1067,14 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE case TSS_LANGUAGENAME: break; + case TSS_LAYER: + break; + case TSS_LAYOUT: - if (fk->KeyboardID == 0) return CERR_LayoutButNoLanguage; + if (fk->KeyboardID == 0) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_LayoutButNoLanguage); + return FALSE; + } q = sp->dpString; @@ -988,12 +1085,16 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE break; case TSS_MNEMONIC: - VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_MnemonicLayout); + if(!VerifyKeyboardVersion(fk, VERSION_60)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_60FeatureOnly_MnemonicLayout); + return FALSE; + }; kmcmp::FMnemonicLayout = atoiW(sp->dpString) == 1; if (kmcmp::FMnemonicLayout && FindSystemStore(fk, TSS_CASEDKEYS) != NULL) { // The &CasedKeys system store is not supported for // mnemonic layouts - return CERR_CasedKeysNotSupportedWithMnemonicLayout; + ReportCompilerMessage(KmnCompilerMessages::ERROR_CasedKeysNotSupportedWithMnemonicLayout); + return FALSE; } break; @@ -1001,19 +1102,31 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE break; case TSS_OLDCHARPOSMATCHING: - VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_OldCharPosMatching); + if(!VerifyKeyboardVersion(fk, VERSION_60)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_60FeatureOnly_OldCharPosMatching); + return FALSE; + } kmcmp::FOldCharPosMatching = atoiW(sp->dpString); break; + case TSS_PLATFORM: + case TSS_PLATFORM_MATCH: + case TSS_PLATFORM_NOMATCH: + // Never matched but included for completeness + break; + case TSS_SHIFTFREESCAPS: if (*sp->dpString == u'1') fk->dwFlags |= KF_SHIFTFREESCAPS; break; case TSS_VERSION: - if ((fk->dwFlags & KF_AUTOMATICVERSION) == 0) return CERR_VersionAlreadyIncluded; + if ((fk->dwFlags & KF_AUTOMATICVERSION) == 0) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_VersionAlreadyIncluded); + return FALSE; + } p = sp->dpString; if (u16tof (p) < 5.0) { - AddWarning(CWARN_OldVersion); + ReportCompilerMessage(KmnCompilerMessages::WARN_OldVersion); } if (u16ncmp(p, u"3.0", 3) == 0) fk->version = VERSION_50; //0x0a0b000n= a.bn @@ -1032,7 +1145,10 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE else if (u16ncmp(p, u"16.0", 4) == 0) fk->version = VERSION_160; // KMXPlus else if (u16ncmp(p, u"17.0", 4) == 0) fk->version = VERSION_170; // Flicks and gestures - else return CERR_InvalidVersion; + else { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidVersion); + return FALSE; + } if (fk->version < VERSION_60) kmcmp::FOldCharPosMatching = TRUE; @@ -1041,7 +1157,10 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE break; case TSS_VISUALKEYBOARD: - VERIFY_KEYBOARD_VERSION(fk, VERSION_70, CERR_70FeatureOnly); + if(!VerifyKeyboardVersion(fk, VERSION_70)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_70FeatureOnly); + return FALSE; + } { // Store extra metadata for callers as we mutate this store during // compilation @@ -1071,27 +1190,44 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE break; case TSS_KMW_RTL: case TSS_KMW_HELPTEXT: - VERIFY_KEYBOARD_VERSION(fk, VERSION_70, CERR_70FeatureOnly); + if(!VerifyKeyboardVersion(fk, VERSION_70)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_70FeatureOnly); + return FALSE; + } break; case TSS_KMW_HELPFILE: case TSS_KMW_EMBEDJS: - VERIFY_KEYBOARD_VERSION(fk, VERSION_70, CERR_70FeatureOnly); + if(!VerifyKeyboardVersion(fk, VERSION_70)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_70FeatureOnly); + return FALSE; + } break; case TSS_KMW_EMBEDCSS: - VERIFY_KEYBOARD_VERSION(fk, VERSION_90, CERR_90FeatureOnlyEmbedCSS); + if(!VerifyKeyboardVersion(fk, VERSION_90)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_90FeatureOnlyEmbedCSS); + return FALSE; + } break; case TSS_TARGETS: // I4504 - VERIFY_KEYBOARD_VERSION(fk, VERSION_90, CERR_90FeatureOnlyTargets); - fk->extra->targets = GetCompileTargetsFromTargetsStore(sp->dpString); + if(!VerifyKeyboardVersion(fk, VERSION_90)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_90FeatureOnlyTargets); + return FALSE; + } + if(!GetCompileTargetsFromTargetsStore(sp->dpString, fk->extra->targets)) { + return FALSE; + } break; case TSS_WINDOWSLANGUAGES: { + if(!VerifyKeyboardVersion(fk, VERSION_70)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_70FeatureOnly); + return FALSE; + } KMX_WCHAR *context = NULL; - VERIFY_KEYBOARD_VERSION(fk, VERSION_70, CERR_70FeatureOnly); size_t szQ = u16len(sp->dpString) * 6 + 1; // I3481 q = new KMX_WCHAR[szQ]; // guaranteed to be enough space for recoding *q = 0; KMX_WCHAR *r = q; @@ -1107,7 +1243,8 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE if (i < 1 || j < 1 || i > 0x3FF || j > 0x3F) { delete[] q; - return CERR_InvalidLanguageLine; + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidLanguageLine); + return FALSE; } u16sprintf(r, szQ - (size_t)(r - q), L"x%04.4x ", n); // I3481 @@ -1122,28 +1259,42 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE break; } case TSS_COMPARISON: - VERIFY_KEYBOARD_VERSION(fk, VERSION_80, CERR_80FeatureOnly); + if(!VerifyKeyboardVersion(fk, VERSION_80)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_80FeatureOnly); + return FALSE; + } break; case TSS_VKDICTIONARY: // I3438 - VERIFY_KEYBOARD_VERSION(fk, VERSION_90, CERR_90FeatureOnlyVirtualKeyDictionary); + if(!VerifyKeyboardVersion(fk, VERSION_90)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_90FeatureOnlyVirtualKeyDictionary); + return FALSE; + } break; case TSS_LAYOUTFILE: // I3483 - VERIFY_KEYBOARD_VERSION(fk, VERSION_90, CERR_90FeatureOnlyLayoutFile); // I4140 + if(!VerifyKeyboardVersion(fk, VERSION_90)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_90FeatureOnlyLayoutFile); + return FALSE; + } // Used by KMW compiler break; case TSS_KEYBOARDVERSION: // I4140 - VERIFY_KEYBOARD_VERSION(fk, VERSION_90, CERR_90FeatureOnlyKeyboardVersion); + if(!VerifyKeyboardVersion(fk, VERSION_90)) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_90FeatureOnlyKeyboardVersion); + return FALSE; + } if (!IsValidKeyboardVersion(sp->dpString)) { - return CERR_KeyboardVersionFormatInvalid; + ReportCompilerMessage(KmnCompilerMessages::ERROR_KeyboardVersionFormatInvalid); + return FALSE; } break; case TSS_CASEDKEYS: - if ((msg = VerifyCasedKeys(sp)) != CERR_None) { - return msg; + if ((msg = VerifyCasedKeys(sp)) != STATUS_Success) { + ReportCompilerMessage(msg); + return FALSE; } break; @@ -1163,12 +1314,13 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE break; default: - return CERR_InvalidSystemStore; + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidSystemStore); + return FALSE; } - return CERR_None; + return TRUE; } -int GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store) { +KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets) { // Compile to .kmx const std::vector KMXKeymanTargets{ u"windows", u"macosx", u"linux", u"desktop" @@ -1182,33 +1334,58 @@ int GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store) { const std::u16string AnyTarget = u"any"; - int result = 0; + targets = 0; auto p = new KMX_WCHAR[u16len(store)+1]; u16cpy(p, store); KMX_WCHAR* ctx; auto token = u16tok(p, u" ", &ctx); while(token) { - if(AnyTarget == token) { - result |= COMPILETARGETS_KMX | COMPILETARGETS_JS; - } - for(auto p: KMXKeymanTargets) { - if(p == token) result |= COMPILETARGETS_KMX; - } - for(auto p: KMWKeymanTargets) { - if(p == token) result |= COMPILETARGETS_JS; - } + bool found = false; + if(*token) { + if(AnyTarget == token) { + targets |= COMPILETARGETS_KMX | COMPILETARGETS_JS; + found = true; + } + for(auto target: KMXKeymanTargets) { + if(target == token) { + targets |= COMPILETARGETS_KMX; + found = true; + } + } + for(auto target: KMWKeymanTargets) { + if(target == token) { + targets |= COMPILETARGETS_JS; + found = true; + } + } + if(!found) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidTarget, { + /*target*/ string_from_u16string(token).c_str() + }); + delete[] p; + targets = 0; + return FALSE; + } + } token = u16tok(nullptr, u" ", &ctx); - - // Future: consider warnings on invalid compile targets? } delete[] p; - return result; + if(targets == 0) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_NoTargetsSpecified); + return FALSE; + } + + return TRUE; } KMX_BOOL IsValidKeyboardVersion(KMX_WCHAR *dpString) { // I4140 - /* version format \d+(\.\d+)* e.g. 9.0.3, 1.0, 1.2.3.4, 6.2.1.4.6.4, blank is not allowed */ + /** + version format: /^\d+(\.\d+)*$/ + e.g. 9.0.3, 1.0, 1.2.3.4, 6.2.1.4.6.4, 11.22.3 are all ok; + empty string is not permitted; whitespace is not permitted + */ do { if (!iswdigit(*dpString)) { @@ -1229,17 +1406,16 @@ KMX_BOOL IsValidKeyboardVersion(KMX_WCHAR *dpString) { // I4140 } -KMX_DWORD kmcmp::AddCompilerVersionStore(PFILE_KEYBOARD fk) -{ - KMX_DWORD msg; - +KMX_BOOL kmcmp::AddCompilerVersionStore(PFILE_KEYBOARD fk) { if(!kmcmp::FShouldAddCompilerVersion) { - return CERR_None; + return TRUE; } - if ((msg = AddStore(fk, TSS_COMPILEDVERSION, KEYMAN_VersionWin_W16)) != CERR_None) return msg; + if (!AddStore(fk, TSS_COMPILEDVERSION, KEYMAN_VersionWin_W16)) { + return FALSE; + } - return CERR_None; + return TRUE; } /**************************** @@ -1259,39 +1435,46 @@ KMX_DWORD CheckStatementOffsets(PFILE_KEYBOARD fk, PFILE_GROUP gp, PKMX_WCHAR co if (*q == 0) { if (!gp->fUsingKeys) // no key in the rule, so offset is past end of context - return CERR_IndexDoesNotPointToAny; + return KmnCompilerMessages::ERROR_IndexDoesNotPointToAny; if (i < contextOffset) // I4914 // offset is beyond the key - return CERR_IndexDoesNotPointToAny; + return KmnCompilerMessages::ERROR_IndexDoesNotPointToAny; q = key; } // find the any if (*q != UC_SENTINEL || *(q + 1) != CODE_ANY) - return CERR_IndexDoesNotPointToAny; + return KmnCompilerMessages::ERROR_IndexDoesNotPointToAny; int anyStore = *(q + 2) - 1; - if (xstrlen(fk->dpStoreArray[indexStore].dpString) < xstrlen(fk->dpStoreArray[anyStore].dpString)) { - AddWarning(CWARN_IndexStoreShort); //TODO: if this fails, then we return FALSE instead of an error + const int anyLength = xstrlen(fk->dpStoreArray[anyStore].dpString); + const int indexLength = xstrlen(fk->dpStoreArray[indexStore].dpString); + + if (indexLength < anyLength) { + ReportCompilerMessage(KmnCompilerMessages::WARN_IndexStoreShort); + } else if(indexLength > anyLength) { + ReportCompilerMessage(KmnCompilerMessages::HINT_IndexStoreLong); } } else if (*(p + 1) == CODE_CONTEXTEX) { int contextOffset = *(p + 2); if (contextOffset > xstrlen(context)) - return CERR_ContextExHasInvalidOffset; + return KmnCompilerMessages::ERROR_ContextExHasInvalidOffset; // Due to a limitation in earlier versions of KeymanWeb, the minimum version // for context() referring to notany() is 14.0. See #917 for details. if (kmcmp::CompileTarget == CKF_KEYMANWEB) { for (q = context, i = 1; *q && i < contextOffset; q = incxstr(q), i++); if (*q == UC_SENTINEL && *(q + 1) == CODE_NOTANY) { - VERIFY_KEYBOARD_VERSION(fk, VERSION_140, CERR_140FeatureOnlyContextAndNotAnyWeb); + if(!VerifyKeyboardVersion(fk, VERSION_140)) { + return KmnCompilerMessages::ERROR_140FeatureOnlyContextAndNotAnyWeb; + } } } } } } - return CERR_None; + return STATUS_Success; } /** @@ -1301,24 +1484,24 @@ KMX_DWORD CheckStatementOffsets(PFILE_KEYBOARD fk, PFILE_GROUP gp, PKMX_WCHAR co * Test that nul is first, then if(), baselayout(), platform() statements are before any other content. * Also verifies that virtual keys are not found in the context. */ -KMX_BOOL CheckContextStatementPositions(PKMX_WCHAR context) { +void CheckContextStatementPositions(PKMX_WCHAR context) { KMX_BOOL hadContextChar = FALSE; for (PKMX_WCHAR p = context; *p; p = incxstr(p)) { if (*p == UC_SENTINEL) { switch (*(p + 1)) { case CODE_NUL: if (p > context) { - AddWarningBool(CWARN_NulNotFirstStatementInContext); + ReportCompilerMessage(KmnCompilerMessages::WARN_NulNotFirstStatementInContext); } break; case CODE_IFOPT: case CODE_IFSYSTEMSTORE: if (hadContextChar) { - AddWarningBool(CWARN_IfShouldBeAtStartOfContext); + ReportCompilerMessage(KmnCompilerMessages::WARN_IfShouldBeAtStartOfContext); } break; case CODE_EXTENDED: - AddCompileError(CERR_VirtualKeyInContext); + ReportCompilerMessage(KmnCompilerMessages::ERROR_VirtualKeyInContext); break; default: hadContextChar = TRUE; @@ -1328,8 +1511,6 @@ KMX_BOOL CheckContextStatementPositions(PKMX_WCHAR context) { hadContextChar = TRUE; } } - - return TRUE; } /** @@ -1342,11 +1523,11 @@ KMX_DWORD CheckUseStatementsInOutput(PKMX_WCHAR output) { if (*p == UC_SENTINEL && *(p + 1) == CODE_USE) { hasUse = TRUE; } else if (hasUse) { - AddWarning(CWARN_UseNotLastStatementInRule); + ReportCompilerMessage(KmnCompilerMessages::WARN_UseNotLastStatementInRule); break; } } - return CERR_None; + return STATUS_Success; } /** @@ -1358,11 +1539,11 @@ KMX_DWORD CheckVirtualKeysInOutput(PKMX_WCHAR output) { PKMX_WCHAR p; for (p = output; *p; p = incxstr(p)) { if (*p == UC_SENTINEL && *(p + 1) == CODE_EXTENDED) { - AddWarning(CWARN_VirtualKeyInOutput); + ReportCompilerMessage(KmnCompilerMessages::WARN_VirtualKeyInOutput); break; } } - return CERR_None; + return STATUS_Success; } /** @@ -1371,13 +1552,13 @@ KMX_DWORD CheckVirtualKeysInOutput(PKMX_WCHAR output) { KMX_DWORD InjectContextToReadonlyOutput(PKMX_WCHAR pklOut) { if (pklOut[0] != UC_SENTINEL || pklOut[1] != CODE_CONTEXT) { if (u16len(pklOut) > GLOBAL_BUFSIZE - 3) { - return CERR_CannotAllocateMemory; + return KmnCompilerMessages::FATAL_CannotAllocateMemory; } memmove(pklOut + 2, pklOut, (u16len(pklOut) + 1) * 2); pklOut[0] = UC_SENTINEL; pklOut[1] = CODE_CONTEXT; } - return CERR_None; + return STATUS_Success; } /** @@ -1388,7 +1569,7 @@ KMX_DWORD CheckOutputIsReadonly(const PFILE_KEYBOARD fk, const PKMX_WCHAR output PKMX_WCHAR p; for (p = output; *p; p = incxstr(p)) { if (*p != UC_SENTINEL) { - return CERR_OutputInReadonlyGroup; + return KmnCompilerMessages::ERROR_OutputInReadonlyGroup; } switch (*(p + 1)) { case CODE_CALL: @@ -1401,7 +1582,7 @@ KMX_DWORD CheckOutputIsReadonly(const PFILE_KEYBOARD fk, const PKMX_WCHAR output { PFILE_GROUP targetGroup = &fk->dpGroupArray[*(p + 2) - 1]; if (!targetGroup->fReadOnly) { - return CERR_CannotUseReadWriteGroupFromReadonlyGroup; + return KmnCompilerMessages::ERROR_CannotUseReadWriteGroupFromReadonlyGroup; } } continue; @@ -1420,15 +1601,15 @@ KMX_DWORD CheckOutputIsReadonly(const PFILE_KEYBOARD fk, const PKMX_WCHAR output if (p == output) { continue; } - return CERR_OutputInReadonlyGroup; + return KmnCompilerMessages::ERROR_OutputInReadonlyGroup; default: // Note: conceptually, CODE_NUL could be transformed to CODE_CONTEXT // if the context was also empty, but it is probably safest to avoid this, // given CODE_CONTEXT does what we need anyway - return CERR_StatementNotPermittedInReadonlyGroup; + return KmnCompilerMessages::ERROR_StatementNotPermittedInReadonlyGroup; } } - return CERR_None; + return STATUS_Success; } KMX_DWORD ProcessKeyLineImpl(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnicode, PKMX_WCHAR pklIn, PKMX_WCHAR pklKey, PKMX_WCHAR pklOut); @@ -1441,7 +1622,7 @@ KMX_DWORD ProcessKeyLine(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnicode) pklKey = new KMX_WCHAR[GLOBAL_BUFSIZE]; pklOut = new KMX_WCHAR[GLOBAL_BUFSIZE]; if (!pklIn || !pklKey || !pklOut) - return CERR_CannotAllocateMemory; // forget about the little leak if pklKey or pklOut fail... + return KmnCompilerMessages::FATAL_CannotAllocateMemory; // forget about the little leak if pklKey or pklOut fail... KMX_DWORD result = ProcessKeyLineImpl(fk, str, IsUnicode, pklIn, pklKey, pklOut); @@ -1463,53 +1644,62 @@ KMX_DWORD ProcessKeyLineImpl(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnico pp = str; if (gp->fUsingKeys) { - if ((msg = GetXString(fk, str, u"+", pklIn, GLOBAL_BUFSIZE - 1, (int)(str - pp), &p, TRUE, IsUnicode)) != CERR_None) return msg; + if ((msg = GetXString(fk, str, u"+", pklIn, GLOBAL_BUFSIZE - 1, (int)(str - pp), &p, TRUE, IsUnicode)) != STATUS_Success) return msg; str = p + 1; - if ((msg = GetXString(fk, str, u">", pklKey, GLOBAL_BUFSIZE - 1, (int)(str - pp), &p, TRUE, IsUnicode)) != CERR_None) return msg; - if (pklKey[0] == 0) return CERR_ZeroLengthString; - if (xstrlen(pklKey) > 1) AddWarning(CWARN_KeyBadLength); + if ((msg = GetXString(fk, str, u">", pklKey, GLOBAL_BUFSIZE - 1, (int)(str - pp), &p, TRUE, IsUnicode)) != STATUS_Success) return msg; + + if (pklKey[0] == 0) return KmnCompilerMessages::ERROR_ZeroLengthString; + + if(Uni_IsSurrogate1(pklKey[0])) { + // #11643: non-BMP characters do not makes sense for key codes + return KmnCompilerMessages::ERROR_NonBMPCharactersNotSupportedInKeySection; + } + + if (xstrlen(pklKey) > 1) { + ReportCompilerMessage(KmnCompilerMessages::WARN_KeyBadLength); + } } else { - if ((msg = GetXString(fk, str, u">", pklIn, GLOBAL_BUFSIZE - 1, (int)(str - pp), &p, TRUE, IsUnicode)) != CERR_None) return msg; - if (pklIn[0] == 0) return CERR_ZeroLengthString; + if ((msg = GetXString(fk, str, u">", pklIn, GLOBAL_BUFSIZE - 1, (int)(str - pp), &p, TRUE, IsUnicode)) != STATUS_Success) return msg; + if (pklIn[0] == 0) return KmnCompilerMessages::ERROR_ZeroLengthString; } str = p + 1; - if ((msg = GetXString(fk, str, u"c\n", pklOut, GLOBAL_BUFSIZE - 1, (int)(str - pp), &p, TRUE, IsUnicode)) != CERR_None) return msg; + if ((msg = GetXString(fk, str, u"c\n", pklOut, GLOBAL_BUFSIZE - 1, (int)(str - pp), &p, TRUE, IsUnicode)) != STATUS_Success) return msg; - if (pklOut[0] == 0) return CERR_ZeroLengthString; + if (pklOut[0] == 0) return KmnCompilerMessages::ERROR_ZeroLengthString; CheckContextStatementPositions(pklIn); // Test index and context offsets in context - if ((msg = CheckStatementOffsets(fk, gp, pklIn, pklOut, pklKey)) != CERR_None) return msg; + if ((msg = CheckStatementOffsets(fk, gp, pklIn, pklOut, pklKey)) != STATUS_Success) return msg; // Test that use() statements are not followed by other content - if ((msg = CheckUseStatementsInOutput(pklOut)) != CERR_None) { + if ((msg = CheckUseStatementsInOutput(pklOut)) != STATUS_Success) { return msg; // I4867 } // Warn if virtual keys are used in the output, as they are unsupported by Core - if ((msg = CheckVirtualKeysInOutput(pklOut)) != CERR_None) { + if ((msg = CheckVirtualKeysInOutput(pklOut)) != STATUS_Success) { return msg; } if (gp->fReadOnly) { // Ensure no output is made from the rule, and that // use() statements meet required readonly semantics - if ((msg = CheckOutputIsReadonly(fk, pklOut)) != CERR_None) { + if ((msg = CheckOutputIsReadonly(fk, pklOut)) != STATUS_Success) { return msg; } // Inject `context` to start of output if group is readonly // to keep the output internally consistent - if ((msg = InjectContextToReadonlyOutput(pklOut)) != CERR_None) { + if ((msg = InjectContextToReadonlyOutput(pklOut)) != STATUS_Success) { return msg; } } if(!resizeKeyArray(gp)) { - return CERR_CannotAllocateMemory; + return KmnCompilerMessages::FATAL_CannotAllocateMemory; } kp = &gp->dpKeyArray[gp->cxKeyArray]; @@ -1531,19 +1721,19 @@ KMX_DWORD ProcessKeyLineImpl(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnico { kp->Key = 0; kp->ShiftFlags = 0; - return CERR_None; + return STATUS_Success; } // Expand each rule out into multiple rules - much faster processing at the key hit time - if (*pklKey == 0) return CERR_ZeroLengthString; + if (*pklKey == 0) return KmnCompilerMessages::ERROR_ZeroLengthString; if (*pklKey == UC_SENTINEL) switch (*(pklKey + 1)) { case CODE_ANY: kp->ShiftFlags = 0; - if ((msg = ExpandKp(fk, kp, *(pklKey + 2) - 1)) != CERR_None) return msg; + if ((msg = ExpandKp(fk, kp, *(pklKey + 2) - 1)) != STATUS_Success) return msg; break; case CODE_EXTENDED: @@ -1552,7 +1742,7 @@ KMX_DWORD ProcessKeyLineImpl(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnico break; default: - return CERR_InvalidCodeInKeyPartOfRule; + return KmnCompilerMessages::ERROR_InvalidCodeInKeyPartOfRule; } else { @@ -1560,7 +1750,7 @@ KMX_DWORD ProcessKeyLineImpl(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnico kp->Key = *pklKey; } - return CERR_None; + return STATUS_Success; } @@ -1595,7 +1785,7 @@ KMX_DWORD ExpandKp_ReplaceIndex(PFILE_KEYBOARD fk, PFILE_KEY k, KMX_DWORD keyInd } } - return CERR_None; + return STATUS_Success; } @@ -1624,7 +1814,7 @@ KMX_DWORD ExpandKp(PFILE_KEYBOARD fk, PFILE_KEY kpp, KMX_DWORD storeIndex) int offset = (int)(kpp - gp->dpKeyArray); if (!resizeKeyArray(gp, nchrs)) { - return CERR_CannotAllocateMemory; + return KmnCompilerMessages::FATAL_CannotAllocateMemory; } kpp = &gp->dpKeyArray[offset]; @@ -1647,11 +1837,12 @@ KMX_DWORD ExpandKp(PFILE_KEYBOARD fk, PFILE_KEY kpp, KMX_DWORD storeIndex) k->ShiftFlags = *(pn + 2); break; default: - return CERR_CodeInvalidInKeyStore; + return KmnCompilerMessages::ERROR_CodeInvalidInKeyStore; } - } - else - { + } else if(Uni_IsSurrogate1(*pn)) { + // #11643: non-BMP characters do not makes sense for key codes + return KmnCompilerMessages::ERROR_NonBMPCharactersNotSupportedInKeySection; + } else { k->Key = *pn; // set the key to store offset. k->ShiftFlags = 0; } @@ -1663,10 +1854,30 @@ KMX_DWORD ExpandKp(PFILE_KEYBOARD fk, PFILE_KEY kpp, KMX_DWORD storeIndex) delete[] dpContext; delete[] dpOutput; - return CERR_None; + return STATUS_Success; } - +/** + * When called with a pointer to a wide-character C-string string, the open and close delimiters, and + * optional flags, returns a pointer to the section of the KMX_WCHAR string identified by the delimiters. + * The supplied string will be terminated by a null where the close delimiter was. The pointer to the supplied + * string is adjusted to point either to the null where the close delimiter was, or if there are trailing + * whitespaces after the close delimiter, to the last of these. Whitespaces before the open delimiter + * are always skipped. If the flag contains GDS_CUTLEAD, whitespaces after the open delimiter are skipped; + * if the flag contains GDS_CUTFOLL, whitespace immediately before the close delimiter is skipped by setting + * the first such character to null. + * + * @param p a pointer to a wide-character C-string + * + * @param Delimiters a pointer to a two-character wide-character C-string containing the open and close + * delimiters + * + * @param Flags include GDS_CUTLEAD and/or GDS_CUTFOLL to cut leading and/or following whitespace from + * the delimited string + * + * @return a pointer to the section of the wide-character C-string identified by the delimiters, or NULL if + * the delimiters cannot be found +*/ PKMX_WCHAR GetDelimitedString(PKMX_WCHAR *p, KMX_WCHAR const * Delimiters, KMX_WORD Flags) { PKMX_WCHAR q, r; @@ -1688,15 +1899,20 @@ PKMX_WCHAR GetDelimitedString(PKMX_WCHAR *p, KMX_WCHAR const * Delimiters, KMX_W while (iswspace(*q)) q++; // cut off leading spaces if (Flags & GDS_CUTFOLL) - if (!iswspace(*(r - 1))) *r = 0; + if (!iswspace(*(r - 1))) *r = 0; // delete close delimiter else { r--; // Cut off following spaces while (iswspace(*r) && r > q) r--; - r++; - *r = 0; r = (PKMX_WCHAR) u16chr((r + 1), dClose); + if (!iswspace(*r)) r++; + + *r = 0; // delete first following space + + r = (PKMX_WCHAR) u16chr((r + 1), dClose); + + *r = 0; // delete close delimiter } - else *r = 0; + else *r = 0; // delete close delimiter r++; while (iswspace(*r)) r++; // Ignore spaces after the close if (*r == 0) r--; // Safety for terminating strings. @@ -1830,7 +2046,7 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX if (mx >= max) { // This is an error condition, we want the compiler // to crash if we reach this - return CERR_BufferOverflow; + return KmnCompilerMessages::FATAL_BufferOverflow; } tokenFound = FALSE; @@ -1879,29 +2095,27 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX { case 99: if (tokenFound) break; - { - snprintf(ErrExtraLIB, ERR_EXTRA_LIB_LEN, "token: %c",(int)*p); - } - return CERR_InvalidToken; + // TODO: report an error about the missing token details --> "token: %c",(int)*p); + return KmnCompilerMessages::ERROR_InvalidToken; case 0: if (u16nicmp(p, u"deadkey", z = 7) == 0 || u16nicmp(p, u"dk", z = 2) == 0) { p += z; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidDeadkey; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidDeadkey; tstr[mx++] = UC_SENTINEL; tstr[mx++] = CODE_DEADKEY; - if (!StrValidChrs(q, DeadKeyChars)) return CERR_InvalidDeadkey; + if (!StrValidChrs(q, DeadKeyChars)) return KmnCompilerMessages::ERROR_InvalidDeadkey; tstr[mx++] = GetDeadKey(fk, q); //atoiW(q); 7-5-01: named deadkeys tstr[mx] = 0; } else { n = xatoi(&p); - if (*p != '\0' && !iswspace(*p)) return CERR_InvalidValue; - if ((err = kmcmp::UTF32ToUTF16(n, &n1, &n2)) != CERR_None) return err; + if (*p != '\0' && !iswspace(*p)) return KmnCompilerMessages::ERROR_InvalidValue; + if ((err = kmcmp::UTF32ToUTF16(n, &n1, &n2)) != STATUS_Success) return err; tstr[mx++] = n1; if (n2 >= 0) tstr[mx++] = n2; tstr[mx] = 0; @@ -1910,9 +2124,9 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX case 1: q = (PKMX_WCHAR) u16chr(p + 1, '\"'); - if (!q) return CERR_UnterminatedString; - if ((int)(q - p) - 1 + mx > max) return CERR_ExtendedStringTooLong; - if (sFlag) return CERR_StringInVirtualKeySection; + if (!q) return KmnCompilerMessages::ERROR_UnterminatedString; + if ((int)(q - p) - 1 + mx > max) return KmnCompilerMessages::ERROR_ExtendedStringTooLong; + if (sFlag) return KmnCompilerMessages::ERROR_StringInVirtualKeySection; u16ncat(tstr, p + 1, (int)(q - p) - 1); // I3481 mx += (int)(q - p) - 1; tstr[mx] = 0; @@ -1920,28 +2134,28 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX continue; case 2: q = (PKMX_WCHAR) u16chr(p + 1, '\''); - if (!q) return CERR_UnterminatedString; - if ((int)(q - p) - 1 + mx > max) return CERR_ExtendedStringTooLong; - if (sFlag) return CERR_StringInVirtualKeySection; + if (!q) return KmnCompilerMessages::ERROR_UnterminatedString; + if ((int)(q - p) - 1 + mx > max) return KmnCompilerMessages::ERROR_ExtendedStringTooLong; + if (sFlag) return KmnCompilerMessages::ERROR_StringInVirtualKeySection; u16ncat(tstr, p + 1, (int)(q - p) - 1); // I3481 mx += (int)(q - p) - 1; tstr[mx] = 0; p = q + 1; continue; case 3: - if (u16nicmp(p, u"any", 3) != 0) return CERR_InvalidToken; - if (sFlag) return CERR_AnyInVirtualKeySection; + if (u16nicmp(p, u"any", 3) != 0) return KmnCompilerMessages::ERROR_InvalidToken; + if (sFlag) return KmnCompilerMessages::ERROR_AnyInVirtualKeySection; p += 3; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidAny; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidAny; for (i = 0; i < fk->cxStoreArray; i++) { if (u16icmp(q, fk->dpStoreArray[i].szName) == 0) break; } - if (i == fk->cxStoreArray) return CERR_StoreDoesNotExist; + if (i == fk->cxStoreArray) return KmnCompilerMessages::ERROR_StoreDoesNotExist; - if (!*fk->dpStoreArray[i].dpString) return CERR_ZeroLengthString; + if (!*fk->dpStoreArray[i].dpString) return KmnCompilerMessages::ERROR_ZeroLengthString; kmcmp::CheckStoreUsage(fk, i, TRUE, FALSE, FALSE); tstr[mx++] = UC_SENTINEL; @@ -1952,7 +2166,7 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX case 4: if (u16nicmp(p, u"beep", 4) == 0) { - if (sFlag) return CERR_BeepInVirtualKeySection; + if (sFlag) return KmnCompilerMessages::ERROR_BeepInVirtualKeySection; p += 4; tstr[mx++] = UC_SENTINEL; tstr[mx++] = CODE_BEEP; @@ -1960,56 +2174,61 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX } else if (u16nicmp(p, u"baselayout", 10) == 0) // I3430 { - VERIFY_KEYBOARD_VERSION(fk, VERSION_90, CERR_90FeatureOnly_IfSystemStores); - if (sFlag) return CERR_InvalidInVirtualKeySection; + if(!VerifyKeyboardVersion(fk, VERSION_90)) { + return KmnCompilerMessages::ERROR_90FeatureOnly_IfSystemStores; + } + if (sFlag) return KmnCompilerMessages::ERROR_InvalidInVirtualKeySection; p += 10; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidToken; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidToken; err = process_baselayout(fk, q, tstr, &mx); - if (err != CERR_None) return err; + if (err != STATUS_Success) return err; } else - return CERR_InvalidToken; + return KmnCompilerMessages::ERROR_InvalidToken; continue; case 5: if (u16nicmp(p, u"if", 2) == 0) { - VERIFY_KEYBOARD_VERSION(fk, VERSION_80, CERR_80FeatureOnly); - if (sFlag) return CERR_InvalidInVirtualKeySection; + if(!VerifyKeyboardVersion(fk, VERSION_80)) { + return KmnCompilerMessages::ERROR_80FeatureOnly; + } + if (sFlag) return KmnCompilerMessages::ERROR_InvalidInVirtualKeySection; p += 2; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidIf; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidIf; err = process_if(fk, q, tstr, &mx); - if (err != CERR_None) return err; + if (err != STATUS_Success) return err; } else { - if (u16nicmp(p, u"index", 5) != 0) return CERR_InvalidToken; - if (sFlag) return CERR_IndexInVirtualKeySection; + if (u16nicmp(p, u"index", 5) != 0) return KmnCompilerMessages::ERROR_InvalidToken; + if (sFlag) return KmnCompilerMessages::ERROR_IndexInVirtualKeySection; p += 5; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidIndex; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidIndex; { KMX_WCHAR *context = NULL; - KMX_WCHAR sep_com[3] = u" ,"; - PKMX_WCHAR p_sep_com = sep_com; - r = u16tok(q, p_sep_com, &context); // I3481 - if (!r) return CERR_InvalidIndex; + r = u16tok(q, ',', &context); + if (!r) return KmnCompilerMessages::ERROR_InvalidIndex; + r = u16trim(r); for (i = 0; i < fk->cxStoreArray; i++) { if (u16icmp(r, fk->dpStoreArray[i].szName) == 0) break; } - if (i == fk->cxStoreArray) return CERR_StoreDoesNotExist; + if (i == fk->cxStoreArray) return KmnCompilerMessages::ERROR_StoreDoesNotExist; kmcmp::CheckStoreUsage(fk, i, TRUE, FALSE, FALSE); - r = u16tok(NULL, p_sep_com, &context); // I3481 - if (!r) return CERR_InvalidIndex; + r = context; + if (!r || !*r ) return KmnCompilerMessages::ERROR_InvalidIndex; + r = u16trim(r); + if (!isIntegerWstring(r) || atoiW(r) < 1) return KmnCompilerMessages::ERROR_InvalidIndex; } tstr[mx++] = UC_SENTINEL; tstr[mx++] = CODE_INDEX; @@ -2020,17 +2239,17 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX } continue; case 6: - if (u16nicmp(p, u"outs", 4) != 0) return CERR_InvalidToken; - if (sFlag) return CERR_OutsInVirtualKeySection; + if (u16nicmp(p, u"outs", 4) != 0) return KmnCompilerMessages::ERROR_InvalidToken; + if (sFlag) return KmnCompilerMessages::ERROR_OutsInVirtualKeySection; p += 4; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidOuts; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidOuts; for (i = 0; i < fk->cxStoreArray; i++) { if (u16icmp(q, fk->dpStoreArray[i].szName) == 0) break; } - if (i == fk->cxStoreArray) return CERR_StoreDoesNotExist; + if (i == fk->cxStoreArray) return KmnCompilerMessages::ERROR_StoreDoesNotExist; kmcmp::CheckStoreUsage(fk, i, TRUE, FALSE, FALSE); @@ -2038,7 +2257,7 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX { tstr[mx++] = *q; if (mx >= max - 1) { - return CERR_OutsTooLong; + return KmnCompilerMessages::ERROR_OutsTooLong; } } tstr[mx] = 0; @@ -2047,16 +2266,18 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX if (iswspace(*(p + 1))) break; // is a comment -- pre-stripped - so why this test? if (u16nicmp(p, u"context", 7) == 0) { - if (sFlag) return CERR_ContextInVirtualKeySection; + if (sFlag) return KmnCompilerMessages::ERROR_ContextInVirtualKeySection; p += 7; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); if (q && *q) { - VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_Contextn); + if(!VerifyKeyboardVersion(fk, VERSION_60)) { + return KmnCompilerMessages::ERROR_60FeatureOnly_Contextn; + } int n1b; n1b = atoiW(q); - if (n1b < 1 || n1b >= 0xF000) return CERR_InvalidToken; + if (n1b < 1 || n1b >= 0xF000) return KmnCompilerMessages::ERROR_InvalidToken; tstr[mx++] = UC_SENTINEL; tstr[mx++] = CODE_CONTEXTEX; tstr[mx++] = n1b; @@ -2078,21 +2299,23 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX } else if (u16nicmp(p, u"call", 4) == 0) { - VERIFY_KEYBOARD_VERSION(fk, VERSION_501, CERR_501FeatureOnly_Call); - if (sFlag) return CERR_CallInVirtualKeySection; + if(!VerifyKeyboardVersion(fk, VERSION_501)) { + return KmnCompilerMessages::ERROR_501FeatureOnly_Call; + } + if (sFlag) return KmnCompilerMessages::ERROR_CallInVirtualKeySection; p += 4; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidCall; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidCall; for (i = 0; i < fk->cxStoreArray; i++) { if (u16icmp(q, fk->dpStoreArray[i].szName) == 0) break; } + if (i == fk->cxStoreArray) return KmnCompilerMessages::ERROR_StoreDoesNotExist; - if (!kmcmp::IsValidCallStore(&fk->dpStoreArray[i])) return CERR_InvalidCall; + if (!kmcmp::IsValidCallStore(&fk->dpStoreArray[i])) return KmnCompilerMessages::ERROR_InvalidCall; kmcmp::CheckStoreUsage(fk, i, FALSE, FALSE, TRUE); - if (i == fk->cxStoreArray) return CERR_StoreDoesNotExist; tstr[mx++] = UC_SENTINEL; tstr[mx++] = CODE_CALL; tstr[mx++] = (KMX_WCHAR)i + 1; @@ -2101,22 +2324,24 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX fk->dpStoreArray[i].dwSystemID = TSS_CALLDEFINITION; } else - return CERR_InvalidToken; + return KmnCompilerMessages::ERROR_InvalidToken; continue; case 8: if (u16nicmp(p, u"notany", 6) == 0) { - VERIFY_KEYBOARD_VERSION(fk, VERSION_70, CERR_70FeatureOnly) - if (sFlag) return CERR_AnyInVirtualKeySection; + if(!VerifyKeyboardVersion(fk, VERSION_70)) { + return KmnCompilerMessages::ERROR_70FeatureOnly; + } + if (sFlag) return KmnCompilerMessages::ERROR_AnyInVirtualKeySection; p += 6; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidAny; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidAny; for (i = 0; i < fk->cxStoreArray; i++) { if (u16icmp(q, fk->dpStoreArray[i].szName) == 0) break; } - if (i == fk->cxStoreArray) return CERR_StoreDoesNotExist; + if (i == fk->cxStoreArray) return KmnCompilerMessages::ERROR_StoreDoesNotExist; kmcmp::CheckStoreUsage(fk, i, TRUE, FALSE, FALSE); tstr[mx++] = UC_SENTINEL; tstr[mx++] = CODE_NOTANY; @@ -2124,7 +2349,7 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX tstr[mx] = 0; continue; } - if (u16nicmp(p, u"nul", 3) != 0) return CERR_InvalidToken; + if (u16nicmp(p, u"nul", 3) != 0) return KmnCompilerMessages::ERROR_InvalidToken; p += 3; tstr[mx++] = UC_SENTINEL; @@ -2137,41 +2362,45 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX if (*(p + 1) == '+') { n = xatoi(&p); - if (*p != '\0' && !iswspace(*p)) return CERR_InvalidValue; - if ((err = kmcmp::UTF32ToUTF16(n, &n1, &n2)) != CERR_None) return err; + if (*p != '\0' && !iswspace(*p)) return KmnCompilerMessages::ERROR_InvalidValue; + if ((err = kmcmp::UTF32ToUTF16(n, &n1, &n2)) != STATUS_Success) return err; tstr[mx++] = n1; if (n2 >= 0) tstr[mx++] = n2; tstr[mx] = 0; - if (!isUnicode) AddWarning(CWARN_UnicodeInANSIGroup); + if (!isUnicode) { + ReportCompilerMessage(KmnCompilerMessages::WARN_UnicodeInANSIGroup); + } continue; } - return CERR_InvalidToken; + return KmnCompilerMessages::ERROR_InvalidToken; } p += 3; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidUse; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidUse; tstr[mx++] = UC_SENTINEL; tstr[mx++] = CODE_USE; tstr[mx] = GetGroupNum(fk, q); - if (tstr[mx] == 0) return CERR_GroupDoesNotExist; + if (tstr[mx] == 0) return KmnCompilerMessages::ERROR_GroupDoesNotExist; tstr[++mx] = 0; continue; case 10: if (u16nicmp(p, u"reset", 5) == 0) { - VERIFY_KEYBOARD_VERSION(fk, VERSION_80, CERR_80FeatureOnly); - if (sFlag) return CERR_InvalidInVirtualKeySection; + if(!VerifyKeyboardVersion(fk, VERSION_80)) { + return KmnCompilerMessages::ERROR_80FeatureOnly; + } + if (sFlag) return KmnCompilerMessages::ERROR_InvalidInVirtualKeySection; p += 5; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidReset; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidReset; err = process_reset(fk, q, tstr, &mx); - if (err != CERR_None) return err; + if (err != STATUS_Success) return err; } else { - if (u16nicmp(p, u"return", 6) != 0) return CERR_InvalidToken; + if (u16nicmp(p, u"return", 6) != 0) return KmnCompilerMessages::ERROR_InvalidToken; p += 6; tstr[mx++] = UC_SENTINEL; @@ -2236,17 +2465,23 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX } while (!finished); if ((sFlag & (LCTRLFLAG | LALTFLAG)) && (sFlag & (RCTRLFLAG | RALTFLAG))) { - AddWarning(CWARN_MixingLeftAndRightModifiers); + ReportCompilerMessage(KmnCompilerMessages::WARN_MixingLeftAndRightModifiers); } - // If we use chiral modifiers, or we use state keys, and we target web in the keyboard, and we don't manually specify a keyboard version, bump the minimum - // version to 10.0. This makes an assumption that if we are using these features in a keyboard and it has no version specified, that we want to use the features - // in the web target platform, even if there are platform() rules excluding this possibility. In that (rare) situation, the keyboard developer should simply specify - // the &version to be 9.0 or whatever to avoid this behaviour. + // If we use chiral modifiers, or we use state keys, and we target web in + // the keyboard, and we don't manually specify a keyboard version, bump + // the minimum version to 10.0. This makes an assumption that if we are + // using these features in a keyboard and it has no version specified, + // that we want to use the features in the web target platform, even if + // there are platform() rules excluding this possibility. In that (rare) + // situation, the keyboard developer should simply specify the &version to + // be 9.0 or whatever to avoid this behaviour. if (sFlag & (LCTRLFLAG | LALTFLAG | RCTRLFLAG | RALTFLAG | CAPITALFLAG | NOTCAPITALFLAG | NUMLOCKFLAG | NOTNUMLOCKFLAG | SCROLLFLAG | NOTSCROLLFLAG) && kmcmp::CompileTarget == CKF_KEYMANWEB && fk->dwFlags & KF_AUTOMATICVERSION) { - VERIFY_KEYBOARD_VERSION(fk, VERSION_100, 0); + if(!VerifyKeyboardVersion(fk, VERSION_100)) { + return STATUS_Success; + } } //printf("sFlag: %x\n", sFlag); @@ -2260,33 +2495,37 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX if (*q == ']') { - return CERR_InvalidToken; // I3137 - key portion of VK is missing e.g. "[CTRL ALT]", this generates invalid kmx file that can crash Keyman or compiler later on // I3511 + return KmnCompilerMessages::ERROR_InvalidToken; // I3137 - key portion of VK is missing e.g. "[CTRL ALT]", this generates invalid kmx file that can crash Keyman or compiler later on // I3511 } while (*q != ']') { if (*q == '\'' || *q == '"') { - VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_VirtualCharKey); - if (!kmcmp::FMnemonicLayout) AddWarning(CWARN_VirtualCharKeyWithPositionalLayout); + if(!VerifyKeyboardVersion(fk, VERSION_60)) { + return KmnCompilerMessages::ERROR_60FeatureOnly_VirtualCharKey; + } + if (!kmcmp::FMnemonicLayout) { + ReportCompilerMessage(KmnCompilerMessages::WARN_VirtualCharKeyWithPositionalLayout); + } KMX_WCHAR chQuote = *q; - q++; if (*q == chQuote || *q == '\n' || *q == 0) return CERR_InvalidToken; + q++; if (*q == chQuote || *q == '\n' || *q == 0) return KmnCompilerMessages::ERROR_InvalidToken; tstr[mx - 1] |= VIRTUALCHARKEY; tstr[mx++] = *q; - q++; if (*q != chQuote) return CERR_InvalidToken; + q++; if (*q != chQuote) return KmnCompilerMessages::ERROR_InvalidToken; q++; while (iswspace(*q)) q++; - if (*q != ']') return CERR_InvalidToken; + if (*q != ']') return KmnCompilerMessages::ERROR_InvalidToken; break; /* out of while loop */ } for (j = 0; !iswspace(*q) && *q != ']' && *q != 0; q++, j++); - if (*q == 0) return CERR_InvalidToken; + if (*q == 0) return KmnCompilerMessages::ERROR_InvalidToken; KMX_WCHAR vkname[SZMAX_VKDICTIONARYNAME]; // I3438 - if (j >= SZMAX_VKDICTIONARYNAME) return CERR_InvalidToken; + if (j >= SZMAX_VKDICTIONARYNAME) return KmnCompilerMessages::ERROR_InvalidToken; u16ncpy(vkname, p, j); // I3481 vkname[j] = 0; @@ -2304,18 +2543,22 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX if (i == VK__MAX + 1) { - VERIFY_KEYBOARD_VERSION(fk, VERSION_90, CERR_InvalidToken); + if(!VerifyKeyboardVersion(fk, VERSION_90)) { + return KmnCompilerMessages::ERROR_InvalidToken; + } i = GetVKCode(fk, vkname); // I3438 if (i == 0) - return CERR_InvalidToken; + return KmnCompilerMessages::ERROR_InvalidToken; } p = q; tstr[mx++] = (int)i; - if (kmcmp::FMnemonicLayout && (i <= VK__MAX) && VKeyMayBeVCKey[i]) AddWarning(CWARN_VirtualKeyWithMnemonicLayout); // I3438 + if (kmcmp::FMnemonicLayout && (i <= VK__MAX) && VKeyMayBeVCKey[i]) { + ReportCompilerMessage(KmnCompilerMessages::WARN_VirtualKeyWithMnemonicLayout); // I3438 + } while (iswspace(*q)) q++; } @@ -2331,30 +2574,34 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX case 14: if (u16nicmp(p, u"set", 3) == 0) { - VERIFY_KEYBOARD_VERSION(fk, VERSION_80, CERR_80FeatureOnly); + if(!VerifyKeyboardVersion(fk, VERSION_80)) { + return KmnCompilerMessages::ERROR_80FeatureOnly; + } p += 3; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidSet; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidSet; err = process_set(fk, q, tstr, &mx); - if (err != CERR_None) return err; + if (err != STATUS_Success) return err; } else if (u16nicmp(p, u"save", 4) == 0) { - VERIFY_KEYBOARD_VERSION(fk, VERSION_80, CERR_80FeatureOnly); + if(!VerifyKeyboardVersion(fk, VERSION_80)) { + return KmnCompilerMessages::ERROR_80FeatureOnly; + } p += 4; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidSave; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidSave; err = process_save(fk, q, tstr, &mx); - if (err != CERR_None) return err; + if (err != STATUS_Success) return err; } else { - if (u16nicmp(p, u"switch", 6) != 0) return CERR_InvalidToken; + if (u16nicmp(p, u"switch", 6) != 0) return KmnCompilerMessages::ERROR_InvalidToken; p += 6; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidSwitch; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidSwitch; tstr[mx++] = UC_SENTINEL; tstr[mx++] = CODE_SWITCH; tstr[mx++] = atoiW(q); @@ -2370,16 +2617,18 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX tstr[mx] = 0; } else - return CERR_InvalidToken; + return KmnCompilerMessages::ERROR_InvalidToken; continue; case 16: - VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_NamedCodes); + if(!VerifyKeyboardVersion(fk, VERSION_60)) { + return KmnCompilerMessages::ERROR_60FeatureOnly_NamedCodes; + } q = p + 1; while (*q && !iswspace(*q)) q++; c = *q; *q = 0; n = kmcmp::CodeConstants->GetCode(p + 1, &i); *q = c; - if (n == 0) return CERR_InvalidNamedCode; + if (n == 0) return KmnCompilerMessages::ERROR_InvalidNamedCode; if (i < 0xFFFFFFFFL) kmcmp::CheckStoreUsage(fk, i, TRUE, FALSE, FALSE); // I2993 if (n > 0xFFFF) { @@ -2392,34 +2641,38 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX p = q; continue; case 17: - if (u16nicmp(p, u"platform", 8) != 0) return CERR_InvalidToken; // I3430 - VERIFY_KEYBOARD_VERSION(fk, VERSION_90, CERR_90FeatureOnly_IfSystemStores); - if (sFlag) return CERR_InvalidInVirtualKeySection; + if (u16nicmp(p, u"platform", 8) != 0) return KmnCompilerMessages::ERROR_InvalidToken; // I3430 + if(!VerifyKeyboardVersion(fk, VERSION_90)) { + return KmnCompilerMessages::ERROR_90FeatureOnly_IfSystemStores; + } + if (sFlag) return KmnCompilerMessages::ERROR_InvalidInVirtualKeySection; p += 8; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidToken; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidToken; err = process_platform(fk, q, tstr, &mx); - if (err != CERR_None) return err; + if (err != STATUS_Success) return err; continue; case 18: // I3437 - if (u16nicmp(p, u"layer", 5) != 0) return CERR_InvalidToken; - VERIFY_KEYBOARD_VERSION(fk, VERSION_90, CERR_90FeatureOnly_SetSystemStores); - if (sFlag) return CERR_InvalidInVirtualKeySection; + if (u16nicmp(p, u"layer", 5) != 0) return KmnCompilerMessages::ERROR_InvalidToken; + if(!VerifyKeyboardVersion(fk, VERSION_90)) { + return KmnCompilerMessages::ERROR_90FeatureOnly_SetSystemStores; + } + if (sFlag) return KmnCompilerMessages::ERROR_InvalidInVirtualKeySection; p += 5; q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); - if (!q || !*q) return CERR_InvalidToken; + if (!q || !*q) return KmnCompilerMessages::ERROR_InvalidToken; err = process_set_synonym(TSS_LAYER, fk, q, tstr, &mx); - if (err != CERR_None) return err; + if (err != STATUS_Success) return err; continue; case 19: // #2241 - if (*(p + 1) != '.') return CERR_InvalidToken; - if (sFlag) return CERR_InvalidInVirtualKeySection; + if (*(p + 1) != '.') return KmnCompilerMessages::ERROR_InvalidToken; + if (sFlag) return KmnCompilerMessages::ERROR_InvalidInVirtualKeySection; p += 2; err = process_expansion(fk, p, tstr, &mx, max); - if (err != CERR_None) return err; + if (err != STATUS_Success) return err; continue; default: - return CERR_InvalidToken; + return KmnCompilerMessages::ERROR_InvalidToken; } if (tokenFound) { @@ -2427,7 +2680,7 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX u16ncpy(output, tstr, max); // I3481 output[max - 1] = 0; ErrChr = 0; - return CERR_None; + return STATUS_Success; } } while (*p); @@ -2437,10 +2690,10 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX u16ncpy(output, tstr, max); // I3481 output[max - 1] = 0; ErrChr = 0; - return CERR_None; + return STATUS_Success; } - return CERR_NoTokensFound; + return KmnCompilerMessages::ERROR_NoTokensFound; } KMX_DWORD process_if_synonym(KMX_DWORD dwSystemID, PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx); // I3430 @@ -2465,7 +2718,7 @@ KMX_DWORD process_if_synonym(KMX_DWORD dwSystemID, PFILE_KEYBOARD fk, PKMX_WCHAR PKMX_WCHAR r; - if ((msg = GetXString(fk, q, u"", temp, GLOBAL_BUFSIZE - 1, 0, &r, FALSE, TRUE)) != CERR_None) + if ((msg = GetXString(fk, q, u"", temp, GLOBAL_BUFSIZE - 1, 0, &r, FALSE, TRUE)) != STATUS_Success) { delete[] temp; return msg; @@ -2473,12 +2726,14 @@ KMX_DWORD process_if_synonym(KMX_DWORD dwSystemID, PFILE_KEYBOARD fk, PKMX_WCHAR KMX_DWORD dwStoreID; - if ((msg = AddStore(fk, TSS_COMPARISON, temp, &dwStoreID)) != CERR_None) - { + if (!AddStore(fk, TSS_COMPARISON, temp, &dwStoreID)) { delete[] temp; - return msg; + // TODO: redundant error + return KmnCompilerMessages::ERROR_InvalidIf; } + delete[] temp; + tstr[(*mx)++] = UC_SENTINEL; tstr[(*mx)++] = (KMX_WCHAR)CODE_IFSYSTEMSTORE; tstr[(*mx)++] = (KMX_WCHAR)(dwSystemID + 1); // I4785 @@ -2486,9 +2741,7 @@ KMX_DWORD process_if_synonym(KMX_DWORD dwSystemID, PFILE_KEYBOARD fk, PKMX_WCHAR tstr[(*mx)++] = (KMX_WCHAR)(dwStoreID + 1); tstr[(*mx)] = 0; - delete[] temp; - - return CERR_None; + return STATUS_Success; } KMX_DWORD process_if(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) // I3431 @@ -2506,7 +2759,7 @@ KMX_DWORD process_if(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) nnot = TRUE; } - if (*s != '=') return CERR_InvalidIf; + if (*s != '=') return KmnCompilerMessages::ERROR_InvalidIf; s++; while (*s == ' ') s++; *r = 0; @@ -2514,12 +2767,14 @@ KMX_DWORD process_if(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) if (r[0] == '&') { - VERIFY_KEYBOARD_VERSION( fk, VERSION_90, CERR_90FeatureOnly_IfSystemStores); + if(!VerifyKeyboardVersion( fk, VERSION_90)) { + return KmnCompilerMessages::ERROR_90FeatureOnly_IfSystemStores; + } for (i = 0; StoreTokens[i]; i++) { if (u16icmp(r, StoreTokens[i]) == 0) break; } - if (!StoreTokens[i]) return CERR_IfSystemStore_NotFound; + if (!StoreTokens[i]) return KmnCompilerMessages::ERROR_IfSystemStore_NotFound; code = CODE_IFSYSTEMSTORE; } else @@ -2530,7 +2785,7 @@ KMX_DWORD process_if(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) { if (u16icmp(r, fk->dpStoreArray[i].szName) == 0) break; } - if (i == fk->cxStoreArray) return CERR_StoreDoesNotExist; + if (i == fk->cxStoreArray) return KmnCompilerMessages::ERROR_StoreDoesNotExist; kmcmp::CheckStoreUsage(fk, i, FALSE, TRUE, FALSE); } @@ -2538,7 +2793,7 @@ KMX_DWORD process_if(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) KMX_DWORD msg; - if ((msg = GetXString(fk, s, u"", temp, GLOBAL_BUFSIZE - 1, 0, &r, FALSE, TRUE)) != CERR_None) + if ((msg = GetXString(fk, s, u"", temp, GLOBAL_BUFSIZE - 1, 0, &r, FALSE, TRUE)) != STATUS_Success) { delete[] temp; return msg; @@ -2546,12 +2801,14 @@ KMX_DWORD process_if(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) KMX_DWORD dwStoreID; - if ((msg = AddStore(fk, TSS_COMPARISON, temp, &dwStoreID)) != CERR_None) - { + if (!AddStore(fk, TSS_COMPARISON, temp, &dwStoreID)) { delete[] temp; - return msg; + // TODO: redundant error + return KmnCompilerMessages::ERROR_InvalidIf; } + delete[] temp; + tstr[(*mx)++] = UC_SENTINEL; tstr[(*mx)++] = (KMX_WCHAR)code; tstr[(*mx)++] = (KMX_WCHAR)(i + 1); @@ -2559,7 +2816,7 @@ KMX_DWORD process_if(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) tstr[(*mx)++] = (KMX_WCHAR)(dwStoreID + 1); tstr[(*mx)] = 0; - return CERR_None; + return STATUS_Success; } KMX_DWORD process_reset(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) @@ -2570,7 +2827,7 @@ KMX_DWORD process_reset(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *m { if (u16icmp(q, fk->dpStoreArray[i].szName) == 0) break; } - if (i == fk->cxStoreArray) return CERR_StoreDoesNotExist; + if (i == fk->cxStoreArray) return KmnCompilerMessages::ERROR_StoreDoesNotExist; kmcmp::CheckStoreUsage(fk, i, FALSE, TRUE, FALSE); tstr[(*mx)++] = UC_SENTINEL; @@ -2578,7 +2835,7 @@ KMX_DWORD process_reset(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *m tstr[(*mx)++] = (KMX_WCHAR)(i + 1); tstr[(*mx)] = 0; - return CERR_None; + return STATUS_Success; } KMX_DWORD process_expansion(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx, int max) { @@ -2588,13 +2845,13 @@ KMX_DWORD process_expansion(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, in KMX_DWORD BaseChar=0; if (*mx == 0) { - return CERR_ExpansionMustFollowCharacterOrVKey; + return KmnCompilerMessages::ERROR_ExpansionMustFollowCharacterOrVKey; } PKMX_WCHAR p = &tstr[*mx]; p = decxstr(p, tstr); if (*p == UC_SENTINEL) { if (*(p + 1) != CODE_EXTENDED) { - return CERR_ExpansionMustFollowCharacterOrVKey; + return KmnCompilerMessages::ERROR_ExpansionMustFollowCharacterOrVKey; } isVKey = TRUE; BaseKey = *(p + 3); @@ -2610,7 +2867,7 @@ KMX_DWORD process_expansion(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, in KMX_DWORD msg; - if ((msg = GetXString(fk, q, u"", temp, (KMX_DWORD)_countof(temp) - 1, 0, &r, FALSE, TRUE)) != CERR_None) + if ((msg = GetXString(fk, q, u"", temp, (KMX_DWORD)_countof(temp) - 1, 0, &r, FALSE, TRUE)) != STATUS_Success) { return msg; } @@ -2620,25 +2877,25 @@ KMX_DWORD process_expansion(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, in switch(temp[0]) { case 0: - return isVKey ? CERR_VKeyExpansionMustBeFollowedByVKey : CERR_CharacterExpansionMustBeFollowedByCharacter; + return isVKey ? KmnCompilerMessages::ERROR_VKeyExpansionMustBeFollowedByVKey : KmnCompilerMessages::ERROR_CharacterExpansionMustBeFollowedByCharacter; case UC_SENTINEL: // Verify that range is valid virtual key range if(!isVKey) { - return CERR_CharacterExpansionMustBeFollowedByCharacter; + return KmnCompilerMessages::ERROR_CharacterExpansionMustBeFollowedByCharacter; } if (temp[1] != CODE_EXTENDED) { - return CERR_VKeyExpansionMustBeFollowedByVKey; + return KmnCompilerMessages::ERROR_VKeyExpansionMustBeFollowedByVKey; } HighKey = temp[3], HighShiftFlags = temp[2]; if (HighShiftFlags != BaseShiftFlags) { - return CERR_VKeyExpansionMustUseConsistentShift; + return KmnCompilerMessages::ERROR_VKeyExpansionMustUseConsistentShift; } if (HighKey <= BaseKey) { - return CERR_ExpansionMustBePositive; + return KmnCompilerMessages::ERROR_ExpansionMustBePositive; } // Verify space in buffer if (*mx + (HighKey - BaseKey) * 5 + 1 >= max) { - return CERR_VirtualKeyExpansionTooLong; + return KmnCompilerMessages::ERROR_VirtualKeyExpansionTooLong; } // Inject an expansion. for (BaseKey++; BaseKey < HighKey; BaseKey++) { @@ -2654,12 +2911,12 @@ KMX_DWORD process_expansion(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, in default: // Verify that range is a valid character range if (isVKey) { - return CERR_VKeyExpansionMustBeFollowedByVKey; + return KmnCompilerMessages::ERROR_VKeyExpansionMustBeFollowedByVKey; } HighChar = Uni_UTF16ToUTF32(temp); if (HighChar <= BaseChar) { - return CERR_ExpansionMustBePositive; + return KmnCompilerMessages::ERROR_ExpansionMustBePositive; } // Inject an expansion. for (BaseChar++; BaseChar < HighChar; BaseChar++) { @@ -2667,14 +2924,14 @@ KMX_DWORD process_expansion(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, in if (Uni_IsSMP(BaseChar)) { // We'll test on each char to avoid complex calculations crossing SMP boundary if (*mx + 3 >= max) { - return CERR_CharacterRangeTooLong; + return KmnCompilerMessages::ERROR_CharacterRangeTooLong; } tstr[(*mx)++] = (KMX_WCHAR) Uni_UTF32ToSurrogate1(BaseChar); tstr[(*mx)++] = (KMX_WCHAR) Uni_UTF32ToSurrogate2(BaseChar); } else { if (*mx + 2 >= max) { - return CERR_CharacterRangeTooLong; + return KmnCompilerMessages::ERROR_CharacterRangeTooLong; } tstr[(*mx)++] = (KMX_WCHAR) BaseChar; } @@ -2682,7 +2939,7 @@ KMX_DWORD process_expansion(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, in tstr[*mx] = 0; } - return CERR_None; + return STATUS_Success; } KMX_DWORD process_set_synonym(KMX_DWORD dwSystemID, PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) // I3437 @@ -2691,7 +2948,7 @@ KMX_DWORD process_set_synonym(KMX_DWORD dwSystemID, PFILE_KEYBOARD fk, PKMX_WCHA PKMX_WCHAR temp = new KMX_WCHAR[GLOBAL_BUFSIZE], r; KMX_DWORD msg; - if ((msg = GetXString(fk, q, u"", temp, GLOBAL_BUFSIZE - 1, 0, &r, FALSE, TRUE)) != CERR_None) + if ((msg = GetXString(fk, q, u"", temp, GLOBAL_BUFSIZE - 1, 0, &r, FALSE, TRUE)) != STATUS_Success) { delete[] temp; return msg; @@ -2699,16 +2956,20 @@ KMX_DWORD process_set_synonym(KMX_DWORD dwSystemID, PFILE_KEYBOARD fk, PKMX_WCHA KMX_DWORD dwStoreID; - msg = AddStore(fk, TSS_COMPARISON, temp, &dwStoreID); + if(!AddStore(fk, TSS_COMPARISON, temp, &dwStoreID)) { + delete[] temp; + // TODO: redundant error + return KmnCompilerMessages::ERROR_InvalidSet; + } + delete[] temp; - if (msg != CERR_None) return msg; tstr[(*mx)++] = UC_SENTINEL; tstr[(*mx)++] = (KMX_WCHAR)CODE_SETSYSTEMSTORE; tstr[(*mx)++] = (KMX_WCHAR)(dwSystemID + 1); tstr[(*mx)++] = (KMX_WCHAR)(dwStoreID + 1); tstr[(*mx)] = 0; - return CERR_None; + return STATUS_Success; } KMX_DWORD process_set(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) @@ -2718,7 +2979,7 @@ KMX_DWORD process_set(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) while (*s && *s != u' ' && *s != u'=') s++; r = s; while (*s == u' ') s++; - if (*s != '=') return CERR_InvalidSet; + if (*s != '=') return KmnCompilerMessages::ERROR_InvalidSet; s++; while (*s == ' ') s++; *r = 0; @@ -2728,12 +2989,14 @@ KMX_DWORD process_set(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) if (r[0] == '&') { - VERIFY_KEYBOARD_VERSION((PFILE_KEYBOARD) fk, VERSION_90, CERR_90FeatureOnly_SetSystemStores); // I3437 + if(!VerifyKeyboardVersion(fk, VERSION_90)) { + return KmnCompilerMessages::ERROR_90FeatureOnly_SetSystemStores; // I3437 + } for (i = 0; StoreTokens[i]; i++) { if (u16icmp(r, StoreTokens[i]) == 0) break; } - if (!StoreTokens[i]) return CERR_SetSystemStore_NotFound; + if (!StoreTokens[i]) return KmnCompilerMessages::ERROR_SetSystemStore_NotFound; code = CODE_SETSYSTEMSTORE; } else @@ -2746,7 +3009,7 @@ KMX_DWORD process_set(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) { if (u16icmp(r2, fk->dpStoreArray[i].szName) == 0) break; } - if (i == fk->cxStoreArray) return CERR_StoreDoesNotExist; + if (i == fk->cxStoreArray) return KmnCompilerMessages::ERROR_StoreDoesNotExist; kmcmp::CheckStoreUsage(fk, i, FALSE, TRUE, FALSE); code = CODE_SETOPT; } @@ -2757,7 +3020,7 @@ KMX_DWORD process_set(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) //r = wcstok(NULL, L" ="); - if ((msg = GetXString(fk, s, u"", temp, GLOBAL_BUFSIZE - 1, 0, &r, FALSE, TRUE)) != CERR_None) + if ((msg = GetXString(fk, s, u"", temp, GLOBAL_BUFSIZE - 1, 0, &r, FALSE, TRUE)) != STATUS_Success) { delete[] temp; return msg; @@ -2765,16 +3028,20 @@ KMX_DWORD process_set(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) KMX_DWORD dwStoreID; - msg = AddStore(fk, TSS_COMPARISON, temp, &dwStoreID); + if(!AddStore(fk, TSS_COMPARISON, temp, &dwStoreID)) { + delete[] temp; + // TODO: redundant error + return KmnCompilerMessages::ERROR_InvalidSet; + } + delete[] temp; - if (msg != CERR_None) return msg; tstr[(*mx)++] = UC_SENTINEL; tstr[(*mx)++] = (KMX_WCHAR)code; tstr[(*mx)++] = (KMX_WCHAR)(i + 1); tstr[(*mx)++] = (KMX_WCHAR)(dwStoreID + 1); tstr[(*mx)] = 0; - return CERR_None; + return STATUS_Success; } KMX_DWORD process_save(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) @@ -2785,14 +3052,14 @@ KMX_DWORD process_save(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx { if (u16icmp(q, fk->dpStoreArray[i].szName) == 0) break; } - if (i == fk->cxStoreArray) return CERR_StoreDoesNotExist; + if (i == fk->cxStoreArray) return KmnCompilerMessages::ERROR_StoreDoesNotExist; kmcmp::CheckStoreUsage(fk, i, FALSE, TRUE, FALSE); tstr[(*mx)++] = UC_SENTINEL; tstr[(*mx)++] = CODE_SAVEOPT; tstr[(*mx)++] = (KMX_WCHAR)(i + 1); tstr[(*mx)] = 0; - return CERR_None; + return STATUS_Success; } int xatoi(PKMX_WCHAR *p) @@ -2842,21 +3109,21 @@ int GetGroupNum(PFILE_KEYBOARD fk, PKMX_WCHAR p) KMX_DWORD ProcessEthnologueStore(PKMX_WCHAR p) // I2646 { - KMX_DWORD res = CERR_None; + KMX_DWORD res = STATUS_Success; PKMX_WCHAR q = NULL; while (*p) { while (u16chr(u" ,;", *p)) { - if (*p != ' ') res = CWARN_PunctuationInEthnologueCode; + if (*p != ' ') res = KmnCompilerMessages::WARN_PunctuationInEthnologueCode; p++; } - if (q == p) return CERR_InvalidEthnologueCode; + if (q == p) return KmnCompilerMessages::ERROR_InvalidEthnologueCode; if (*p) { for (int i = 0; i < 3; i++) { - if (!iswalpha(*p)) return CERR_InvalidEthnologueCode; + if (!iswalpha(*p)) return KmnCompilerMessages::ERROR_InvalidEthnologueCode; p++; } } @@ -2882,7 +3149,7 @@ KMX_DWORD ProcessHotKey(PKMX_WCHAR p, KMX_DWORD *hk) // Convert virtual key to hotkey (different bitflags) if (ShiftFlags & ~K_HOTKEYSHIFTFLAGS) { - AddWarning(CWARN_HotkeyHasInvalidModifier); + ReportCompilerMessage(KmnCompilerMessages::WARN_HotkeyHasInvalidModifier); } if (ShiftFlags & K_SHIFTFLAG) *hk |= HK_SHIFT; @@ -2891,7 +3158,7 @@ KMX_DWORD ProcessHotKey(PKMX_WCHAR p, KMX_DWORD *hk) *hk |= Key; - return CERR_None; + return STATUS_Success; } q = (PKMX_WCHAR) u16chr(p, '['); @@ -2907,7 +3174,7 @@ KMX_DWORD ProcessHotKey(PKMX_WCHAR p, KMX_DWORD *hk) if (u16nicmp(q, u"ALT", 3) == 0) sFlag |= HK_ALT, q += 3; else if (u16nicmp(q, u"CTRL", 4) == 0) sFlag |= HK_CTRL, q += 4; else if (u16nicmp(q, u"SHIFT", 5) == 0) sFlag |= HK_SHIFT, q += 5; - else if (towupper(*q) != 'K') return CERR_InvalidToken; + else if (towupper(*q) != 'K') return KmnCompilerMessages::ERROR_InvalidToken; } while (towupper(*q) != 'K'); r = (PKMX_WCHAR) u16chr(q, ']'); @@ -2917,18 +3184,18 @@ KMX_DWORD ProcessHotKey(PKMX_WCHAR p, KMX_DWORD *hk) while (iswspace(*r) && r > q) r--; r++; } - else return CERR_NoTokensFound; + else return KmnCompilerMessages::ERROR_NoTokensFound; j = (int)(r - q); for (i = 0; i <= VK__MAX; i++) // I3438 if (j == (int) u16len(VKeyNames[i]) && u16nicmp(q, VKeyNames[i], j) == 0) break; - if (i == VK__MAX + 1) return CERR_InvalidToken; // I3438 + if (i == VK__MAX + 1) return KmnCompilerMessages::ERROR_InvalidToken; // I3438 *hk = i | sFlag; - return CERR_None; + return STATUS_Success; } q = GetDelimitedString(&p, u"\"\"", GDS_CUTLEAD | GDS_CUTFOLL); @@ -2939,10 +3206,10 @@ KMX_DWORD ProcessHotKey(PKMX_WCHAR p, KMX_DWORD *hk) if (u16chr(q, '%')) *hk |= HK_ALT; q = (PKMX_WCHAR) u16chr(q, 0) - 1; *hk |= *q; - return CERR_None; + return STATUS_Success; } - return CERR_CodeInvalidInThisSection; + return KmnCompilerMessages::ERROR_CodeInvalidInThisSection; } @@ -2953,29 +3220,24 @@ void SetChecksum(PKMX_BYTE buf, PKMX_DWORD CheckSum, KMX_DWORD sz) } -KMX_BOOL kmcmp::CheckStoreUsage(PFILE_KEYBOARD fk, int storeIndex, KMX_BOOL fIsStore, KMX_BOOL fIsOption, KMX_BOOL fIsCall) -{ +void kmcmp::CheckStoreUsage(PFILE_KEYBOARD fk, int storeIndex, KMX_BOOL fIsStore, KMX_BOOL fIsOption, KMX_BOOL fIsCall) { PFILE_STORE sp = &fk->dpStoreArray[storeIndex]; - if (fIsStore && !sp->fIsStore) - { - if (sp->fIsDebug || sp->fIsOption || sp->fIsReserved || sp->fIsCall) - AddWarningBool(CWARN_StoreAlreadyUsedAsOptionOrCall); + if (fIsStore && !sp->fIsStore) { + if (sp->fIsDebug || sp->fIsOption || sp->fIsReserved || sp->fIsCall) { + ReportCompilerMessage(KmnCompilerMessages::WARN_StoreAlreadyUsedAsOptionOrCall); + } sp->fIsStore = TRUE; - } - else if (fIsOption && !sp->fIsOption) - { - if (sp->fIsDebug || sp->fIsStore || sp->fIsReserved || sp->fIsCall) - AddWarningBool(CWARN_StoreAlreadyUsedAsStoreOrCall); + } else if (fIsOption && !sp->fIsOption) { + if (sp->fIsDebug || sp->fIsStore || sp->fIsReserved || sp->fIsCall) { + ReportCompilerMessage(KmnCompilerMessages::WARN_StoreAlreadyUsedAsStoreOrCall); + } sp->fIsOption = TRUE; - } - else if (fIsCall && !sp->fIsCall) - { - if (sp->fIsDebug || sp->fIsStore || sp->fIsReserved || sp->fIsOption) - AddWarningBool(CWARN_StoreAlreadyUsedAsStoreOrOption); + } else if (fIsCall && !sp->fIsCall) { + if (sp->fIsDebug || sp->fIsStore || sp->fIsReserved || sp->fIsOption) { + ReportCompilerMessage(KmnCompilerMessages::WARN_StoreAlreadyUsedAsStoreOrOption); + } sp->fIsCall = TRUE; } - - return TRUE; } KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, KMX_BYTE**data, size_t& dataSize) @@ -3028,7 +3290,7 @@ KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, KMX_BYTE**data, size_t& dataS } buf = new KMX_BYTE[size]; - if (!buf) return CERR_CannotAllocateMemory; + if (!buf) return KmnCompilerMessages::FATAL_CannotAllocateMemory; memset(buf, 0, size); ck = (PCOMP_KEYBOARD)buf; @@ -3144,7 +3406,7 @@ KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, KMX_BYTE**data, size_t& dataS if (offset != size) { delete[] buf; - return CERR_SomewhereIGotItWrong; + return KmnCompilerMessages::FATAL_SomewhereIGotItWrong; } SetChecksum(buf, &ck->dwCheckSum, (KMX_DWORD)size); @@ -3152,7 +3414,7 @@ KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, KMX_BYTE**data, size_t& dataS *data = buf; dataSize = size; - return CERR_None; + return STATUS_Success; } KMX_DWORD ReadLine(KMX_BYTE* infile, int sz, int& offset, PKMX_WCHAR wstr, KMX_BOOL PreProcess) @@ -3165,7 +3427,7 @@ KMX_DWORD ReadLine(KMX_BYTE* infile, int sz, int& offset, PKMX_WCHAR wstr, KMX_B KMX_WCHAR str[LINESIZE + 3]; if(offset >= sz) { - return CERR_EndOfFile; + return STATUS_EndOfFile; } len = offset + LINESIZE*2 > sz ? sz-offset : LINESIZE*2; @@ -3181,7 +3443,7 @@ KMX_DWORD ReadLine(KMX_BYTE* infile, int sz, int& offset, PKMX_WCHAR wstr, KMX_B } if (len == 0) { - return CERR_EndOfFile; + return STATUS_EndOfFile; } @@ -3193,7 +3455,7 @@ KMX_DWORD ReadLine(KMX_BYTE* infile, int sz, int& offset, PKMX_WCHAR wstr, KMX_B { *p = 0; // I2525 u16ncpy(wstr, str, LINESIZE); // I3481 - return (PreProcess ? CERR_None : CERR_UnterminatedString); + return (PreProcess ? STATUS_Success : KmnCompilerMessages::ERROR_UnterminatedString); } if (*p == currentQuotes) currentQuotes = 0; continue; @@ -3225,7 +3487,7 @@ KMX_DWORD ReadLine(KMX_BYTE* infile, int sz, int& offset, PKMX_WCHAR wstr, KMX_B } *p = 0; // I2525 u16ncpy(wstr, str, LINESIZE); // I3481 - return (PreProcess ? CERR_None : CERR_InvalidLineContinuation); + return (PreProcess ? STATUS_Success : KmnCompilerMessages::ERROR_InvalidLineContinuation); } if (*p == L'\n') break; @@ -3254,7 +3516,7 @@ KMX_DWORD ReadLine(KMX_BYTE* infile, int sz, int& offset, PKMX_WCHAR wstr, KMX_B str[LINESIZE - 1] = 0; // I2525 u16ncpy(wstr, str, LINESIZE); // I3481 if (len == LINESIZE) - return (PreProcess ? CERR_None : CERR_LineTooLong); + return (PreProcess ? STATUS_Success : KmnCompilerMessages::ERROR_LineTooLong); } kmcmp::currentLine++; @@ -3273,7 +3535,7 @@ KMX_DWORD ReadLine(KMX_BYTE* infile, int sz, int& offset, PKMX_WCHAR wstr, KMX_B // trim spaces now, why not? u16ncpy(wstr, str, LINESIZE); // I3481 - return CERR_None; + return STATUS_Success; } KMX_DWORD GetRHS(PFILE_KEYBOARD fk, PKMX_WCHAR p, PKMX_WCHAR buf, int bufsize, int offset, int IsUnicode) @@ -3281,7 +3543,7 @@ KMX_DWORD GetRHS(PFILE_KEYBOARD fk, PKMX_WCHAR p, PKMX_WCHAR buf, int bufsize, i PKMX_WCHAR q; p = (const PKMX_WCHAR) u16chr(p, '>'); - if (!p) return CERR_NoTokensFound; + if (!p) return KmnCompilerMessages::ERROR_NoTokensFound; p++; @@ -3318,35 +3580,34 @@ KMX_DWORD ImportBitmapFile(PFILE_KEYBOARD fk, PKMX_WCHAR szName, PKMX_DWORD File { auto szNameUtf8 = string_from_u16string(szName); - if(!loadfileproc(szNameUtf8.c_str(), fk->extra->kmnFilename.c_str(), nullptr, (int*) FileSize, msgprocContext)) { + std::vector bufvec = loadfileproc(szNameUtf8, fk->extra->kmnFilename); + if(!bufvec.size()) { // Append .bmp and try again if(endsWith(szNameUtf8, ".bmp")) { - return CERR_CannotReadBitmapFile; + return KmnCompilerMessages::ERROR_CannotReadBitmapFile; } szNameUtf8.append(".bmp"); - if(!loadfileproc(szNameUtf8.c_str(), fk->extra->kmnFilename.c_str(), nullptr, (int*) FileSize, msgprocContext)) { - return CERR_CannotReadBitmapFile; - } + bufvec = loadfileproc(szNameUtf8, fk->extra->kmnFilename); } - if(*FileSize < 2) { + if(bufvec.size() < 2) { // Zero-byte file is invalid; 2 byte file is too, but we only really care // about the prolog at this point so we don't overrun our buffer - return CERR_CannotReadBitmapFile; + return KmnCompilerMessages::ERROR_CannotReadBitmapFile; } + *FileSize = static_cast(bufvec.size()); *Buf = new KMX_BYTE[*FileSize]; - if(!loadfileproc(szNameUtf8.c_str(), fk->extra->kmnFilename.c_str(), *Buf, (int*) FileSize, msgprocContext)) { - delete[] *Buf; - return CERR_CannotReadBitmapFile; - } + std::copy(bufvec.begin(), bufvec.end(), *Buf); /* Test for version 7.0 icon support */ if (*((PKMX_CHAR)*Buf) != 'B' && *(((PKMX_CHAR)*Buf) + 1) != 'M') { - VERIFY_KEYBOARD_VERSION(fk, VERSION_70, CERR_70FeatureOnly); + if(!VerifyKeyboardVersion(fk, VERSION_70)) { + return KmnCompilerMessages::ERROR_70FeatureOnly; + } } - return CERR_None; + return STATUS_Success; } int atoiW(PKMX_WCHAR p) @@ -3357,6 +3618,26 @@ int atoiW(PKMX_WCHAR p) return i; } +/** + * Checks if a wide-character C-string represents an integer. + * It does not strip whitespace, and depends on the action of atoi() + * to determine if the C-string is an integer. + * + * @param p a pointer to a wide-character C-string + * + * @return true if p represents an integer, false otherwise +*/ +bool isIntegerWstring(PKMX_WCHAR p) { + if (!p || !*p) + return false; + PKMX_STR q = wstrtostr(p); + std::ostringstream os; + os << atoi(q); + int cmp = strcmp(q, os.str().c_str()); + delete[] q; + return cmp == 0 ? true : false; +} + KMX_DWORD kmcmp::CheckUTF16(int n) { const int res[] = { @@ -3366,14 +3647,14 @@ KMX_DWORD kmcmp::CheckUTF16(int n) 0xFDE8, 0xFDE9, 0xFDEA, 0xFDEB, 0xFDEC, 0xFDED, 0xFDEE, 0xFDEF, 0xFFFF, 0xFFFE, 0 }; - if (n == 0) return CERR_ReservedCharacter; + if (n == 0) return KmnCompilerMessages::ERROR_ReservedCharacter; for (int i = 0; res[i] > 0; i++) if (n == res[i]) { - AddWarning(CWARN_ReservedCharacter); + ReportCompilerMessage(KmnCompilerMessages::WARN_ReservedCharacter); break; } - return CERR_None; + return STATUS_Success; } KMX_DWORD kmcmp::UTF32ToUTF16(int n, int *n1, int *n2) @@ -3382,25 +3663,30 @@ KMX_DWORD kmcmp::UTF32ToUTF16(int n, int *n1, int *n2) if (n <= 0xFFFF) { *n1 = n; - if (n >= 0xD800 && n <= 0xDFFF) AddWarning(CWARN_UnicodeSurrogateUsed); + if (n >= 0xD800 && n <= 0xDFFF) { + ReportCompilerMessage(KmnCompilerMessages::WARN_UnicodeSurrogateUsed); + } return kmcmp::CheckUTF16(*n1); } - if ((n & 0xFFFF) == 0xFFFF || (n & 0xFFFF) == 0xFFFE) AddWarning(CWARN_ReservedCharacter); - if (n < 0 || n > 0x10FFFF) return CERR_InvalidCharacter; + if ((n & 0xFFFF) == 0xFFFF || (n & 0xFFFF) == 0xFFFE) { + ReportCompilerMessage(KmnCompilerMessages::WARN_ReservedCharacter); + } + if (n < 0 || n > 0x10FFFF) { + return KmnCompilerMessages::ERROR_InvalidCharacter; + } n = n - 0x10000; *n1 = (n / 0x400) + 0xD800; *n2 = (n % 0x400) + 0xDC00; KMX_DWORD msg; - if ((msg = kmcmp::CheckUTF16(*n1)) != CERR_None) return msg; + if ((msg = kmcmp::CheckUTF16(*n1)) != STATUS_Success) return msg; return kmcmp::CheckUTF16(*n2); } -KMX_DWORD BuildVKDictionary(PFILE_KEYBOARD fk) -{ +KMX_BOOL BuildVKDictionary(PFILE_KEYBOARD fk) { KMX_DWORD i; size_t len = 0; - if (fk->cxVKDictionary == 0) return CERR_None; + if (fk->cxVKDictionary == 0) return TRUE; for (i = 0; i < fk->cxVKDictionary; i++) { len += u16len(fk->dpVKDictionary[i].szName) + 1; @@ -3418,9 +3704,12 @@ KMX_DWORD BuildVKDictionary(PFILE_KEYBOARD fk) *p = 0; KMX_DWORD dwStoreID; - KMX_DWORD msg = AddStore(fk, TSS_VKDICTIONARY, storeval, &dwStoreID); + if(!AddStore(fk, TSS_VKDICTIONARY, storeval, &dwStoreID)) { + delete[] storeval; + return FALSE; + } delete[] storeval; - return msg; + return TRUE; } int GetVKCode(PFILE_KEYBOARD fk, PKMX_WCHAR p) @@ -3473,7 +3762,7 @@ void kmcmp::RecordDeadkeyNames(PFILE_KEYBOARD fk) KMX_DWORD i; for (i = 0; i < fk->cxDeadKeyArray; i++) { - u16sprintf(buf, _countof(buf), L"%ls%d ", u16fmt(DEBUGSTORE_DEADKEY).c_str(), (int)i); + u16sprintf(buf, _countof(buf), L"%ls%d ", DEBUGSTORE_DEADKEY_L, (int)i); u16ncat(buf, fk->dpDeadKeyArray[i].szName, _countof(buf)); AddDebugStore(fk, buf); @@ -3512,8 +3801,8 @@ bool UTF16TempFromUTF8(KMX_BYTE* infile, int sz, KMX_BYTE** tempfile, int *sz16) try { std::wstring_convert, char16_t> converter; result = converter.from_bytes((char*)infile, (char*)infile+sz); - } catch(std::range_error e) { - AddCompileError(CHINT_NonUnicodeFile); + } catch(std::range_error& e) { + ReportCompilerMessage(KmnCompilerMessages::HINT_NonUnicodeFile); result.resize(sz); for(int i = 0; i < sz; i++) { result[i] = CP1252_UNICODE[infile[i]]; @@ -3521,11 +3810,11 @@ bool UTF16TempFromUTF8(KMX_BYTE* infile, int sz, KMX_BYTE** tempfile, int *sz16) } if(hasPreamble(result)) { - *sz16 = result.size() * 2 - 2; + *sz16 = static_cast(result.size()) * 2 - 2; *tempfile = new KMX_BYTE[*sz16]; memcpy(*tempfile, result.c_str() + 1, *sz16); } else { - *sz16 = result.size() * 2; + *sz16 = static_cast(result.size()) * 2; *tempfile = new KMX_BYTE[*sz16]; memcpy(*tempfile, result.c_str(), *sz16); } diff --git a/developer/src/kmcmplib/src/CompilerErrors.cpp b/developer/src/kmcmplib/src/CompilerErrors.cpp new file mode 100644 index 00000000000..6de261c8e98 --- /dev/null +++ b/developer/src/kmcmplib/src/CompilerErrors.cpp @@ -0,0 +1,31 @@ +#include "pch.h" + +#include +#include +#include + +namespace kmcmp { + int ErrChr; + int nErrors = 0; + kmcmp_CompilerMessageProc msgproc = nullptr; + void* msgprocContext = NULL; + std::string messageFilename; +} + +void ReportCompilerMessage(KMX_DWORD msg, const std::vector& parameters) { + const KMX_DWORD severity = msg & MESSAGE_SEVERITY_MASK; + + if (severity == CompilerErrorSeverity::Error || severity == CompilerErrorSeverity::Fatal) { + kmcmp::nErrors++; + } + + KMCMP_COMPILER_RESULT_MESSAGE message; + message.errorCode = msg; + message.lineNumber = kmcmp::currentLine + 1; + message.columnNumber = kmcmp::ErrChr; + message.parameters = parameters; + message.filename = kmcmp::messageFilename; + (*kmcmp::msgproc)(message, kmcmp::msgprocContext); + + kmcmp::ErrChr = 0; +} diff --git a/developer/src/kmcmplib/src/CompilerErrors.h b/developer/src/kmcmplib/src/CompilerErrors.h new file mode 100644 index 00000000000..ba57784639e --- /dev/null +++ b/developer/src/kmcmplib/src/CompilerErrors.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +namespace kmcmp { + extern kmcmp_CompilerMessageProc msgproc; + extern void* msgprocContext; + extern std::string messageFilename; +} + +void ReportCompilerMessage(KMX_DWORD msg, const std::vector& parameters = {}); diff --git a/developer/src/kmcmplib/src/CompilerInterfaces.cpp b/developer/src/kmcmplib/src/CompilerInterfaces.cpp index 17968590001..ba368028322 100644 --- a/developer/src/kmcmplib/src/CompilerInterfaces.cpp +++ b/developer/src/kmcmplib/src/CompilerInterfaces.cpp @@ -25,39 +25,38 @@ EXTERN bool kmcmp_CompileKeyboard( kmcmp::CompileTarget = options.target; if (!messageProc || !loadFileProc || !pszInfile) { - AddCompileError(CERR_BadCallParams); + ReportCompilerMessage(KmnCompilerMessages::FATAL_BadCallParams); return FALSE; } - msgproc = messageProc; - loadfileproc = loadFileProc; - msgprocContext = (void*)procContext; + kmcmp::messageFilename = pszInfile; + kmcmp::msgproc = messageProc; + kmcmp::loadfileproc = loadFileProc; + kmcmp::msgprocContext = (void*)procContext; kmcmp::currentLine = 0; kmcmp::nErrors = 0; int sz; - if(!loadFileProc(pszInfile, "", nullptr, &sz, msgprocContext)) { - AddCompileError(CERR_InfileNotExist); + std::vector bufvec = kmcmp::loadfileproc(pszInfile, ""); + sz = static_cast(bufvec.size()); + if(!sz) { + ReportCompilerMessage(KmnCompilerMessages::ERROR_InfileNotExist); return FALSE; } if(sz < 3) { // Technically, a 3 byte file can never be a valid .kmn, so we can shortcut // here and avoid testing outside memory bounds for looking at BOM - AddCompileError(CERR_CannotReadInfile); + ReportCompilerMessage(KmnCompilerMessages::ERROR_CannotReadInfile); return FALSE; } KMX_BYTE* infile = new KMX_BYTE[sz+1]; if(!infile) { - AddCompileError(CERR_CannotAllocateMemory); - return FALSE; - } - if(!loadFileProc(pszInfile, "", infile, &sz, msgprocContext)) { - delete[] infile; - AddCompileError(CERR_CannotReadInfile); + ReportCompilerMessage(KmnCompilerMessages::FATAL_CannotAllocateMemory); return FALSE; } + std::copy(bufvec.begin(), bufvec.end(), infile); infile[sz] = 0; // zero-terminate for safety, not technically needed but helps avoid memory bugs int offset = 0; @@ -70,7 +69,7 @@ EXTERN bool kmcmp_CompileKeyboard( int sz16; if(!UTF16TempFromUTF8(infile, sz, &infile16, &sz16)) { delete[] infile; - AddCompileError(CERR_CannotCreateTempfile); + ReportCompilerMessage(KmnCompilerMessages::FATAL_CannotCreateTempfile); return FALSE; } delete[] infile; @@ -96,8 +95,8 @@ EXTERN bool kmcmp_CompileKeyboard( //TODO: FreeKeyboardPointers(fk); - if(msg != CERR_None) { - AddCompileError(msg); + if(msg != STATUS_Success) { + ReportCompilerMessage(msg); return FALSE; } diff --git a/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp b/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp index 977b3b74436..1ddd45dd3f8 100644 --- a/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp +++ b/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp @@ -3,49 +3,20 @@ #ifdef __EMSCRIPTEN__ -/* - WASM interface for compiler message callback -*/ -EM_JS(int, wasm_msgproc, (int line, int msgcode, const char* text, char* context), { - const proc = globalThis[UTF8ToString(context)].message; - if(!proc || typeof proc != 'function') { - console.log(`[${line}: ${msgcode}: ${UTF8ToString(text)}]`); - return 0; - } else { - return proc(line, msgcode, UTF8ToString(text)); - } -}); - -EM_JS(int, wasm_loadfileproc, (const char* filename, const char* baseFilename, void* buffer, int bufferSize, char* context), { - const proc = globalThis[UTF8ToString(context)].loadFile; - if(!proc || typeof proc != 'function') { - return 0; - } else { - if(buffer == 0) { - return proc(UTF8ToString(filename), UTF8ToString(baseFilename), 0, 0); - } else { - return proc(UTF8ToString(filename), UTF8ToString(baseFilename), buffer, bufferSize); - } +struct WasmCallbackInterface { + virtual void message(const KMCMP_COMPILER_RESULT_MESSAGE& message) = 0; + virtual const std::vector loadFile(const std::string& filename, const std::string& baseFilename) = 0; + virtual ~WasmCallbackInterface() {} +}; + +struct WasmCallbackInterfaceWrapper : public emscripten::wrapper { + EMSCRIPTEN_WRAPPER(WasmCallbackInterfaceWrapper); + void message(const KMCMP_COMPILER_RESULT_MESSAGE& message) { + return call("message", message); } -}); - -bool wasm_LoadFileProc(const char* filename, const char* baseFilename, void* buffer, int* bufferSize, void* context) { - char* msgProc = static_cast(context); - if(buffer == nullptr) { - *bufferSize = wasm_loadfileproc(filename, baseFilename, 0, 0, msgProc); - return *bufferSize != -1; - } else { - return wasm_loadfileproc(filename, baseFilename, buffer, *bufferSize, msgProc) == 1; + const std::vector loadFile(const std::string& filename, const std::string& baseFilename) { + return call>("loadFile", filename, baseFilename); } -} - -int wasm_CompilerMessageProc(int line, uint32_t dwMsgCode, const char* szText, void* context) { - char* msgProc = static_cast(context); - return wasm_msgproc(line, dwMsgCode, szText, msgProc); -} - -struct WASM_COMPILER_INTERFACE { - std::string callbacksKey; // key of callbacks object on globalThis }; struct WASM_COMPILER_RESULT { @@ -56,16 +27,28 @@ struct WASM_COMPILER_RESULT { KMCMP_COMPILER_RESULT_EXTRA extra; }; -WASM_COMPILER_RESULT kmcmp_wasm_compile(std::string pszInfile, const KMCMP_COMPILER_OPTIONS options, const WASM_COMPILER_INTERFACE intf) { +WasmCallbackInterface* globalCallbacks = nullptr; + +const std::vector wasm_LoadFileProc(const std::string& filename, const std::string& baseFilename) { + return globalCallbacks->loadFile(filename, baseFilename); +} + +void wasm_CompilerMessageProc(const KMCMP_COMPILER_RESULT_MESSAGE& message, void* context) { + globalCallbacks->message(message); +} + +WASM_COMPILER_RESULT kmcmp_wasm_compile(std::string pszInfile, const KMCMP_COMPILER_OPTIONS options, WasmCallbackInterface& callbacks) { WASM_COMPILER_RESULT r = {false}; KMCMP_COMPILER_RESULT kr; + globalCallbacks = &callbacks; + r.result = kmcmp_CompileKeyboard( pszInfile.c_str(), options, wasm_CompilerMessageProc, wasm_LoadFileProc, - intf.callbacksKey.c_str(), + "", //TODO: eliminate? kr ); @@ -76,6 +59,8 @@ WASM_COMPILER_RESULT kmcmp_wasm_compile(std::string pszInfile, const KMCMP_COMPI r.extra = kr.extra; } + globalCallbacks = nullptr; + return r; } @@ -90,9 +75,16 @@ struct BindingType> { using ValBinding = BindingType; using WireType = ValBinding::WireType; +#if __EMSCRIPTEN_major__ == 3 && __EMSCRIPTEN_minor__ == 1 && __EMSCRIPTEN_tiny__ >= 60 + // emscripten-core/emscripten#21692 + static WireType toWireType(const std::vector &vec, rvp::default_tag) { + return ValBinding::toWireType(val::array(vec), rvp::default_tag{}); + } +#else static WireType toWireType(const std::vector &vec) { return ValBinding::toWireType(val::array(vec)); } +#endif static std::vector fromWireType(WireType value) { return vecFromJSArray(ValBinding::fromWireType(value)); @@ -119,6 +111,12 @@ int kmcmp_testSentry() { EMSCRIPTEN_BINDINGS(compiler_interface) { + emscripten::class_("WasmCallbackInterface") + .function("message", &WasmCallbackInterface::message, emscripten::pure_virtual()) + .function("loadFile", &WasmCallbackInterface::loadFile, emscripten::pure_virtual()) + .allow_subclass("WasmCallbackInterfaceWrapper") + ; + emscripten::class_("CompilerOptions") .constructor<>() .property("saveDebug", &KMCMP_COMPILER_OPTIONS::saveDebug) @@ -128,11 +126,6 @@ EMSCRIPTEN_BINDINGS(compiler_interface) { .property("target", &KMCMP_COMPILER_OPTIONS::target) ; - emscripten::class_("CompilerInterface") - .constructor<>() - .property("callbacksKey", &WASM_COMPILER_INTERFACE::callbacksKey) - ; - emscripten::class_("CompilerResult") .constructor<>() .property("result", &WASM_COMPILER_RESULT::result) @@ -141,6 +134,15 @@ EMSCRIPTEN_BINDINGS(compiler_interface) { .property("extra", &WASM_COMPILER_RESULT::extra) ; + emscripten::class_("CompilerResultMessage") + .constructor<>() + .property("errorCode", &KMCMP_COMPILER_RESULT_MESSAGE::errorCode) + .property("lineNumber", &KMCMP_COMPILER_RESULT_MESSAGE::lineNumber) + .property("columnNumber", &KMCMP_COMPILER_RESULT_MESSAGE::columnNumber) + .property("filename", &KMCMP_COMPILER_RESULT_MESSAGE::filename) + .property("parameters", &KMCMP_COMPILER_RESULT_MESSAGE::parameters) + ; + emscripten::class_("CompilerResultExtra") .constructor<>() .property("targets", &KMCMP_COMPILER_RESULT_EXTRA::targets) diff --git a/developer/src/kmcmplib/src/DeprecationChecks.cpp b/developer/src/kmcmplib/src/DeprecationChecks.cpp index 11042f0c132..671f3ec9a4e 100644 --- a/developer/src/kmcmplib/src/DeprecationChecks.cpp +++ b/developer/src/kmcmplib/src/DeprecationChecks.cpp @@ -6,15 +6,14 @@ #include "kmcmplib.h" #include "DeprecationChecks.h" -KMX_BOOL kmcmp::WarnDeprecatedHeader() { // I4866 -if( AWarnDeprecatedCode_GLOBAL_LIB){ - AddWarningBool(CWARN_HeaderStatementIsDeprecated); +void kmcmp::WarnDeprecatedHeader() { // I4866 + if (AWarnDeprecatedCode_GLOBAL_LIB) { + ReportCompilerMessage(KmnCompilerMessages::WARN_HeaderStatementIsDeprecated); } - return TRUE; } /* Flag presence of deprecated features */ -KMX_BOOL kmcmp::CheckForDeprecatedFeatures(PFILE_KEYBOARD fk) { +void kmcmp::CheckForDeprecatedFeatures(PFILE_KEYBOARD fk) { /* For Keyman 10, we deprecated: // < Keyman 7 @@ -31,7 +30,7 @@ KMX_BOOL kmcmp::CheckForDeprecatedFeatures(PFILE_KEYBOARD fk) { PFILE_STORE sp; if (!AWarnDeprecatedCode_GLOBAL_LIB) { - return TRUE; + return; } if (fk->version >= VERSION_100) { @@ -42,12 +41,10 @@ KMX_BOOL kmcmp::CheckForDeprecatedFeatures(PFILE_KEYBOARD fk) { sp->dwSystemID == TSS_ETHNOLOGUECODE || sp->dwSystemID == TSS_WINDOWSLANGUAGES) { kmcmp::currentLine = sp->line; - AddWarningBool(CWARN_LanguageHeadersDeprecatedInKeyman10); + ReportCompilerMessage(KmnCompilerMessages::WARN_LanguageHeadersDeprecatedInKeyman10); } } } kmcmp::currentLine = oldCurrentLine; - - return TRUE; } diff --git a/developer/src/kmcmplib/src/DeprecationChecks.h b/developer/src/kmcmplib/src/DeprecationChecks.h index 941b87f1a4d..329519ec328 100644 --- a/developer/src/kmcmplib/src/DeprecationChecks.h +++ b/developer/src/kmcmplib/src/DeprecationChecks.h @@ -4,6 +4,6 @@ #include "kmcmplib.h" namespace kmcmp{ -KMX_BOOL WarnDeprecatedHeader(); -KMX_BOOL CheckForDeprecatedFeatures(PFILE_KEYBOARD fk); + void WarnDeprecatedHeader(); + void CheckForDeprecatedFeatures(PFILE_KEYBOARD fk); } diff --git a/developer/src/kmcmplib/src/NamedCodeConstants.cpp b/developer/src/kmcmplib/src/NamedCodeConstants.cpp index 35273542ebb..9cf322ab72a 100644 --- a/developer/src/kmcmplib/src/NamedCodeConstants.cpp +++ b/developer/src/kmcmplib/src/NamedCodeConstants.cpp @@ -120,15 +120,14 @@ KMX_BOOL NamedCodeConstants::LoadFile(PFILE_KEYBOARD fk, const KMX_WCHAR *filena int FileSize; KMX_BYTE* Buf; - if(!loadfileproc(szNameUtf8.c_str(), fk->extra->kmnFilename.c_str(), nullptr, &FileSize, msgprocContext)) { + std::vector bufvec = loadfileproc(szNameUtf8, fk->extra->kmnFilename); + FileSize = static_cast(bufvec.size()); + if(!FileSize) { return FALSE; } Buf = new KMX_BYTE[FileSize+1]; - if(!loadfileproc(szNameUtf8.c_str(), fk->extra->kmnFilename.c_str(), Buf, &FileSize, msgprocContext)) { - delete[] Buf; - return FALSE; - } + std::copy(bufvec.begin(), bufvec.end(), Buf); Buf[FileSize] = 0; // zero-terminate for strtok char* filetok; @@ -268,7 +267,7 @@ int IsHangulSyllable(const KMX_WCHAR *codename, int *code) if(strchr("GNDRMBSJCKTPH", ch)) { /* Has an initial syllable */ - int isDoubled = towupper(*(codename+1)) == ch; + int isDoubled = towupper(*(codename+1)) == (wint_t)ch; LIndex = -1; for(i = 0; i < HangulLCount; i++) { diff --git a/developer/src/kmcmplib/src/UnreachableRules.cpp b/developer/src/kmcmplib/src/UnreachableRules.cpp index 9d114a80a43..40965db4d67 100644 --- a/developer/src/kmcmplib/src/UnreachableRules.cpp +++ b/developer/src/kmcmplib/src/UnreachableRules.cpp @@ -18,14 +18,15 @@ namespace kmcmp { std::wstringstream key; key << kp->Key << "," << kp->ShiftFlags << ","; if (kp->dpContext) { - std::wstring Context_ws = u16fmt((const PKMX_WCHAR) kp->dpContext); + std::wstring Context_ws = wstring_from_u16string((const PKMX_WCHAR) kp->dpContext); key << Context_ws; } return key.str(); } } -KMX_DWORD VerifyUnreachableRules(PFILE_GROUP gp) { + +void VerifyUnreachableRules(PFILE_GROUP gp) { PFILE_KEY kp = gp->dpKeyArray; KMX_DWORD i; @@ -41,8 +42,9 @@ KMX_DWORD VerifyUnreachableRules(PFILE_GROUP gp) { if (kp->Line != k1.Line && reportedLines.count(kp->Line) == 0) { reportedLines.insert(kp->Line); kmcmp::currentLine = kp->Line; - snprintf(ErrExtraLIB, ERR_EXTRA_LIB_LEN, " Overridden by rule on line %d", k1.Line); - AddWarning(CHINT_UnreachableRule); + ReportCompilerMessage(KmnCompilerMessages::HINT_UnreachableRule, { + /* LineNumber */ std::to_string(k1.Line) + }); } } else { @@ -51,6 +53,4 @@ KMX_DWORD VerifyUnreachableRules(PFILE_GROUP gp) { } kmcmp::currentLine = oldCurrentLine; - - return CERR_None; } diff --git a/developer/src/kmcmplib/src/UnreachableRules.h b/developer/src/kmcmplib/src/UnreachableRules.h index d9eac65ce3c..a7ccba3466a 100644 --- a/developer/src/kmcmplib/src/UnreachableRules.h +++ b/developer/src/kmcmplib/src/UnreachableRules.h @@ -1,3 +1,3 @@ #pragma once -KMX_DWORD VerifyUnreachableRules(PFILE_GROUP gp); +void VerifyUnreachableRules(PFILE_GROUP gp); diff --git a/developer/src/kmcmplib/src/compfile.h b/developer/src/kmcmplib/src/compfile.h index f45cb3ea348..e32b2c26319 100644 --- a/developer/src/kmcmplib/src/compfile.h +++ b/developer/src/kmcmplib/src/compfile.h @@ -31,7 +31,6 @@ #define SZMAX_STORENAME 80 #define SZMAX_GROUPNAME 80 #define SZMAX_DEADKEYNAME 80 -#define SZMAX_ERRORTEXT 512 #define SZMAX_VKDICTIONARYNAME 80 #define MAX_WARNINGS 100 @@ -167,26 +166,4 @@ const KMX_DWORD sz_FILE_DEADKEY = sizeof(FILE_DEADKEY); const KMX_DWORD sz_FILE_VKDICTIONARY = sizeof(FILE_VKDICTIONARY); const KMX_DWORD sz_FILE_KEYBOARD = sizeof(FILE_KEYBOARD); -struct COMPMSG { - KMX_CHAR szText[SZMAX_ERRORTEXT]; - KMX_DWORD Line; - KMX_DWORD dwMsgCode; -}; - -typedef COMPMSG *PCOMPMSG; - -struct COMPILEMESSAGES { - int nMessages; - int nErrors; - - PCOMPMSG cm; - - KMX_DWORD fatalCode; - KMX_CHAR szFatalText[SZMAX_ERRORTEXT]; - - KMX_DWORD currentLine; -}; - -typedef COMPILEMESSAGES *PCOMPILEMESSAGES; - #endif // _COMPFILE_H diff --git a/developer/src/kmcmplib/src/debugstore.h b/developer/src/kmcmplib/src/debugstore.h index c54e3b45c34..d8bd4643df0 100644 --- a/developer/src/kmcmplib/src/debugstore.h +++ b/developer/src/kmcmplib/src/debugstore.h @@ -5,16 +5,16 @@ #define DEBUGSTORE_BEGIN u"B" #define DEBUGSTORE_BEGIN_C u'B' -#define DEBUGSTORE_MATCH u"M" -#define DEBUGSTORE_MATCH_C u'M' +#define DEBUGSTORE_MATCH_U u"M" +#define DEBUGSTORE_MATCH_L L"M" -#define DEBUGSTORE_NOMATCH u"N" -#define DEBUGSTORE_NOMATCH_C u'N' +#define DEBUGSTORE_NOMATCH_U u"M" +#define DEBUGSTORE_NOMATCH_L L"M" -#define DEBUGSTORE_GROUP u"G" -#define DEBUGSTORE_GROUP_C u'G' +#define DEBUGSTORE_GROUP_U u"G" +#define DEBUGSTORE_GROUP_L L"G" -#define DEBUGSTORE_DEADKEY u"D" -#define DEBUGSTORE_DEADKEY_C u'D' +#define DEBUGSTORE_DEADKEY_U u"D" +#define DEBUGSTORE_DEADKEY_L L"D" #endif /* DEBUGSTORE_H */ diff --git a/developer/src/kmcmplib/src/kmcmplib.h b/developer/src/kmcmplib/src/kmcmplib.h index 0d7c4f90042..91edfe1b260 100644 --- a/developer/src/kmcmplib/src/kmcmplib.h +++ b/developer/src/kmcmplib/src/kmcmplib.h @@ -3,9 +3,9 @@ #include #include "compfile.h" #include "NamedCodeConstants.h" +#include namespace kmcmp { - KMX_BOOL AddCompileWarning(char* buf); extern int currentLine; extern KMX_BOOL FShouldAddCompilerVersion; extern KMX_BOOL FSaveDebug, FCompilerWarningsAsErrors; // I4865 // I4866 @@ -17,34 +17,24 @@ namespace kmcmp { extern int BeginLine[4]; extern int currentLine; extern NamedCodeConstants *CodeConstants; + extern kmcmp_LoadFileProc loadfileproc; void RecordDeadkeyNames(PFILE_KEYBOARD fk); - KMX_DWORD AddCompilerVersionStore(PFILE_KEYBOARD fk); + KMX_BOOL AddCompilerVersionStore(PFILE_KEYBOARD fk); } -extern kmcmp_CompilerMessageProc msgproc; -extern kmcmp_LoadFileProc loadfileproc; -extern void* msgprocContext; extern KMX_BOOL AWarnDeprecatedCode_GLOBAL_LIB; -#define ERR_EXTRA_LIB_LEN 256 -extern char ErrExtraLIB[ERR_EXTRA_LIB_LEN]; -KMX_BOOL AddCompileError(KMX_DWORD msg); - -/// Use AddWarningBool for functions that return bool or KMX_BOOL -#define AddWarningBool(warn) { if(AddCompileError(warn)) return FALSE; } -/// Use AddWarning for functions that return KMX_DWORD -#define AddWarning(warn) { if(AddCompileError(warn)) return CERR_Break; } PKMX_WCHAR strtowstr(PKMX_STR in); PFILE_STORE FindSystemStore(PFILE_KEYBOARD fk, KMX_DWORD dwSystemID); bool UTF16TempFromUTF8(KMX_BYTE* infile, int sz, KMX_BYTE** tempfile, int *sz16); KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, KMX_BYTE**data, size_t& dataSize); -KMX_DWORD AddStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, const KMX_WCHAR * str, KMX_DWORD *dwStoreID= NULL); +KMX_BOOL AddStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, const KMX_WCHAR * str, KMX_DWORD *dwStoreID= NULL); KMX_DWORD ReadLine(KMX_BYTE* infile, int sz, int& offset, PKMX_WCHAR wstr, KMX_BOOL PreProcess); -KMX_DWORD ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str); -KMX_DWORD ProcessGroupLine(PFILE_KEYBOARD fk, PKMX_WCHAR p); -KMX_DWORD ProcessGroupFinish(PFILE_KEYBOARD fk); -KMX_DWORD ProcessStoreLine(PFILE_KEYBOARD fk, PKMX_WCHAR p); +KMX_BOOL ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str); +KMX_BOOL ProcessGroupLine(PFILE_KEYBOARD fk, PKMX_WCHAR p); +KMX_BOOL ProcessGroupFinish(PFILE_KEYBOARD fk); +KMX_BOOL ProcessStoreLine(PFILE_KEYBOARD fk, PKMX_WCHAR p); int LineTokenType(PKMX_WCHAR *str); -KMX_DWORD BuildVKDictionary(PFILE_KEYBOARD fk); // I3438 +KMX_BOOL BuildVKDictionary(PFILE_KEYBOARD fk); // I3438 diff --git a/developer/src/kmcmplib/src/kmx_u16.cpp b/developer/src/kmcmplib/src/kmx_u16.cpp deleted file mode 100644 index c73796e9b6b..00000000000 --- a/developer/src/kmcmplib/src/kmx_u16.cpp +++ /dev/null @@ -1,295 +0,0 @@ - -//#include "../../kmcompx/include/kmcompx.h" -#include -#include "kmx_u16.h" - -#include -#include -#include -#include -#include -#include - -//String <- wstring -std::string string_from_wstring(std::wstring const str) { - std::wstring_convert, wchar_t> converter; - return converter.to_bytes(str); -} -//wstring <- string -std::wstring wstring_from_string(std::string const str) { - std::wstring_convert, wchar_t> converter; - return converter.from_bytes(str); -} - -//u16String <- string -std::u16string u16string_from_string(std::string const str) { - std::wstring_convert, char16_t> converter; - return converter.from_bytes(str); -} - -//string <- u16string -std::string string_from_u16string(std::u16string const str) { - std::wstring_convert, char16_t> converter; - return converter.to_bytes(str); -} - -// often used with c_str() e.g. u16fmt( DEBUGSTORE_MATCH).c_str() -// UTF16 (= const char16_t*) -> UTF8 (= std::string) -> UTF16 ( = std::wstring 16 bit) -std::wstring u16fmt(const KMX_WCHAR * str) { - std::wstring_convert, wchar_t> convert_wstring; - std::wstring_convert, char16_t> convert; - - // UTF16 (= const char16_t*) -> UTF8 (= std::string) -> UTF16 ( = std::wstring 16 bit) - std::string utf8str = convert.to_bytes(str); // UTF16 (= const char16_t*) -> UTF8 (= std::string) - std::wstring wstr = convert_wstring.from_bytes(utf8str); // UTF8 (= std::string) -> UTF16 ( = std::wstring 16 bit) - return wstr; -} - -void u16sprintf(KMX_WCHAR * dst, const size_t sz, const wchar_t* fmt, ...) { - // UTF16 (=const wchar_t*) -> -> std::string -> std::u16string -> UTF16 ( = char16_t*) - wchar_t* wbuf = new wchar_t[sz]; - va_list args; - va_start(args, fmt); - vswprintf(wbuf, sz, fmt, args); - va_end(args); - - std::wstring_convert, wchar_t> convert_wstring; - std::wstring_convert, char16_t> convert; - - // UTF16 (=const wchar_t*) -> -> std::string -> std::u16string -> UTF16 ( = char16_t*) - std::string utf8str = convert_wstring.to_bytes(wbuf); // UTF16 ( = const wchar_t*) -> std::string - std::u16string u16str = convert.from_bytes(utf8str); // std::string -> std::u16string - u16ncpy(dst, u16str.c_str(), sz); // std::u16string.c_str() -> char16_t* - delete[] wbuf; -} - - std::wstring convert_pchar16T_To_wstr(KMX_WCHAR *Name){ - // convert char16_t* -> std::u16string -> std::string -> std::wstring - // char16_t* -> std::u16string - std::u16string u16str(Name); - // std::u16string -> std::string - std::string stri = string_from_u16string(u16str); - // std::string -> std::wstring - std::wstring wstr = wstring_from_string(stri); - return wstr; - } - -long int u16tol(const KMX_WCHAR* str, KMX_WCHAR** endptr, int base) -{ - auto s = string_from_u16string(str); - char* t; - long int result = strtol(s.c_str(), &t, base); - if (endptr != nullptr) *endptr = (KMX_WCHAR*)str + (t - s.c_str()); - return result; -} - -std::string toHex(int num1) { - if (num1 == 0) - return "0"; - int num = num1; - std::string s = ""; - while (num) { - int temp = num % 16; - if (temp <= 9) - s += (48 + temp); - else - s += (87 + temp); - num = num / 16; - } - reverse(s.begin(), s.end()); - return s; -} - -const KMX_WCHAR * u16ncat(KMX_WCHAR *dst, const KMX_WCHAR *src, size_t max) { - KMX_WCHAR* o = dst; - dst = (KMX_WCHAR*) u16chr(dst, 0); - //max -= (dst-o); - while (*src && max > 0) { - *dst++ = *src++; - max--; - } - if(max > 0) - *dst = 0; - return o; -} - -const KMX_WCHAR* u16rchr_slash(KMX_WCHAR const* Name) -{ - const KMX_WCHAR* cp = NULL; - cp = u16rchr(Name, '\\'); - if (cp == NULL) - cp = u16rchr(Name, '/'); - return cp; -} - -KMX_CHAR* strrchr_slash(KMX_CHAR* Name) -{ - KMX_CHAR* cp = NULL; - cp = strrchr(Name, '\\'); - if (cp == NULL) - cp = strrchr(Name, '/'); - return cp; -} - -// u16rchr returns last occurence of ch in p; It returns NULL if ch = '\0' and NULL if ch is not found -const KMX_WCHAR* u16rchr(const KMX_WCHAR* p, KMX_WCHAR ch) { - const KMX_WCHAR* p_end = p + u16len(p) - 1; - - if (ch == '\0') return p_end + 1; - while (p_end >= p) { - if (*p_end == ch) return p_end; - p_end--; - } - return NULL; -} - -const KMX_WCHAR * u16chr(const KMX_WCHAR *p, KMX_WCHAR ch) { - while (*p) { - if (*p == ch) return p; - p++; - } - return ch == 0 ? p : NULL; -} - -const KMX_WCHAR * u16cpy(KMX_WCHAR *dst, const KMX_WCHAR *src) { - KMX_WCHAR *o = dst; - while (*src) { - *dst++ = *src++; - } - *dst = 0; - return o; -} - -const KMX_WCHAR * u16ncpy(KMX_WCHAR *dst, const KMX_WCHAR *src, size_t max) { - KMX_WCHAR *o = dst; - while (*src && max > 0) { - *dst++ = *src++; - max--; - } - if(max > 0) { - *dst = 0; - } - return o; -} - -size_t u16len(const KMX_WCHAR *p) { - int i = 0; - while (*p) { - p++; - i++; - } - return i; -} - -int u16cmp(const KMX_WCHAR *p, const KMX_WCHAR *q) { - while (*p && *q) { - if (*p != *q) return *p - *q; - p++; - q++; - } - return *p - *q; -} - -int u16nicmp(const KMX_WCHAR *p, const KMX_WCHAR *q, size_t count) { - while (*p && *q && count) { - if (toupper(*p) != toupper(*q)) return *p - *q; - p++; - q++; - count--; - } - if (count) - return *p - *q; - return 0; -} - -int u16icmp(const KMX_WCHAR *p, const KMX_WCHAR *q) { - while (*p && *q) { - if (toupper(*p) != toupper(*q)) return *p - *q; - p++; - q++; - } - return *p - *q; -} - -int u16ncmp(const KMX_WCHAR *p, const KMX_WCHAR *q, size_t count) { - while (*p && *q && count) { - if (*p != *q) return *p - *q; - p++; - q++; - count--; - } - if (count) - return *p - *q; - return 0; -} - -KMX_WCHAR * u16tok(KMX_WCHAR *p, const KMX_WCHAR ch, KMX_WCHAR **ctx) { - if (!p) { - p = *ctx; - if (!p) return NULL; - } - - KMX_WCHAR *q = p; - while (*q && *q != ch) { - q++; - } - if (*q) { - *q = 0; - q++; - while (*q == ch) q++; - *ctx = q; - } - else { - *ctx = NULL; - } - return p; -} - -KMX_WCHAR * u16tok(KMX_WCHAR* p, const KMX_WCHAR* delim, KMX_WCHAR** ctx) { - if (!p) { - p = *ctx; - if (!p) return NULL; - } - - KMX_WCHAR * q = p; - while (*q && !u16chr(delim, *q)) { - q++; - } - if (*q) { - *q = 0; - q++; - while (u16chr(delim, *q)) q++; - *ctx = q; - } - else { - *ctx = NULL; - } - return p; -} - -double u16tof( KMX_WCHAR* str) -{ - double val = 0; - int offsetdot=0; - char digit; - - PKMX_WCHAR q = (PKMX_WCHAR)u16chr(str, '.'); - size_t pos_dot = q-str ; - - if (pos_dot < 0) - pos_dot = u16len(str); - - for (size_t i = 0; i < u16len(str); i++) - { - digit = static_cast(towupper(*str)); - - if (i > pos_dot - 1) - offsetdot = 1; - - if (digit != '.') - val =val+ ((int(digit)) - 48) * pow(10, (pos_dot - 1- i + offsetdot)); - - str++; - } - return val; -} diff --git a/developer/src/kmcmplib/src/kmx_u16.h b/developer/src/kmcmplib/src/kmx_u16.h deleted file mode 100644 index 6a54c0f00db..00000000000 --- a/developer/src/kmcmplib/src/kmx_u16.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "kmcompx.h" - -std::string string_from_wstring(std::wstring const str); -std::wstring wstring_from_string(std::string const str); -std::u16string u16string_from_string(std::string const str); -std::string string_from_u16string(std::u16string const str); - -std::wstring u16fmt(const KMX_WCHAR * str); -void u16sprintf(KMX_WCHAR * dst, const size_t sz, const wchar_t* fmt, ...) ; - -std::wstring convert_pchar16T_To_wstr(KMX_WCHAR *Name); - -size_t u16len(const KMX_WCHAR *p); -int u16cmp(const KMX_WCHAR *p, const KMX_WCHAR *q); -int u16icmp(const KMX_WCHAR *p, const KMX_WCHAR *q); -int u16ncmp(const KMX_WCHAR *p, const KMX_WCHAR *q, size_t count); -int u16nicmp(const KMX_WCHAR *p, const KMX_WCHAR *q, size_t count) ; -const KMX_WCHAR * u16ncpy(KMX_WCHAR *dst, const KMX_WCHAR *src, size_t max); -const KMX_WCHAR * u16cpy(KMX_WCHAR *dst, const KMX_WCHAR *src); -const KMX_WCHAR * u16rchr(const KMX_WCHAR *p, KMX_WCHAR ch) ; -const KMX_WCHAR * u16chr(const KMX_WCHAR *p, KMX_WCHAR ch) ; -const KMX_WCHAR * u16ncat(KMX_WCHAR *dst, const KMX_WCHAR *src, size_t max); -KMX_WCHAR * u16tok(KMX_WCHAR *p, const KMX_WCHAR ch, KMX_WCHAR **ctx) ; -KMX_WCHAR * u16tok(KMX_WCHAR* p, const KMX_WCHAR* ch, KMX_WCHAR** ctx) ; -long int u16tol(const KMX_WCHAR* str, KMX_WCHAR** endptr, int base) ; -double u16tof( KMX_WCHAR* str); - -KMX_CHAR* strrchr_slash(KMX_CHAR* Name); -const KMX_WCHAR* u16rchr_slash(KMX_WCHAR const* Name); - -std::string toHex(int num1); diff --git a/developer/src/kmcmplib/src/meson.build b/developer/src/kmcmplib/src/meson.build index e3e4efcd48a..51acf704090 100644 --- a/developer/src/kmcmplib/src/meson.build +++ b/developer/src/kmcmplib/src/meson.build @@ -12,8 +12,10 @@ lib_links = [] if cpp_compiler.get_id() == 'gcc' or cpp_compiler.get_id() == 'clang' warns += [ '-Wall', - '-Wextra' + '-Wextra', + '-Wno-ignored-qualifiers' ] + flags += ['-D__cdecl= '] endif if cpp_compiler.get_id() == 'msvc' @@ -54,6 +56,10 @@ if cpp_compiler.get_id() == 'emscripten' # emscripten < 3.1.44 does not include `wasmExports` links += ['-sEXPORTED_RUNTIME_METHODS=[\'UTF8ToString\']'] endif + + # For Google Test + add_global_arguments('-pthread', language: [ 'cpp', 'c' ] ) + endif icu = subproject('icu-for-uset', default_options: [ 'default_library=static', 'cpp_std=c++17', 'warning_level=0', 'werror=false']) @@ -66,19 +72,20 @@ lib = library('kmcmplib', 'CheckNCapsConsistency.cpp', 'CompileKeyboardBuffer.cpp', 'Compiler.cpp', + 'CompilerErrors.cpp', 'CompilerInterfaces.cpp', 'CompilerInterfacesWasm.cpp', - 'CompMsg.cpp', 'cp1252.cpp', 'DeprecationChecks.cpp', 'Edition.cpp', - 'kmx_u16.cpp', 'NamedCodeConstants.cpp', 'UnreachableRules.cpp', 'uset-api.cpp', 'versioning.cpp', 'virtualcharkeys.cpp', 'xstring.cpp', + '../../../../common/cpp/km_u16.cpp', + '../../../../common/cpp/utfcodec.cpp', '../../../../common/windows/cpp/src/ConvertUTF.c', '../../../../common/windows/cpp/src/crc32.cpp', '../../../../common/windows/cpp/src/vkeys.cpp', @@ -101,7 +108,7 @@ if cpp_compiler.get_id() == 'emscripten' cpp_args: defns, include_directories: inc, link_args: links + lib_links, - objects: lib.extract_all_objects(), + objects: lib.extract_all_objects(recursive: false), dependencies: icuuc_dep) if get_option('buildtype') == 'release' @@ -116,3 +123,7 @@ if cpp_compiler.get_id() == 'emscripten' endif endif + +gtest = subproject('gtest') +gtest_dep = gtest.get_variable('gtest_main_dep') +gmock_dep = gtest.get_variable('gmock_dep') diff --git a/developer/src/kmcmplib/src/pch.h b/developer/src/kmcmplib/src/pch.h index 358e28998ab..80bb639fb8b 100644 --- a/developer/src/kmcmplib/src/pch.h +++ b/developer/src/kmcmplib/src/pch.h @@ -4,7 +4,7 @@ #define USE_CHAR16_T #include -#include "kmx_u16.h" +#include #include #include "../../../../common/windows/cpp/include/crc32.h" diff --git a/developer/src/kmcmplib/src/versioning.cpp b/developer/src/kmcmplib/src/versioning.cpp index c00dc78e802..87c7d0070b5 100644 --- a/developer/src/kmcmplib/src/versioning.cpp +++ b/developer/src/kmcmplib/src/versioning.cpp @@ -5,15 +5,19 @@ #include "versioning.h" KMX_BOOL kmcmp::CheckKeyboardFinalVersion(PFILE_KEYBOARD fk) { - KMX_CHAR buf[128]; - if (fk->dwFlags & KF_AUTOMATICVERSION) { if (fk->version <= 0) { fk->version = VERSION_60; // minimum version that we can be safe with } - snprintf(buf, 128, "The compiler has assigned a minimum engine version of %d.%d based on features used in this keyboard", (int)((fk->version & 0xFF00) >> 8), (int)(fk->version & 0xFF)); - AddCompileWarning(buf); + if(kmcmp::CompileTarget != CKF_KEYMANWEB) { + // Note: the KeymanWeb compiler is responsible for reporting minimum + // version for the web targets + ReportCompilerMessage(KmnCompilerMessages::INFO_MinimumCoreEngineVersion, { + /* majorVersion */ std::to_string((fk->version & 0xFF00) >> 8), + /* minorVersion */ std::to_string(fk->version & 0xFF) + }); + } } return TRUE; diff --git a/developer/src/kmcmplib/src/versioning.h b/developer/src/kmcmplib/src/versioning.h index a3a1c597bf6..a172b45b2d5 100644 --- a/developer/src/kmcmplib/src/versioning.h +++ b/developer/src/kmcmplib/src/versioning.h @@ -3,11 +3,6 @@ #include "compfile.h" #include "../../../../common/include/km_types.h" -#define VERIFY_KEYBOARD_VERSION(fk, ver, err) { \ - if(!VerifyKeyboardVersion((fk), (ver))) \ - return (err); \ -} - namespace kmcmp { KMX_BOOL CheckKeyboardFinalVersion(PFILE_KEYBOARD fk); } diff --git a/developer/src/kmcmplib/subprojects/.gitignore b/developer/src/kmcmplib/subprojects/.gitignore index 99479aa952c..0c5134859ce 100644 --- a/developer/src/kmcmplib/subprojects/.gitignore +++ b/developer/src/kmcmplib/subprojects/.gitignore @@ -2,3 +2,4 @@ /*.zip /*.tgz /packagecache +/googletest-* diff --git a/developer/src/kmcmplib/subprojects/gtest.wrap b/developer/src/kmcmplib/subprojects/gtest.wrap new file mode 100644 index 00000000000..a77eb315bfa --- /dev/null +++ b/developer/src/kmcmplib/subprojects/gtest.wrap @@ -0,0 +1,16 @@ +[wrap-file] +directory = googletest-1.14.0 +source_url = https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz +source_filename = gtest-1.14.0.tar.gz +source_hash = 8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7 +patch_filename = gtest_1.14.0-2_patch.zip +patch_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.14.0-2/gtest_1.14.0-2_patch.zip +patch_hash = 4ec7f767364386a99f7b2d61678287a73ad6ba0f9998be43b51794c464a63732 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.14.0-2/gtest-1.14.0.tar.gz +wrapdb_version = 1.14.0-2 + +[provide] +gtest = gtest_dep +gtest_main = gtest_main_dep +gmock = gmock_dep +gmock_main = gmock_main_dep diff --git a/developer/src/kmcmplib/tests/api-test.cpp b/developer/src/kmcmplib/tests/api-test.cpp index 2e721d7886e..52769f2b042 100644 --- a/developer/src/kmcmplib/tests/api-test.cpp +++ b/developer/src/kmcmplib/tests/api-test.cpp @@ -34,6 +34,7 @@ int main(int argc, char *argv[]) { setup(); test_kmcmp_CompileKeyboard(argv[1]); + setup(); test_GetCompileTargetsFromTargetsStore(); return 0; @@ -65,20 +66,72 @@ void test_kmcmp_CompileKeyboard(char *kmn_file) { options.target = CKF_KEYMAN; assert(!kmcmp_CompileKeyboard(kmn_file, options, msgproc, loadfileProc, nullptr, result)); assert(error_vec.size() == 1); - assert(error_vec[0] == CERR_CannotReadInfile); + assert(error_vec[0] == KmnCompilerMessages::ERROR_InfileNotExist); // zero byte no longer gives us KmnCompilerMessages::ERROR_CannotReadInfile unlink(kmn_file); } -extern int GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store); +extern KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets); void test_GetCompileTargetsFromTargetsStore() { - assert(GetCompileTargetsFromTargetsStore(u"any") == (COMPILETARGETS_KMX | COMPILETARGETS_JS)); - assert(GetCompileTargetsFromTargetsStore(u"windows") == COMPILETARGETS_KMX); - assert(GetCompileTargetsFromTargetsStore(u"desktop") == COMPILETARGETS_KMX); - assert(GetCompileTargetsFromTargetsStore(u"mobile") == COMPILETARGETS_JS); - assert(GetCompileTargetsFromTargetsStore(u"web") == COMPILETARGETS_JS); - assert(GetCompileTargetsFromTargetsStore(u"desktop mobile") == (COMPILETARGETS_KMX | COMPILETARGETS_JS)); - assert(GetCompileTargetsFromTargetsStore(u"desktop tablet") == (COMPILETARGETS_KMX | COMPILETARGETS_JS)); - assert(GetCompileTargetsFromTargetsStore(u"foo bar baz") == 0); + int targets = 0; + + setup(); + assert(GetCompileTargetsFromTargetsStore(u"any", targets)); + assert(error_vec.size() == 0); + assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS)); + + setup(); + assert(GetCompileTargetsFromTargetsStore(u"windows", targets)); + assert(error_vec.size() == 0); + assert(targets == COMPILETARGETS_KMX); + + setup(); + assert(GetCompileTargetsFromTargetsStore(u"desktop", targets)); + assert(error_vec.size() == 0); + assert(targets == COMPILETARGETS_KMX); + + setup(); + assert(GetCompileTargetsFromTargetsStore(u"mobile", targets)); + assert(error_vec.size() == 0); + assert(targets == COMPILETARGETS_JS); + + setup(); + assert(GetCompileTargetsFromTargetsStore(u"web", targets)); + assert(error_vec.size() == 0); + assert(targets == COMPILETARGETS_JS); + + setup(); + assert(GetCompileTargetsFromTargetsStore(u"desktop mobile", targets)); + assert(error_vec.size() == 0); + assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS)); + + setup(); + assert(GetCompileTargetsFromTargetsStore(u"desktop tablet", targets)); + assert(error_vec.size() == 0); + assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS)); + + setup(); + assert(!GetCompileTargetsFromTargetsStore(u"foo bar baz", targets)); + assert(error_vec.size() == 1); + assert(error_vec[0] == KmnCompilerMessages::ERROR_InvalidTarget); + assert(targets == 0); + + setup(); + assert(!GetCompileTargetsFromTargetsStore(u"windows chromeos", targets)); + assert(error_vec.size() == 1); + assert(error_vec[0] == KmnCompilerMessages::ERROR_InvalidTarget); + assert(targets == 0); + + setup(); + assert(!GetCompileTargetsFromTargetsStore(u" ", targets)); + assert(error_vec.size() == 1); + assert(error_vec[0] == KmnCompilerMessages::ERROR_NoTargetsSpecified); + assert(targets == 0); + + setup(); + assert(!GetCompileTargetsFromTargetsStore(u"", targets)); + assert(error_vec.size() == 1); + assert(error_vec[0] == KmnCompilerMessages::ERROR_NoTargetsSpecified); + assert(targets == 0); } diff --git a/developer/src/kmcmplib/tests/gtest-compiler-test.cpp b/developer/src/kmcmplib/tests/gtest-compiler-test.cpp new file mode 100644 index 00000000000..39c47372d42 --- /dev/null +++ b/developer/src/kmcmplib/tests/gtest-compiler-test.cpp @@ -0,0 +1,1654 @@ +#include +#include +#include "../include/kmcompx.h" +#include "../include/kmcmplibapi.h" +#include "../src/compfile.h" +#include "../src/CompilerErrors.h" +#include "../../common/include/kmn_compiler_errors.h" +#include "../../../../common/include/km_types.h" +#include "../../../../common/include/kmx_file.h" + +PKMX_WCHAR strtowstr(PKMX_STR in); +PKMX_STR wstrtostr(PKMX_WCHAR in); +KMX_BOOL ProcessBeginLine(PFILE_KEYBOARD fk, PKMX_WCHAR p); +KMX_DWORD ValidateMatchNomatchOutput(PKMX_WCHAR p); +KMX_BOOL IsValidKeyboardVersion(KMX_WCHAR *dpString); +PKMX_WCHAR GetDelimitedString(PKMX_WCHAR *p, KMX_WCHAR const * Delimiters, KMX_WORD Flags); +int LineTokenType(PKMX_WCHAR *str); +KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_WCHAR const * token, + PKMX_WCHAR output, int max, int offset, PKMX_WCHAR *newp, int isUnicode +); +KMX_DWORD ProcessEthnologueStore(PKMX_WCHAR p); +KMX_DWORD GetRHS(PFILE_KEYBOARD fk, PKMX_WCHAR p, PKMX_WCHAR buf, int bufsize, int offset, int IsUnicode); +bool isIntegerWstring(PKMX_WCHAR p); +bool hasPreamble(std::u16string result); +KMX_DWORD ProcessKeyLineImpl(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnicode, PKMX_WCHAR pklIn, PKMX_WCHAR pklKey, PKMX_WCHAR pklOut); + +namespace kmcmp { + extern int nErrors; + extern int currentLine; + extern int ErrChr; + extern std::string messageFilename; + extern int BeginLine[4]; + extern int CompileTarget; +} + +class CompilerTest : public testing::Test { + protected: + FILE_KEYBOARD fileKeyboard; + + CompilerTest() {} + ~CompilerTest() override {} + void SetUp() override { + initGlobals(); + initFileKeyboard(fileKeyboard); + } + void TearDown() override { + deleteFileKeyboard(fileKeyboard); + } + + void initGlobals() { + kmcmp::msgproc = msgproc_collect; + msgproc_errors.clear(); + kmcmp::nErrors = 0; + kmcmp::currentLine = 0; + kmcmp::ErrChr = 0; + kmcmp::messageFilename = ""; + kmcmp::BeginLine[BEGIN_ANSI] = -1; + kmcmp::BeginLine[BEGIN_UNICODE] = -1; + kmcmp::BeginLine[BEGIN_NEWCONTEXT] = -1; + kmcmp::BeginLine[BEGIN_POSTKEYSTROKE] = -1; + kmcmp::CompileTarget = CKF_KEYMAN; + } + + void initFileKeyboard(FILE_KEYBOARD &fk) { + fk.KeyboardID = 0; + fk.version = VERSION_90; + fk.dpStoreArray = nullptr; + fk.dpGroupArray = nullptr; + fk.cxStoreArray = 0; + fk.cxGroupArray = 0; + fk.StartGroup[0] = 0; + fk.StartGroup[1] = 0; + fk.dwHotKey = 0; + fk.szName[0] = u'\0'; + fk.szLanguageName[0] = u'\0'; + fk.szCopyright[0] = u'\0'; + fk.szMessage[0] = u'\0'; + fk.lpBitmap = nullptr; + fk.dwBitmapSize = 0; + fk.dwFlags = 0; + fk.currentGroup = 0; + fk.currentStore = 0; + fk.cxDeadKeyArray = 0; + fk.dpDeadKeyArray = nullptr; + fk.cxVKDictionary = 0; + fk.dpVKDictionary = nullptr; + fk.extra = nullptr; + } + + void initFileGroupArray(FILE_KEYBOARD &fk, KMX_BOOL fUsingKeys) { + fk.dpGroupArray = new FILE_GROUP[1]; + fk.cxGroupArray = 1; + + fk.dpGroupArray->szName[0] = 0; + fk.dpGroupArray->cxKeyArray = 0; + fk.dpGroupArray->dpKeyArray = nullptr; + fk.dpGroupArray->dpMatch = nullptr; + fk.dpGroupArray->dpNoMatch = nullptr; + fk.dpGroupArray->fUsingKeys = fUsingKeys; + fk.dpGroupArray->fReadOnly = FALSE; + fk.dpGroupArray->Line = 0; + } + + void deleteFileKeyboard(FILE_KEYBOARD &fk) { + if (fk.dpStoreArray) { delete[] fk.dpStoreArray; } + if (fk.dpGroupArray) { delete[] fk.dpGroupArray; } + if (fk.lpBitmap) { delete fk.lpBitmap; } + if (fk.dpDeadKeyArray) { delete[] fk.dpDeadKeyArray; } + if (fk.dpVKDictionary) { delete fk.dpVKDictionary; } + if (fk.extra) { delete fk.extra; } + } + + public: + static std::vector msgproc_errors; + + static void msgproc_collect(const KMCMP_COMPILER_RESULT_MESSAGE &message, void* context) { + msgproc_errors.push_back(message); + } +}; + +std::vector CompilerTest::msgproc_errors; + +TEST_F(CompilerTest, strtowstr_test) { + EXPECT_EQ(0, u16cmp(u"hello", strtowstr((PKMX_STR)"hello"))); + EXPECT_EQ(0, u16cmp(u"", strtowstr((PKMX_STR)""))); +}; + +TEST_F(CompilerTest, wstrtostr_test) { + EXPECT_EQ(0, strcmp("hello", wstrtostr((PKMX_WCHAR)u"hello"))); + EXPECT_EQ(0, strcmp("", wstrtostr((PKMX_WCHAR)u""))); +}; + +TEST_F(CompilerTest, ReportCompilerMessage_test) { + kmcmp::msgproc = msgproc_collect; + kmcmp::currentLine = 42; + std::vector params{"parameter"}; + kmcmp::messageFilename = "filename"; + kmcmp::ErrChr = 0; + + // SevFatal + EXPECT_EQ(0, kmcmp::nErrors); + EXPECT_EQ(SevFatal, KmnCompilerMessages::FATAL_CannotCreateTempfile & SevFatal); + ReportCompilerMessage(KmnCompilerMessages::FATAL_CannotCreateTempfile, params); + EXPECT_EQ(1, kmcmp::nErrors); + EXPECT_EQ(KmnCompilerMessages::FATAL_CannotCreateTempfile, msgproc_errors[0].errorCode); + EXPECT_EQ(kmcmp::currentLine+1, msgproc_errors[0].lineNumber); + EXPECT_EQ(kmcmp::ErrChr, msgproc_errors[0].columnNumber); + EXPECT_TRUE(msgproc_errors[0].filename == kmcmp::messageFilename); + EXPECT_TRUE(msgproc_errors[0].parameters == params); + + // SevError + EXPECT_EQ(SevError, KmnCompilerMessages::ERROR_InvalidLayoutLine & SevError); + ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidLayoutLine); + EXPECT_EQ(2, kmcmp::nErrors); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidLayoutLine, msgproc_errors[1].errorCode); + + // SevWarn + EXPECT_EQ(SevWarn, KmnCompilerMessages::WARN_ReservedCharacter & SevWarn); + ReportCompilerMessage(KmnCompilerMessages::WARN_ReservedCharacter); + EXPECT_EQ(2, kmcmp::nErrors); + EXPECT_EQ(KmnCompilerMessages::WARN_ReservedCharacter, msgproc_errors[2].errorCode); + + // SevHint + EXPECT_EQ(SevHint, KmnCompilerMessages::HINT_NonUnicodeFile & SevHint); + ReportCompilerMessage(KmnCompilerMessages::HINT_NonUnicodeFile); + EXPECT_EQ(2, kmcmp::nErrors); + EXPECT_EQ(KmnCompilerMessages::HINT_NonUnicodeFile, msgproc_errors[3].errorCode); + + // SevInfo + EXPECT_EQ(SevInfo, KmnCompilerMessages::INFO_MinimumCoreEngineVersion & SevInfo); + ReportCompilerMessage(KmnCompilerMessages::INFO_MinimumCoreEngineVersion); + EXPECT_EQ(2, kmcmp::nErrors); + EXPECT_EQ(KmnCompilerMessages::INFO_MinimumCoreEngineVersion, msgproc_errors[4].errorCode); +}; + +TEST_F(CompilerTest, ProcessBeginLine_test) { + KMX_WCHAR str[LINESIZE]; + + // KmnCompilerMessages::ERROR_NoTokensFound + msgproc_errors.clear(); + u16cpy(str, u""); + EXPECT_EQ(FALSE, ProcessBeginLine(&fileKeyboard, str)); + EXPECT_EQ(1, msgproc_errors.size()); + EXPECT_EQ(KmnCompilerMessages::ERROR_NoTokensFound, msgproc_errors[0].errorCode); + + // KmnCompilerMessages::ERROR_InvalidToken + msgproc_errors.clear(); + u16cpy(str, u"abc >"); + EXPECT_EQ(FALSE, ProcessBeginLine(&fileKeyboard, str)); + EXPECT_EQ(1, msgproc_errors.size()); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, msgproc_errors[0].errorCode); + + // KmnCompilerMessages::ERROR_RepeatedBegin, BEGIN_UNICODE + msgproc_errors.clear(); + kmcmp::BeginLine[BEGIN_UNICODE] = 0; // not -1 + u16cpy(str, u" unicode>"); + EXPECT_EQ(FALSE, ProcessBeginLine(&fileKeyboard, str)); + EXPECT_EQ(1, msgproc_errors.size()); + EXPECT_EQ(KmnCompilerMessages::ERROR_RepeatedBegin, msgproc_errors[0].errorCode); + kmcmp::BeginLine[BEGIN_UNICODE] = -1; + + // KmnCompilerMessages::ERROR_RepeatedBegin, BEGIN_ANSI + msgproc_errors.clear(); + kmcmp::BeginLine[BEGIN_ANSI] = 0; // not -1 + u16cpy(str, u" ansi>"); + EXPECT_EQ(FALSE, ProcessBeginLine(&fileKeyboard, str)); + EXPECT_EQ(1, msgproc_errors.size()); + EXPECT_EQ(KmnCompilerMessages::ERROR_RepeatedBegin, msgproc_errors[0].errorCode); + kmcmp::BeginLine[BEGIN_ANSI] = -1; + + // KmnCompilerMessages::ERROR_RepeatedBegin, BEGIN_NEWCONTEXT + msgproc_errors.clear(); + kmcmp::BeginLine[BEGIN_NEWCONTEXT] = 0; // not -1 + u16cpy(str, u" newContext>"); + EXPECT_EQ(FALSE, ProcessBeginLine(&fileKeyboard, str)); + EXPECT_EQ(1, msgproc_errors.size()); + EXPECT_EQ(KmnCompilerMessages::ERROR_RepeatedBegin, msgproc_errors[0].errorCode); + kmcmp::BeginLine[BEGIN_NEWCONTEXT] = -1; + + // KmnCompilerMessages::ERROR_RepeatedBegin, BEGIN_POSTKEYSTROKE + msgproc_errors.clear(); + kmcmp::BeginLine[BEGIN_POSTKEYSTROKE] = 0; // not -1 + u16cpy(str, u" postKeystroke>"); + EXPECT_EQ(FALSE, ProcessBeginLine(&fileKeyboard, str)); + EXPECT_EQ(1, msgproc_errors.size()); + EXPECT_EQ(KmnCompilerMessages::ERROR_RepeatedBegin, msgproc_errors[0].errorCode); + kmcmp::BeginLine[BEGIN_POSTKEYSTROKE] = -1; +}; + +TEST_F(CompilerTest, ValidateMatchNomatchOutput_test) { + EXPECT_EQ(STATUS_Success, ValidateMatchNomatchOutput(NULL)); + EXPECT_EQ(STATUS_Success, ValidateMatchNomatchOutput((PKMX_WCHAR)u"")); + const KMX_WCHAR context[] = { 'a', 'b', 'c', UC_SENTINEL, CODE_CONTEXT, 'd', 'e', 'f', 0 }; + EXPECT_EQ(KmnCompilerMessages::ERROR_ContextAndIndexInvalidInMatchNomatch, ValidateMatchNomatchOutput((PKMX_WCHAR)context)); + const KMX_WCHAR contextex[] = { 'a', 'b', 'c', UC_SENTINEL, CODE_CONTEXTEX, 'd', 'e', 'f', 0 }; + EXPECT_EQ(KmnCompilerMessages::ERROR_ContextAndIndexInvalidInMatchNomatch, ValidateMatchNomatchOutput((PKMX_WCHAR)contextex)); + const KMX_WCHAR index[] = { 'a', 'b', 'c', UC_SENTINEL, CODE_INDEX, 'd', 'e', 'f', 0 }; + EXPECT_EQ(KmnCompilerMessages::ERROR_ContextAndIndexInvalidInMatchNomatch, ValidateMatchNomatchOutput((PKMX_WCHAR)index)); + const KMX_WCHAR sentinel[] = { 'a', 'b', 'c', UC_SENTINEL, 'd', 'e', 'f', 0 }; + EXPECT_EQ(STATUS_Success, ValidateMatchNomatchOutput((PKMX_WCHAR)sentinel)); +}; + +// KMX_BOOL ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str) +// KMX_BOOL ProcessGroupLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) +// int kmcmp::cmpkeys(const void *key, const void *elem) +// KMX_BOOL ProcessGroupFinish(PFILE_KEYBOARD fk) +// KMX_BOOL ProcessStoreLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) +// bool resizeStoreArray(PFILE_KEYBOARD fk) +// bool resizeKeyArray(PFILE_GROUP gp, int increment) +// KMX_BOOL AddStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, const KMX_WCHAR * str, KMX_DWORD *dwStoreID) +// KMX_DWORD AddDebugStore(PFILE_KEYBOARD fk, KMX_WCHAR const * str) +// KMX_BOOL ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE sp) +// int GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store) + +TEST_F(CompilerTest, IsValidKeyboardVersion_test) { + EXPECT_FALSE(IsValidKeyboardVersion((KMX_WCHAR *)u"")); + EXPECT_FALSE(IsValidKeyboardVersion((KMX_WCHAR *)u" ")); + EXPECT_FALSE(IsValidKeyboardVersion((KMX_WCHAR *)u"\t")); + EXPECT_FALSE(IsValidKeyboardVersion((KMX_WCHAR *)u" 1.1")); + EXPECT_TRUE(IsValidKeyboardVersion((KMX_WCHAR *)u"1.1")); + EXPECT_TRUE(IsValidKeyboardVersion((KMX_WCHAR *)u"1.0")); + EXPECT_FALSE(IsValidKeyboardVersion((KMX_WCHAR *)u"1.")); + EXPECT_TRUE(IsValidKeyboardVersion((KMX_WCHAR *)u"1.2.3")); + EXPECT_FALSE(IsValidKeyboardVersion((KMX_WCHAR *)u"a")); + EXPECT_FALSE(IsValidKeyboardVersion((KMX_WCHAR *)u"1.a")); +}; + +// KMX_DWORD kmcmp::AddCompilerVersionStore(PFILE_KEYBOARD fk) +// KMX_DWORD CheckStatementOffsets(PFILE_KEYBOARD fk, PFILE_GROUP gp, PKMX_WCHAR context, PKMX_WCHAR output, PKMX_WCHAR key) +// void CheckContextStatementPositions(PKMX_WCHAR context) +// KMX_DWORD CheckUseStatementsInOutput(PKMX_WCHAR output) +// KMX_DWORD CheckVirtualKeysInOutput(PKMX_WCHAR output) +// KMX_DWORD InjectContextToReadonlyOutput(PKMX_WCHAR pklOut) +// KMX_DWORD CheckOutputIsReadonly(const PFILE_KEYBOARD fk, const PKMX_WCHAR output) +// KMX_DWORD ProcessKeyLine(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnicode) + +TEST_F(CompilerTest, ProcessKeyLineImpl_test) { + initFileGroupArray(fileKeyboard, TRUE); + + PKMX_WCHAR pklIn, pklKey, pklOut; + KMX_WCHAR str[128]; + + pklIn = new KMX_WCHAR[GLOBAL_BUFSIZE]; + pklKey = new KMX_WCHAR[GLOBAL_BUFSIZE]; + pklOut = new KMX_WCHAR[GLOBAL_BUFSIZE]; + + // #11643: non-BMP characters do not makes sense for key codes + u16cpy(str, u"+ 'A' > 'test'\n"); // baseline + EXPECT_EQ(STATUS_Success, ProcessKeyLineImpl(&fileKeyboard, str, TRUE, pklIn, pklKey, pklOut)); + + u16cpy(str, u"+ '\U00010000' > 'test'\n"); // surrogate pair + EXPECT_EQ(KmnCompilerMessages::ERROR_NonBMPCharactersNotSupportedInKeySection, ProcessKeyLineImpl(&fileKeyboard, str, TRUE, pklIn, pklKey, pklOut)); + + delete[] pklIn; + delete[] pklKey; + delete[] pklOut; + + // TODO: other tests for this function + +} + +// KMX_DWORD ExpandKp_ReplaceIndex(PFILE_KEYBOARD fk, PFILE_KEY k, KMX_DWORD keyIndex, int nAnyIndex) +// KMX_DWORD ExpandKp(PFILE_KEYBOARD fk, PFILE_KEY kpp, KMX_DWORD storeIndex) + +TEST_F(CompilerTest, GetDelimitedString_test) { + KMX_WCHAR str[LINESIZE]; + PKMX_WCHAR p = str; + PKMX_WCHAR q = nullptr; + + // no open delimiter, cut spaces after open and before close delimiter + u16cpy(str, u""); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_FALSE(q); + + // no close delimiter, cut spaces after open and before close delimiter + u16cpy(str, u"("); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_FALSE(q); + + // no argument, cut spaces after open and before close delimiter + u16cpy(str, u"()"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(1, p-str); // deleted close delimiter + + // no argument, single space, no flags + u16cpy(str, u"( )"); + p = str; + q = GetDelimitedString(&p, u"()", 0x00); + EXPECT_EQ(0, u16cmp(u" ", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(2, p-str); // deleted close delimiter + + // no argument, single space, cut spaces after open delimiter + u16cpy(str, u"( )"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD); + EXPECT_EQ(0, u16cmp(u"", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(2, p-str); // deleted close delimiter + + // no argument, single space, cut spaces before close delimiter + u16cpy(str, u"( )"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(2, p-str); // deleted close delimiter + + // no argument, single space, cut spaces after open and before close delimiter + u16cpy(str, u"( )"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(2, p-str); // deleted close delimiter + + // no argument, two spaces, no flags + u16cpy(str, u"( )"); + p = str; + q = GetDelimitedString(&p, u"()", 0x00); + EXPECT_EQ(0, u16cmp(u" ", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(3, p-str); // deleted close delimiter + + // no argument, two spaces, cut spaces after open delimiter + u16cpy(str, u"( )"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD); + EXPECT_EQ(0, u16cmp(u"", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(3, p-str); // deleted close delimiter + + // no argument, two spaces, cut spaces before close delimiter + u16cpy(str, u"( )"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(3, p-str); // deleted close delimiter + + // no argument, two spaces, cut spaces after open and before close delimiter + u16cpy(str, u"( )"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(3, p-str); // deleted close delimiter + + // single-character argument, cut spaces after open and before close delimiter, valid + u16cpy(str, u"(b)"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(2, p-str); // deleted close delimiter + + // multi-character argument, cut spaces after open and before close delimiter, valid + u16cpy(str, u"(abc)"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"abc", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(4, p-str); // deleted close delimiter + + // multi-word argument, cut spaces after open and before close delimiter, valid + u16cpy(str, u"(abc def)"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"abc def", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(8, p-str); // deleted close delimiter + + // single-character argument, leading single space, cut spaces after open and before close delimiter, valid + u16cpy(str, u" (b)"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(3, p-str); // deleted close delimiter + + // single-character argument, leading double space, cut open and close delimiter, valid + u16cpy(str, u" (b)"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(4, p-str); // deleted close delimiter + + // single-character argument, space before argument, cut spaces after open and before close delimiter, valid + u16cpy(str, u"( b)"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(3, p-str); // deleted close delimiter + + // single-character argument, space before argument, no flags, valid + u16cpy(str, u"( b)"); + p = str; + q = GetDelimitedString(&p, u"()", 0x00); + EXPECT_EQ(0, u16cmp(u" b", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(3, p-str); // deleted close delimiter + + // single-character argument, double space before argument, cut spaces after open and before close delimiter, valid + u16cpy(str, u"( b)"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(4, p-str); // deleted close delimiter + + // single-character argument, double space before argument, no flags, valid + u16cpy(str, u"( b)"); + p = str; + q = GetDelimitedString(&p, u"()", 0x00); + EXPECT_EQ(0, u16cmp(u" b", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(4, p-str); // deleted close delimiter + + // single-character argument, space after argument, cut spaces after open and before close delimiter, valid + u16cpy(str, u"(b )"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(3, p-str); // deleted close delimiter + + // single-character argument, space after argument, no flags, valid + u16cpy(str, u"(b )"); + p = str; + q = GetDelimitedString(&p, u"()", 0x00); + EXPECT_EQ(0, u16cmp(u"b ", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(3, p-str); // deleted close delimiter + + // single-character argument, two spaces after argument, cut spaces after open and before close delimiter, valid + u16cpy(str, u"(b )"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(4, p-str); // deleted close delimiter + + // single-character argument, two spaces after argument, no flags, valid + u16cpy(str, u"(b )"); + p = str; + q = GetDelimitedString(&p, u"()", 0x00); + EXPECT_EQ(0, u16cmp(u"b ", q)); + EXPECT_FALSE(*p); + EXPECT_EQ(4, p-str); // deleted close delimiter + + // single-character argument, space after close delimiter, cut spaces after open and before close delimiter, valid + u16cpy(str, u"(b) "); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_EQ(' ', *p); + EXPECT_EQ(3, p-str); // space after the close delimiter + + // single-character argument, two spaces after close delimiter, cut spaces after open and before close delimiter, valid + u16cpy(str, u"(b) "); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_EQ(' ', *p); + EXPECT_EQ(4, p-str); // last space after the close delimiter + + // single-character argument, two spaces after argument and two spaces after close delimiter, + // cut spaces after open and before close delimiter, valid + u16cpy(str, u"(b ) "); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_EQ(' ', *p); + EXPECT_EQ(6, p-str); // last space after the close delimiter + + // single-character argument, two spaces after argument and two spaces after close delimiter, no flags, valid + u16cpy(str, u"(b ) "); + p = str; + q = GetDelimitedString(&p, u"()", 0x00); + EXPECT_EQ(0, u16cmp(u"b ", q)); + EXPECT_EQ(' ', *p); + EXPECT_EQ(6, p-str); // last space after the close delimiter + + // single-character argument, two spaces and text after close delimiter, cut spaces after open and before close delimiter, valid + u16cpy(str, u"(b) def"); + p = str; + q = GetDelimitedString(&p, u"()", GDS_CUTLEAD | GDS_CUTFOLL); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_EQ('d', *p); + EXPECT_EQ(5, p-str); // first text character after the close delimiter + + // single-character argument, two spaces and text after close delimiter, no flags, valid + u16cpy(str, u"(b) def"); + p = str; + q = GetDelimitedString(&p, u"()", 0x00); + EXPECT_EQ(0, u16cmp(u"b", q)); + EXPECT_EQ('d', *p); + EXPECT_EQ(5, p-str); // first text character after the close delimiter +} + +// LinePrefixType GetLinePrefixType(PKMX_WCHAR *p) + +TEST_F(CompilerTest, LineTokenType_test) { + KMX_WCHAR str[LINESIZE]; + PKMX_WCHAR p = nullptr; + + // T_BLANK, lptOther, empty string + u16cpy(str, u""); + p = str; + EXPECT_EQ(T_BLANK, LineTokenType(&p)); + + // T_BLANK, lptOther, one space + u16cpy(str, u" "); + p = str; + EXPECT_EQ(T_BLANK, LineTokenType(&p)); + + // T_BLANK, mismatched prefix, CKF_KEYMAN, lptKeymanWebOnly + u16cpy(str, u"$keymanweb:"); + p = str; + kmcmp::CompileTarget = CKF_KEYMAN; + EXPECT_EQ(T_BLANK, LineTokenType(&p)); + + // T_BLANK, mismatched prefix, CKF_KEYMANWEB, lptKeymanOnly + u16cpy(str, u"$keymanonly:"); + p = str; + kmcmp::CompileTarget = CKF_KEYMANWEB; + EXPECT_EQ(T_BLANK, LineTokenType(&p)); + + // T_BLANK, nothing after prefix + u16cpy(str, u"$keyman:"); + p = str; + kmcmp::CompileTarget = CKF_KEYMAN; + EXPECT_EQ(T_BLANK, LineTokenType(&p)); + + // T_STORE (=T_W_START) + u16cpy(str, u"store(b)"); + p = str; + EXPECT_EQ(T_STORE, LineTokenType(&p)); + EXPECT_EQ(u16len(u"store"), p - str); + EXPECT_TRUE(!u16cmp(p, u"(b)")); + + // T_BITMAPS (=T_W_END) + u16cpy(str, u"bitmaps \"b\""); + p = str; + EXPECT_EQ(T_BITMAPS, LineTokenType(&p)); + EXPECT_EQ(u16len(u"bitmaps "), p - str); + EXPECT_TRUE(!u16cmp(p, u"\"b\"")); + + // T_STORE, preceeded by one space + u16cpy(str, u" store(b)"); + p = str; + EXPECT_EQ(T_STORE, LineTokenType(&p)); + EXPECT_EQ(u16len(u" store"), p - str); + EXPECT_TRUE(!u16cmp(p, u"(b)")); + + // T_STORE, preceeded by two spaces + u16cpy(str, u" store(b)"); + p = str; + EXPECT_EQ(T_STORE, LineTokenType(&p)); + EXPECT_EQ(u16len(u" store"), p - str); + EXPECT_TRUE(!u16cmp(p, u"(b)")); + + // T_STORE, followed by one space + u16cpy(str, u"store (b)"); + p = str; + EXPECT_EQ(T_STORE, LineTokenType(&p)); + EXPECT_EQ(u16len(u"store "), p - str); + EXPECT_TRUE(!u16cmp(p, u"(b)")); + + // T_STORE, followed by two spaces + u16cpy(str, u"store (b)"); + p = str; + EXPECT_EQ(T_STORE, LineTokenType(&p)); + EXPECT_EQ(u16len(u"store "), p - str); + EXPECT_TRUE(!u16cmp(p, u"(b)")); + + // T_COMMENT + u16cpy(str, u"c "); + p = str; + EXPECT_EQ(T_COMMENT, LineTokenType(&p)); + EXPECT_EQ(0, p - str); + + // comment without following space ... potential bug, but ReadLine() currently ensures following space + u16cpy(str, u"c"); + p = str; + EXPECT_EQ(T_UNKNOWN, LineTokenType(&p)); + EXPECT_EQ(0, p - str); + + // T_KEYTOKEY + u16cpy(str, u"abc"); + p = str; + EXPECT_EQ(T_KEYTOKEY, LineTokenType(&p)); + EXPECT_EQ(0, p - str); + + // T_UNKNOWN + u16cpy(str, u"z"); + p = str; + EXPECT_EQ(T_UNKNOWN, LineTokenType(&p)); + EXPECT_EQ(0, p - str); +} + +// KMX_BOOL StrValidChrs(PKMX_WCHAR q, KMX_WCHAR const * chrs) +// KMX_DWORD GetXString(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_WCHAR const * token, +// PKMX_WCHAR output, int max, int offset, PKMX_WCHAR *newp, int /*isVKey*/, int isUnicode +// ) + +TEST_F(CompilerTest, GetXStringImpl_test) { + KMX_WCHAR tstr[128]; + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR output[GLOBAL_BUFSIZE]; + PKMX_WCHAR newp = nullptr; + KMX_WCHAR token[128]; + + // KmnCompilerMessages::FATAL_BufferOverflow, max=0 + EXPECT_EQ(KmnCompilerMessages::FATAL_BufferOverflow, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 0, 0, &newp, FALSE)); + + // STATUS_Success, no token + u16cpy(str, u""); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // KmnCompilerMessages::ERROR_NoTokensFound, empty + u16cpy(str, u""); + u16cpy(token, u"c"); + EXPECT_EQ(KmnCompilerMessages::ERROR_NoTokensFound, GetXStringImpl(tstr, &fileKeyboard, str,token, output, 80, 0, &newp, FALSE)); + + // KmnCompilerMessages::ERROR_NoTokensFound, whitespace + u16cpy(str, u" "); + u16cpy(token, u"c"); + EXPECT_EQ(KmnCompilerMessages::ERROR_NoTokensFound, GetXStringImpl(tstr, &fileKeyboard, str, token, output, 80, 0, &newp, FALSE)); +} + +// tests strings starting with 'x' or 'd' +TEST_F(CompilerTest, GetXStringImpl_type_xd_test) { + KMX_WCHAR tstr[128]; + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR output[GLOBAL_BUFSIZE]; + PKMX_WCHAR newp = nullptr; + + // hex 32-bit + u16cpy(str, u"x10330"); // Gothic A + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_GothicA[] = { 0xD800, 0xDF30, 0 }; // see UTF32ToUTF16 + EXPECT_EQ(0, u16cmp(tstr_GothicA, tstr)); + + // decimal 8-bit + u16cpy(str, u"d18"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(u"\u0012", tstr)); + + // hex capital 8-bit + u16cpy(str, u"X12"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(u"\u0012", tstr)); + + // hex 32-bit, KmnCompilerMessages::ERROR_InvalidCharacter + u16cpy(str, u"x110000"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidCharacter, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // dk, valid + u16cpy(str, u"dk(A)"); + EXPECT_EQ(0, (int)fileKeyboard.cxDeadKeyArray); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_dk_valid[] = { UC_SENTINEL, CODE_DEADKEY, 1, 0 }; // setup deadkeys + EXPECT_EQ(0, u16cmp(tstr_dk_valid, tstr)); + fileKeyboard.cxDeadKeyArray = 0; + + // deadkey, valid + u16cpy(str, u"deadkey(A)"); + EXPECT_EQ(0, (int)fileKeyboard.cxDeadKeyArray); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_deadkey_valid[] = { UC_SENTINEL, CODE_DEADKEY, 1, 0 }; // setup deadkeys + EXPECT_EQ(0, u16cmp(tstr_deadkey_valid, tstr)); + fileKeyboard.cxDeadKeyArray = 0; + + // dk, KmnCompilerMessages::ERROR_InvalidDeadkey, bad character + u16cpy(str, u"dk(%)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidDeadkey, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // dk, KmnCompilerMessages::ERROR_InvalidDeadkey, no close delimiter => NULL + u16cpy(str, u"dk("); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidDeadkey, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // dk, KmnCompilerMessages::ERROR_InvalidDeadkey, empty delimiters => empty string + u16cpy(str, u"dk()"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidDeadkey, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); +} + +// tests strings starting with double quote +TEST_F(CompilerTest, GetXStringImpl_type_double_quote_test) { + KMX_WCHAR tstr[128]; + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR output[GLOBAL_BUFSIZE]; + PKMX_WCHAR newp = nullptr; + + // valid + u16cpy(str, u"\"abc\""); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(u"abc", tstr)); + + // KmnCompilerMessages::ERROR_UnterminatedString + u16cpy(str, u"\"abc"); + EXPECT_EQ(KmnCompilerMessages::ERROR_UnterminatedString, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // KmnCompilerMessages::ERROR_ExtendedStringTooLong + u16cpy(str, u"\"abc\""); + EXPECT_EQ(KmnCompilerMessages::ERROR_ExtendedStringTooLong, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 2, 0, &newp, FALSE)); // max reduced to force error + + // KmnCompilerMessages::ERROR_StringInVirtualKeySection *** TODO *** +} + +// tests strings starting with single quote +TEST_F(CompilerTest, GetXStringImpl_type_single_quote_test) { + KMX_WCHAR tstr[128]; + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR output[GLOBAL_BUFSIZE]; + PKMX_WCHAR newp = nullptr; + + // valid + u16cpy(str, u"\'abc\'"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(u"abc", tstr)); + + // KmnCompilerMessages::ERROR_UnterminatedString + u16cpy(str, u"\'abc"); + EXPECT_EQ(KmnCompilerMessages::ERROR_UnterminatedString, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // KmnCompilerMessages::ERROR_ExtendedStringTooLong + u16cpy(str, u"\'abc\'"); + EXPECT_EQ(KmnCompilerMessages::ERROR_ExtendedStringTooLong, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 2, 0, &newp, FALSE)); // max reduced to force error + + // KmnCompilerMessages::ERROR_StringInVirtualKeySection *** TODO *** +} + +// tests strings starting with 'a' +TEST_F(CompilerTest, GetXStringImpl_type_a_test) { + KMX_WCHAR tstr[128]; + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR output[GLOBAL_BUFSIZE]; + PKMX_WCHAR newp = nullptr; + PFILE_STORE file_store = new FILE_STORE[100]; + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + u16cpy(file_store[0].szName, u"a"); + u16cpy(file_store[1].szName, u"b"); + u16cpy(file_store[2].szName, u"c"); + + // KmnCompilerMessages::ERROR_InvalidToken + u16cpy(str, u"abc"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // KmnCompilerMessages::ERROR_AnyInVirtualKeySection *** TODO *** + + // KmnCompilerMessages::ERROR_InvalidAny, no close delimiter => NULL + u16cpy(str, u"any("); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidAny, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // KmnCompilerMessages::ERROR_InvalidAny, empty delimiters => empty string + u16cpy(str, u"any()"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidAny, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // KmnCompilerMessages::ERROR_InvalidAny, space in delimiters (see #11814, #11937, #11910, #11894, #11938) + u16cpy(str, u"any( )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidAny, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // KmnCompilerMessages::ERROR_StoreDoesNotExist + u16cpy(str, u"any(d)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // KmnCompilerMessages::ERROR_StoreDoesNotExist, space before store + u16cpy(str, u"any( d)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // KmnCompilerMessages::ERROR_StoreDoesNotExist, space after store + u16cpy(str, u"any(d )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // KmnCompilerMessages::ERROR_ZeroLengthString + u16cpy(str, u"any(b)"); + file_store[1].dpString = (PKMX_WCHAR)u""; + EXPECT_EQ(KmnCompilerMessages::ERROR_ZeroLengthString, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // valid + u16cpy(str, u"any(b)"); + file_store[1].dpString = (PKMX_WCHAR)u"abc"; // non-empty + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_any_valid[] = { UC_SENTINEL, CODE_ANY, 2, 0 }; + EXPECT_EQ(0, u16cmp(tstr_any_valid, tstr)); + + // space before store, valid + u16cpy(str, u"any( b)"); + file_store[1].dpString = (PKMX_WCHAR)u"abc"; // non-empty + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_any_valid, tstr)); + + // space after store, valid (see #11937, #11938) + u16cpy(str, u"any(b )"); + file_store[1].dpString = (PKMX_WCHAR)u"abc"; // non-empty + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_any_valid, tstr)); +} + +// tests strings starting with 'b' +TEST_F(CompilerTest, GetXStringImpl_type_b_test) { + KMX_WCHAR tstr[128]; + fileKeyboard.version = VERSION_90; + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR output[GLOBAL_BUFSIZE]; + PKMX_WCHAR newp = nullptr; + + // KmnCompilerMessages::ERROR_InvalidToken + u16cpy(str, u"bcd"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // beep, KmnCompilerMessages::ERROR_BeepInVirtualKeySection *** TODO *** + + // beep, valid + u16cpy(str, u"beep"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_beep_valid[] = { UC_SENTINEL, CODE_BEEP, 0 }; + EXPECT_EQ(0, u16cmp(tstr_beep_valid, tstr)); + + // baselayout, KmnCompilerMessages::ERROR_90FeatureOnly_IfSystemStores + fileKeyboard.version = VERSION_80; + fileKeyboard.dwFlags = 0u; + u16cpy(str, u"baselayout"); + EXPECT_EQ(KmnCompilerMessages::ERROR_90FeatureOnly_IfSystemStores, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + fileKeyboard.version = VERSION_90; + + // baselayout, KmnCompilerMessages::ERROR_InvalidInVirtualKeySection *** TODO *** + + // baselayout, no close delimiter => NULL + fileKeyboard.version = VERSION_90; + u16cpy(str, u"baselayout("); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // baselayout, empty delimiters => empty string + fileKeyboard.version = VERSION_90; + u16cpy(str, u"baselayout()"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // baselayout, space in delimiters (see #11814, #11937, #11910, #11894, #11938) + fileKeyboard.version = VERSION_90; + u16cpy(str, u"baselayout( )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // baselayout, KmnCompilerMessages::ERROR_InvalidToken from process_baselayout + fileKeyboard.version = VERSION_90; + u16cpy(str, u"baselayout(abc)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // baselayout, valid + fileKeyboard.version = VERSION_90; + fileKeyboard.cxStoreArray = 0; + fileKeyboard.dpStoreArray = nullptr; + u16cpy(str, u"baselayout(beep)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_baselayout_valid[] = { UC_SENTINEL, CODE_IFSYSTEMSTORE, TSS_BASELAYOUT+1, 2, 1, 0 }; + EXPECT_EQ(0, u16cmp(tstr_baselayout_valid, tstr)); + + // baselayout, space before argument, valid + fileKeyboard.version = VERSION_90; + fileKeyboard.cxStoreArray = 0; + fileKeyboard.dpStoreArray = nullptr; + u16cpy(str, u"baselayout( beep)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_baselayout_valid, tstr)); + + // baselayout, space after argument, valid (see #11937, #11938) + fileKeyboard.version = VERSION_90; + fileKeyboard.cxStoreArray = 0; + fileKeyboard.dpStoreArray = nullptr; + u16cpy(str, u"baselayout(beep )"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_baselayout_valid, tstr)); +} + +// tests strings starting with 'i' +TEST_F(CompilerTest, GetXStringImpl_type_i_test) { + KMX_WCHAR tstr[128]; + fileKeyboard.version = VERSION_80; + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR output[GLOBAL_BUFSIZE]; + PKMX_WCHAR newp = nullptr; + PFILE_STORE option = new FILE_STORE[100]; + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = option; + u16cpy(option[0].szName, u"a"); + u16cpy(option[1].szName, u"b"); + u16cpy(option[2].szName, u"c"); + + // KmnCompilerMessages::ERROR_InvalidToken + u16cpy(str, u"ijk"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // if, KmnCompilerMessages::ERROR_80FeatureOnly + fileKeyboard.version = VERSION_70; + fileKeyboard.dwFlags = 0u; + u16cpy(str, u"if"); + EXPECT_EQ(KmnCompilerMessages::ERROR_80FeatureOnly, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + fileKeyboard.version = VERSION_80; + + // if, KmnCompilerMessages::ERROR_InvalidInVirtualKeySection *** TODO *** + + // if, no close delimiter => NULL + fileKeyboard.version = VERSION_80; + u16cpy(str, u"if("); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIf, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // if, empty delimiters => empty string + fileKeyboard.version = VERSION_80; + fileKeyboard.dwFlags = 0u; + u16cpy(str, u"if()"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIf, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // if, space in delimiters (see #11814, #11937, #11910, #11894, #11938) + fileKeyboard.version = VERSION_80; + fileKeyboard.dwFlags = 0u; + u16cpy(str, u"if( )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIf, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // if, invalid + fileKeyboard.version = VERSION_80; + u16cpy(str, u"if(abc)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIf, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // if, KmnCompilerMessages::ERROR_90FeatureOnly_IfSystemStores + fileKeyboard.version = VERSION_80; + u16cpy(str, u"if(&BITMAP=)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_90FeatureOnly_IfSystemStores, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // if, KmnCompilerMessages::ERROR_IfSystemStore_NotFound + fileKeyboard.version = VERSION_90; + u16cpy(str, u"if(&abc=)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_IfSystemStore_NotFound, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // if, system store, equal, valid + fileKeyboard.version = VERSION_90; + fileKeyboard.cxStoreArray = 3u; + u16cpy(str, u"if(&BITMAP=beep)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_if_equal_system_store_valid[] = { UC_SENTINEL, CODE_IFSYSTEMSTORE, 2, 2, 4, 0 }; + EXPECT_EQ(0, u16cmp(tstr_if_equal_system_store_valid, tstr)); + + // if, system store, not equal, valid + fileKeyboard.version = VERSION_90; + fileKeyboard.cxStoreArray = 3u; + u16cpy(str, u"if(&BITMAP!=beep)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_if_not_equal_system_store_valid[] = { UC_SENTINEL, CODE_IFSYSTEMSTORE, 2, 1, 4, 0 }; + EXPECT_EQ(0, u16cmp(tstr_if_not_equal_system_store_valid, tstr)); + + // if, option, KmnCompilerMessages::ERROR_StoreDoesNotExist + fileKeyboard.version = VERSION_80; + u16cpy(str, u"if(d=beep)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // if, option, equal, valid + fileKeyboard.version = VERSION_80; + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = option; + option[1].fIsOption = TRUE; + u16cpy(str, u"if(b=beep)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_if_option_valid[] = { UC_SENTINEL, CODE_IFOPT, 2, 2, 4, 0 }; + EXPECT_EQ(0, u16cmp(tstr_if_option_valid, tstr)); + + // if, option, equal, space before assign, valid + fileKeyboard.version = VERSION_80; + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = option; + option[1].fIsOption = TRUE; + u16cpy(str, u"if(b =beep)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_if_option_valid, tstr)); + + // if, option, equal, space before rhs, valid + fileKeyboard.version = VERSION_80; + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = option; + option[1].fIsOption = TRUE; + u16cpy(str, u"if(b= beep)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_if_option_valid, tstr)); + + // if, option, equal, space after rhs, valid (see #11937, #11938) + fileKeyboard.version = VERSION_80; + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = option; + option[1].fIsOption = TRUE; + u16cpy(str, u"if(b=beep )"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_if_option_valid, tstr)); + + delete[] option; + PFILE_STORE file_store = new FILE_STORE[100]; + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + file_store[1].fIsStore = TRUE; + u16cpy(file_store[0].szName, u"a"); + u16cpy(file_store[1].szName, u"b"); + u16cpy(file_store[2].szName, u"c"); + + // index, KmnCompilerMessages::ERROR_InvalidInVirtualKeySection *** TODO *** + + // index, no close delimiter => NULL + u16cpy(str, u"index("); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, empty delimiters => empty string + u16cpy(str, u"index()"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, space in delimiters (see #11814, #11937, #11910, #11894, #11938) + u16cpy(str, u"index( )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, no comma or space + u16cpy(str, u"index(b)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, no comma, space before store + u16cpy(str, u"index( b)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, no comma, space after store + u16cpy(str, u"index(b )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, KmnCompilerMessages::ERROR_StoreDoesNotExist + u16cpy(str, u"index(d,4)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, comma, offset=0 + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + u16cpy(str, u"index(b,0)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, comma, negative offset + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + u16cpy(str, u"index(b,-1)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, comma, valid + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + u16cpy(str, u"index(b,4)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_index_valid[] = { UC_SENTINEL, CODE_INDEX, 2, 4, 0 }; + EXPECT_EQ(0, u16cmp(tstr_index_valid, tstr)); + + // index, space before store, comma, valid + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + u16cpy(str, u"index( b,4)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_index_valid, tstr)); + + // index, space after store, comma, valid + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + u16cpy(str, u"index(b ,4)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_index_valid, tstr)); + + // index, comma and space, valid + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + u16cpy(str, u"index(b, 4)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_index_valid, tstr)); + + // index, comma, space after offset, valid + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + u16cpy(str, u"index(b,4 )"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_index_valid, tstr)); + + // index, space, KmnCompilerMessages::ERROR_StoreDoesNotExist (see issue #11833) + u16cpy(str, u"index(b 4)"); // store name appears to be 'b 4' + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, two commas and extra parameter, KmnCompilerMessages::ERROR_InvalidIndex + u16cpy(str, u"index(b,4,5)"); + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, two-digit offset, valid + u16cpy(str, u"index(b,42)"); + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_index_two_digit_valid[] = { UC_SENTINEL, CODE_INDEX, 2, 42, 0 }; + EXPECT_EQ(0, u16cmp(tstr_index_two_digit_valid, tstr)); + + // index, comma, non-digit offset, KmnCompilerMessages::ERROR_InvalidIndex + u16cpy(str, u"index(b,g)"); + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, comma, no offset, KmnCompilerMessages::ERROR_InvalidIndex + u16cpy(str, u"index(b,)"); + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, space and comma, no offset, KmnCompilerMessages::ERROR_InvalidIndex + u16cpy(str, u"index(b ,)"); + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // index, comma, no offset but space, KmnCompilerMessages::ERROR_InvalidIndex + u16cpy(str, u"index(b, )"); + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidIndex, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); +} + +// tests strings starting with 'o' +TEST_F(CompilerTest, GetXStringImpl_type_o_test) { + KMX_WCHAR tstr[128]; + fileKeyboard.version = VERSION_80; + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR output[GLOBAL_BUFSIZE]; + PKMX_WCHAR newp = nullptr; + PFILE_STORE file_store = new FILE_STORE[100]; + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + file_store[1].fIsStore = TRUE; + u16cpy(file_store[0].szName, u"a"); + u16cpy(file_store[1].szName, u"b"); + u16cpy(file_store[2].szName, u"c"); + + // KmnCompilerMessages::ERROR_InvalidToken + u16cpy(str, u"opq"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // outs, KmnCompilerMessages::ERROR_OutsInVirtualKeySection *** TODO *** + + // outs, no close delimiter => NULL + u16cpy(str, u"outs("); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidOuts, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // outs, empty delimiters => empty string + u16cpy(str, u"outs()"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidOuts, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // outs, space in delimiters (see #11814, #11937, #11910, #11894, #11938) + u16cpy(str, u"outs( )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidOuts, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // outs, KmnCompilerMessages::ERROR_StoreDoesNotExist + u16cpy(str, u"outs(d)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // outs, KmnCompilerMessages::ERROR_StoreDoesNotExist, space before store + u16cpy(str, u"outs( d)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // outs, KmnCompilerMessages::ERROR_StoreDoesNotExist, space after store + u16cpy(str, u"outs(d )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // outs, KmnCompilerMessages::ERROR_OutsTooLong + PKMX_WCHAR dpString = (PKMX_WCHAR)u"abc"; + file_store[1].dpString = dpString; // length 4 => max should be > 4, otherwise a ERROR_OutsTooLong is emitted + int max = u16len(dpString) + 1; // 4, including terminating '\0' + u16cpy(str, u"outs(b)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_OutsTooLong, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, max, 0, &newp, FALSE)); // max reduced to force error + + // outs, valid + file_store[1].dpString = (PKMX_WCHAR)u"abc"; + u16cpy(str, u"outs(b)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_outs_valid[] = { 'a', 'b', 'c', 0 }; + EXPECT_EQ(0, u16cmp(tstr_outs_valid, tstr)); + + // outs, space before store, valid + file_store[1].dpString = (PKMX_WCHAR)u"abc"; + u16cpy(str, u"outs( b)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_outs_valid, tstr)); + + // outs, space after store, valid (see #11937, #11938) + file_store[1].dpString = (PKMX_WCHAR)u"abc"; + u16cpy(str, u"outs(b )"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_outs_valid, tstr)); +} + +// tests strings starting with 'c' +TEST_F(CompilerTest, GetXStringImpl_type_c_test) { + KMX_WCHAR tstr[128]; + fileKeyboard.version = VERSION_60; + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR output[GLOBAL_BUFSIZE]; + PKMX_WCHAR newp = nullptr; + PFILE_STORE file_store = new FILE_STORE[100]; + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + file_store[1].fIsCall = TRUE; + file_store[1].dwSystemID = TSS_NONE; + u16cpy(file_store[0].szName, u"a"); + u16cpy(file_store[1].szName, u"b"); + u16cpy(file_store[2].szName, u"c"); + + // are comments stripped before this point? + // if so, why the test on whitespace after 'c'? + + // KmnCompilerMessages::ERROR_InvalidToken + fileKeyboard.version = VERSION_60; + u16cpy(str, u"cde"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // context, KmnCompilerMessages::ERROR_ContextInVirtualKeySection *** TODO *** + + // context, no offset, valid + fileKeyboard.version = VERSION_60; + u16cpy(str, u"context"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_context_no_offset_valid[] = { UC_SENTINEL, CODE_CONTEXT, 0 }; + EXPECT_EQ(0, u16cmp(tstr_context_no_offset_valid, tstr)); + + // context, KmnCompilerMessages::ERROR_InvalidToken, no close delimiter => NULL + fileKeyboard.version = VERSION_60; + u16cpy(str, u"context("); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // context, empty delimiters => empty string, valid + fileKeyboard.version = VERSION_60; + u16cpy(str, u"context()"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_context_empty_offset_valid[] = { UC_SENTINEL, CODE_CONTEXT, 0 }; + EXPECT_EQ(0, u16cmp(tstr_context_empty_offset_valid, tstr)); + + // context, space in delimiters (see #11814, #11937, #11910, #11894, #11938) + fileKeyboard.version = VERSION_60; + u16cpy(str, u"context( )"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // context, offset, valid + fileKeyboard.version = VERSION_60; + u16cpy(str, u"context(1)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_context_offset_valid[] = { UC_SENTINEL, CODE_CONTEXTEX, 1, 0 }; + EXPECT_EQ(0, u16cmp(tstr_context_offset_valid, tstr)); + + // context, CERR_InvalidToke, offset < 1 + fileKeyboard.version = VERSION_60; + u16cpy(str, u"context(0)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // context, large offset < 0xF000, valid + fileKeyboard.version = VERSION_60; + u16cpy(str, u"context(61439)"); //0xF000 - 1 + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_context_large_offset_valid[] = { UC_SENTINEL, CODE_CONTEXTEX, 61439, 0 }; + EXPECT_EQ(0, u16cmp(tstr_context_large_offset_valid, tstr)); + + // context, KmnCompilerMessages::ERROR_InvalidToken, too large offset == 0xF000 + fileKeyboard.version = VERSION_60; + u16cpy(str, u"context(61440)"); //0xF000 + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // context, KmnCompilerMessages::ERROR_60FeatureOnly_Contextn + fileKeyboard.version = VERSION_50; + u16cpy(str, u"context(1)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_60FeatureOnly_Contextn, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // context, valid + fileKeyboard.version = VERSION_60; + u16cpy(str, u"context(1)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_context_valid[] = { UC_SENTINEL, CODE_CONTEXTEX, 1, 0 }; + EXPECT_EQ(0, u16cmp(tstr_context_valid, tstr)); + + // context, space before offset, valid + fileKeyboard.version = VERSION_60; + u16cpy(str, u"context( 1)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_context_valid, tstr)); + + // context, space after offset, valid (see #11937, #11938) + fileKeyboard.version = VERSION_60; + u16cpy(str, u"context(1 )"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_context_valid, tstr)); + + // clearcontext, valid + fileKeyboard.version = VERSION_60; + u16cpy(str, u"clearcontext"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_clearcontext_valid[] = { UC_SENTINEL, CODE_CLEARCONTEXT, 0 }; + EXPECT_EQ(0, u16cmp(tstr_clearcontext_valid, tstr)); + + // call, KmnCompilerMessages::ERROR_501FeatureOnly_Call + fileKeyboard.version = VERSION_50; + u16cpy(str, u"call"); + EXPECT_EQ(KmnCompilerMessages::ERROR_501FeatureOnly_Call, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // call, KmnCompilerMessages::ERROR_CallInVirtualKeySection *** TODO *** + + // call, no close delimiter => NULL + fileKeyboard.version = VERSION_501; + u16cpy(str, u"call("); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidCall, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // call, empty delimiters => empty string + fileKeyboard.version = VERSION_501; + u16cpy(str, u"call()"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidCall, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // call, space in delimiters (see #11814, #11937, #11910, #11894, #11938) + fileKeyboard.version = VERSION_501; + u16cpy(str, u"call( )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidCall, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // call, KmnCompilerMessages::ERROR_StoreDoesNotExist + fileKeyboard.version = VERSION_501; + u16cpy(str, u"call(d)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // call, KmnCompilerMessages::ERROR_StoreDoesNotExist, space before store + fileKeyboard.version = VERSION_501; + u16cpy(str, u"call( d)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // call, KmnCompilerMessages::ERROR_StoreDoesNotExist, space after store + fileKeyboard.version = VERSION_501; + u16cpy(str, u"call(d )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // call, KmnCompilerMessages::ERROR_InvalidCall + fileKeyboard.version = VERSION_501; + file_store[1].dpString = (PKMX_WCHAR)u"*"; // cause IsValidCallStore() to fail + u16cpy(str, u"call(b)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidCall, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // call, valid + fileKeyboard.version = VERSION_501; + file_store[1].dpString = (PKMX_WCHAR)u"a.dll:A"; + file_store[1].dwSystemID = TSS_NONE; + u16cpy(str, u"call(b)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_call_valid[] = { UC_SENTINEL, CODE_CALL, 2, 0 }; + EXPECT_EQ(0, u16cmp(tstr_call_valid, tstr)); + EXPECT_EQ(TSS_CALLDEFINITION, file_store[1].dwSystemID); + + // call, space before store, valid + fileKeyboard.version = VERSION_501; + file_store[1].dpString = (PKMX_WCHAR)u"a.dll:A"; + file_store[1].dwSystemID = TSS_NONE; + u16cpy(str, u"call( b)"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_call_valid, tstr)); + EXPECT_EQ(TSS_CALLDEFINITION, file_store[1].dwSystemID); + + // call, space after store, valid (see #11937, #11938) + fileKeyboard.version = VERSION_501; + file_store[1].dpString = (PKMX_WCHAR)u"a.dll:A"; + file_store[1].dwSystemID = TSS_NONE; + u16cpy(str, u"call(b )"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_call_valid, tstr)); + EXPECT_EQ(TSS_CALLDEFINITION, file_store[1].dwSystemID); +} + +// tests strings starting with 'n' +TEST_F(CompilerTest, GetXStringImpl_type_n_test) { + KMX_WCHAR tstr[128]; + fileKeyboard.version = VERSION_70; + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR output[GLOBAL_BUFSIZE]; + PKMX_WCHAR newp = nullptr; + PFILE_STORE file_store = new FILE_STORE[100]; + fileKeyboard.cxStoreArray = 3u; + fileKeyboard.dpStoreArray = file_store; + u16cpy(file_store[0].szName, u"a"); + u16cpy(file_store[1].szName, u"b"); + u16cpy(file_store[2].szName, u"c"); + + // KmnCompilerMessages::ERROR_InvalidToken + fileKeyboard.version = VERSION_70; + u16cpy(str, u"nmo"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // notany, KmnCompilerMessages::ERROR_60FeatureOnly_Contextn + fileKeyboard.version = VERSION_60; + u16cpy(str, u"notany"); + EXPECT_EQ(KmnCompilerMessages::ERROR_70FeatureOnly, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // notany, KmnCompilerMessages::ERROR_AnyInVirtualKeySection *** TODO *** + + // notany, no close delimiter => NULL + fileKeyboard.version = VERSION_70; + u16cpy(str, u"notany("); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidAny, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // notany, empty delimiters => empty string + fileKeyboard.version = VERSION_70; + u16cpy(str, u"notany()"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidAny, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // notany, space in delimiters (see #11814, #11937, #11910, #11894, #11938) + fileKeyboard.version = VERSION_70; + u16cpy(str, u"notany( )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidAny, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // notany, KmnCompilerMessages::ERROR_StoreDoesNotExist + fileKeyboard.version = VERSION_70; + u16cpy(str, u"notany(d)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // notany, KmnCompilerMessages::ERROR_StoreDoesNotExist, space before store + fileKeyboard.version = VERSION_70; + u16cpy(str, u"notany( d)"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // notany, KmnCompilerMessages::ERROR_StoreDoesNotExist, space after store + fileKeyboard.version = VERSION_70; + u16cpy(str, u"notany(d )"); + EXPECT_EQ(KmnCompilerMessages::ERROR_StoreDoesNotExist, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // notany, valid + fileKeyboard.version = VERSION_70; + u16cpy(str, u"notany(b)"); + file_store[1].dpString = (PKMX_WCHAR)u"abc"; // non-empty + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_notany_valid[] = { UC_SENTINEL, CODE_NOTANY, 2, 0 }; + EXPECT_EQ(0, u16cmp(tstr_notany_valid, tstr)); + + // notany, valid, empy store + fileKeyboard.version = VERSION_70; + u16cpy(str, u"notany(b)"); + file_store[1].dpString = (PKMX_WCHAR)u""; // empty + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_notany_valid, tstr)); + + // notany, space before store, valid + fileKeyboard.version = VERSION_70; + u16cpy(str, u"notany( b)"); + file_store[1].dpString = (PKMX_WCHAR)u"abc"; // non-empty + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_notany_valid, tstr)); + + // notany, space after store, valid (see #11937, #11938) + fileKeyboard.version = VERSION_70; + u16cpy(str, u"notany(b )"); + file_store[1].dpString = (PKMX_WCHAR)u"abc"; // non-empty + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_notany_valid, tstr)); + + // null + fileKeyboard.version = VERSION_70; + u16cpy(str, u"nul"); + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + const KMX_WCHAR tstr_null_valid[] = { UC_SENTINEL, CODE_NUL, 0 }; + EXPECT_EQ(0, u16cmp(tstr_null_valid, tstr)); +} + +// tests strings starting with 'u' +TEST_F(CompilerTest, GetXStringImpl_type_u_test) { + KMX_WCHAR tstr[128]; + fileKeyboard.version = VERSION_70; + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR output[GLOBAL_BUFSIZE]; + PKMX_WCHAR newp = nullptr; + + // KmnCompilerMessages::ERROR_InvalidToken + u16cpy(str, u"uvw"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + + // unicode, n1 and n2, valid + u16cpy(str, u"u+10330"); // Gothic A + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, TRUE)); + const KMX_WCHAR tstr_unicode_valid[] = { 0xD800, 0xDF30, 0 }; // see UTF32ToUTF16 + EXPECT_EQ(0, u16cmp(tstr_unicode_valid, tstr)); + EXPECT_EQ(0, msgproc_errors.size()); + + // unicode, ERROR_InvalidValue + u16cpy(str, u"u+10330z"); // Gothic A, unexpected character 'z' + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidValue, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, TRUE)); + + // unicode, space after, valid + u16cpy(str, u"u+10330 "); // Gothic A + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, TRUE)); + EXPECT_EQ(0, u16cmp(tstr_unicode_valid, tstr)); + EXPECT_EQ(0, msgproc_errors.size()); + + // unicode, KmnCompilerMessages::ERROR_InvalidCharacter + u16cpy(str, u"u+110000"); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidCharacter, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, TRUE)); + + // unicode, n1 only, valid + u16cpy(str, u"u+0061"); // a + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, TRUE)); + const KMX_WCHAR tstr_unicode_n1_only_valid[] = { 0x0061, 0 }; // see UTF32ToUTF16 + EXPECT_EQ(0, u16cmp(tstr_unicode_n1_only_valid, tstr)); + EXPECT_EQ(0, msgproc_errors.size()); + + // unicode, n1 and n2, valid, KmnCompilerMessages::WARN_UnicodeInANSIGroup + u16cpy(str, u"u+10330"); // Gothic A + EXPECT_EQ(STATUS_Success, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE)); + EXPECT_EQ(0, u16cmp(tstr_unicode_valid, tstr)); + EXPECT_EQ(1, msgproc_errors.size()); + EXPECT_EQ(KmnCompilerMessages::WARN_UnicodeInANSIGroup, msgproc_errors[0].errorCode); +} + +// KMX_DWORD process_baselayout(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) +// KMX_DWORD process_platform(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) +// KMX_DWORD process_if_synonym(KMX_DWORD dwSystemID, PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) +// KMX_DWORD process_if(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) +// KMX_DWORD process_reset(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) +// KMX_DWORD process_expansion(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx, int max) +// KMX_DWORD process_set_synonym(KMX_DWORD dwSystemID, PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) +// KMX_DWORD process_set(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) +// KMX_DWORD process_save(PFILE_KEYBOARD fk, PKMX_WCHAR q, PKMX_WCHAR tstr, int *mx) +// int xatoi(PKMX_WCHAR *p) +// int GetGroupNum(PFILE_KEYBOARD fk, PKMX_WCHAR p) + +TEST_F(CompilerTest, ProcessEthnologueStore_test) { + EXPECT_EQ(STATUS_Success, ProcessEthnologueStore((PKMX_WCHAR)u"abc")); + EXPECT_EQ(KmnCompilerMessages::WARN_PunctuationInEthnologueCode, ProcessEthnologueStore((PKMX_WCHAR)u";abc")); + EXPECT_EQ(KmnCompilerMessages::WARN_PunctuationInEthnologueCode, ProcessEthnologueStore((PKMX_WCHAR)u",abc")); + EXPECT_EQ(STATUS_Success, ProcessEthnologueStore((PKMX_WCHAR)u" abc")); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidEthnologueCode, ProcessEthnologueStore((PKMX_WCHAR)u"abc ")); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidEthnologueCode, ProcessEthnologueStore((PKMX_WCHAR)u"abcd")); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidEthnologueCode, ProcessEthnologueStore((PKMX_WCHAR)u"ab")); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidEthnologueCode, ProcessEthnologueStore((PKMX_WCHAR)u"a")); + EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidEthnologueCode, ProcessEthnologueStore((PKMX_WCHAR)u"a2b")); + EXPECT_EQ(STATUS_Success, ProcessEthnologueStore((PKMX_WCHAR)u"")); // needs correcting ... see #11955 + EXPECT_EQ(STATUS_Success, ProcessEthnologueStore((PKMX_WCHAR)u"abc def")); + EXPECT_EQ(STATUS_Success, ProcessEthnologueStore((PKMX_WCHAR)u"abc def")); + EXPECT_EQ(KmnCompilerMessages::WARN_PunctuationInEthnologueCode, ProcessEthnologueStore((PKMX_WCHAR)u"abc,def")); + EXPECT_EQ(KmnCompilerMessages::WARN_PunctuationInEthnologueCode, ProcessEthnologueStore((PKMX_WCHAR)u"abc;def")); +} + +// KMX_DWORD ProcessHotKey(PKMX_WCHAR p, KMX_DWORD *hk) +// void SetChecksum(PKMX_BYTE buf, PKMX_DWORD CheckSum, KMX_DWORD sz) +// void kmcmp::CheckStoreUsage(PFILE_KEYBOARD fk, int storeIndex, KMX_BOOL fIsStore, KMX_BOOL fIsOption, KMX_BOOL fIsCall) +// KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, KMX_BYTE**data, size_t& dataSize) +// KMX_DWORD ReadLine(KMX_BYTE* infile, int sz, int& offset, PKMX_WCHAR wstr, KMX_BOOL PreProcess) + +TEST_F(CompilerTest, GetRHS_test) { + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR tstr[128]; + + // KmnCompilerMessages::ERROR_NoTokensFound, empty string + u16cpy(str, u""); + EXPECT_EQ(KmnCompilerMessages::ERROR_NoTokensFound, GetRHS(&fileKeyboard, str, tstr, 80, 0, FALSE)); + + // KmnCompilerMessages::ERROR_NoTokensFound, no '>' + u16cpy(str, u"abc"); + EXPECT_EQ(KmnCompilerMessages::ERROR_NoTokensFound, GetRHS(&fileKeyboard, str, tstr, 80, 0, FALSE)); + + // STATUS_Success + u16cpy(str, u"> nul c\n"); + EXPECT_EQ(STATUS_Success, GetRHS(&fileKeyboard, str, tstr, 80, 0, FALSE)); +} + +// void safe_wcsncpy(PKMX_WCHAR out, PKMX_WCHAR in, int cbMax) +// KMX_BOOL IsSameToken(PKMX_WCHAR *p, KMX_WCHAR const * token) +// static bool endsWith(const std::string& str, const std::string& suffix) +// KMX_DWORD ImportBitmapFile(PFILE_KEYBOARD fk, PKMX_WCHAR szName, PKMX_DWORD FileSize, PKMX_BYTE *Buf) +// int atoiW(PKMX_WCHAR p) +// KMX_DWORD kmcmp::CheckUTF16(int n) + +TEST_F(CompilerTest, isIntegerWstring_test) { + EXPECT_FALSE(isIntegerWstring(nullptr)); + EXPECT_FALSE(isIntegerWstring((PKMX_WCHAR)u"")); + EXPECT_FALSE(isIntegerWstring((PKMX_WCHAR)u"a")); + EXPECT_TRUE(isIntegerWstring((PKMX_WCHAR)u"-1")); + EXPECT_TRUE(isIntegerWstring((PKMX_WCHAR)u"1")); + EXPECT_FALSE(isIntegerWstring((PKMX_WCHAR)u" 1")); + EXPECT_FALSE(isIntegerWstring((PKMX_WCHAR)u"1 ")); + EXPECT_TRUE(isIntegerWstring((PKMX_WCHAR)u"42")); + EXPECT_TRUE(isIntegerWstring((PKMX_WCHAR)u"2147483647")); // INT_MAX + EXPECT_FALSE(isIntegerWstring((PKMX_WCHAR)u"2147483648")); // INT_MAX + 1 + EXPECT_FALSE(isIntegerWstring((PKMX_WCHAR)u"9999999999")); // > INT_MAX + EXPECT_TRUE(isIntegerWstring((PKMX_WCHAR)u"-2147483648")); // -INT_MAX - 1 + EXPECT_FALSE(isIntegerWstring((PKMX_WCHAR)u"-2147483649")); // -INT_MAX - 2 + EXPECT_FALSE(isIntegerWstring((PKMX_WCHAR)u"-9999999999")); // < -INT_MAX - 1 +} + +// KMX_DWORD kmcmp::UTF32ToUTF16(int n, int *n1, int *n2) +// KMX_BOOL BuildVKDictionary(PFILE_KEYBOARD fk) +// int GetVKCode(PFILE_KEYBOARD fk, PKMX_WCHAR p) +// int GetDeadKey(PFILE_KEYBOARD fk, PKMX_WCHAR p) +// void kmcmp::RecordDeadkeyNames(PFILE_KEYBOARD fk) +// KMX_BOOL kmcmp::IsValidCallStore(PFILE_STORE fs) + +TEST_F(CompilerTest, hasPreamble_test) { + EXPECT_FALSE(hasPreamble(u"")); + EXPECT_FALSE(hasPreamble(u"\uFEFE")); // not \uFEFF + EXPECT_TRUE(hasPreamble(u"\uFEFF")); + EXPECT_FALSE(hasPreamble(u"a\uFEFF")); +} + +// bool UTF16TempFromUTF8(KMX_BYTE* infile, int sz, KMX_BYTE** tempfile, int *sz16) +// PFILE_STORE FindSystemStore(PFILE_KEYBOARD fk, KMX_DWORD dwSystemID) diff --git a/developer/src/kmcmplib/tests/gtest-km_u16-test.cpp b/developer/src/kmcmplib/tests/gtest-km_u16-test.cpp new file mode 100644 index 00000000000..75d74607cd4 --- /dev/null +++ b/developer/src/kmcmplib/tests/gtest-km_u16-test.cpp @@ -0,0 +1,256 @@ +#include +#include +#include +#include "../src/compfile.h" + +TEST(km_u16_Test, u16chr) { + KMX_WCHAR str[LINESIZE]; + + u16cpy(str, u"abc"); + EXPECT_EQ(1, u16chr(str, 'b') - str); // in string + u16cpy(str, u"abc"); + EXPECT_EQ(NULL, u16chr(str, 'd')); // not in string + u16cpy(str, u"abc"); + EXPECT_EQ(3, u16chr(str, '\0') - str); // locate null terminator +} + +TEST(km_u16_Test, u16chr_compare_to_strchr) { + // Compare behaviour of strchr: + char str[LINESIZE]; + + strcpy(str, "abc"); + EXPECT_EQ(1, strchr(str, 'b') - str); // in string + strcpy(str, "abc"); + EXPECT_EQ(NULL, strchr(str, 'd')); // not in string + strcpy(str, "abc"); + EXPECT_EQ(3, strchr(str, '\0') - str); // locate null terminator +} + +TEST(km_u16_Test, u16tok_char_delim) { + // For char delimiter: KMX_WCHAR * u16tok(KMX_WCHAR *p, const KMX_WCHAR ch, KMX_WCHAR **ctx) ; + + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR *ctx = nullptr; + + // sequence of tokens + u16cpy(str, u"test a space and two"); + ctx = nullptr; + EXPECT_TRUE(!u16cmp(u"test", u16tok(str, ' ', &ctx))); + EXPECT_TRUE(!u16cmp(u"a", u16tok(nullptr, ' ', &ctx))); + EXPECT_TRUE(!u16cmp(u"space", u16tok(nullptr, ' ', &ctx))); + EXPECT_TRUE(!u16cmp(u"and", u16tok(nullptr, ' ', &ctx))); + EXPECT_TRUE(!u16cmp(u"two", u16tok(nullptr, ' ', &ctx))); + EXPECT_EQ(nullptr, ctx); + + // only a delimiter + u16cpy(str, u" "); + ctx = nullptr; + EXPECT_EQ(nullptr, u16tok(str, ' ', &ctx)); + EXPECT_EQ(nullptr, u16tok(nullptr, ' ', &ctx)); + + // delimiters at end + u16cpy(str, u"a b "); + ctx = nullptr; + EXPECT_TRUE(!u16cmp(u"a", u16tok(str, ' ', &ctx))); + EXPECT_TRUE(!u16cmp(u"b", u16tok(nullptr, ' ', &ctx))); + EXPECT_EQ(nullptr, u16tok(nullptr, ' ', &ctx)); + + // no string, no context + ctx = nullptr; + EXPECT_EQ(nullptr, u16tok(nullptr, ' ', &ctx)); + + // delimited string + u16cpy(str, u"abc|def"); + ctx = nullptr; + EXPECT_EQ(str, u16tok(str, '|', &ctx)); + EXPECT_TRUE(!u16cmp(u"abc", str)); + EXPECT_TRUE(!u16cmp(u"def", ctx)); +} + +TEST(km_u16_Test, u16tok_str_delim) { + // For string delimiter: KMX_WCHAR * u16tok(KMX_WCHAR* p, const KMX_WCHAR* ch, KMX_WCHAR** ctx) ; + + KMX_WCHAR str[LINESIZE]; + KMX_WCHAR *ctx = nullptr; + + // sequence of tokens + u16cpy(str, u"test a space and two"); + ctx = nullptr; + EXPECT_TRUE(!u16cmp(u"test", u16tok(str, u" ", &ctx))); + EXPECT_TRUE(!u16cmp(u"a", u16tok(nullptr, u" ", &ctx))); + EXPECT_TRUE(!u16cmp(u"space", u16tok(nullptr, u" ", &ctx))); + EXPECT_TRUE(!u16cmp(u"and", u16tok(nullptr, u" ", &ctx))); + EXPECT_TRUE(!u16cmp(u"two", u16tok(nullptr, u" ", &ctx))); + EXPECT_EQ(nullptr, ctx); + + // only a delimiter + u16cpy(str, u" "); + ctx = nullptr; + EXPECT_EQ(nullptr, u16tok(str, u" ", &ctx)); + EXPECT_EQ(nullptr, u16tok(nullptr, u" ", &ctx)); + + // delimiters at end + u16cpy(str, u"a b "); + ctx = nullptr; + EXPECT_TRUE(!u16cmp(u"a", u16tok(str, u" ", &ctx))); + EXPECT_TRUE(!u16cmp(u"b", u16tok(nullptr, u" ", &ctx))); + EXPECT_EQ(nullptr, u16tok(nullptr, u" ", &ctx)); + + // no string, no context + ctx = nullptr; + EXPECT_EQ(nullptr, u16tok(nullptr, u"", &ctx)); + + // delimited string + u16cpy(str, u"abc|def"); + ctx = nullptr; + EXPECT_EQ(str, u16tok(str, u"|", &ctx)); + EXPECT_TRUE(!u16cmp(u"abc", str)); + EXPECT_TRUE(!u16cmp(u"def", ctx)); + + // multiple delimiters + u16cpy(str, u"abcghi"); + ctx = nullptr; + const KMX_WCHAR *delim = u"<>"; + EXPECT_TRUE(!u16cmp(u"abc", u16tok(str, delim, &ctx))); + EXPECT_TRUE(!u16cmp(u"def", u16tok(nullptr, delim, &ctx))); + EXPECT_TRUE(!u16cmp(u"ghi", u16tok(nullptr, delim, &ctx))); + EXPECT_EQ(nullptr, ctx); + + // check sensitivity of ordering with multiple delimiters + u16cpy(str, u"abcghi"); + ctx = nullptr; + const KMX_WCHAR *reverse_delim = u"><"; + EXPECT_TRUE(!u16cmp(u"abc", u16tok(str, reverse_delim, &ctx))); + EXPECT_TRUE(!u16cmp(u"def", u16tok(nullptr, reverse_delim, &ctx))); + EXPECT_TRUE(!u16cmp(u"ghi", u16tok(nullptr, reverse_delim, &ctx))); + EXPECT_EQ(nullptr, ctx); +} + +TEST(km_u16_Test, u16tok_str_compare_to_strtok) { + // Compare behaviour of strtok: + char str[LINESIZE]; + + // sequence of tokens + strcpy(str, "test a space and two"); + EXPECT_TRUE(!strcmp("test", strtok(str, " "))); + EXPECT_TRUE(!strcmp("a", strtok(nullptr, " "))); + EXPECT_TRUE(!strcmp("space", strtok(nullptr, " "))); + EXPECT_TRUE(!strcmp("and", strtok(nullptr, " "))); + EXPECT_TRUE(!strcmp("two", strtok(nullptr, " "))); + EXPECT_EQ(nullptr, strtok(nullptr, " ")); + + // only a delimiter + strcpy(str, " "); + EXPECT_EQ(nullptr, strtok(str, " ")); + EXPECT_EQ(nullptr, strtok(nullptr, " ")); + + // delimiters at end + strcpy(str, "a b "); + EXPECT_TRUE(!strcmp("a", strtok(str, " "))); + EXPECT_TRUE(!strcmp("b", strtok(nullptr, " "))); + EXPECT_EQ(nullptr, strtok(nullptr, " ")); + + // multiple delimiters + strcpy(str, "abcghi"); + const char *delim = "<>"; + EXPECT_TRUE(!strcmp("abc", strtok(str, delim))); + EXPECT_TRUE(!strcmp("def", strtok(nullptr, delim))); + EXPECT_TRUE(!strcmp("ghi", strtok(nullptr, delim))); + EXPECT_EQ(nullptr, strtok(nullptr, delim)); + + // check sensitivity of ordering with multiple delimiters + strcpy(str, "abcghi"); + const char *reverse_delim = "><"; + EXPECT_TRUE(!strcmp("abc", strtok(str, reverse_delim))); + EXPECT_TRUE(!strcmp("def", strtok(nullptr, reverse_delim))); + EXPECT_TRUE(!strcmp("ghi", strtok(nullptr, reverse_delim))); + EXPECT_EQ(nullptr, strtok(nullptr, reverse_delim)); +} + +TEST(km_u16_Test, u16ltrim) { + KMX_WCHAR str[LINESIZE]; + PKMX_WCHAR q; + + EXPECT_TRUE(!u16ltrim(nullptr)); + + std::map m{ + // input output + {u"", u"" }, + {u" ", u"" }, + {u" ", u"" }, + {u"abc", u"abc" }, + {u" abc", u"abc" }, + {u" abc", u"abc" }, + {u"abc ", u"abc " }, + {u"\tabc", u"abc" }, + {u"abc def", u"abc def"}, + {u" abc def", u"abc def"}, + }; + + for (auto i = m.begin(); i != m.end(); i++) { + u16cpy(str, i->first); + q = u16ltrim(str); + EXPECT_TRUE(!u16cmp(i->second, q)); + EXPECT_EQ(str, q); + } +} + +TEST(km_u16_Test, u16rtrim) { + KMX_WCHAR str[LINESIZE]; + PKMX_WCHAR q; + + EXPECT_TRUE(!u16rtrim(nullptr)); + + std::map m{ + // input output + {u"", u"" }, + {u" ", u"" }, + {u" ", u"" }, + {u"abc", u"abc" }, + {u"abc ", u"abc" }, + {u"abc ", u"abc" }, + {u" abc", u" abc" }, + {u"abc\t", u"abc" }, + {u"abc def", u"abc def"}, + {u"abc def ", u"abc def"}, + }; + + for (auto i = m.begin(); i != m.end(); i++) { + u16cpy(str, i->first); + q = u16rtrim(str); + EXPECT_TRUE(!u16cmp(i->second, q)); + EXPECT_EQ(str, q); + } +} + +TEST(km_u16_Test, u16trim) { + KMX_WCHAR str[LINESIZE]; + PKMX_WCHAR q; + + EXPECT_TRUE(!u16trim(nullptr)); + + std::map m{ + // input output + {u"", u"" }, + {u" ", u"" }, + {u" ", u"" }, + {u"abc", u"abc" }, + {u"abc ", u"abc" }, + {u"abc ", u"abc" }, + {u" abc", u"abc" }, + {u" abc", u"abc" }, + {u" abc ", u"abc" }, + {u" abc ", u"abc" }, + {u"abc\t", u"abc" }, + {u"\tabc", u"abc" }, + {u"abc def", u"abc def" }, + {u" abc def ", u"abc def"}, + }; + + for (auto i = m.begin(); i != m.end(); i++) { + u16cpy(str, i->first); + q = u16trim(str); + EXPECT_TRUE(!u16cmp(i->second, q)); + EXPECT_EQ(str, q); + } +} diff --git a/developer/src/kmcmplib/tests/kmcompxtest.cpp b/developer/src/kmcmplib/tests/kmcompxtest.cpp index 454481ff5ec..827199967e6 100644 --- a/developer/src/kmcmplib/tests/kmcompxtest.cpp +++ b/developer/src/kmcmplib/tests/kmcompxtest.cpp @@ -19,15 +19,11 @@ #ifdef _MSC_VER #else #include +#include #endif using namespace std; -#define CERR_FATAL 0x00008000 -#define CERR_ERROR 0x00004000 -#define CERR_WARNING 0x00002000 -#define CERR_HINT 0x00001000 - int main(int argc, char *argv[]) { if(argc < 4) { @@ -52,8 +48,7 @@ int main(int argc, char *argv[]) if(*p == '\\') *p = '/'; } - char first5[6] = "CERR_"; - char* pfirst5 = first5; + const char* first5 = "CERR_"; KMCMP_COMPILER_RESULT result; KMCMP_COMPILER_OPTIONS options; @@ -65,7 +60,7 @@ int main(int argc, char *argv[]) if(kmcmp_CompileKeyboard(kmn_file, options, msgproc, loadfileProc, nullptr, result)) { char* testname = strrchr( (char*) kmn_file, '/') + 1; - if(strncmp(testname, pfirst5, 5) == 0){ + if(strncmp(testname, first5, strlen(first5)) == 0){ return __LINE__; // exit code: CERR_ in Name + no Error found } @@ -85,10 +80,11 @@ int main(int argc, char *argv[]) fseek(fp2, 0, SEEK_END); auto sz2 = ftell(fp2); fseek(fp2, 0, SEEK_SET); - if (result.kmxSize != sz2) return __LINE__; // exit code: size of kmx-file in build differs from size of kmx-file in source folder + if (result.kmxSize != (size_t)sz2) return __LINE__; // exit code: size of kmx-file in build differs from size of kmx-file in source folder char* buf2 = new char[result.kmxSize]; - fread(buf2, 1, result.kmxSize, fp2); + auto sz3 = fread(buf2, 1, result.kmxSize, fp2); + if (result.kmxSize != sz3) return __LINE__; // exit code: when not able to read the build into the buffer return memcmp(result.kmx, buf2, result.kmxSize) ? __LINE__ : 0; // exit code: when contents of kmx-file in build differs from contents of kmx-file in source folder // success: when contents of kmx-file in build and source folder are the same } @@ -97,12 +93,12 @@ int main(int argc, char *argv[]) char* testname = strrchr( (char*) kmn_file, '/') + 1; // Does testname contain CERR_ && contains '_' on pos 9 ? -> Get ErrorValue - if ((strncmp(testname, pfirst5, 5) == 0) && (testname[9] == '_')) { + if ((strncmp(testname, first5, strlen(first5)) == 0) && (testname[9] == '_')) { char* ErrNr = strchr(testname, '_') +1 ; std::istringstream(ErrNr) >> std::hex >> error_val; // check if error_val is in Array of Errors; if it is found return 0 (it's not an error) - for (int i = 0; i < error_vec.size() ; i++) { + for (size_t i = 0; i < error_vec.size() ; i++) { if (error_vec[i] == error_val) { return 0; // success: CERR_ in Name + Error (specified in CERR_Name) IS found } @@ -156,7 +152,7 @@ bool isDesktopKeyboard(FILE* fp) { PCOMP_STORE s = pfs; - for(int i = 0; i < fk.cxStoreArray; i++, s++) { + for(KMX_DWORD i = 0; i < fk.cxStoreArray; i++, s++) { if(s->dwSystemID != TSS_TARGETS) { continue; } diff --git a/developer/src/kmcmplib/tests/meson.build b/developer/src/kmcmplib/tests/meson.build index 1ae32590e2e..65915d1e9d9 100644 --- a/developer/src/kmcmplib/tests/meson.build +++ b/developer/src/kmcmplib/tests/meson.build @@ -20,7 +20,7 @@ kmcompxtest = executable('kmcompxtest', ['kmcompxtest.cpp','util_filesystem.cpp' include_directories: inc, name_suffix: name_suffix, link_args: links + tests_links, - objects: lib.extract_all_objects(), + objects: lib.extract_all_objects(recursive: false), dependencies: icuuc_dep, ) @@ -129,7 +129,7 @@ if get_option('full_test') endif -common_test_files = [ meson.source_root() / '../../../common/include/test_color.cpp' ] +common_test_files = [ meson.global_source_root() / '../../../common/include/test_color.cpp' ] # Test the API endpoints @@ -138,7 +138,7 @@ apitest = executable('api-test', ['api-test.cpp','util_filesystem.cpp','util_cal include_directories: inc, name_suffix: name_suffix, link_args: links + tests_links, - objects: lib.extract_all_objects(), + objects: lib.extract_all_objects(recursive: false), dependencies: icuuc_dep ) @@ -149,8 +149,32 @@ usetapitest = executable('uset-api-test', 'uset-api-test.cpp', common_test_files include_directories: inc, name_suffix: name_suffix, link_args: links + tests_links, - objects: lib.extract_all_objects(), + objects: lib.extract_all_objects(recursive: false), dependencies: icuuc_dep, ) test('uset-api-test', usetapitest) + +# Google Test + +gtestcompilertest = executable('gtest-compiler-test', 'gtest-compiler-test.cpp', + cpp_args: defns + flags, + include_directories: inc, + name_suffix: name_suffix, + link_args: links + tests_links, + objects: lib.extract_all_objects(), + dependencies: [ icuuc_dep, gtest_dep, gmock_dep ], + ) + +test('gtest-compiler-test', gtestcompilertest) + +gtest_km_u16_test = executable('gtest-km_u16-test', 'gtest-km_u16-test.cpp', + cpp_args: defns + flags, + include_directories: inc, + name_suffix: name_suffix, + link_args: links + tests_links, + objects: lib.extract_all_objects(), + dependencies: [ icuuc_dep, gtest_dep, gmock_dep ], + ) + +test('gtest-km_u16-test', gtest_km_u16_test) diff --git a/developer/src/kmcmplib/tests/util_callbacks.cpp b/developer/src/kmcmplib/tests/util_callbacks.cpp index e610a54c09e..d512463a0b4 100644 --- a/developer/src/kmcmplib/tests/util_callbacks.cpp +++ b/developer/src/kmcmplib/tests/util_callbacks.cpp @@ -7,53 +7,57 @@ std::vector error_vec; -int msgproc(int line, uint32_t dwMsgCode, const char* szText, void* context) { - error_vec.push_back(dwMsgCode); +void msgproc(const KMCMP_COMPILER_RESULT_MESSAGE &message, void* context) { + error_vec.push_back(message.errorCode); const char*t = "unknown"; - switch(dwMsgCode & 0xF000) { - case CERR_HINT: t=" hint"; break; - case CERR_WARNING: t="warning"; break; - case CERR_ERROR: t=" error"; break; - case CERR_FATAL: t=" fatal"; break; + switch(message.errorCode & MESSAGE_SEVERITY_MASK) { + case CompilerErrorSeverity::Hint: t=" SevHint"; break; + case CompilerErrorSeverity::Warn: t=" SevWarn"; break; + case CompilerErrorSeverity::Error: t="SevError"; break; + case CompilerErrorSeverity::Fatal: t="SevFatal"; break; + case CompilerErrorSeverity::Info: t=" SevInfo"; break; } - printf("line %d %s %04.4x: %s\n", line, t, (unsigned int)dwMsgCode, szText); - return 1; + printf("[CompilerMessage] %s(%d): %s | 0x%3.3x (", + message.filename.c_str(), + message.lineNumber, + t, + (unsigned int)message.errorCode & MESSAGE_BASEERROR_MASK + ); + for(auto it = message.parameters.begin(); it != message.parameters.end(); it++) { + printf("%s'%s'", it == message.parameters.begin() ? "" : ", ", it->c_str()); + } + printf(")\n"); } -bool loadfileProc(const char* filename, const char* baseFilename, void* data, int* size, void* context) { +const std::vector loadfileProc(const std::string& filename, const std::string& baseFilename) { std::string resolvedFilename = filename; - if(baseFilename && *baseFilename && IsRelativePath(filename)) { + if(baseFilename.length() && IsRelativePath(filename.c_str())) { char* p; - if ((p = strrchr_slash((char*)baseFilename)) != nullptr) { - std::string basePath = std::string(baseFilename, (int)(p - baseFilename + 1)); - resolvedFilename = basePath; + const char *base = baseFilename.c_str(); + if ((p = strrchr_slash((char*)base)) != nullptr) { + resolvedFilename = std::string(baseFilename, 0, (int)(p - base + 1)); resolvedFilename.append(filename); } } + std::vector buf; + FILE* fp = Open_File(resolvedFilename.c_str(), "rb"); if(!fp) { - return false; + return buf; } - if(!data) { - // return size - if(fseek(fp, 0, SEEK_END) != 0) { - fclose(fp); - return false; - } - *size = ftell(fp); - if(*size == -1L) { - fclose(fp); - return false; - } - } else { - // return data - if(fread(data, 1, *size, fp) != *size) { - fclose(fp); - return false; - } + if(fseek(fp, 0, SEEK_END) != 0) { + fclose(fp); + return buf; + } + size_t size = ftell(fp); + + buf.resize(size); + fseek(fp, 0, SEEK_SET); + if(fread(buf.data(), 1, size, fp) != size) { + buf.resize(0); } fclose(fp); - return true; -} \ No newline at end of file + return buf; +} diff --git a/developer/src/kmcmplib/tests/util_callbacks.h b/developer/src/kmcmplib/tests/util_callbacks.h index 345de64b9f4..6c208b9d4db 100644 --- a/developer/src/kmcmplib/tests/util_callbacks.h +++ b/developer/src/kmcmplib/tests/util_callbacks.h @@ -2,7 +2,7 @@ #include -int msgproc(int line, uint32_t dwMsgCode, const char* szText, void* context); -bool loadfileProc(const char* filename, const char* baseFilename, void* data, int* size, void* context); +void msgproc(const KMCMP_COMPILER_RESULT_MESSAGE &message, void* context); +const std::vector loadfileProc(const std::string& filename, const std::string& baseFilename); extern std::vector error_vec; \ No newline at end of file diff --git a/developer/src/kmcmplib/tests/util_filesystem.cpp b/developer/src/kmcmplib/tests/util_filesystem.cpp index 7f9edd6b367..0873ff13288 100644 --- a/developer/src/kmcmplib/tests/util_filesystem.cpp +++ b/developer/src/kmcmplib/tests/util_filesystem.cpp @@ -117,23 +117,10 @@ FILE* Open_File(const KMX_CHAR* Filename, const KMX_CHAR* mode) { #endif }; -FILE* Open_File(const KMX_WCHART* Filename, const KMX_WCHART* mode) { -#ifdef _MSC_VER - std::wstring cpath = Filename; //, cmode = mode; - std::replace(cpath.begin(), cpath.end(), '/', '\\'); - return _wfsopen(cpath.c_str(), mode, _SH_DENYWR); -#else - std::string cpath, cmode; - cpath = string_from_wstring(Filename); - cmode = string_from_wstring(mode); - return fopen_wrapper(cpath.c_str(), cmode.c_str()); -#endif -}; - FILE* Open_File(const KMX_WCHAR* Filename, const KMX_WCHAR* mode) { #ifdef _MSC_VER - std::wstring cpath = convert_pchar16T_To_wstr((KMX_WCHAR*) Filename); - std::wstring cmode = convert_pchar16T_To_wstr((KMX_WCHAR*) mode); + std::wstring cpath = wstring_from_u16string(Filename); + std::wstring cmode = wstring_from_u16string(mode); std::replace(cpath.begin(), cpath.end(), '/', '\\'); return _wfsopen(cpath.c_str(), cmode.c_str(), _SH_DENYWR); #else diff --git a/developer/src/kmcmplib/tests/util_filesystem.h b/developer/src/kmcmplib/tests/util_filesystem.h index 4c94dae6bf4..c35967b05e8 100644 --- a/developer/src/kmcmplib/tests/util_filesystem.h +++ b/developer/src/kmcmplib/tests/util_filesystem.h @@ -1,12 +1,11 @@ #pragma once #include -#include "../src/kmx_u16.h" +#include // Opens files on windows and non-windows platforms. Datatypes for Filename and mode must be the same. // returns FILE* if file could be opened; FILE needs to be closed in calling function FILE* Open_File(const KMX_CHAR* Filename, const KMX_CHAR* mode); -FILE* Open_File(const KMX_WCHART* Filename, const KMX_WCHART* mode); FILE* Open_File(const KMX_WCHAR* Filename, const KMX_WCHAR* mode); KMX_BOOL kmcmp_FileExists(const KMX_CHAR *filename); KMX_BOOL kmcmp_FileExists(const KMX_WCHAR *filename); diff --git a/developer/src/kmcmplib/wasm.build.linux.in b/developer/src/kmcmplib/wasm.build.linux.in index c5e61a9bda9..27cb7ba68fe 100644 --- a/developer/src/kmcmplib/wasm.build.linux.in +++ b/developer/src/kmcmplib/wasm.build.linux.in @@ -1,4 +1,4 @@ [binaries] -c = ['$EMSCRIPTEN_BASE/emcc.py'] -cpp = ['$EMSCRIPTEN_BASE/em++.py'] -ar = ['$EMSCRIPTEN_BASE/emar.py'] +c = ['$EMSCRIPTEN_BASE/emcc'] +cpp = ['$EMSCRIPTEN_BASE/em++'] +ar = ['$EMSCRIPTEN_BASE/emar'] diff --git a/developer/src/kmcmplib/wasm.build.mac.in b/developer/src/kmcmplib/wasm.build.mac.in index c5e61a9bda9..27cb7ba68fe 100644 --- a/developer/src/kmcmplib/wasm.build.mac.in +++ b/developer/src/kmcmplib/wasm.build.mac.in @@ -1,4 +1,4 @@ [binaries] -c = ['$EMSCRIPTEN_BASE/emcc.py'] -cpp = ['$EMSCRIPTEN_BASE/em++.py'] -ar = ['$EMSCRIPTEN_BASE/emar.py'] +c = ['$EMSCRIPTEN_BASE/emcc'] +cpp = ['$EMSCRIPTEN_BASE/em++'] +ar = ['$EMSCRIPTEN_BASE/emar'] diff --git a/developer/src/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas b/developer/src/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas index fb0e22541cd..f5384903d90 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas @@ -80,12 +80,13 @@ implementation BCP47Tag, Keyman.Developer.System.ImportKeyboardDLL, Keyman.Developer.System.GenerateKeyboardIcon, - Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter, + Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter, Keyman.System.KeyboardUtils, KeymanVersion, KeyboardParser, kmxfileconsts, RegistryKeys, + TempFileManager, utilfiletypes; { TImportWindowsKeyboard } @@ -206,6 +207,8 @@ procedure TImportWindowsKeyboard.SetVersion(const Value: string); function TImportWindowsKeyboard.Execute: Boolean; var FTemplate: TKeyboardProjectTemplate; + DestinationOSKTempFile: TTempFile; + DestinationOSKFilename: string; begin if not ForceDirectories(FDestinationPath) then Exit(Fail('The destination path '+FDestinationPath+' could not be created.')); @@ -216,6 +219,7 @@ function TImportWindowsKeyboard.Execute: Boolean; // Create a new folder in destination path + DestinationOSKTempFile := nil; FTemplate := TKeyboardProjectTemplate.Create(FDestinationPath, Format(FKeyboardIDTemplate, [FBaseKeyboardID]), FTargets); try // @@ -251,27 +255,43 @@ function TImportWindowsKeyboard.Execute: Boolean; FProjectFilename := FTemplate.ProjectFilename; + if FTemplate.OSKFilename = '' then + begin + // We need an OSK file in order to generate the touch layout. OSKFilename + // will be empty if the user selects a touch-only target. So we'll use a + // temp file in this scenario. + DestinationOSKTempFile := TTempFileManager.Get(Ext_VisualKeyboardSource); + DestinationOSKFilename := DestinationOSKTempFile.Name; + end + else + DestinationOSKFilename := FTemplate.OSKFilename; + + // Run importkeyboard into destination file; this replaces the keyboard template // file that has been generated - if not ImportKeyboard(FTemplate.KeyboardFilename, FTemplate.OSKFilename) then + if not ImportKeyboard(FTemplate.KeyboardFilename, DestinationOSKFilename) then Exit(Fail('Unable to run importkeyboard on '+FSourceKLID)); // Replace .ico with a new one based on the language id // TODO: this goes in the keyboard project template generation I think - if not GenerateIcon(FTemplate.IconFilename) then - Exit(Fail('Unable to generate an icon for '+FTemplate.KeyboardFilename)); + if FTemplate.IconFilename <> '' then + if not GenerateIcon(FTemplate.IconFilename) then + Exit(Fail('Unable to generate an icon for '+FTemplate.KeyboardFilename)); // Load the source .kmn and add bitmap, copyright, visualkeyboard, touch layout fields + // note, we use FTemplate.OSKFilename here deliberately, because we only + // generate OSK-related fields if they are in use InjectSystemStores(FTemplate.KeyboardFilename, FTemplate.OSKFilename, FTemplate.IconFilename, FTemplate.TouchLayoutFilename); if FTemplate.TouchLayoutFilename <> '' then begin // Take the generated OSK and convert it into a default touch layout - if not ConvertOSKToTouchLayout(FTemplate.OSKFilename, FTemplate.TouchLayoutFilename) then + if not ConvertOSKToTouchLayout(DestinationOSKFilename, FTemplate.TouchLayoutFilename) then Exit(Fail('Unable to create a default touch layout based on the OSK for '+FTemplate.KeyboardFilename)); end; finally FreeAndNil(FTemplate); + FreeAndNil(DestinationOSKTempFile); end; Result := True; @@ -302,8 +322,10 @@ procedure TImportWindowsKeyboard.InjectSystemStores(const KeyboardFilename, OSKF try kp.FileName := KeyboardFilename; kp.LoadFromFile(KeyboardFilename); - kp.Features.Add(kfIcon); - kp.Features.Add(kfOSK); + if IconFilename <> '' then + kp.Features.Add(kfIcon); + if OSKFilename <> '' then + kp.Features.Add(kfOSK); if (TouchKeymanTargets+[ktAny]) * FTargets <> [] then kp.Features.Add(kfTouchLayout); // TODO: Are these file settings actually doing anything? Or is it controlled @@ -311,8 +333,10 @@ procedure TImportWindowsKeyboard.InjectSystemStores(const KeyboardFilename, OSKF // if we change filenames for any reason in the future kp.SetSystemStoreValue(ssName, Format(FNameTemplate, [FBaseName])); kp.SetSystemStoreValue(ssVersion, SKeymanKeyboardVersion); - kp.SetSystemStoreValue(ssVisualKeyboard, ExtractFileName(OSKFilename)); - kp.SetSystemStoreValue(ssBitmap, ExtractFilename(IconFilename)); + if OSKFilename <> '' then + kp.SetSystemStoreValue(ssVisualKeyboard, ExtractFileName(OSKFilename)); + if IconFilename <> '' then + kp.SetSystemStoreValue(ssBitmap, ExtractFilename(IconFilename)); kp.SetSystemStoreValue(ssLayoutFile, ExtractFileName(TouchLayoutFilename)); kp.SetSystemStoreValue(ssTargets, KeymanTargetsToString(FTargets)); kp.SetSystemStoreValue(ssCopyright, FCopyright); @@ -383,11 +407,11 @@ function TImportWindowsKeyboard.GenerateIcon( function TImportWindowsKeyboard.ConvertOSKToTouchLayout(const OSKFilename, TouchLayoutFilename: string): Boolean; var - converter: TTouchLayoutToVisualKeyboardConverter; + converter: TVisualKeyboardToTouchLayoutConverter; FNewLayout: string; ss: TStringStream; begin - converter := TTouchLayoutToVisualKeyboardConverter.Create(OSKFilename); + converter := TVisualKeyboardToTouchLayoutConverter.Create(OSKFilename); try if converter.Execute(FNewLayout) then begin diff --git a/developer/src/kmconvert/Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter.pas b/developer/src/kmconvert/Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas similarity index 94% rename from developer/src/kmconvert/Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter.pas rename to developer/src/kmconvert/Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas index 0f9a2861fe7..5f19d98a2ae 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas @@ -1,4 +1,4 @@ -unit Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter; +unit Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter; interface @@ -8,7 +8,7 @@ interface VisualKeyboard; type - TTouchLayoutToVisualKeyboardConverter = class + TVisualKeyboardToTouchLayoutConverter = class private FOwnsVisualKeyboard: Boolean; FVisualKeyboard: TVisualKeyboard; @@ -34,7 +34,7 @@ implementation Unicode, VKeys; -constructor TTouchLayoutToVisualKeyboardConverter.Create( +constructor TVisualKeyboardToTouchLayoutConverter.Create( const AFileName: string); begin inherited Create; @@ -43,7 +43,7 @@ constructor TTouchLayoutToVisualKeyboardConverter.Create( FVisualKeyboard.LoadFromFile(AFileName); end; -constructor TTouchLayoutToVisualKeyboardConverter.Create( +constructor TVisualKeyboardToTouchLayoutConverter.Create( AVisualKeyboard: TVisualKeyboard); begin inherited Create; @@ -51,7 +51,7 @@ constructor TTouchLayoutToVisualKeyboardConverter.Create( FVisualKeyboard := AVisualKeyboard; end; -constructor TTouchLayoutToVisualKeyboardConverter.Create( +constructor TVisualKeyboardToTouchLayoutConverter.Create( const AStream: TStream); begin inherited Create; @@ -60,21 +60,21 @@ constructor TTouchLayoutToVisualKeyboardConverter.Create( FVisualKeyboard.LoadFromStream(AStream); end; -destructor TTouchLayoutToVisualKeyboardConverter.Destroy; +destructor TVisualKeyboardToTouchLayoutConverter.Destroy; begin if FOwnsVisualKeyboard then FreeAndNil(FVisualKeyboard); inherited Destroy; end; -function TTouchLayoutToVisualKeyboardConverter.Execute( +function TVisualKeyboardToTouchLayoutConverter.Execute( var KeymanTouchLayout: string): Boolean; begin KeymanTouchLayout := ImportFromKVK; Result := KeymanTouchLayout <> ''; end; -function TTouchLayoutToVisualKeyboardConverter.ImportFromKVK: string; +function TVisualKeyboardToTouchLayoutConverter.ImportFromKVK: string; type TKeyData = record Data: TOnScreenKeyboardKeyData; @@ -296,7 +296,7 @@ TLayoutShiftState = record Shift: Word; id: ansistring end; * each layer on each given platform. Will rewrite the K_LCONTROL key * or remove it if there are no extended layers. *} -function TTouchLayoutToVisualKeyboardConverter.SetupModifierKeysForImportedLayout(TouchLayout: string): string; +function TVisualKeyboardToTouchLayoutConverter.SetupModifierKeysForImportedLayout(TouchLayout: string): string; procedure SetupModifierKeysForPlatform(p: TTouchLayoutPlatform); var l, l2: TTouchLayoutLayer; diff --git a/developer/src/kmconvert/kmconvert.dpr b/developer/src/kmconvert/kmconvert.dpr index ee650baf2fa..9430142e6f0 100644 --- a/developer/src/kmconvert/kmconvert.dpr +++ b/developer/src/kmconvert/kmconvert.dpr @@ -68,7 +68,7 @@ uses Keyman.Developer.System.KMConvertParameters in 'Keyman.Developer.System.KMConvertParameters.pas', Keyman.Developer.System.ImportKeyboardDLL in 'Keyman.Developer.System.ImportKeyboardDLL.pas', ScanCodeMap in '..\..\..\common\windows\delphi\general\ScanCodeMap.pas', - Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter in 'Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter.pas', + Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter in 'Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas', OnScreenKeyboardData in '..\..\..\common\windows\delphi\visualkeyboard\OnScreenKeyboardData.pas', TouchLayout in '..\TIKE\oskbuilder\TouchLayout.pas', TouchLayoutDefinitions in '..\TIKE\oskbuilder\TouchLayoutDefinitions.pas', diff --git a/developer/src/kmconvert/kmconvert.dproj b/developer/src/kmconvert/kmconvert.dproj index 0fed3d1be3f..5a99fcb0f82 100644 --- a/developer/src/kmconvert/kmconvert.dproj +++ b/developer/src/kmconvert/kmconvert.dproj @@ -174,7 +174,7 @@ - + diff --git a/developer/src/packages.inc.sh b/developer/src/packages.inc.sh index 28f2513f974..0726209973a 100644 --- a/developer/src/packages.inc.sh +++ b/developer/src/packages.inc.sh @@ -6,7 +6,6 @@ readonly PACKAGES=( common/web/keyman-version common/web/types - common/models/types core/include/ldml developer/src/common/web/utils developer/src/kmc-analyze diff --git a/developer/src/samples/imsample/imdllref.rtf b/developer/src/samples/imsample/imdllref.rtf index 79cd6f01789..178021b5e5a 100644 --- a/developer/src/samples/imsample/imdllref.rtf +++ b/developer/src/samples/imsample/imdllref.rtf @@ -1,274 +1,316 @@ -{\rtf1\ansi\ansicpg1252\uc1 \deff0\deflang1033\deflangfe1033{\fonttbl{\f0\froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f1\fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;} -{\f2\fmodern\fcharset0\fprq1{\*\panose 02070309020205020404}Courier New;}{\f3\froman\fcharset2\fprq2{\*\panose 05050102010706020507}Symbol;}{\f74\froman\fcharset238\fprq2 Times New Roman CE;}{\f75\froman\fcharset204\fprq2 Times New Roman Cyr;} -{\f77\froman\fcharset161\fprq2 Times New Roman Greek;}{\f78\froman\fcharset162\fprq2 Times New Roman Tur;}{\f79\froman\fcharset186\fprq2 Times New Roman Baltic;}{\f80\fswiss\fcharset238\fprq2 Arial CE;}{\f81\fswiss\fcharset204\fprq2 Arial Cyr;} -{\f83\fswiss\fcharset161\fprq2 Arial Greek;}{\f84\fswiss\fcharset162\fprq2 Arial Tur;}{\f85\fswiss\fcharset186\fprq2 Arial Baltic;}{\f86\fmodern\fcharset238\fprq1 Courier New CE;}{\f87\fmodern\fcharset204\fprq1 Courier New Cyr;} -{\f89\fmodern\fcharset161\fprq1 Courier New Greek;}{\f90\fmodern\fcharset162\fprq1 Courier New Tur;}{\f91\fmodern\fcharset186\fprq1 Courier New Baltic;}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0; -\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128; -\red192\green192\blue192;}{\stylesheet{\widctlpar\adjustright \fs20\lang3081\cgrid \snext0 Normal;}{\s1\sb240\sa60\keepn\widctlpar\adjustright \b\f1\fs28\lang3081\kerning28\cgrid \sbasedon0 \snext0 heading 1;}{\s2\sb240\sa60\keepn\widctlpar\adjustright -\b\i\f1\lang3081\cgrid \sbasedon0 \snext0 heading 2;}{\s3\sb120\keepn\widctlpar\adjustright \b\f1\fs20\lang3081\cgrid \sbasedon0 \snext0 heading 3;}{\*\cs10 \additive Default Paragraph Font;}{\s15\fi-425\li709\widctlpar\adjustright -\f2\fs16\lang3081\cgrid \sbasedon0 \snext15 Code;}{\s16\widctlpar\adjustright \b\f1\fs16\lang3081\cgrid \sbasedon0 \snext16 TableHeader;}{\*\cs17 \additive \f2\fs16 \sbasedon10 PCode;}{\s18\widctlpar\adjustright \f1\fs16\lang3081\cgrid -\sbasedon0 \snext18 TCell;}{\s19\qc\sb240\sa60\widctlpar\outlinelevel0\adjustright \b\f1\fs32\lang3081\kerning28\cgrid \sbasedon0 \snext19 Title;}{\s20\fi-283\li567\widctlpar\adjustright \fs20\lang3081\cgrid \sbasedon0 \snext20 Arial;}{\*\cs21 \additive -\f1\fs18 \sbasedon10 filename;}{\s22\widctlpar\adjustright \fs20\lang3081\cgrid \sbasedon0 \snext22 pc;}{\s23\widctlpar\tqc\tx4153\tqr\tx8306\adjustright \fs20\lang3081\cgrid \sbasedon0 \snext23 header;}{\s24\widctlpar\tqc\tx4153\tqr\tx8306\adjustright -\fs20\lang3081\cgrid \sbasedon0 \snext24 footer;}{\*\cs25 \additive \sbasedon10 page number;}}{\*\listtable{\list\listtemplateid-963727900\listsimple{\listlevel\levelnfc0\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext -\'02\'00.;}{\levelnumbers\'01;}\fi-360\li1492\jclisttab\tx1492 }{\listname ;}\listid-132}{\list\listtemplateid1736061876\listsimple{\listlevel\levelnfc0\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'00.;}{\levelnumbers\'01;} -\fi-360\li1209\jclisttab\tx1209 }{\listname ;}\listid-131}{\list\listtemplateid-1759738300\listsimple{\listlevel\levelnfc0\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'00.;}{\levelnumbers\'01;}\fi-360\li926\jclisttab\tx926 -}{\listname ;}\listid-130}{\list\listtemplateid-1902884938\listsimple{\listlevel\levelnfc0\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'00.;}{\levelnumbers\'01;}\fi-360\li643\jclisttab\tx643 }{\listname ;}\listid-129} -{\list\listtemplateid1956535276\listsimple{\listlevel\levelnfc23\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li1492\jclisttab\tx1492 }{\listname ;}\listid-128} -{\list\listtemplateid-392499776\listsimple{\listlevel\levelnfc23\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li1209\jclisttab\tx1209 }{\listname ;}\listid-127} -{\list\listtemplateid-967173064\listsimple{\listlevel\levelnfc23\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li926\jclisttab\tx926 }{\listname ;}\listid-126} -{\list\listtemplateid-1624359846\listsimple{\listlevel\levelnfc23\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li643\jclisttab\tx643 }{\listname ;}\listid-125} -{\list\listtemplateid-323179536\listsimple{\listlevel\levelnfc0\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'00.;}{\levelnumbers\'01;}\fi-360\li360\jclisttab\tx360 }{\listname ;}\listid-120}{\list\listtemplateid1800809868 -\listsimple{\listlevel\levelnfc23\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li360\jclisttab\tx360 }{\listname ;}\listid-119}{\list\listtemplateid201916431\listsimple{\listlevel -\levelnfc0\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'00.;}{\levelnumbers\'01;}\fi-360\li360\jclisttab\tx360 }{\listname ;}\listid1081215738}{\list\listtemplateid201916431\listsimple{\listlevel\levelnfc0\leveljc0 -\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'00.;}{\levelnumbers\'01;}\fi-360\li360\jclisttab\tx360 }{\listname ;}\listid1620605434}}{\*\listoverridetable{\listoverride\listid-119\listoverridecount0\ls1}{\listoverride\listid-125 -\listoverridecount0\ls2}{\listoverride\listid-126\listoverridecount0\ls3}{\listoverride\listid-127\listoverridecount0\ls4}{\listoverride\listid-128\listoverridecount0\ls5}{\listoverride\listid-120\listoverridecount0\ls6}{\listoverride\listid-129 -\listoverridecount0\ls7}{\listoverride\listid-130\listoverridecount0\ls8}{\listoverride\listid-131\listoverridecount0\ls9}{\listoverride\listid-132\listoverridecount0\ls10}{\listoverride\listid1620605434\listoverridecount0\ls11} -{\listoverride\listid1081215738\listoverridecount0\ls12}}{\info{\title DLL Interface for Keyman}{\author Marc Durdin}{\operator Marc Durdin}{\creatim\yr2001\mo6\dy5\hr11\min48}{\revtim\yr2001\mo6\dy5\hr12\min28}{\version3}{\edmins1}{\nofpages6} -{\nofwords1968}{\nofchars11219}{\*\company Tavultesoft}{\nofcharsws13777}{\vern89}}\paperw11906\paperh16838 \widowctrl\ftnbj\aenddoc\hyphcaps0\formshade\viewkind1\viewscale117\viewzk2\pgbrdrhead\pgbrdrfoot \fet0\sectd -\linex0\headery709\footery709\colsx709\endnhere\sectdefaultcl {\header \pard\plain \s23\widctlpar\brdrb\brdrs\brdrw10\brsp20 \tqc\tx4153\tqr\tx8306\adjustright \fs20\lang3081\cgrid {\b\f1 DLL Interface for Keyman\tab \tab 5 June 2001 -\par }}{\footer \pard\plain \s24\widctlpar\brdrt\brdrs\brdrw10\brsp20 \tqc\tx4153\tqr\tx8306\adjustright \fs20\lang3081\cgrid {\b\f1 Page }{\field{\*\fldinst {\cs25\b\f1 PAGE }}{\fldrslt {\cs25\b\f1\lang1024 5}}}{\cs25\b\f1 of }{\field{\*\fldinst { -\cs25\b\f1 NUMPAGES }}{\fldrslt {\cs25\b\f1\lang1024 1}}}{\cs25\b\f1 \tab \tab \'a9 2001 Tavultesoft}{\b\f1 -\par }}{\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang{\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang{\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang{\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang{\pntxta )}} -{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}{\*\pnseclvl8 -\pnlcltr\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}\pard\plain \s19\qc\sb240\sa60\widctlpar\outlinelevel0\adjustright \b\f1\fs32\lang3081\kerning28\cgrid { -DLL Interface for Keyman -\par }\pard\plain \s1\sb240\sa60\keepn\widctlpar\outlinelevel0\adjustright \b\f1\fs28\lang3081\kerning28\cgrid {Introduction -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -Keyman's multiple group processing is powerful, but sometimes you need to be able to do something a bit more complex, such as a dictionary lookup. Keyman's DLL interface let you do this. You can call a function in a DLL in the same way as you call anoth -er group. The function can read the context, deadkeys and the current keystroke, and output characters, deadkeys, virtual keys, beeps and other items. -\par -\par The DLL interface also allows you to create a popup Input Method Composition (IMC) window. This window -allows the user to select visually the characters they are wishing to input. The window can be set to be visible when the keyboard is active, or after an appropriate key sequence. When the window is visible, it can be set to capture all keyboard input, -or be passive. -\par }\pard\plain \s1\sb240\sa60\keepn\widctlpar\outlinelevel0\adjustright \b\f1\fs28\lang3081\kerning28\cgrid {File locations -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid {The DLL should be placed in any of the following locations: -\par -\par {\pntext\pard\plain\fs20\cgrid \hich\af0\dbch\af0\loch\f0 1.\tab}}\pard \fi-360\li1080\widctlpar\jclisttab\tx1080{\*\pn \pnlvlbody\ilvl0\ls12\pnrnot0\pndec\pnstart1\pnindent360\pnhang{\pntxta .}}\ls12\adjustright { -The same directory as the .kmx file (e.g. use a package to install it) -\par {\pntext\pard\plain\fs20\cgrid \hich\af0\dbch\af0\loch\f0 2.\tab}}\pard \fi-360\li1080\widctlpar\jclisttab\tx1080{\*\pn \pnlvlbody\ilvl0\ls12\pnrnot0\pndec\pnstart1\pnindent360\pnhang{\pntxta .}}\ls12\adjustright { -The Keyman program directory (same place as keyman32.dll) -\par {\pntext\pard\plain\fs20\cgrid \hich\af0\dbch\af0\loch\f0 3.\tab}}\pard \fi-360\li1080\widctlpar\jclisttab\tx1080{\*\pn \pnlvlbody\ilvl0\ls12\pnrnot0\pndec\pnstart1\pnindent360\pnhang{\pntxta .}}\ls12\adjustright {Anywhere on the path (suc -h as the Windows directory) -\par }\pard \widctlpar\adjustright { -\par The best option is (1), as you can then include the DLL in a Keyman package for easy installation and uninstallation. -\par }\pard\plain \s1\sb240\sa60\keepn\widctlpar\outlinelevel0\adjustright \b\f1\fs28\lang3081\kerning28\cgrid {General usage information -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid {DLL functions used in place of groups are called }{\i DLL group functions}{. -\par -\par All strings, apart from keyboard names, are passed as WCHAR, regardless of whether the active window is a Unicode window or not. ANSI characters are represented as 16-bit WCHAR, with high bits zeroed out. -\par -\par The DLL will be loaded for each process in which the keyboard is activated. Remember that the DLL will }{\b\i not}{ - share memory between these processes by default, so if you have large memory requirements, you should use memory mapped files or possibly SHARDATA segments to minimize the memory consumption. -\par -\par DLL group functi -ons are called in a fairly time-critical environment. It is important that you minimise the processing time in these functions. It is essential that you avoid any window focus or activation -- message boxes are definitely out of the question. For debug -ging purposes, there is a Keyman32.dll function exported for writing to the logfile (see the section titled }{\i Keyman32 imports}{). -\par -\par DLLs can handle multiple keyboards at once. The keyboards are identified by a name which is the filename of the keyboard, minus path and extension. For example, given }{\f1 c:\\keyman\\imsample\\imsample.kmx}{, the keyboard name is }{\cs17\f2\fs16 -imsample}{. These are the same names that Keyman uses internally, for example in the registry and directory names. -\par }\pard\plain \s1\sb240\sa60\keepn\widctlpar\outlinelevel0\adjustright \b\f1\fs28\lang3081\kerning28\cgrid {Registry settings -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par Par -ameters for the DLL can be stored in two locations in the registry. They should always be stored under HKEY_CURRENT_USER, as the user may not have permission to change machine-wide settings, and the settings should not affect other users. The following -locations are recommended: -\par -\par }\pard\plain \s20\fi-283\li567\widctlpar\adjustright \fs20\lang3081\cgrid {\f1\fs18 HKEY_CURRENT_USER\\Software\\Tavultesoft\\Keyman\\5.0\\ -\par HKEY_CURRENT_USER\\Software\\Tavultesoft\\Keyman\\5.0\\Installed Keyboards\\ -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par The first key should be used for settings that pertain to any keyboard associated with t -he DLL. The second key should be used for any keyboard-specific settings. Values stored under the second key should be prefixed with the name of the dll, so that they will not conflict with Keyman or other dll values. -\par }\pard\plain \s1\sb240\sa60\keepn\widctlpar\outlinelevel0\adjustright \b\f1\fs28\lang3081\kerning28\cgrid {The .kmn interface -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid {Inside a .kmn file, you define the DLL group function interface as follows: -\par -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {store(DLLFunction) "myfile.dll:KeyEvent" -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par You can use this anywhere where you would place the }{\cs17\f2\fs16 use}{ statement (except in the }{\cs17\f2\fs16 begin}{ statement), with the }{\cs17\f2\fs16 call}{ statement. For example, -\par -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {+ 'a' > call(DLLFunction) -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par A single .kmn file can reference multiple DLL group functions, in a single or multiple DLLs. -\par }\pard\plain \s1\sb240\sa60\keepn\widctlpar\outlinelevel0\adjustright \b\f1\fs28\lang3081\kerning28\cgrid {DLL exports -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid {The DLL is called from Keyman with LoadLibrary. All functions are then found with GetProcAddress. You must ensure that the function expor -ts do not have ordinals encoded in the names. The best way to accomplish this in C/C++ is to use a .def file. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {DLL group function exports -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid {The function declaration for the DLL group function is: -\par -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI }{\i KeyEvent}{(HWND hwndFocus, WORD KeyStroke, WCHAR KeyChar, DWORD ShiftFlags); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par Note that }{\cs17\f2\fs16 KeyEvent}{ is a placeholder for any name that you wish to use. You can have multiple exports for Keyman use in a single DLL. -\par }\pard\plain \s3\sb120\keepn\widctlpar\outlinelevel2\adjustright \b\f1\fs20\lang3081\cgrid {Parameters -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par }\trowd \trgaph108\trbrdrt\brdrs\brdrw10 \trbrdrl\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrh\brdrs\brdrw10 \trbrdrv\brdrs\brdrw10 \clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr -\brdrs\brdrw10 \cltxlrtb \cellx1418\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx6238\pard\plain \s16\widctlpar\intbl\adjustright \b\f1\fs16\lang3081\cgrid {Parameter\cell Description -\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\trowd \trgaph108\trbrdrt\brdrs\brdrw10 \trbrdrl\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrh\brdrs\brdrw10 \trbrdrv\brdrs\brdrw10 \clvertalt\clbrdrt -\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx1418\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx6238\pard -\widctlpar\intbl\adjustright {\cs17\f2\fs16 hwndFocus\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {The currently focused window. You will probably never have a need to use this. \cell }\pard\plain -\widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 KeyStroke\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {The virtual key code for the current key.\cell }\pard\plain -\widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 KeyChar\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid { -The character code for the current key (based on US English layout). This will be 0 if KeyStroke does not generate a character (e.g. function keys).\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\trowd \trgaph108\trbrdrt -\brdrs\brdrw10 \trbrdrl\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrh\brdrs\brdrw10 \trbrdrv\brdrs\brdrw10 \clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx1418 -\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx6238\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 ShiftFlags\cell }\pard\plain \s18\widctlpar\intbl\adjustright -\f1\fs16\lang3081\cgrid {The shift state for the current key. See the table in the Notes section for possible shift states. \cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\pard\plain -\s3\sb120\keepn\widctlpar\outlinelevel2\adjustright \b\f1\fs20\lang3081\cgrid {Notes -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par The following shift states are possible: -\par -\par }\trowd \trgaph108\trbrdrt\brdrs\brdrw10 \trbrdrl\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrh\brdrs\brdrw10 \trbrdrv\brdrs\brdrw10 \clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr -\brdrs\brdrw10 \cltxlrtb \cellx1701\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx2694\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr -\brdrs\brdrw10 \cltxlrtb \cellx5670\pard\plain \s16\widctlpar\intbl\adjustright \b\f1\fs16\lang3081\cgrid {Flag\cell Value\cell Description\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\trowd \trgaph108\trbrdrt\brdrs\brdrw10 -\trbrdrl\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrh\brdrs\brdrw10 \trbrdrv\brdrs\brdrw10 \clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx1701\clvertalt\clbrdrt -\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx2694\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx5670\pard -\widctlpar\intbl\adjustright {\cs17\f2\fs16 LCTRLFLAG\cell 0x0001\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Left Control flag\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\fs16 \row }\pard -\widctlpar\intbl\adjustright {\cs17\f2\fs16 RCTRLFLAG\cell 0x0002\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Right Control flag\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\fs16 \row }\pard -\widctlpar\intbl\adjustright {\cs17\f2\fs16 LALTFLAG\cell 0x0004\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Left Alt flag\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\fs16 \row }\pard -\widctlpar\intbl\adjustright {\cs17\f2\fs16 RALTFLAG\cell 0x0008\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Right Alt flag\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\fs16 \row }\pard -\widctlpar\intbl\adjustright {\cs17\f2\fs16 K_SHIFTFLAG\cell 0x0010\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Shift flag\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\pard -\widctlpar\intbl\adjustright {\cs17\f2\fs16 K_CTRLFLAG\cell 0x0020\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Ctrl flag (unused here; see l/r flags) \cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid { -\row }\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 K_ALTFLAG\cell 0x0040\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Alt flag (unused here; see l/r flags)\cell }\pard\plain \widctlpar\intbl\adjustright -\fs20\lang3081\cgrid {\row }\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 CAPITALFLAG\cell 0x0100\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Caps lock on\cell }\pard\plain \widctlpar\intbl\adjustright -\fs20\lang3081\cgrid {\fs16 \row }\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 NUMLOCKFLAG\cell 0x0400\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Num lock on\cell }\pard\plain \widctlpar\intbl\adjustright -\fs20\lang3081\cgrid {\fs16 \row }\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 SCROLLFLAG\cell 0x1000\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Scroll lock on\cell }\pard\plain \widctlpar\intbl\adjustright -\fs20\lang3081\cgrid {\fs16 \row }\trowd \trgaph108\trbrdrt\brdrs\brdrw10 \trbrdrl\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrh\brdrs\brdrw10 \trbrdrv\brdrs\brdrw10 \clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb -\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx1701\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx2694\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb -\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx5670\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 ISVIRTUALKEY\cell 0x4000\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {It is a Virtual Key Sequence\cell -}\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\fs16 \row }\pard\plain \s1\sb240\sa60\keepn\widctlpar\outlinelevel0\adjustright \b\f1\fs28\lang3081\kerning28\cgrid {Optional DLL exports -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -Keyman recognises a number of other exports, if they are defined in the DLL. None of these are required. These functions will be called when a keyboard that references the DLL is manipulated. They will not be called for keyboards that do not reference -the DLL. -\par -\par The following exports are available: -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KeymanIMInit -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KeymanIMInit(PSTR keyboardname); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par }{\cs17\f2\fs16 KeymanIMInit}{ is called once when the keyboard identified by }{\cs17\f2\fs16 keyboardname}{ is loaded for a given process. It is called for each process in which the keyboard is loaded. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KeymanIMDestroy -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KeymanIMDestroy(PSTR keyboardname); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par This is called once when the keyboard identified by }{\cs17\f2\fs16 keyboardname}{ is unloaded in a given process -. It is called when the process exits normally, or when Keyman refreshes its keyboard list after keyboards are added or removed. If the keyboard is subsequently reloaded, }{\cs17\f2\fs16 KeymanIMInit}{ will be called again. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KeymanIMActivate -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KeymanIMActivate(PSTR keyboardname); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par This function is called whenever the user or a program activates the keyboard. It is never called before KeymanIMInit (unless KeymanIMInit is not exported). This is an appropriate place to switch on permanently-visible IMC windows. }{\cs17\f2\fs16 Ke -ymanIMActivate}{ can also be called when switching processes and the target process has a related keyboard already active. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KeymanIMDeactivate -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KeymanIMDeactivate(PSTR keyboardname); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par This function is called when the user or a program switches off a related keyboard. It is always called before }{\cs17\f2\fs16 KeymanIMActivate}{ - for the next keyboard. It will also be called when the user activates another process, to give the DLL a chance to hide top-most IMC windows. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KeymanIMConfigure -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KeymanIMConfigure(PSTR keyboardname, HWND hwndParent); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par KeymanIMConfigure is called when the user clicks the Configure button in KMShell to configure the DLL-specific functionality for the keyboard. The appropriate behaviour is to display a dialog box, and save the settings in the registry. -\par -\par This function is separate from all the other functions. It can be called when there are no keyboards loaded, or even if Keyman itself is not loaded. You should not attempt to call Keyman32.dll from this function. -\par }\pard\plain \s1\sb240\sa60\keepn\widctlpar\outlinelevel0\adjustright \b\f1\fs28\lang3081\kerning28\cgrid {The imlib.cpp library module -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid {imlib.cpp, included in the development kit, contains a set of useful functions for interfacing to Keyman. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {PrepIM -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL PrepIM(void); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par }{\cs17\f2\fs16 PrepIM}{ initialises the Keyman32 imports. You should not call any of the Keyman imports without calling }{\cs17\f2\fs16 PrepIM}{ first. If }{\cs17\f2\fs16 PrepIM}{ fails, you should exit without doing any processing. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {IMDefWindowProc -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL IMDefWindowProc(HWND hwnd, UINT msg, WPARAM wParam, \line LPARAM lParam, LRESULT *lResult); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par }{\cs17\f2\fs16 IMDefWindowProc}{ should be called from an IMC window procedure (see section titled }{\i Input Method Composition windows}{). If it returns TRUE you should return the value stored in }{\cs17\f2\fs16 lResult}{ - without any further processing. }{\cs17\f2\fs16 IMDefWindowProc}{ mostly manages window activation and movement. -\par -\par }\pard\plain \s1\sb240\sa60\keepn\widctlpar\outlinelevel0\adjustright \b\f1\fs28\lang3081\kerning28\cgrid {Keyman Imports -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -The DLL can call Keyman functions to interact with Keyman and the target application. It should not attempt to directly control the application as Keyman will be doing this. You should never call any of the functions here from the }{\cs17\f2\fs16 -KeymanIMConfigure}{ callback. -\par -\par You can use }{\cs17\f2\fs16 PrepIM()}{, declared in }{\cs21\f1\fs18 imlib.cpp}{ to get access to the the Keyman functions. When using }{\cs21\f1\fs18 imlib.cpp}{ -, the functions are declared as pointers, so you need to dereference them to call them in C (e.g. for }{\cs17\f2\fs16 KMGetContext}{, call }{\cs17\f2\fs16 (*KMGetcontext)(buf,len);}{ -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KMGetContext -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KMGetContext(PWSTR buf, DWORD len); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par }{\cs17\f2\fs16 KMGetContext}{ returns the last }{\cs17\f2\fs16 len-1}{ characters of the context stack. If there are not enough characters in the context stack, it will return as many as it can. On success, the }{\cs17\f2\fs16 buf}{ - variable will be null terminated. -\par -\par The context stack can contain a special code for deadkeys. See }{\cs17\f2\fs16 KMQueueAction}{ for a way to output a deadkey. The code sequence for a deadkey is (3 words): -\par -\par }\pard\plain \s15\fi436\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {UC_SENTINEL, CODE_DEADKEY, }{\i deadkeyID}{ -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par }{\cs17\f2\fs16 UC_SENTINEL}{ is }{\cs17\f2\fs16 0xFFFF}{; }{\cs17\f2\fs16 CODE_DEADKEY}{ is }{\cs17\f2\fs16 0x0008}{; }{\cs17\i\f2\fs16 deadkeyID}{ can be any value from }{\cs17\f2\fs16 0x0001}{ to }{\cs17\f2\fs16 0xFFFE}{. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KMSetOutput -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KMSetOutput(PWSTR buf, DWORD backlen); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par }{\cs17\f2\fs16 KMSetOutput}{ is a wrapper for }{\cs17\f2\fs16 KMQueueAction}{. It simplifies the process of deleting contextual characte -rs and outputting a new string. The results will not be output to the screen until the current function returns. If called within the context of an IMC window, the results will not be output to the screen until the window posts the }{\cs17\f2\fs16 -wm_keymanim_close}{ message. -\par -\par }{\cs17\f2\fs16 buf}{ is a pointer to a null-terminated string of characters to output. -\par }{\cs17\f2\fs16 backlen}{ is the number of characters to backspace from the current context before displaying }{\cs17\f2\fs16 buf}{. -\par -\par This function modifies the context returned from }{\cs17\f2\fs16 KMGetContext}{, even if the output is not yet on the screen. -\par -\par Internally, this function does the following code: -\par -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {while(backlen-- > 0) KMQueueAction(QIT_BACK, 0); -\par while(*buf) KMQueueAction(QIT_CHAR, *buf++); -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KMQueueAction -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KMQueueAction(int itemType, DWORD dwData); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par KMQueueAction lets you send any Keyman action to a target application. This can be virtual keys, characters, shift keys up and down, deadkeys, beeps, or backspaces (a special case of virtual keys). -\par -\par }\trowd \trgaph108\trleft284\trbrdrt\brdrs\brdrw10 \trbrdrl\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrh\brdrs\brdrw10 \trbrdrv\brdrs\brdrw10 \clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr -\brdrs\brdrw10 \cltxlrtb \cellx2519\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx6780\pard\plain \s16\widctlpar\intbl\adjustright \b\f1\fs16\lang3081\cgrid {itemType code\cell -Description\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\trowd \trgaph108\trleft284\trbrdrt\brdrs\brdrw10 \trbrdrl\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrh\brdrs\brdrw10 \trbrdrv\brdrs\brdrw10 -\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx2519\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx6780\pard -\widctlpar\intbl\adjustright {\cs17\f2\fs16 QIT_VKEYDOWN\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Simulate any key press on the keyboard; }{\b dwData}{ is the virtual key code\cell }\pard\plain -\widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 QIT_VKEYUP\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Simulate any key release on the keyboard; }{\b dwData}{ - is the virtual key code\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 QIT_VSHIFTDOWN\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid { -Simulate pressing a set of shift keys. }{\b dwData}{ can be a combination of the following flags: -\par LCTRLFLAG, RCTRLFLAG, LALTFLAG, RALTFLAG, K_SHIFTFLAG, K_CTRLFLAG, K_ALTFLAG\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 QIT_VSHIFTUP\cell }\pard\plain -\s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {Release the shift state, }{\b dwData}{ is the same as the previous flags.\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\pard \widctlpar\intbl\adjustright { -\cs17\f2\fs16 QIT_CHAR\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {\b dwdata}{ is any WCHAR. For an ANSI window, zero-pad an 8-bit character.\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row -}\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 QIT_DEADKEY\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {\b dwData }{is any value from 0x0001 to 0xFFFE. This can be matched in the context with }{\b KMGetContext}{.\cell -}\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\pard \widctlpar\intbl\adjustright {\cs17\f2\fs16 QIT_BELL\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {\b dwData}{ should be zero (0).\cell }\pard\plain -\widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\trowd \trgaph108\trleft284\trbrdrt\brdrs\brdrw10 \trbrdrl\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrh\brdrs\brdrw10 \trbrdrv\brdrs\brdrw10 \clvertalt\clbrdrt\brdrs\brdrw10 -\clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx2519\clvertalt\clbrdrt\brdrs\brdrw10 \clbrdrl\brdrs\brdrw10 \clbrdrb\brdrs\brdrw10 \clbrdrr\brdrs\brdrw10 \cltxlrtb \cellx6780\pard \widctlpar\intbl\adjustright { -\cs17\f2\fs16 QIT_BACK\cell }\pard\plain \s18\widctlpar\intbl\adjustright \f1\fs16\lang3081\cgrid {\b dwData}{ should be zero (0).\cell }\pard\plain \widctlpar\intbl\adjustright \fs20\lang3081\cgrid {\row }\pard\plain -\s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KMHideIM -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KMHideIM(HWND hwndIM); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par }{\cs17\f2\fs16 KMHideIM}{ hides the IMC window referred to by }{\cs17\f2\fs16 hwndIM}{ and ensures that Keyman processes input from the keyboard through the correct method. You should call this rather than hiding the window manually with }{ -\cs17\f2\fs16 ShowWindow(hwnd, SW_HIDE);}{ or post the message }{\cs17\f2\fs16 wm_keymanim_close}{ to hide the window. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KMDisplayIM -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KMDisplayIM(HWND hwndIM, BOOL FCaptureAll); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par }{\cs17\f2\fs16 KMDisplayIM}{ displays the IMC window referred to by }{\cs17\f2\fs16 hwndIM}{. It does not do any movement of the window. If the }{\cs17\f2\fs16 FCaptureAll}{ flag is set, all keyboard input (character-generating keys only) will be red -irected to the IMC window until the message }{\cs17\f2\fs16 wm_keymanim_close}{ is posted, }{\cs17\f2\fs16 KMHideIM}{ is called, or }{\cs17\f2\fs16 KMDisplayIM}{ with }{\cs17\f2\fs16 FCaptureAll}{ is set to false. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KMGetKeyboardPath -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KMGetKeyboardPath(PSTR keyboardname, PWSTR dir, DWORD length); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par This function returns the full path to the keyboard referred to by }{\cs17\f2\fs16 keyboardname}{. The buffer }{\cs17\f2\fs16 dir}{ should be 260 characters long. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KMGetActiveKeyboard -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {BOOL WINAPI KMGetActiveKeyboard(PSTR keyboardname, DWORD length); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par This function can be called while processing to determine which is the active keyboard. Alternatively, use the callbacks }{\cs17\f2\fs16 KeymanIMActivate}{ and }{\cs17\f2\fs16 KeymanIMDeactivate}{. -\par }\pard\plain \s2\sb240\sa60\keepn\widctlpar\outlinelevel1\adjustright \b\i\f1\lang3081\cgrid {KMSendDebugString -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid {\cs17 BOOL WINAPI KMSendDebugString(PSTR str); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par This function outputs the string }{\cs17\f2\fs16 str}{ to the Keyman debug window or debug log file (usually }{\cs21\f1\fs18 c:\\keyman.log}{). -\par }\pard\plain \s1\sb240\sa60\keepn\widctlpar\outlinelevel0\adjustright \b\f1\fs28\lang3081\kerning28\cgrid {The Input Method Composition window -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid {The IMC window can be shown or hidden at any time that the associated keyboard is active. This means that you can have an IMC window permanently open or open at appropriate times. -\par -\par The keyboard IMSample included with Keyman is a good example of manipulating the IMC display. -\par -\par The window should be created invisible, most probaly as a popup window. The window can use }{\cs17\f2\fs16 KMGetContext}{, }{\cs17\f2\fs16 KMSetOutput}{ at any time, but output will not be put to the screen until it has posted (}{\b not }{sent) }{ -\cs17\f2\fs16 wm_keymanim_close}{ to itself. -\par }\pard\plain \s15\fi-425\li709\widctlpar\adjustright \f2\fs16\lang3081\cgrid { -\par PostMessage(hwnd, wm_keymanim_close, (WPARAM) FSuccess, (LPARAM) FActuallyClose); -\par }\pard\plain \widctlpar\adjustright \fs20\lang3081\cgrid { -\par Keyman will manage the window display, focus, and message loop. The window procedure should set the position and size appropriately. -\par -\par Keyman will recognise this window and any child windows to be part of the IM and will not attempt to process any input that goes through the window. -\par -\par The IMC window must not take focus at any time. -\par }\pard\plain \s3\sb120\keepn\widctlpar\outlinelevel2\adjustright \b\f1\fs20\lang3081\cgrid {Limitations -\par {\pntext\pard\plain\fs20\cgrid \hich\af0\dbch\af0\loch\f0 1.\tab}}\pard\plain \fi-360\li360\widctlpar\jclisttab\tx360{\*\pn \pnlvlbody\ilvl0\ls11\pnrnot0\pndec\pnstart1\pnindent360\pnhang{\pntxta .}}\ls11\adjustright \fs20\lang3081\cgrid { -Clicks outside the window will cancel the IM and lose context. -\par {\pntext\pard\plain\fs20\cgrid \hich\af0\dbch\af0\loch\f0 2.\tab}}\pard \fi-360\li360\widctlpar\jclisttab\tx360{\*\pn \pnlvlbody\ilvl0\ls11\pnrnot0\pndec\pnstart1\pnindent360\pnhang{\pntxta .}}\ls11\adjustright { -Switching applications will cancel the IM and lose context. -\par }\pard \widctlpar\adjustright { -\par }} \ No newline at end of file +{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033\deflangfe1033{\fonttbl{\f0\fswiss\fprq2\fcharset0 Arial;}{\f1\froman\fprq2\fcharset0 Times New Roman;}{\f2\fmodern\fprq1\fcharset0 Courier New;}} +{\stylesheet{ Normal;}{\s1 heading 1;}{\s2 heading 2;}{\s3 heading 3;}} +{\*\generator Riched20 10.0.19041}\viewkind4\uc1 +\pard\widctlpar\sb240\sa60\qc\kerning28\b\f0\fs32\lang3081 DLL Interface for Keyman\par + +\pard\keepn\widctlpar\s1\sb240\sa60\fs28 Introduction\par + +\pard\widctlpar\kerning0\b0\f1\fs20 Keyman's multiple group processing is powerful, but sometimes you need to be able to do something a bit more complex, such as a dictionary lookup. Keyman's DLL interface let you do this. You can call a function in a DLL in the same way as you call another group. The function can read the context, deadkeys and the current keystroke, and output characters, deadkeys, virtual keys, beeps and other items.\par +\par +The DLL interface also allows you to create a popup Input Method Composition (IMC) window. This window allows the user to select visually the characters they are wishing to input. The window can be set to be visible when the keyboard is active, or after an appropriate key sequence. When the window is visible, it can be set to capture all keyboard input, or be passive.\par + +\pard\keepn\widctlpar\s1\sb240\sa60\kerning28\b\f0\fs28 File locations\par + +\pard\widctlpar\kerning0\b0\f1\fs20 The DLL should be placed in any of the following locations:\par +\par + +\pard +{\pntext\f1 1.\tab}{\*\pn\pnlvlbody\pnf1\pnindent360\pnstart1\pndec{\pntxta.}} +\widctlpar\fi-360\li1080 The same directory as the .kmx file (e.g. use a package to install it)\par +{\pntext\f1 2.\tab}The Keyman program directory (same place as keyman32.dll)\par +{\pntext\f1 3.\tab}Anywhere on the path (such as the Windows directory)\par + +\pard\widctlpar\par +The best option is (1), as you can then include the DLL in a Keyman package for easy installation and uninstallation.\par + +\pard\keepn\widctlpar\s1\sb240\sa60\kerning28\b\f0\fs28 General usage information\par + +\pard\widctlpar\kerning0\b0\f1\fs20 DLL functions used in place of groups are called \i DLL group functions\i0 .\par +\par +All strings, apart from keyboard names, are passed as WCHAR, regardless of whether the active window is a Unicode window or not. ANSI characters are represented as 16-bit WCHAR, with high bits zeroed out.\par +\par +The DLL will be loaded for each process in which the keyboard is activated. Remember that the DLL will \b\i not\b0\i0 share memory between these processes by default, so if you have large memory requirements, you should use memory mapped files or possibly SHARDATA segments to minimize the memory consumption.\par +\par +DLL group functions are called in a fairly time-critical environment. It is important that you minimise the processing time in these functions. It is essential that you avoid any window focus or activation -- message boxes are definitely out of the question. For debugging purposes, there is a Keyman32.dll function exported for writing to the logfile (see the section titled \i Keyman32 imports\i0 ).\par +\par +DLLs can handle multiple keyboards at once. The keyboards are identified by a name which is the filename of the keyboard, minus path and extension. For example, given \f0 c:\\keyman\\imsample\\imsample.kmx\f1 , the keyboard name is \f2\fs16 imsample\f1\fs20 . These are the same names that Keyman uses internally, for example in the registry and directory names.\par + +\pard\keepn\widctlpar\s1\sb240\sa60\kerning28\b\f0\fs28 Registry settings\par + +\pard\widctlpar\kerning0\b0\f1\fs20\par +Parameters for the DLL can be stored in two locations in the registry. They should always be stored under HKEY_CURRENT_USER, as the user may not have permission to change machine-wide settings, and the settings should not affect other users. The following locations are recommended:\par +\par + +\pard\widctlpar\fi-283\li567\f0\fs18 HKEY_CURRENT_USER\\Software\\Tavultesoft\\Keyman\\5.0\\\par +HKEY_CURRENT_USER\\Software\\Tavultesoft\\Keyman\\5.0\\Installed Keyboards\\\par + +\pard\widctlpar\f1\fs20\par +The first key should be used for settings that pertain to any keyboard associated with the DLL. The second key should be used for any keyboard-specific settings. Values stored under the second key should be prefixed with the name of the dll, so that they will not conflict with Keyman or other dll values.\par + +\pard\keepn\widctlpar\s1\sb240\sa60\kerning28\b\f0\fs28 The .kmn interface\par + +\pard\widctlpar\kerning0\b0\f1\fs20 Inside a .kmn file, you define the DLL group function interface as follows:\par +\par + +\pard\widctlpar\fi-425\li709\f2\fs16 store(DLLFunction) "myfile.dll:KeyEvent"\par + +\pard\widctlpar\f1\fs20\par +You can use this anywhere where you would place the \f2\fs16 use\f1\fs20 statement (except in the \f2\fs16 begin\f1\fs20 statement), with the \f2\fs16 call\f1\fs20 statement. For example,\par +\par + +\pard\widctlpar\fi-425\li709\f2\fs16 + 'a' > call(DLLFunction)\par + +\pard\widctlpar\f1\fs20\par +A single .kmn file can reference multiple DLL group functions, in a single or multiple DLLs.\par + +\pard\keepn\widctlpar\s1\sb240\sa60\kerning28\b\f0\fs28 DLL exports\par + +\pard\widctlpar\kerning0\b0\f1\fs20 The DLL is called from Keyman with LoadLibrary. All functions are then found with GetProcAddress. You must ensure that the function exports do not have ordinals encoded in the names. The best way to accomplish this in C/C++ is to use a .def file.\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 DLL group function exports\par + +\pard\widctlpar\b0\i0\f1\fs20 The function declaration for the DLL group function is:\par +\par + +\pard\widctlpar\fi-425\li709\f2\fs16 BOOL WINAPI \i KeyEvent\i0 (HWND hwndFocus, WORD KeyStroke, WCHAR KeyChar, DWORD ShiftFlags);\par + +\pard\widctlpar\f1\fs20\par +Note that \f2\fs16 KeyEvent\f1\fs20 is a placeholder for any name that you wish to use. You can have multiple exports for Keyman use in a single DLL.\par + +\pard\keepn\widctlpar\s3\sb120\b\f0 Parameters\par + +\pard\widctlpar\b0\f1\par +\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1418\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6238 +\pard\intbl\widctlpar\b\f0\fs16 Parameter\cell Description\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1418\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6238 +\pard\intbl\widctlpar\b0\f2 hwndFocus\cell\f0 The currently focused window. You will probably never have a need to use this. \cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1418\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6238 +\pard\intbl\widctlpar\f2 KeyStroke\cell\f0 The virtual key code for the current key.\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1418\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6238 +\pard\intbl\widctlpar\f2 KeyChar\cell\f0 The character code for the current key (based on US English layout). This will be 0 if KeyStroke does not generate a character (e.g. function keys).\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1418\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6238 +\pard\intbl\widctlpar\f2 ShiftFlags\cell\f0 The shift state for the current key. See the table in the Notes section for possible shift states. \cell\row +\pard\keepn\widctlpar\s3\sb120\b\fs20 Notes\par + +\pard\widctlpar\b0\f1\par +The following shift states are possible:\par +\par +\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\b\f0\fs16 Flag\cell Value\cell Description\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\b0\f2 LCTRLFLAG\cell 0x0001\cell\f0 Left Control flag\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\f2 RCTRLFLAG\cell 0x0002\cell\f0 Right Control flag\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\f2 LALTFLAG\cell 0x0004\cell\f0 Left Alt flag\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\f2 RALTFLAG\cell 0x0008\cell\f0 Right Alt flag\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\f2 K_SHIFTFLAG\cell 0x0010\cell\f0 Shift flag\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\f2 K_CTRLFLAG\cell 0x0020\cell\f0 Ctrl flag (unused here; see l/r flags) \cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\f2 K_ALTFLAG\cell 0x0040\cell\f0 Alt flag (unused here; see l/r flags)\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\f2 CAPITALFLAG\cell 0x0100\cell\f0 Caps lock on\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\f2 NUMLOCKFLAG\cell 0x0400\cell\f0 Num lock on\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\f2 SCROLLFLAG\cell 0x1000\cell\f0 Scroll lock on\cell\row\trowd\trgaph108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1701\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2694\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5670 +\pard\intbl\widctlpar\f2 ISVIRTUALKEY\cell 0x4000\cell\f0 It is a Virtual Key Sequence\cell\row +\pard\keepn\widctlpar\s1\sb240\sa60\kerning28\b\fs28 Optional DLL exports\par + +\pard\widctlpar\kerning0\b0\f1\fs20 Keyman recognises a number of other exports, if they are defined in the DLL. None of these are required. These functions will be called when a keyboard that references the DLL is manipulated. They will not be called for keyboards that do not reference the DLL.\par +\par +The following exports are available:\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KeymanIMInit\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KeymanIMInit(PSTR keyboardname);\par + +\pard\widctlpar\f1\fs20\par +\f2\fs16 KeymanIMInit\f1\fs20 is called once when the keyboard identified by \f2\fs16 keyboardname\f1\fs20 is loaded for a given process. It is called for each process in which the keyboard is loaded.\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KeymanIMDestroy\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KeymanIMDestroy(PSTR keyboardname);\par + +\pard\widctlpar\f1\fs20\par +This is called once when the keyboard identified by \f2\fs16 keyboardname\f1\fs20 is unloaded in a given process. It is called when the process exits normally, or when Keyman refreshes its keyboard list after keyboards are added or removed. If the keyboard is subsequently reloaded, \f2\fs16 KeymanIMInit\f1\fs20 will be called again.\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KeymanIMActivate\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KeymanIMActivate(PSTR keyboardname);\par + +\pard\widctlpar\f1\fs20\par +This function is called whenever the user or a program activates the keyboard. It is never called before KeymanIMInit (unless KeymanIMInit is not exported). This is an appropriate place to switch on permanently-visible IMC windows. \f2\fs16 KeymanIMActivate\f1\fs20 can also be called when switching processes and the target process has a related keyboard already active.\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KeymanIMDeactivate\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KeymanIMDeactivate(PSTR keyboardname);\par + +\pard\widctlpar\f1\fs20\par +This function is called when the user or a program switches off a related keyboard. It is always called before \f2\fs16 KeymanIMActivate\f1\fs20 for the next keyboard. It will also be called when the user activates another process, to give the DLL a chance to hide top-most IMC windows.\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KeymanIMConfigure\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KeymanIMConfigure(PSTR keyboardname, HWND hwndParent);\par + +\pard\widctlpar\f1\fs20\par +KeymanIMConfigure is called when the user clicks the Configure button in KMShell to configure the DLL-specific functionality for the keyboard. The appropriate behaviour is to display a dialog box, and save the settings in the registry.\par +\par +This function is separate from all the other functions. It can be called when there are no keyboards loaded, or even if Keyman itself is not loaded. You should not attempt to call Keyman32.dll from this function.\par + +\pard\keepn\widctlpar\s1\sb240\sa60\kerning28\b\f0\fs28 The imlib.cpp library module\par + +\pard\widctlpar\kerning0\b0\f1\fs20 imlib.cpp, included in the development kit, contains a set of useful functions for interfacing to Keyman.\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 PrepIM\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL PrepIM(void);\par + +\pard\widctlpar\f1\fs20\par +\f2\fs16 PrepIM\f1\fs20 initialises the Keyman32 imports. You should not call any of the Keyman imports without calling \f2\fs16 PrepIM\f1\fs20 first. If \f2\fs16 PrepIM\f1\fs20 fails, you should exit without doing any processing.\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 IMDefWindowProc\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL IMDefWindowProc(HWND hwnd, UINT msg, WPARAM wParam, \line LPARAM lParam, LRESULT *lResult);\par + +\pard\widctlpar\f1\fs20\par +\f2\fs16 IMDefWindowProc\f1\fs20 should be called from an IMC window procedure (see section titled \i Input Method Composition windows\i0 ). If it returns TRUE you should return the value stored in \f2\fs16 lResult\f1\fs20 without any further processing. \f2\fs16 IMDefWindowProc\f1\fs20 mostly manages window activation and movement.\par +\par + +\pard\keepn\widctlpar\s1\sb240\sa60\kerning28\b\f0\fs28 Keyman Imports\par + +\pard\widctlpar\kerning0\b0\f1\fs20 The DLL can call Keyman functions to interact with Keyman and the target application. It should not attempt to directly control the application as Keyman will be doing this. You should never call any of the functions here from the \f2\fs16 KeymanIMConfigure\f1\fs20 callback.\par +\par +You can use \f2\fs16 PrepIM()\f1\fs20 , declared in \f0\fs18 imlib.cpp\f1\fs20 to get access to the the Keyman functions. When using \f0\fs18 imlib.cpp\f1\fs20 , the functions are declared as pointers, so you need to dereference them to call them in C (e.g. for \f2\fs16 KMGetContext\f1\fs20 , call \f2\fs16 (*KMGetcontext)(buf,len);\f1\fs20\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KMGetContext\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KMGetContext(PWSTR buf, DWORD len);\par + +\pard\widctlpar\f1\fs20\par +\f2\fs16 KMGetContext\f1\fs20 returns the last \f2\fs16 len-1\f1\fs20 characters of the context stack. If there are not enough characters in the context stack, it will return as many as it can. On success, the \f2\fs16 buf\f1\fs20 variable will be null terminated.\par +\par +The context stack can contain a special code for deadkeys. See \f2\fs16 KMQueueAction\f1\fs20 for a way to output a deadkey. The code sequence for a deadkey is (3 words):\par + \par + +\pard\widctlpar\fi436\li709\f2\fs16 UC_SENTINEL, CODE_DEADKEY, \i deadkeyID\i0\par + +\pard\widctlpar\f1\fs20\par +\f2\fs16 UC_SENTINEL\f1\fs20 is \f2\fs16 0xFFFF\f1\fs20 ; \f2\fs16 CODE_DEADKEY\f1\fs20 is \f2\fs16 0x0008\f1\fs20 ; \i\f2\fs16 deadkeyID\i0\f1\fs20 can be any value from \f2\fs16 0x0001\f1\fs20 to \f2\fs16 0xFFFE\f1\fs20 .\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KMSetOutput\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KMSetOutput(PWSTR buf, DWORD backlen); \par + +\pard\widctlpar\f1\fs20\par +\f2\fs16 KMSetOutput\f1\fs20 is a wrapper for \f2\fs16 KMQueueAction\f1\fs20 . It simplifies the process of deleting contextual characters and outputting a new string. The results will not be output to the screen until the current function returns. If called within the context of an IMC window, the results will not be output to the screen until the window posts the \f2\fs16 wm_keymanim_close\f1\fs20 message.\par +\par +\f2\fs16 buf\f1\fs20 is a pointer to a null-terminated string of characters to output.\par +\f2\fs16 backlen\f1\fs20 is the number of characters to backspace from the current context before displaying \f2\fs16 buf\f1\fs20 .\par +\par +This function modifies the context returned from \f2\fs16 KMGetContext\f1\fs20 , even if the output is not yet on the screen.\par +\par +Internally, this function does the following code:\par +\par + +\pard\widctlpar\fi-425\li709\f2\fs16 while(backlen-- > 0) KMQueueAction(QIT_BACK, 0);\par +while(*buf) KMQueueAction(QIT_CHAR, *buf++);\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KMQueueAction\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KMQueueAction(int itemType, DWORD dwData);\par + +\pard\widctlpar\f1\fs20\par +KMQueueAction lets you send any Keyman action to a target application. This can be virtual keys, characters, shift keys up and down, deadkeys, beeps, or backspaces (a special case of virtual keys).\par +\par +\trowd\trgaph108\trleft284\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2519\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6780 +\pard\intbl\widctlpar\b\f0\fs16 itemType code\cell Description\cell\row\trowd\trgaph108\trleft284\trrh375\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2519\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6780 +\pard\intbl\widctlpar\b0\f2 QIT_VKEYDOWN\cell\f0 Simulate any key press on the keyboard; \b dwData\b0 is the virtual key code\cell\row\trowd\trgaph108\trleft284\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2519\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6780 +\pard\intbl\widctlpar\f2 QIT_VKEYUP\cell\f0 Simulate any key release on the keyboard; \b dwData\b0 is the virtual key code\cell\row\trowd\trgaph108\trleft284\trrh870\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2519\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6780 +\pard\intbl\widctlpar\strike\f2 QIT_VSHIFTDOWN\strike0\cell\f0 Simulate pressing a set of shift keys. \b dwData\b0 can be a combination of the following flags:\par +LCTRLFLAG, RCTRLFLAG, LALTFLAG, RALTFLAG, K_SHIFTFLAG, K_CTRLFLAG, K_ALTFLAG\par +\b From Version: 17.0 this is not supported\b0\cell\row\trowd\trgaph108\trleft284\trrh450\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2519\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6780 +\pard\intbl\widctlpar\strike\f2 QIT_VSHIFTUP\strike0\cell\f0 Release the shift state, \b dwData\b0 is the same as the previous flags.\par +\b From Version: 17.0 this is not supported\b0\cell\row\trowd\trgaph108\trleft284\trrh495\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2519\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6780 +\pard\intbl\widctlpar\f2 QIT_CHAR\cell\b\f0 dwdata\b0 is any WCHAR. For an ANSI window, zero-pad an 8-bit character.\cell\row\trowd\trgaph108\trleft284\trrh450\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2519\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6780 +\pard\intbl\widctlpar\f2 QIT_DEADKEY\cell\b\f0 dwData \b0 is any value from 0x0001 to 0xFFFE. This can be matched in the context with \b KMGetContext\b0 .\cell\row\trowd\trgaph108\trleft284\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2519\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6780 +\pard\intbl\widctlpar\f2 QIT_BELL\cell\b\f0 dwData\b0 should be zero (0).\cell\row\trowd\trgaph108\trleft284\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3 +\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx2519\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx6780 +\pard\intbl\widctlpar\f2 QIT_BACK\cell\b\f0 dwData\b0 should be zero (0).\cell\row +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\fs24 KMHideIM\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KMHideIM(HWND hwndIM);\par + +\pard\widctlpar\f1\fs20\par +\f2\fs16 KMHideIM\f1\fs20 hides the IMC window referred to by \f2\fs16 hwndIM\f1\fs20 and ensures that Keyman processes input from the keyboard through the correct method. You should call this rather than hiding the window manually with \f2\fs16 ShowWindow(hwnd, SW_HIDE);\f1\fs20 or post the message \f2\fs16 wm_keymanim_close\f1\fs20 to hide the window.\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KMDisplayIM\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KMDisplayIM(HWND hwndIM, BOOL FCaptureAll);\par + +\pard\widctlpar\f1\fs20\par +\f2\fs16 KMDisplayIM\f1\fs20 displays the IMC window referred to by \f2\fs16 hwndIM\f1\fs20 . It does not do any movement of the window. If the \f2\fs16 FCaptureAll\f1\fs20 flag is set, all keyboard input (character-generating keys only) will be redirected to the IMC window until the message \f2\fs16 wm_keymanim_close\f1\fs20 is posted, \f2\fs16 KMHideIM\f1\fs20 is called, or \f2\fs16 KMDisplayIM\f1\fs20 with \f2\fs16 FCaptureAll\f1\fs20 is set to false.\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KMGetKeyboardPath\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KMGetKeyboardPath(PSTR keyboardname, PWSTR dir, DWORD length);\par + +\pard\widctlpar\f1\fs20\par +This function returns the full path to the keyboard referred to by \f2\fs16 keyboardname\f1\fs20 . The buffer \f2\fs16 dir\f1\fs20 should be 260 characters long.\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KMGetActiveKeyboard\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KMGetActiveKeyboard(PSTR keyboardname, DWORD length);\par + +\pard\widctlpar\f1\fs20\par +This function can be called while processing to determine which is the active keyboard. Alternatively, use the callbacks \f2\fs16 KeymanIMActivate\f1\fs20 and \f2\fs16 KeymanIMDeactivate\f1\fs20 .\par + +\pard\keepn\widctlpar\s2\sb240\sa60\b\i\f0\fs24 KMSendDebugString\par + +\pard\widctlpar\fi-425\li709\b0\i0\f2\fs16 BOOL WINAPI KMSendDebugString(PSTR str);\par + +\pard\widctlpar\f1\fs20\par +This function outputs the string \f2\fs16 str\f1\fs20 to the Keyman debug window or debug log file (usually \f0\fs18 c:\\keyman.log\f1\fs20 ).\par + +\pard\keepn\widctlpar\s1\sb240\sa60\kerning28\b\f0\fs28 The Input Method Composition window\par + +\pard\widctlpar\kerning0\b0\f1\fs20 The IMC window can be shown or hidden at any time that the associated keyboard is active. This means that you can have an IMC window permanently open or open at appropriate times.\par +\par +The keyboard IMSample included with Keyman is a good example of manipulating the IMC display.\par +\par +The window should be created invisible, most probaly as a popup window. The window can use \f2\fs16 KMGetContext\f1\fs20 , \f2\fs16 KMSetOutput\f1\fs20 at any time, but output will not be put to the screen until it has posted (\b not \b0 sent) \f2\fs16 wm_keymanim_close\f1\fs20 to itself.\par + +\pard\widctlpar\fi-425\li709\f2\fs16\par +PostMessage(hwnd, wm_keymanim_close, (WPARAM) FSuccess, (LPARAM) FActuallyClose);\par + +\pard\widctlpar\f1\fs20\par +Keyman will manage the window display, focus, and message loop. The window procedure should set the position and size appropriately.\par +\par +Keyman will recognise this window and any child windows to be part of the IM and will not attempt to process any input that goes through the window.\par +\par +The IMC window must not take focus at any time.\par + +\pard\keepn\widctlpar\s3\sb120\b\f0 Limitations\par + +\pard +{\pntext\f1 1.\tab}{\*\pn\pnlvlbody\pnf1\pnindent360\pnstart1\pndec{\pntxta.}} +\widctlpar\fi-360\li360\b0\f1 Clicks outside the window will cancel the IM and lose context.\par +{\pntext\f1 2.\tab}Switching applications will cancel the IM and lose context.\par + +\pard\widctlpar\par +} + \ No newline at end of file diff --git a/developer/src/samples/imsample/imlib.h b/developer/src/samples/imsample/imlib.h index ab2d30dc74f..1af2a06cd58 100644 --- a/developer/src/samples/imsample/imlib.h +++ b/developer/src/samples/imsample/imlib.h @@ -4,8 +4,8 @@ #define QIT_VKEYDOWN 0 #define QIT_VKEYUP 1 -#define QIT_VSHIFTDOWN 2 -#define QIT_VSHIFTUP 3 +//#define QIT_VSHIFTDOWN 2 // deprecated see #11925 +//#define QIT_VSHIFTUP 3 #define QIT_CHAR 4 #define QIT_DEADKEY 5 #define QIT_BELL 6 diff --git a/developer/src/server/build-addins.inc.sh b/developer/src/server/build-addins.inc.sh index d48d44d1e74..052a24d77b9 100644 --- a/developer/src/server/build-addins.inc.sh +++ b/developer/src/server/build-addins.inc.sh @@ -41,9 +41,10 @@ do_build_addins() { # Build node-windows-trayicon # - pushd "$KEYMAN_ROOT/node_modules/node-windows-trayicon" + pushd "$KEYMAN_ROOT/developer/src/server/src/win32/trayicon/addon-src" rm -rf build - npx node-gyp clean configure build --arch=$ARCH --silent + npm ci + "$KEYMAN_ROOT/node_modules/.bin/node-gyp" clean configure build --arch=$ARCH --silent cp build/Release/addon.node "$TRAYICON_SRC_TARGET" cp build/Release/addon.node "$TRAYICON_BIN_TARGET" popd @@ -52,9 +53,9 @@ do_build_addins() { # Build hetrodo-node-hide-console-window-napi # - pushd "$KEYMAN_ROOT/node_modules/hetrodo-node-hide-console-window-napi" + pushd "$KEYMAN_ROOT/node_modules/node-hide-console-window" rm -rf build - npx node-gyp clean configure build --arch=$ARCH --silent + "$KEYMAN_ROOT/node_modules/.bin/node-gyp" clean configure build --arch=$ARCH --silent cp build/Release/node-hide-console-window.node "$HIDECONSOLE_SRC_TARGET" cp build/Release/node-hide-console-window.node "$HIDECONSOLE_BIN_TARGET" popd diff --git a/developer/src/server/build.sh b/developer/src/server/build.sh index 5c3ed8bce5e..c73ed853f77 100755 --- a/developer/src/server/build.sh +++ b/developer/src/server/build.sh @@ -87,9 +87,16 @@ function build_server() { # Post build mkdir -p "$THIS_SCRIPT_PATH/build/src/site/" - mkdir -p "$THIS_SCRIPT_PATH/build/src/win32/" cp -r "$THIS_SCRIPT_PATH/src/site/"** "$THIS_SCRIPT_PATH/build/src/site/" - cp -r "$THIS_SCRIPT_PATH/src/win32/"** "$THIS_SCRIPT_PATH/build/src/win32/" + + mkdir -p "$THIS_SCRIPT_PATH/build/src/win32/" + mkdir -p "$THIS_SCRIPT_PATH/build/src/win32/console" + mkdir -p "$THIS_SCRIPT_PATH/build/src/win32/trayicon" + cp "$THIS_SCRIPT_PATH/src/win32/README.md" "$THIS_SCRIPT_PATH/build/src/win32/" + cp "$THIS_SCRIPT_PATH/src/win32/console/node-hide-console-window.node" "$THIS_SCRIPT_PATH/build/src/win32/console/" + cp "$THIS_SCRIPT_PATH/src/win32/console/node-hide-console-window.x64.node" "$THIS_SCRIPT_PATH/build/src/win32/console/" + cp "$THIS_SCRIPT_PATH/src/win32/trayicon/addon.node" "$THIS_SCRIPT_PATH/build/src/win32/trayicon/" + cp "$THIS_SCRIPT_PATH/src/win32/trayicon/addon.x64.node" "$THIS_SCRIPT_PATH/build/src/win32/trayicon/" replaceVersionStrings "$THIS_SCRIPT_PATH/build/src/site/lib/sentry/init.js.in" "$THIS_SCRIPT_PATH/build/src/site/lib/sentry/init.js" rm "$THIS_SCRIPT_PATH/build/src/site/lib/sentry/init.js.in" @@ -114,10 +121,13 @@ function installer_server() { rm -f node_modules/ngrok/bin/ngrok.exe popd - # @keymanapp/keyman-version is required in build/ now but we need to copy it in manually + # Dependencies are required in build/ but we need to copy them in manually mkdir -p "$PRODBUILDTEMP/node_modules/@keymanapp/" cp -R "$KEYMAN_ROOT/node_modules/@keymanapp/keyman-version/" "$PRODBUILDTEMP/node_modules/@keymanapp/" cp -R "$KEYMAN_ROOT/node_modules/@keymanapp/developer-utils/" "$PRODBUILDTEMP/node_modules/@keymanapp/" + cp -R "$KEYMAN_ROOT/node_modules/@keymanapp/common-types/" "$PRODBUILDTEMP/node_modules/@keymanapp/" + cp -R "$KEYMAN_ROOT/node_modules/@keymanapp/ldml-keyboard-constants/" "$PRODBUILDTEMP/node_modules/@keymanapp/" + cp -R "$KEYMAN_ROOT/node_modules/eventemitter3/" "$PRODBUILDTEMP/node_modules/" # We'll build in the $KEYMAN_ROOT/developer/bin/server/ folder rm -rf "$KEYMAN_ROOT/developer/bin/server/" @@ -131,7 +141,7 @@ function test_server() { # eslint . tsc --build test # c8 --reporter=lcov --reporter=text - mocha + mocha --recursive build/test } function publish_server() { @@ -144,8 +154,8 @@ function publish_server() { builder_run_action clean:server clean_server builder_run_action configure:server configure_server -builder_run_action build:server build_server builder_run_action build:addins build_addins +builder_run_action build:server build_server builder_run_action test:server test_server # builder_run_action test:addins # no op builder_run_action installer:server installer_server # TODO: rename to install-prep diff --git a/developer/src/server/package.json b/developer/src/server/package.json index 74fc7ed6215..8619adceecc 100644 --- a/developer/src/server/package.json +++ b/developer/src/server/package.json @@ -12,15 +12,18 @@ "@keymanapp/developer-utils": "*", "@sentry/node": "^7.57.0", "chalk": "^4.1.2", - "express": "^4.19.2", + "express": "^4.20.0", "multer": "^1.4.5-lts.1", "ngrok": "^5.0.0-beta.2", "open": "^8.4.0", - "ws": "^8.3.0" - }, + "restructure": "^3.0.1", + "sax": ">=0.6.0", + "semver": "^7.5.4", + "ws": "^8.17.1", + "xmlbuilder": "~11.0.0" +}, "optionalDependencies": { - "hetrodo-node-hide-console-window-napi": "keymanapp/hetrodo-node-hide-console-window-napi#keyman-15.0", - "node-windows-trayicon": "keymanapp/node-windows-trayicon#keyman-16.0" + "node-hide-console-window": "^2.2.0" }, "devDependencies": { "@keymanapp/keyman-version": "*", @@ -32,12 +35,8 @@ "@types/ws": "^8.2.2", "copyfiles": "^2.4.1", "mocha": "^10.0.0", - "ts-node": "^10.9.1", + "node-gyp": "^10.2.0", "tsc-watch": "^4.5.0", - "typescript": "^4.9.5" - }, - "mocha": { - "require": "ts-node/register", - "spec": "build/**/*.test.js" + "typescript": "^5.4.5" } } diff --git a/developer/src/server/src/win32/trayicon/README.md b/developer/src/server/src/win32/trayicon/README.md index 2a6dfcff37c..9dbc4b19f5a 100644 --- a/developer/src/server/src/win32/trayicon/README.md +++ b/developer/src/server/src/win32/trayicon/README.md @@ -8,3 +8,8 @@ bundled libraries target for specific versions of node. * Tree: https://github.com/mceSystems/node-windows-trayicon/tree/cbd7543ac186ca15ee8d7141aac43f26ceae1655 * License: MIT * Built from: /developer/src/server/win32/node-windows-trayicon + +--- + +The source for trayicon is currently in addon-src, as the original package is +unmaintained. \ No newline at end of file diff --git a/developer/src/server/src/win32/trayicon/addon-src/.gitignore b/developer/src/server/src/win32/trayicon/addon-src/.gitignore new file mode 100644 index 00000000000..c6bf5be1c71 --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/.gitignore @@ -0,0 +1,62 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +.vscode +build/ +test/icon.ico \ No newline at end of file diff --git a/developer/src/server/src/win32/trayicon/addon-src/LICENSE b/developer/src/server/src/win32/trayicon/addon-src/LICENSE new file mode 100644 index 00000000000..454c5bc10ab --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 mceSystems + +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. diff --git a/developer/src/server/src/win32/trayicon/addon-src/README.md b/developer/src/server/src/win32/trayicon/addon-src/README.md new file mode 100644 index 00000000000..6c429beb599 --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/README.md @@ -0,0 +1,57 @@ +# windows-trayicon +Native addon to add a windows tray icon with menu, built on windows-native libraries (no .NET dependency) + +# Installation +``` +npm install --save windows-trayicon +``` + +# Usage +``` +const WindowsTrayicon = require("windows-trayicon"); +const path = require("path"); +const fs = require("fs"); + +const myTrayApp = new WindowsTrayicon({ + title: "Trayicon Test", + icon: path.resolve(__dirname, "icon.ico"), + menu: [ + { + id: "item-1-id", + caption: "First Item" + }, + { + id: "item-2-id", + caption: "Second Item" + }, + { + id: "item-3-id-exit", + caption: "Exit" + } + ] +}); + +myTrayApp.item((id) => { + console.log(`Menu id selected=${id}`); + switch (id) { + case "item-1-id": { + console.log("First item selected..."); + break; + } + case "item-2-id": { + myTrayApp.balloon("Hello There!", "This is my message to you").then(() => { + console.log("Balloon clicked"); + }) + break; + } + case "item-3-id-exit": { + myTrayApp.exit(); + process.exit(0) + break; + } + } +}); + +process.stdin.resume() + +``` \ No newline at end of file diff --git a/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.cpp b/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.cpp new file mode 100644 index 00000000000..549ff935e39 --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.cpp @@ -0,0 +1,512 @@ +/* +* Copyright (c) Istvan Pasztor +* This source has been published on www.codeproject.com under the CPOL license. +*/ +#include "stdafx.h" +#include "TrayIcon.h" +#include + +#define TRAY_WINDOW_MESSAGE (WM_USER + 100) + +namespace +{ +// A map that never holds allocated memory when it is empty. This map will be created with placement new as a static variable, +// and its destructor will be never called, and it shouldn't leak memory if it contains no items at program exit. +// This dirty trick is useful when you create your trayicon object as static. In this case we can not control the +// order of destruction of this map object and the static trayicon object. However this dirty trick ensures that +// the map is freed exactly when the destructor of the last static trayicon is unregistering itself. +class CIdToTrayIconMap +{ + public: + typedef UINT KeyType; + typedef CTrayIcon *ValueType; + + // typedef didn't work with VC++6 + struct StdMap : public std::map + { + }; + typedef StdMap::iterator iterator; + + CIdToTrayIconMap() : m_Empty(true) {} + ValueType &operator[](KeyType k) + { + return GetOrCreateStdMap()[k]; + } + ValueType *find(KeyType k) + { + if (m_Empty) + return false; + StdMap::iterator it = GetStdMap().find(k); + if (it == GetStdMap().end()) + return NULL; + return &it->second; + } + int erase(KeyType k) + { + if (m_Empty) + return 0; + StdMap &m = GetStdMap(); + int res = (int)m.erase(k); + if (m.empty()) + { + m.~StdMap(); + m_Empty = true; + } + return res; + } + bool empty() const + { + return m_Empty; + } + // Call this only when the container is not empty!!! + iterator begin() + { + assert(!m_Empty); // Call this only when the container is not empty!!! + return m_Empty ? iterator() : GetStdMap().begin(); + } + // Call this only when the container is not empty!!! + iterator end() + { + assert(!m_Empty); // Call this only when the container is not empty!!! + return m_Empty ? iterator() : GetStdMap().end(); + } + + private: + StdMap &GetStdMap() + { + assert(!m_Empty); + return (StdMap &)m_MapBuffer; + } + StdMap &GetOrCreateStdMap() + { + if (m_Empty) + { + new ((void *)&m_MapBuffer) StdMap(); + m_Empty = false; + } + return (StdMap &)m_MapBuffer; + } + + private: + bool m_Empty; + char m_MapBuffer[sizeof(StdMap)]; +}; + +static CIdToTrayIconMap &GetIdToTrayIconMap() +{ + // This hack prevents running the destructor of our map, so it isn't problem if someone tries to reach this from a static destructor. + // Because of using MyMap this will not cause a memory leak if the user removes all items from the container before exiting. + static char id_to_tray_icon_buffer[sizeof(CIdToTrayIconMap)]; + static bool initialized = false; + if (!initialized) + { + initialized = true; + new ((void *)id_to_tray_icon_buffer) CIdToTrayIconMap(); + } + return (CIdToTrayIconMap &)id_to_tray_icon_buffer; +} + +static UINT GetNextTrayIconId() +{ + static UINT next_id = 1; + return next_id++; +} +} + +void CallJsWithOptionalString(Napi::Env env, Function callback, Context *context, std::string* data) { + if (env != nullptr) { + if (callback != nullptr) { + if (data) { + callback.Call(context->Value(), { String::New(env, *data) }); + } else { + callback.Call(context->Value(), {}); + } + + } + } + if (data != nullptr) { + delete data; + } +} + +static const UINT g_WndMsgTaskbarCreated = RegisterWindowMessage(TEXT("TaskbarCreated")); +LRESULT CALLBACK CTrayIcon::MessageProcessorWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + if (uMsg == TRAY_WINDOW_MESSAGE) + { + if (CTrayIcon **ppIcon = GetIdToTrayIconMap().find((UINT)wParam)) + (*ppIcon)->OnMessage((UINT)lParam); + return 0; + } + else if (uMsg == g_WndMsgTaskbarCreated) + { + CIdToTrayIconMap &id_to_tray = GetIdToTrayIconMap(); + if (!id_to_tray.empty()) + { + for (std::map::const_iterator it = id_to_tray.begin(), eit = id_to_tray.end(); it != eit; ++it) + { + CTrayIcon *pTrayIcon = it->second; + pTrayIcon->OnTaskbarCreated(); + } + } + } + return DefWindowProc(hWnd, uMsg, wParam, lParam); +} + +HWND CTrayIcon::GetMessageProcessorHWND() +{ + static HWND hWnd = NULL; + if (!hWnd) + { + static const TCHAR TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASSNAME[] = TEXT("TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASS"); + HINSTANCE hInstance = (HINSTANCE)GetModuleHandle(NULL); + + WNDCLASSEX wc; + wc.cbSize = sizeof(wc); + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); + wc.hIconSm = NULL; + wc.hInstance = hInstance; + wc.lpfnWndProc = MessageProcessorWndProc; + wc.lpszClassName = TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASSNAME; + wc.lpszMenuName = NULL; + wc.style = 0; + if (!RegisterClassEx(&wc)) + return NULL; + + hWnd = CreateWindowEx( + 0, + TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASSNAME, + TEXT("TRAY_ICON_MESSAGE_PROCESSOR_WND"), + WS_POPUP, + 0, 0, 0, 0, + NULL, + NULL, + hInstance, + NULL); + } + return hWnd; +} + +CTrayIcon::CTrayIcon(const char *name, bool visible, HICON hIcon, bool destroy_icon_in_destructor) + : m_Id(GetNextTrayIconId()), m_Name(name), m_hIcon(hIcon), m_Visible(false), m_DestroyIconInDestructor(destroy_icon_in_destructor), m_pOnMessageFunc(NULL), m_pListener(NULL) +{ + GetIdToTrayIconMap()[m_Id] = this; + SetVisible(visible); +} + +CTrayIcon::~CTrayIcon() +{ + SetVisible(false); + SetIcon(NULL, m_DestroyIconInDestructor); + GetIdToTrayIconMap().erase(m_Id); +} + +HICON CTrayIcon::InternalGetIcon() const +{ + return m_hIcon ? m_hIcon : ::LoadIcon(NULL, IDI_APPLICATION); +} + +bool CTrayIcon::AddIcon() +{ + NOTIFYICONDATAA data; + FillNotifyIconData(data); + data.uFlags |= NIF_MESSAGE | NIF_ICON | NIF_TIP; + data.uCallbackMessage = TRAY_WINDOW_MESSAGE; + data.hIcon = InternalGetIcon(); + + size_t tip_len = max(sizeof(data.szTip) - 1, strlen(m_Name.c_str())); + memcpy(data.szTip, m_Name.c_str(), tip_len); + data.szTip[tip_len] = 0; + + return FALSE != Shell_NotifyIconA(NIM_ADD, &data); +} + +bool CTrayIcon::RemoveIcon() +{ + NOTIFYICONDATAA data; + FillNotifyIconData(data); + return FALSE != Shell_NotifyIconA(NIM_DELETE, &data); +} + +void CTrayIcon::OnTaskbarCreated() +{ + if (m_Visible) + AddIcon(); +} + +void CTrayIcon::SetName(const char *name) +{ + m_Name = name; + if (m_Visible) + { + NOTIFYICONDATAA data; + FillNotifyIconData(data); + data.uFlags |= NIF_TIP; + + size_t tip_len = max(sizeof(data.szTip) - 1, strlen(name)); + memcpy(data.szTip, name, tip_len); + data.szTip[tip_len] = 0; + + Shell_NotifyIconA(NIM_MODIFY, &data); + } +} + +bool CTrayIcon::SetVisible(bool visible) +{ + if (m_Visible == visible) + return true; + m_Visible = visible; + if (m_Visible) + return AddIcon(); + return RemoveIcon(); +} + +void CTrayIcon::SetIcon(HICON hNewIcon, bool destroy_current_icon) +{ + if (m_hIcon == hNewIcon) + return; + if (destroy_current_icon && m_hIcon) + DestroyIcon(m_hIcon); + m_hIcon = hNewIcon; + + if (m_Visible) + { + NOTIFYICONDATAA data; + FillNotifyIconData(data); + data.uFlags |= NIF_ICON; + data.hIcon = InternalGetIcon(); + Shell_NotifyIconA(NIM_MODIFY, &data); + } +} + +bool CTrayIcon::ShowBalloonTooltip(const char *title, const char *msg, ETooltipIcon icon) +{ +#ifndef NOTIFYICONDATA_V2_SIZE + return false; +#else + if (!m_Visible) + return false; + + NOTIFYICONDATAA data; + FillNotifyIconData(data); + data.cbSize = NOTIFYICONDATAA_V2_SIZE; // win2k and later + data.uFlags |= NIF_INFO; + data.dwInfoFlags = icon; + data.uTimeout = 10000; // deprecated as of Windows Vista, it has a min(10000) and max(30000) value on previous Windows versions. + + strcpy_s(data.szInfoTitle, title); + strcpy_s(data.szInfo, msg); + + return FALSE != Shell_NotifyIconA(NIM_MODIFY, &data); +#endif +} + +void CTrayIcon::OnMessage(UINT uMsg) +{ + if (m_pOnMessageFunc) + m_pOnMessageFunc(this, uMsg); + if (m_pListener) + m_pListener->OnTrayIconMessage(this, uMsg); +} + +void CTrayIcon::FillNotifyIconData(NOTIFYICONDATAA &data) +{ + memset(&data, 0, sizeof(data)); + // the basic functions need only V1 +#ifdef NOTIFYICONDATA_V1_SIZE + data.cbSize = NOTIFYICONDATA_V1_SIZE; +#else + data.cbSize = sizeof(data); +#endif + data.hWnd = GetMessageProcessorHWND(); + assert(data.hWnd); + data.uID = m_Id; +} + +void TrayOnMessage(CTrayIcon *pTrayIcon, UINT uMsg) +{ + switch (uMsg) + { + case WM_LBUTTONUP: + case WM_RBUTTONUP: + ((CTrayIconContainer *)pTrayIcon->GetUserData())->PopupMenu(); + break; + + case NIN_BALLOONUSERCLICK: + ((CTrayIconContainer *)pTrayIcon->GetUserData())->BalloonClick(); + break; + } +} + +HICON GetIconHandle(std::string iconPath) +{ + return (HICON)LoadImage(NULL, iconPath.c_str(), IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED); +} + + +CTrayIconContainer::CTrayIconContainer(const CallbackInfo& info) : ObjectWrap(info), m_OnBalloonClick(nullptr), m_OnMenuItem(nullptr) {} + +Object CTrayIconContainer::Init(Napi::Env env, Object exports){ + Function func = + DefineClass(env, + "CTrayIconContainer", + { + InstanceMethod("Start", &CTrayIconContainer::Start), + InstanceMethod("SetIconPath", &CTrayIconContainer::SetIconPath), + InstanceMethod("SetTitle", &CTrayIconContainer::SetTitle), + InstanceMethod("Stop", &CTrayIconContainer::Stop), + InstanceMethod("AddMenuItem", &CTrayIconContainer::AddMenuItem), + InstanceMethod("OnMenuItem", &CTrayIconContainer::OnMenuItem), + InstanceMethod("ShowBalloon", &CTrayIconContainer::ShowBalloon), + } + ); + + FunctionReference* constructor = new FunctionReference(); + *constructor = Napi::Persistent(func); + env.SetInstanceData(constructor); + + exports.Set("CTrayIconContainer", func); + return exports; +} + +void CTrayIconContainer::Stop(const CallbackInfo& info) +{ + if (m_OnMenuItem) { + m_OnMenuItem.Release(); + m_OnMenuItem = nullptr; + } + if (m_OnBalloonClick) { + m_OnBalloonClick.Release(); + m_OnBalloonClick = nullptr; + } + PostThreadMessage(GetThreadId(m_worker->native_handle()), WM_QUIT, NULL, NULL); + m_worker->join(); + delete m_worker; + m_worker = nullptr; + m_tray.SetVisible(false); +} + +LRESULT CALLBACK pWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + return DefWindowProc(hWnd, uMsg, wParam, lParam); +} + +void CTrayIconContainer::SetIconPath(const CallbackInfo& info) +{ + std::string iconPath = info[0].As(); + m_tray.SetIcon(GetIconHandle(iconPath)); +} +void CTrayIconContainer::SetTitle(const CallbackInfo& info) +{ + std::string title = info[0].As(); + m_tray.SetName(title.c_str()); +} +void CTrayIconContainer::Start(const CallbackInfo& info) +{ + HANDLE ready = CreateEvent(nullptr, true, false, nullptr); + + m_worker = new std::thread([this, ready] { + m_tray.SetUserData(this); + m_tray.SetVisible(true); + m_tray.SetListener(TrayOnMessage); + + SetEvent(ready); + + static const TCHAR *class_name = TEXT("MCE_HWND_MESSAGE"); + WNDCLASSEX wx = {}; + wx.cbSize = sizeof(WNDCLASSEX); + wx.lpfnWndProc = pWndProc; + wx.hInstance = 0; + wx.lpszClassName = class_name; + RegisterClassEx(&wx); + m_hwnd = CreateWindowEx(0, class_name, TEXT("MCE_HWND_MESSAGE"), 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL); + + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + DestroyWindow(m_hwnd); + + return 0; + }); + + WaitForSingleObject(ready, INFINITE); + CloseHandle(ready); +} + +void CTrayIconContainer::BalloonClick() +{ + m_OnBalloonClick.BlockingCall(nullptr); +} + +void CTrayIconContainer::PopupMenu() +{ + POINT pt; + if (GetCursorPos(&pt)) + { + HMENU menu = CreatePopupMenu(); + int i = 1; + for (auto const &item : m_menuItems) + { + if(item.m_caption == "-") + { + AppendMenuA(menu, MF_SEPARATOR, -1, NULL); + } + else + { + AppendMenuA(menu, MF_STRING, i, item.m_caption.c_str()); + } + i++; + } + + HWND activeHwnd = GetForegroundWindow(); + SetForegroundWindow(m_hwnd); + UINT cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hwnd, NULL); + PostMessage(m_hwnd, WM_NULL, 0, 0); + SetForegroundWindow(activeHwnd); + if(cmd > 0) { + m_OnMenuItem.BlockingCall(new std::string(m_menuItems[cmd-1].m_id)); + } + } +} + +void CTrayIconContainer::AddMenuItem(const CallbackInfo& info) +{ + std::string id = info[0].As(); + std::string caption = info[1].As(); + m_menuItems.push_back(CTrayIconMenuItem(id, caption)); +} + +void CTrayIconContainer::OnMenuItem(const CallbackInfo& info) +{ + Function cb = info[0].As(); + Context *context = new Reference(Persistent(info.This())); + if (m_OnMenuItem) { + m_OnMenuItem.Release(); + } + m_OnMenuItem = TSFNOptString::New(info.Env(), cb, "wintrayicon_OnMenuItem", 0, 1, context); +} + +void CTrayIconContainer::ShowBalloon(const CallbackInfo& info) +{ + std::string title = info[0].As(); + std::string text = info[1].As(); + int timeout = info[2].As(); + Function cb = info[3].As(); + + Context *context = new Reference(Persistent(info.This())); + + if (m_OnBalloonClick) { + m_OnBalloonClick.Release(); + } + + m_OnBalloonClick = TSFNOptString::New(info.Env(), cb, "wintrayicon_ShowBalloon", 0, 1, context); + m_tray.ShowBalloonTooltip(title.c_str(), text.c_str()); +} diff --git a/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.h b/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.h new file mode 100644 index 00000000000..ee0c85561ec --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/TrayIcon.h @@ -0,0 +1,193 @@ +/* +* Copyright (c) Istvan Pasztor +* This source has been published on www.codeproject.com under the CPOL license. +*/ +#ifndef __TRAY_ICON_H__ +#define __TRAY_ICON_H__ +#pragma once + +// NOTE: include the following headers in your stdafx.h: +#include +#include +#include +#include +#include +#include +#include +#include +#include "napi.h" + +using namespace Napi; + +using Context = Reference; +void CallJsWithOptionalString(Napi::Env env, Function callback, Context *context, std::string* data); +using TSFNOptString = TypedThreadSafeFunction; + +struct ITrayIconListener; + +class CTrayIconMenuItem +{ +public: + CTrayIconMenuItem(std::string id, std::string caption) : + m_id(id), m_caption(caption) {} + + std::string m_id; + std::string m_caption; +}; + + +// You can use this class either by inheriting from it and overriding the OnMessage() method, +// or by instantiating this class directly and setting its listener object or function. +class CTrayIcon +{ +public: + CTrayIcon(const char* name="tray_icon", bool visible=false, HICON hIcon=NULL, bool destroy_icon_in_destructor=false); + // destroys the current m_hIcon if set + virtual ~CTrayIcon(); + + virtual void SetName(const char* name); + const char* GetName() const { return m_Name.c_str(); } + + virtual bool SetVisible(bool visible); + bool IsVisible() const { return m_Visible; } + + // The destructor may destroy the specified hIcon. If you want to avoid that, call + // SetIcon(NULL, false) or SetDestroyIconInDestructor(false). + virtual void SetIcon(HICON hNewIcon, bool destroy_current_icon=true); + HICON GetIcon() const { return m_hIcon; } + + void SetDestroyIconInDestructor(bool b) { m_DestroyIconInDestructor = b; } + bool GetDestroyIconInDestructor() const { return m_DestroyIconInDestructor; } + + enum ETooltipIcon + { + eTI_None, // NIIF_NONE(0) + eTI_Info, // NIIF_INFO(1) + eTI_Warning, // NIIF_WARNING(2) + eTI_Error // NIIF_ERROR(3) + }; + // ShowBalloonTooltip() works only on win2k and later + bool ShowBalloonTooltip(const char* title, const char* msg, ETooltipIcon icon=eTI_None); + + typedef void (*POnMessageFunc)(CTrayIcon* pTrayIcon, UINT uMsg); + void SetListener(POnMessageFunc pOnMessageFunc) { m_pOnMessageFunc = pOnMessageFunc; } + void SetListener(ITrayIconListener *pListener) { m_pListener = pListener; } + void SetUserData(const void* UserData) { m_userData = UserData; } + const void* GetUserData() { return m_userData; } + +protected: + // uMsg can be one of the following window messages: + // - WM_MOUSEMOVE + // - WM_LBUTTONDOWN, WM_LBUTTONUP, WM_LBUTTONDBLCLK + // - WM_RBUTTONDOWN, WM_RBUTTONUP, WM_RBUTTONDBLCLK + // - WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MBUTTONDBLCLK + // WinXP and later: + // - NIN_BALLOONXXX messages (eg.: NIN_BALLOONUSERCLICK) + // + // Use GetCursorPos() if you need the location of the cursor. + // The default implementation calls the listener. + virtual void OnMessage(UINT uMsg); + +private: + void FillNotifyIconData(NOTIFYICONDATAA& data); + // Never returns NULL! If GetIcon()==NULL, then this returns a system icon + HICON InternalGetIcon() const; + bool AddIcon(); + bool RemoveIcon(); + void OnTaskbarCreated(); + +private: + UINT m_Id; + std::string m_Name; + bool m_Visible; + HICON m_hIcon; + bool m_DestroyIconInDestructor; + POnMessageFunc m_pOnMessageFunc; + ITrayIconListener* m_pListener; + + static LRESULT CALLBACK MessageProcessorWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + static HWND GetMessageProcessorHWND(); + const void* m_userData; +}; + + +//------------------------------------------------------------------------------------------------- + + +struct ITrayIconListener +{ + virtual void OnTrayIconMouseMove(CTrayIcon* pTrayIcon) {} + + virtual void OnTrayIconLButtonDown(CTrayIcon* pTrayIcon) {} + virtual void OnTrayIconLButtonUp(CTrayIcon* pTrayIcon) {} + virtual void OnTrayIconLButtonDblClk(CTrayIcon* pTrayIcon) {} + + virtual void OnTrayIconRButtonDown(CTrayIcon* pTrayIcon) {} + virtual void OnTrayIconRButtonUp(CTrayIcon* pTrayIcon) {} + virtual void OnTrayIconRButtonDblClk(CTrayIcon* pTrayIcon) {} + + virtual void OnTrayIconMButtonDown(CTrayIcon* pTrayIcon) {} + virtual void OnTrayIconMButtonUp(CTrayIcon* pTrayIcon) {} + virtual void OnTrayIconMButtonDblClk(CTrayIcon* pTrayIcon) {} + + // WinXP and later + virtual void OnTrayIconSelect(CTrayIcon* pTrayIcon) {} + virtual void OnTrayIconBalloonShow(CTrayIcon* pTrayIcon) {} + virtual void OnTrayIconBalloonHide(CTrayIcon* pTrayIcon) {} + virtual void OnTrayIconBalloonTimeout(CTrayIcon* pTrayIcon) {} + virtual void OnTrayIconBalloonUserClick(CTrayIcon* pTrayIcon) {} + + // Use GetCursorPos() if you need the location of the cursor. + virtual void OnTrayIconMessage(CTrayIcon* pTrayIcon, UINT uMsg) + { + switch (uMsg) + { + case WM_MOUSEMOVE: OnTrayIconMouseMove(pTrayIcon); break; + case WM_LBUTTONDOWN: OnTrayIconLButtonDown(pTrayIcon); break; + case WM_LBUTTONUP: OnTrayIconLButtonUp(pTrayIcon); break; + case WM_LBUTTONDBLCLK: OnTrayIconLButtonDblClk(pTrayIcon); break; + case WM_RBUTTONDOWN: OnTrayIconRButtonDown(pTrayIcon); break; + case WM_RBUTTONUP: OnTrayIconRButtonUp(pTrayIcon); break; + case WM_RBUTTONDBLCLK: OnTrayIconRButtonDblClk(pTrayIcon); break; + case WM_MBUTTONDOWN: OnTrayIconMButtonDown(pTrayIcon); break; + case WM_MBUTTONUP: OnTrayIconMButtonUp(pTrayIcon); break; + case WM_MBUTTONDBLCLK: OnTrayIconMButtonDblClk(pTrayIcon); break; + +#ifdef NIN_SELECT + case NIN_SELECT: OnTrayIconSelect(pTrayIcon); break; + case NIN_BALLOONSHOW: OnTrayIconBalloonShow(pTrayIcon); break; + case NIN_BALLOONHIDE: OnTrayIconBalloonHide(pTrayIcon); break; + case NIN_BALLOONTIMEOUT: OnTrayIconBalloonTimeout(pTrayIcon); break; + case NIN_BALLOONUSERCLICK: OnTrayIconBalloonUserClick(pTrayIcon); break; +#endif + } + } +}; + + + +class CTrayIconContainer : public ObjectWrap +{ +public: + CTrayIconContainer(const CallbackInfo& info); + static Object Init(Napi::Env env, Object exports); + void Start(const CallbackInfo& info); + void SetIconPath(const CallbackInfo& info); + void SetTitle(const CallbackInfo& info); + void AddMenuItem(const CallbackInfo& info); + void OnMenuItem(const CallbackInfo& info); + void ShowBalloon(const CallbackInfo& info); + void Stop(const CallbackInfo& info); + void PopupMenu(); + void BalloonClick(); + +private: + CTrayIcon m_tray; + std::thread* m_worker; + HWND m_hwnd; + std::vector m_menuItems; + TSFNOptString m_OnMenuItem; + TSFNOptString m_OnBalloonClick; +}; + +#endif //!__TRAY_ICON_H__ diff --git a/developer/src/server/src/win32/trayicon/addon-src/TrayWrapper.cpp b/developer/src/server/src/win32/trayicon/addon-src/TrayWrapper.cpp new file mode 100644 index 00000000000..5ff13e6a049 --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/TrayWrapper.cpp @@ -0,0 +1,8 @@ +#include "TrayIcon.h" + +Object Init(Env env, Object exports) { + CTrayIconContainer::Init(env, exports); + return exports; +} + +NODE_API_MODULE(addon, Init) \ No newline at end of file diff --git a/developer/src/server/src/win32/trayicon/addon-src/binding.gyp b/developer/src/server/src/win32/trayicon/addon-src/binding.gyp new file mode 100644 index 00000000000..b560112b45d --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/binding.gyp @@ -0,0 +1,23 @@ +{ + "targets": [ + { + "target_name": "addon", + "conditions": [ + ['OS=="win"', { + "sources": [ + "TrayWrapper.cpp", + "TrayIcon.cpp" + ], + "include_dirs": [ + " { + for (const cb of this.__itemCallbacks) { + cb(id); + } + }) + this.__nativeTray.SetTitle(this.__trayTitle); + if(this.__icon && "string" === typeof this.__icon){ + this.__nativeTray.SetIconPath(this.__icon); + } + this.__nativeTray.Start(); + } + item(cb) { + if ("function" === typeof cb) { + this.__itemCallbacks.push(cb); + } + } + balloon(title, text, timeout = 5000) { + return new Promise((resolve) => { + this.__nativeTray.ShowBalloon(title, text, timeout, () => { + resolve(); + }) + }); + } + exit() { + this.__nativeTray.Stop(); + } +} + +module.exports = WindowsTrayicon; diff --git a/developer/src/server/src/win32/trayicon/addon-src/package-lock.json b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json new file mode 100644 index 00000000000..d16aad04aa6 --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json @@ -0,0 +1,1905 @@ +{ + "name": "windows-trayicon", + "version": "3.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "windows-trayicon", + "version": "3.0.0", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32" + ], + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^3.1.0", + "node-gyp": "^10.2.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==" + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", + "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==" + }, + "node_modules/node-gyp": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", + "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "optional": true + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + }, + "dependencies": { + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + } + }, + "@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "requires": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + } + }, + "@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "requires": { + "semver": "^7.3.5" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true + }, + "abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==" + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "requires": { + "debug": "^4.3.4" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "requires": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "requires": { + "ms": "2.1.2" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + } + }, + "env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + }, + "exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, + "fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "requires": { + "minipass": "^7.0.3" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, + "ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "requires": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==" + }, + "isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==" + }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "requires": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "requires": { + "minipass": "^7.0.3" + } + }, + "minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "requires": { + "encoding": "^0.1.13", + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-addon-api": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", + "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==" + }, + "node-gyp": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", + "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", + "requires": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + } + }, + "nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "requires": { + "abbrev": "^2.0.0" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + } + }, + "proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==" + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "optional": true + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + }, + "socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "requires": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "requires": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + } + }, + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "requires": { + "minipass": "^7.0.3" + } + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + } + } + }, + "tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + } + } + }, + "unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "requires": { + "unique-slug": "^4.0.0" + } + }, + "unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "requires": { + "isexe": "^3.1.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/developer/src/server/src/win32/trayicon/addon-src/package.json b/developer/src/server/src/win32/trayicon/addon-src/package.json new file mode 100644 index 00000000000..54dcd5578a4 --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/package.json @@ -0,0 +1,23 @@ +{ + "name": "windows-trayicon", + "version": "3.0.0", + "description": "Create tray icon for windows", + "main": "index.js", + "os": [ + "win32" + ], + "scripts": { + "install": "node-gyp configure build" + }, + "author": "mce", + "license": "MIT", + "dependencies": { + "node-gyp": "^10.2.0", + "bindings": "^1.5.0", + "node-addon-api": "^3.1.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/mceSystems/node-windows-trayicon" + } +} diff --git a/developer/src/server/src/win32/trayicon/addon-src/stdafx.cpp b/developer/src/server/src/win32/trayicon/addon-src/stdafx.cpp new file mode 100644 index 00000000000..f1d63e0b438 --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/stdafx.cpp @@ -0,0 +1,8 @@ +// stdafx.cpp : source file that includes just the standard includes +// ConsoleApplication1.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file diff --git a/developer/src/server/src/win32/trayicon/addon-src/stdafx.h b/developer/src/server/src/win32/trayicon/addon-src/stdafx.h new file mode 100644 index 00000000000..b005a839def --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/stdafx.h @@ -0,0 +1,15 @@ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#include "targetver.h" + +#include +#include + + + +// TODO: reference additional headers your program requires here diff --git a/developer/src/server/src/win32/trayicon/addon-src/targetver.h b/developer/src/server/src/win32/trayicon/addon-src/targetver.h new file mode 100644 index 00000000000..87c0086de75 --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/developer/src/server/src/win32/trayicon/addon-src/test/index.js b/developer/src/server/src/win32/trayicon/addon-src/test/index.js new file mode 100644 index 00000000000..1f0a0326023 --- /dev/null +++ b/developer/src/server/src/win32/trayicon/addon-src/test/index.js @@ -0,0 +1,45 @@ +const WindowsTrayicon = require(".."); +const path = require("path"); +const fs = require("fs"); + +const myTrayApp = new WindowsTrayicon({ + title: "Trayicon Test", + icon: path.resolve(__dirname, "icon.ico"), + menu: [ + { + id: "item-1-id", + caption: "First Item" + }, + { + id: "item-2-id", + caption: "Second Item" + }, + { + id: "item-3-id-exit", + caption: "Exit" + } + ] +}); + +myTrayApp.item((id) => { + console.log(`Menu id selected=${id}`); + switch (id) { + case "item-1-id": { + console.log("First item selected..."); + break; + } + case "item-2-id": { + myTrayApp.balloon("Hello There!", "This is my message to you").then(() => { + console.log("Balloon clicked"); + }) + break; + } + case "item-3-id-exit": { + myTrayApp.exit(); + process.exit(0) + break; + } + } +}); + +process.stdin.resume() \ No newline at end of file diff --git a/developer/src/server/tsconfig.json b/developer/src/server/tsconfig.json index 5afa4ea2d06..c17961206f8 100644 --- a/developer/src/server/tsconfig.json +++ b/developer/src/server/tsconfig.json @@ -18,10 +18,6 @@ "noUnusedLocals": true, "allowJs": true, "lib": ["es2022"], - - "paths": { - "@keymanapp/developer-utils": ["../common/web/utils/index"], - } }, "include": [ "src/**/*.ts" diff --git a/developer/src/tike/build.sh b/developer/src/tike/build.sh index 1ab9dbe320d..fd81ca692d6 100755 --- a/developer/src/tike/build.sh +++ b/developer/src/tike/build.sh @@ -19,7 +19,7 @@ source "$KEYMAN_ROOT/resources/build/win/environment.inc.sh" WIN32_TARGET="$WIN32_TARGET_PATH/tike.exe" builder_describe_outputs \ - configure:project /resources/build/win/delphi_environment_generated.inc.sh \ + configure:project /developer/src/tike/xml/layoutbuilder/keymanweb-osk.ttf \ build:project /developer/src/tike/$WIN32_TARGET #------------------------------------------------------------------------------------------------------------------- @@ -31,6 +31,7 @@ function do_configure() { mkdir -p "$DEVELOPER_PROGRAM" cp "$KEYMAN_ROOT/common/schemas/kps/kps.xsd" "$DEVELOPER_PROGRAM" cp "$KEYMAN_ROOT/common/resources/fonts/keymanweb-osk.ttf" "$DEVELOPER_ROOT/src/tike/xml/layoutbuilder/keymanweb-osk.ttf" + run_in_vs_env rc icons.rc } function do_monaco_copy() { @@ -52,8 +53,6 @@ function do_monaco_copy() { pushd "$DEVELOPER_ROOT/src/tike/xml/app/lib/sentry" replaceVersionStrings_Mkver init.js.in init.js popd - - run_in_vs_env rc icons.rc } KEYMANCORE_DLL=keymancore-2.dll diff --git a/developer/src/tike/child/Keyman.Developer.UI.UframeWordlistEditor.pas b/developer/src/tike/child/Keyman.Developer.UI.UframeWordlistEditor.pas index 08dcdc92ee7..60fb9464c20 100644 --- a/developer/src/tike/child/Keyman.Developer.UI.UframeWordlistEditor.pas +++ b/developer/src/tike/child/Keyman.Developer.UI.UframeWordlistEditor.pas @@ -186,7 +186,15 @@ function TframeWordlistEditor.DoOpenFile: Boolean; begin if FileExists(FileName) then begin - FWordlist.LoadFromFile(FileName); + try + FWordlist.LoadFromFile(FileName); + except + on E:EEncodingError do + begin + ShowMessage('Error loading '+FileName+': '+E.Message); + Exit(False); + end; + end; FModified := False; end; UpdateData; diff --git a/developer/src/tike/child/UfrmDebug.pas b/developer/src/tike/child/UfrmDebug.pas index 85b2edc2810..66116034dc6 100644 --- a/developer/src/tike/child/UfrmDebug.pas +++ b/developer/src/tike/child/UfrmDebug.pas @@ -424,14 +424,21 @@ function TfrmDebug.ProcessKeyEvent(var Message: TMessage): Boolean; end; end; var - vkey, modifier: uint16_t; + scan, vkey, modifier: uint16_t; begin Assert(Assigned(FDebugCore)); // We always use the US virtual key code as a basis for our keystroke // mapping; the best way to do this is to extract the scan code from // the message data and work from that - vkey := MapScanCodeToUSVK((Message.LParam and $FF0000) shr 16); + + // Note: if a key event has a zero scan code, it has probably been + // injected, so we will do our best with it, using the VK as provided. + // See also #11978 + scan := (Message.LParam and $FF0000) shr 16; + if scan = 0 + then vkey := Message.WParam + else vkey := MapScanCodeToUSVK(scan); // We don't support the Right Shift modifier in Keyman; // we treat it as Left Shift, even though MapScanCodeToUSVK @@ -705,11 +712,21 @@ TMemoSelectionState = record UpdateDeadkeys; end; - procedure DoBackspace(BackspaceType: km_core_backspace_type); + procedure DoBackspace(BackspaceType: km_core_backspace_type; ExpectedValue: NativeUInt); var m, n: Integer; dk: TDeadKeyInfo; state: TMemoSelectionState; + + function AssertionMessage: string; + begin + Result := 'Assertion failed. Extra data: '+ + 'BackspaceType='+IntToStr(Ord(BackspaceType))+'; '+ + 'ExpectedValue=U+'+IntToHex(ExpectedValue, 4)+'; '+ + 'memo.SelStart='+IntToStr(memo.SelStart)+'; '+ + 'memo.SelLength='+IntToStr(memo.SelLength)+'; '+ + 'memo.Text='+Copy(memo.Text, 1, 256); + end; begin // Offset is zero-based, but string is 1-based. Beware! state := SaveMemoSelectionState; @@ -721,7 +738,7 @@ TMemoSelectionState = record // If the memo has a selection, we have given Core an empty context, // which forces it to emit a KM_CORE_BT_UNKNOWN backspace, which is // exactly what we want here. We just delete the selection - Assert(BackspaceType = KM_CORE_BT_UNKNOWN); + Assert(BackspaceType = KM_CORE_BT_UNKNOWN, AssertionMessage); memo.SelText := ''; RealignMemoSelectionState(state); Exit; @@ -730,8 +747,8 @@ TMemoSelectionState = record case BackspaceType of KM_CORE_BT_MARKER: begin - Assert(m >= 1); - Assert(memo.Text[m] = #$FFFC); + Assert(m >= 1, AssertionMessage); + Assert(memo.Text[m] = #$FFFC, AssertionMessage); dk := FDeadkeys.GetFromPosition(m-1); Assert(Assigned(dk)); dk.Delete; @@ -739,8 +756,8 @@ TMemoSelectionState = record end; KM_CORE_BT_CHAR: begin - Assert(m >= 1); - Assert(memo.Text[m] <> #$FFFC); + Assert(m >= 1, AssertionMessage); + Assert(memo.Text[m] <> #$FFFC, AssertionMessage); // Delete surrogate pairs if (m > 1) and Uni_IsSurrogate2(memo.Text[m]) and @@ -759,7 +776,7 @@ TMemoSelectionState = record while (m >= 1) and (memo.Text[m] = #$FFFC) do begin dk := FDeadkeys.GetFromPosition(m-1); - Assert(Assigned(dk)); + Assert(Assigned(dk), AssertionMessage); dk.Delete; Dec(m); end; @@ -776,13 +793,13 @@ TMemoSelectionState = record while (m >= 1) and (memo.Text[m] = #$FFFC) do begin dk := FDeadkeys.GetFromPosition(m-1); - Assert(Assigned(dk)); + Assert(Assigned(dk), AssertionMessage); dk.Delete; Dec(m); end; end; else - Assert(False, 'Unrecognised backspace type'); + Assert(False, AssertionMessage); // Unrecognised backspace type end; memo.Text := Copy(memo.Text, 1, m) + Copy(memo.Text, n+1, MaxInt); @@ -904,7 +921,7 @@ TMemoSelectionState = record KM_CORE_IT_CHAR: DoChar(Text); KM_CORE_IT_MARKER: DoDeadkey(dwData); KM_CORE_IT_ALERT: DoBell; - KM_CORE_IT_BACK: DoBackspace(km_core_backspace_type(dwData)); + KM_CORE_IT_BACK: DoBackspace(km_core_backspace_type(dwData), nExpectedValue); KM_CORE_IT_PERSIST_OPT: ; //TODO KM_CORE_IT_CAPSLOCK: ; //TODO KM_CORE_IT_INVALIDATE_CONTEXT: ; // no-op diff --git a/developer/src/tike/child/UfrmKeymanWizard.pas b/developer/src/tike/child/UfrmKeymanWizard.pas index d8a62db410e..baa906e8369 100644 --- a/developer/src/tike/child/UfrmKeymanWizard.pas +++ b/developer/src/tike/child/UfrmKeymanWizard.pas @@ -453,13 +453,13 @@ TfrmKeymanWizard = class(TfrmTikeEditor, IKMDPrintActions {TODO:, IKMDPrintPre procedure ConfirmSaveOfOldEditorWindows; procedure ConfirmSaveOfOldEditorWindow(FeatureID: TKeyboardParser_FeatureID; FModified: Boolean; const FOldFilename: string; DoSave: TProc; DoLoad: TProc); - procedure LoadFeature(ID: TKeyboardParser_FeatureID); + function LoadFeature(ID: TKeyboardParser_FeatureID): Boolean; function FeatureTab(kf: TKeyboardParser_FeatureID): TTabSheet; procedure InitFeatureTab(ID: TKeyboardParser_FeatureID); procedure FeatureModified(Sender: TObject); function SaveFeature(ID: TKeyboardParser_FeatureID): Boolean; procedure SelectTouchLayoutTemplate(APromptChange: Boolean); - procedure LoadTouchLayout; // I4034 + function LoadTouchLayout: Boolean; // I4034 function GetFontInfo(Index: TKeyboardFont): TKeyboardFontInfo; // I4057 procedure SetFontInfo(Index: TKeyboardFont; const Value: TKeyboardFontInfo); // I4057 @@ -1628,7 +1628,7 @@ procedure TfrmKeymanWizard.cmdFixupShiftStatesClick(Sender: TObject); FLayoutSetup := FOldLayoutSetup; end; -procedure TfrmKeymanWizard.LoadFeature(ID: TKeyboardParser_FeatureID); +function TfrmKeymanWizard.LoadFeature(ID: TKeyboardParser_FeatureID): Boolean; begin if FKeyboardParser.Features.ContainsKey(ID) then begin @@ -1651,7 +1651,8 @@ procedure TfrmKeymanWizard.LoadFeature(ID: TKeyboardParser_FeatureID); end; kfTouchLayout: begin - LoadTouchLayout; // I4034 + if not LoadTouchLayout then + Exit(False); end; else begin @@ -1667,6 +1668,7 @@ procedure TfrmKeymanWizard.LoadFeature(ID: TKeyboardParser_FeatureID); end; end; FFeature[ID].Modified := False; + Result := True; end; function TfrmKeymanWizard.SaveFeature(ID: TKeyboardParser_FeatureID): Boolean; @@ -2007,7 +2009,12 @@ function TfrmKeymanWizard.DoOpenFileFormat(FFormat: TTextFileFormat; FUseFormat: LoadSettings; for kf in FKeyboardParser.Features.Keys do - LoadFeature(kf); + begin + if not LoadFeature(kf) then + begin + Exit(False); + end; + end; if FKeyboardParser.IsComplex then // I4557 pagesLayout.ActivePage := pageLayoutCode; @@ -3163,18 +3170,17 @@ procedure TfrmKeymanWizard.TouchLayoutModified(Sender: TObject); // I3885 FFeature[kfTouchLayout].Modified := True; end; -procedure TfrmKeymanWizard.LoadTouchLayout; // I4034 +function TfrmKeymanWizard.LoadTouchLayout: Boolean; // I4034 begin if pagesTouchLayout.ActivePage = pageTouchLayoutDesign then begin - if not frameTouchLayout.Load(FFeature[kfTouchLayout].Filename, False, False) then - begin - pagesTouchLayout.ActivePage := pageTouchLayoutCode; - frameTouchLayoutSource.LoadFromFile(FFeature[kfTouchLayout].Filename, tffUTF8); - end; + Result := frameTouchLayout.Load(FFeature[kfTouchLayout].Filename, False, False); end else + begin frameTouchLayoutSource.LoadFromFile(FFeature[kfTouchLayout].Filename, tffUTF8); + Result := True; + end; end; procedure TfrmKeymanWizard.SaveTouchLayout; // I3885 diff --git a/developer/src/tike/debug/Keyman.System.Debug.DebugEvent.pas b/developer/src/tike/debug/Keyman.System.Debug.DebugEvent.pas index a9831392090..a5853faba08 100644 --- a/developer/src/tike/debug/Keyman.System.Debug.DebugEvent.pas +++ b/developer/src/tike/debug/Keyman.System.Debug.DebugEvent.pas @@ -33,6 +33,7 @@ TAIDebugKeyInfo = record TDebugEventActionData = class ActionType: km_core_action_type; dwData: Integer; + nExpectedValue: NativeUInt; Text: WideString; end; @@ -198,6 +199,7 @@ procedure TDebugEventList.Action_DeleteBack( event.EventType := etAction; event.Action.ActionType := KM_CORE_IT_BACK; event.Action.dwData := expected_type; + event.Action.nExpectedValue := expected_value; Add(event); end; diff --git a/developer/src/tike/debug/UfrmDebugStatus_Events.pas b/developer/src/tike/debug/UfrmDebugStatus_Events.pas index 4726b081058..29348374703 100644 --- a/developer/src/tike/debug/UfrmDebugStatus_Events.pas +++ b/developer/src/tike/debug/UfrmDebugStatus_Events.pas @@ -116,8 +116,6 @@ procedure TfrmDebugStatus_Events.AddActionEvent(action: TDebugEventActionData); begin case action.ActionType of KM_CORE_IT_EMIT_KEYSTROKE: AddItem('emit_keystroke', action); -// QIT_VSHIFTDOWN: AddItem('vshiftdown', action); -// QIT_VSHIFTUP: AddItem('vshiftup', action); KM_CORE_IT_CHAR: AddItem('char', action); KM_CORE_IT_MARKER: AddItem('marker', action); KM_CORE_IT_ALERT: AddItem('alert', action); diff --git a/developer/src/tike/dialogs/UfrmAboutTike.dfm b/developer/src/tike/dialogs/UfrmAboutTike.dfm index 3210c1e0e3a..d1146e625b0 100644 --- a/developer/src/tike/dialogs/UfrmAboutTike.dfm +++ b/developer/src/tike/dialogs/UfrmAboutTike.dfm @@ -504,12 +504,12 @@ inherited frmAboutTike: TfrmAboutTike 6082} end object lblCopyright: TLabel - Left = 125 - Top = 347 + Left = 156 + Top = 304 Width = 200 Height = 11 Anchors = [akLeft, akBottom] - Caption = 'Copyright SIL International. All Rights Reserved' + Caption = 'Copyright SIL Global. All Rights Reserved' Font.Charset = ANSI_CHARSET Font.Color = clBlack Font.Height = -9 @@ -544,50 +544,98 @@ inherited frmAboutTike: TfrmAboutTike ExplicitWidth = 634 end object Image1: TImage - Left = 55 + Left = 39 Top = 256 - Width = 52 - Height = 58 + Width = 107 + Height = 64 AutoSize = True Picture.Data = { - 0954506E67496D61676589504E470D0A1A0A0000000D49484452000000340000 - 003A08020000007010EDCC000000097048597300000B1100000B11017F645F91 - 0000001674455874536F667477617265007061696E742E6E657420342E314CEF - C5FF000003664944415478DA6364889DC9305801E3A8E3461D37EAB851C78D3A - 6ED471A38E1B75DCA000A38E1B75DCA8E348751C2323839182A8BD86A48A181F - 2323E3C3379F2F3E7A7BEEC19B979FBE232B6366022A6484B0FF31FCFFF7EF3F - 5C8A152C0707BFFF20C951E2384B55F149315646F2A2C8A683ACFFFFBF76ED99 - F6CDE7215C0E56E6CB6DA10AA2BC10EEB26377E267ED872B3E54ED67A52A0EE7 - 8AE72C7EFBE507A58EF337525891E5CCCECA8C55367DFEE1D907AEC31D77B53D - 4C11E6B8A5C76EC7CE4438EE488D3FB2E344B31751EA38097ECEEB9DE1FC9C6C - B81418D5ADBDF0F0EDC038AE23CCACCCDB0059E4EBCFDFB75F7CFAF3EF9FB420 - B700179B40C682DF7FFF0D80E38049F86C53908E8C105CE4D8ED97811377BEFD - F213226BA8207CF2CE2B78BAA6ABE37838586F76854BF273C1451CDB371FBCF1 - 1C974174775C67B8A400C2712E9D5BF75D7B3A281C072C980ED7F85928234CDC - 7BEDA9FF849DDF7EFE1978C7014182ADDABC14076491D3F75F27CC3A70FDD9FB - 81771C30D5EFAFF44536140880213761D7E5E60DE77EFEF93B908E030259219E - DDE5DE6A12FC68E2C0200C99B4EBF1BBAF03E9382010E1E5589CEEE8AA23C384 - 5A7FDD7CFEC1AA69E3FB6F3F07D2710CE0CC116DA53A39D69A978315597CC1E1 - 5BC9730EFC1F58C7410030723716B8AB4B0AC045BEFCF8AD5CB2FCF5E71F03EF - 38205095E0BFDC1ACAC6C20417899CB677E5C9BB83C271C0F83DD51064282F0C - 17A9597BBA6DD3F941E1381626C68BADA19A5288982D5E7EA27FC7255A398E2D - 710E234E4906B814331313B00595EEA459E2A98FACC4AE75D3915B2F48759C6C - C1D2775F7FFEFF8FAF390C94637CFFE527FE9085D340EBD958505A9D0FDF7ED1 - AA58F5FDD71F521DF7F9FBEF7F0C841BEA8CFF886ACD6307590B8FCCD8770DC2 - 26C9714402321DF7F7DFFF093B2F97AD38415E7B8E568E0356A9579FBC6F587F - 66EBC547C86986268E2B5A761C1EFBFF71A4038823FEFCFDF7E4FDD7F30FDF3C - 7DF715B36BC7C2CC54EAA527C4CD0EE19E7BF076F9893B70D96C176D05111E92 - 1D37E43BD5A38E1B75DCA8E306011875DC30701CB0FF1466AE2C2BC4FDF0CD97 - 35A7EEFD1F548E9311E276D39171D395D97BF5E9B2E377BEFEFC33881CC7C3C1 - 5AE8AEFBE0CD676531BEB6CDE77FFDF937881C0704DCECAC5A5202D79E7DF8FA - F33703385A670DB4937002000B26443581ED65730000000049454E44AE426082} + 0954506E67496D61676589504E470D0A1A0A0000000D494844520000006B0000 + 004008060000001DD489F9000000017352474200AECE1CE90000000467414D41 + 0000B18F0BFC6105000000097048597300002E2200002E2201AAE2DD92000009 + 584944415478DAED9C7D6C55E51DC77FF7BDEFF6B65494F262CB4A41A0142302 + CA26533A20CE85B0CD04D1A0C365739B714BD438CDD8CCD8B24463BA65CB92C5 + B89704131707D9FE603AC44DDBA1825005456094BE496BDFDBDBDEB6F7F5ECF9 + 5E7AEF3DE7DCE79C7BCE6DE1DE933EDFA494F39C7BCE7D389FE7F77B7EBFDFF3 + 1C6C246419D9B2DD0121E312B02C2401CB4212B02C2401CB42D2845572A0CDCB + 7EDD96ED0EF254E6B1D38A52774ADF4702D1F677EFA93C173FAE79F86F2BA212 + 2DC9767FCDC861B39DB8F0C79D83BC737AB0EE60BF9AB3DD799E567BDD749DDB + AE689B8C48746628D838B0EBA61FC5DBAAF7BCD62849F478B6FB6B4676BBADA1 + F54F5F7F9377CE72B02A0B9C5455EC54B431EBA1D343411A0F471B7DBBAB04AC + 5C509EC346B7947BC8AEEA75DB58982E4F84F157012B57B4B2D44D5E8FD2FDF9 + 4251B83F92AE1C0A58B920041537335872C1FDB50C0568322CC59B04AC6C0B9D + 5CCBDC5F8153D9DD2E7F983AC6C3F226012BDB9A9FEFA09A1297A22DC0A2BF93 + 83819875C924606553E8E02DF33C94EF5076F5C26888FAA622EA8F0B58D9D4BC + 3C072DBF4E6955136C8E6A615625A57E5CC0E20903BDDCE3A012969C62D4E338 + C49EDE68304A3D2C8C8E48342BAA2B7353894B19019E6756D59F6A559080A5B8 + 19BB6251A19316B0E4D4A17135E6934F4682310B9889105020AF52DFFB8301AE + 554102565C456C84D7329794EF485FFFC54385AB9A092F542A50B190AB9D457F + 9FF9C35A97085810E68E652C22B39BA8D3E3A1B68F878D5FA0D23A1658786403 + 03DC4FF4072818D51C0102D6F50C540DB328AD0F4B1A370AB1877ABC5FD365E9 + AA9859F19A3265123C1C88C6DCAB8EE636AC521640ACF4BAB91F1C61C10492D2 + B1503466012807A913D78F8682B1F366B5A4C8199B1BE5FA9F2F44BD9311BDCB + E62EAC3C87ADB99E8D6E27C7F7753217D7A9727115CC026B5561F645F6803FD7 + 7FC05CE17B8B5451E0FBCC4A43515D3B9D9BB0BCAFB4DDC12CAA591D3643B0A6 + 2ECE24CF735D3CA8E904E35C7F7D9EA273FE7094052CC17497CE4D585B8FF46C + 629EAD49DD8EAA01AA073C016C9D0A5626418677DAF5CAD53D11A14B63A17497 + 660CCB6EB3D19A6A2FDDBCA4940AF39C34E80BD0B9AED1D84F243A4B49A3917E + 64026BFB9B3D9BD849052CACC67EC8C271AD8417F3DB2AD543E6145BD30A7315 + E62CB9741261B932827567DD7CDAB7BB9EAA6E284A3937EA0FD237F6FF875ABB + C7126DAF3E73272D5D509C38BEFD878729184ECECBEFFDFA1ECA733B12C7F7EE + 3B4A5DFD7E43FFF6598375663818AB506889571AD272997AC23D702FB94EB141 + 6220C9360DEB6B1B17D18BDF5917B32C9ED8F554FFE83F686C3269D5FFFC4503 + D52E2C49F677EF2105AC4F5FDA411E57B2FF5F7EEA0DEAE81D37F46F9F15587A + EE2F2E5435AA554BEEADCC75F54C980B30B01C52E854E657C77AA78CA400A660 + 95157BE8ED17B6C5DC9E963AFBFCB4F9C9D7156D390D0B2E1BCB11813405BFEA + 621703A6B488730CF0407AF7A5D046165CC88B2470BF270702462E3505EBA186 + 2FD0BE07D628DACEB40DD36B4D1DE46396B4F4C6622AF03868FF2BA7159FC969 + 58062777EEB2BBD93C0B69C2860A653D10AE172ED8804CC17AE1DBB7D2CE4DC9 + 9D6A7D2353B4F989D7692AA43FB87216166C0985D3745605DD3ACF13DBD422D7 + 71FDF2508A78C55B0416E747D30F163209EB773FD840DBD755268E4F5F1AA61D + CFBD95F64B721696D10785BC79A32A3702A3637D53863A19172FFCC79CD76AC0 + B2C924AC27BFB98A1EFD6A6DE2381C89D28E9FFD9BCE768EE87E49CEC2C27E3C + 9F0137C6B308446F88E2CC88976399C8D54CC15AC61EF8E1FD5B1491A06F2244 + 3FFD4B0BFDFDDD2ECD2FC949585311A9C9E0C4CE0DDB8702513A3B6268AE49A8 + FCCAD668451B2A209DC6C27FD3A1FBB3F7D7D1DEAD3529EDCD1FF7D2D32F9FA2 + EEC18994733909AB6B3CDC64F021D16296C82E5625B2D878890D986684D5E715 + A519E76AA6613998FF7E9E051A3B6E5F9C726E682C407B9E6FA64F3A946E3127 + 61B50C069A8CAEF6A2805BA14A643329E25E6B58F187F0C0DD4BE9A9FB56A5E4 + 5C8810B73F7B8486C7931E22E7602D3FD4B98985EC4D6450BC2AF9C72CDC1E09 + 9A5B1EE16DE63451B29A512177514521FDF6FBEB69759557D1FE87C317E857AF + 9E491CE71C2CB3BB9BD4892C84907FCAE4CE19BC1DB25A15609870A733AEBA17 + E5BB188C2D54595E9068FBACDF4F5F7A2259C5B0342C17F3FBEB5589AC891291 + F26139ED545FAE8405570A976A40B3B244F2CCAE3A7A645B32E8407D70D9B70E + 26AAEF9686853ADE5A55D81E9C5ED2372A9402115162CE525741609C58CF4225 + 0335CAC9F0D5DD83F1F33D6B69F75DD589E310FBEEE58F1C8A41832C0D8BE7BA + 0C2E16C6AC7261A1836EC8D7DEDAA61612F54BCC2D72568D15B0B6FEF85F8DEC + 135C58BCAF2A2A70D11757CDA7EFDD5B4B2E4772C09C6E1B66C972B2B26116D6 + DE17FF9B920248893F946D2CE56B78E3975FB97AB0784101126924D45A42C563 + 6181932A0B8D43920B967B76388417E8E4CD0A5892243512CD7CA5F8277F6EA1 + 036F5D4A1C9B8565525B6C36DB51DE895981C50BB7F560012E2AF47999509209 + 968542B12C88997558C7CEF6C5722DF96AB1A561618E59A9B22CDE9C85CDA155 + 0C5299C7AE7B3F148D3D2A9028398109AE2D96A508AAED69B3060B70FEFA4E3B + ED3FF0114D0695B9A2A561F1EA82D0C5E9AD63F9EC3C1626F1EA8E9E2D217800 + 1424E2D8782387D2C7EE73613A2254BF577C22B92AA08075B0B9C3102C494A5A + CD4480459EDD3E3AFA610F5D1E98E07EFEB907EBA9725E32B4FFEE6FDE8B1581 + E3FAFD631BC8E5D41F903A6AB87BED82AB3767E1261B3879965161DE69F58515 + EB5EBCEABBBC228254C135BD454E967CCFCDDD4D669362BCEC06CB31230C68D4 + 1E614DBC60FCA622278B1495E59F4136F23147C152E39D97AD0C085846A4F526 + BD96FCCCD5614F873FAC5D8EC2AD96716A8E7261DE7FBF7F2ABEE34AC0322A24 + B5B5CCC26C3AC010C1A1D6870545A3D50D6C4D43559F775FD5265201CB8C50CD + C05289D7ED485819463FDC149259FC64B26712967B63FE9597F7B04F03D01174 + A8AAFA025626C28DE37BE443D76E47AB80652109581692806521590ED6AECD55 + 545AE4A6931707E9F8B901012BDB9DD292D361A7BDDB6A28128952479F9F8E9C + EA16B0B2DD293D6D5C51117BE3A4EDF3716AEF1D17B0B2DD2933CA1416CACAF5 + D9EEBC495D66B05AE307350F1FAC952469F14C6E78ADE5B0DB4E9E7F79E710EF + 9CF80F8D2D2401CB4212B02C2401CB4212B02C2401CB42FA3FF896A69B0C9D8C + 0A0000000049454E44AE426082} end object Label1: TLabel - Left = 125 + Left = 156 Top = 284 Width = 184 Height = 18 - Caption = 'Created by SIL International' + Caption = 'Created by SIL Global' Color = clWhite Font.Charset = ANSI_CHARSET Font.Color = clBlack diff --git a/developer/src/tike/main/UfrmMain.pas b/developer/src/tike/main/UfrmMain.pas index 44b58732450..8fa7a45c9b7 100644 --- a/developer/src/tike/main/UfrmMain.pas +++ b/developer/src/tike/main/UfrmMain.pas @@ -1477,8 +1477,12 @@ function TfrmKeymanDeveloper.OpenEditor(FFileName: string; frmClass: TfrmTikeEdi if n >= 0 then Result.ProjectFile := FGlobalProject.Files[n]; - (Result as frmClass).OpenFile(FFileName); LockWindowUpdate(0); + + if not (Result as frmClass).OpenFile(FFileName) then + begin + Result.Release; + end; end; procedure TfrmKeymanDeveloper.HelpTopic(s: string); diff --git a/developer/src/tike/oskbuilder/UframeTouchLayoutBuilder.pas b/developer/src/tike/oskbuilder/UframeTouchLayoutBuilder.pas index 4b0405a32f7..1bc2c5c8de3 100644 --- a/developer/src/tike/oskbuilder/UframeTouchLayoutBuilder.pas +++ b/developer/src/tike/oskbuilder/UframeTouchLayoutBuilder.pas @@ -90,7 +90,7 @@ TframeTouchLayoutBuilder = class(TTikeForm, IKMDEditActions) procedure cefCommand(Sender: TObject; const command: string; params: TStringList); procedure cefLoadEnd(Sender: TObject); - procedure RegisterSource; + procedure RegisterSources(const AState: string); procedure CharMapDragDrop(Sender, Source: TObject; X, Y: Integer); procedure CharMapDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); @@ -140,7 +140,7 @@ implementation xmldoc, Keyman.Developer.System.HelpTopics, - Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter, + Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter, CharacterDragObject, CharMapDropTool, @@ -238,15 +238,17 @@ procedure TframeTouchLayoutBuilder.UnregisterSources; modWebHttpServer.AppSource.UnregisterSource(FFilename+'#state'); end; -procedure TframeTouchLayoutBuilder.RegisterSource; +procedure TframeTouchLayoutBuilder.RegisterSources(const AState: string); begin if FFilename <> '' then modWebHttpServer.AppSource.RegisterSource(FFilename, FSavedLayoutJS); + if (FFileName <> '') and (AState <> '') then + modWebHttpServer.AppSource.RegisterSource(FFilename + '#state', AState, True); end; procedure TframeTouchLayoutBuilder.ImportFromKVK(const KVKFileName: string); // I3945 var - converter: TTouchLayoutToVisualKeyboardConverter; + converter: TVisualKeyboardToTouchLayoutConverter; FVK: TVisualKeyboard; FJS: string; begin @@ -268,7 +270,7 @@ procedure TframeTouchLayoutBuilder.ImportFromKVK(const KVKFileName: string); / end; end; - converter := TTouchLayoutToVisualKeyboardConverter.Create(FVK); + converter := TVisualKeyboardToTouchLayoutConverter.Create(FVK); try if not converter.Execute(FJS) then begin @@ -332,71 +334,85 @@ function TframeTouchLayoutBuilder.Load(const AFilename: string; ALoadFromTemplat end; UnregisterSources; - try - if ALoadFromString then + if ALoadFromString then + begin + FNewLayoutJS := AFilename; + FFilename := GetNextFilename; + end + else + begin + if ALoadFromTemplate or (AFileName = '') or not FileExists(AFileName) then begin - FNewLayoutJS := AFilename; + FBaseFileName := FTemplateFileName; FFilename := GetNextFilename; end else begin - if ALoadFromTemplate or (AFileName = '') or not FileExists(AFileName) then - begin - FBaseFileName := FTemplateFileName; - FFilename := GetNextFilename; - end - else - begin - FBaseFileName := AFileName; - FFilename := AFileName; - end; - - with TStringList.Create do - try - LoadFromFile(FBaseFileName, TEncoding.UTF8); - FNewLayoutJS := Text; - finally - Free; - end; + FBaseFileName := AFileName; + FFilename := AFileName; end; - FTouchLayout := TTouchLayout.Create; // I3642 + with TStringList.Create do try - if not FTouchLayout.Load(FNewLayoutJS) then + LoadFromFile(FBaseFileName, TEncoding.UTF8); + FNewLayoutJS := Text; + finally + Free; + end; + end; + + FTouchLayout := TTouchLayout.Create; // I3642 + try + if not FTouchLayout.Load(FNewLayoutJS) then + begin + FLastError := FTouchLayout.LoadError; // I4083 + FLastErrorOffset := FTouchLayout.LoadErrorOffset; // I4083 + FFilename := FLastFilename; + RegisterSources(FState); + Exit(False); + end + else + begin + if (FSavedLayoutJS <> '') and ALoadFromTemplate then begin - FLastError := FTouchLayout.LoadError; // I4083 - FLastErrorOffset := FTouchLayout.LoadErrorOffset; // I4083 - FFilename := FLastFilename; - Exit(False); + FOldLayout := TTouchLayout.Create; + try + FOldLayout.Load(FSavedLayoutJS); + if FTouchLayout.Merge(FOldLayout) + then FSavedLayoutJS := FTouchLayout.Save(False) + else FSavedLayoutJS := FNewLayoutJS; + finally + FOldLayout.Free; + end; end else - begin - if (FSavedLayoutJS <> '') and ALoadFromTemplate then - begin - FOldLayout := TTouchLayout.Create; - try - FOldLayout.Load(FSavedLayoutJS); - if FTouchLayout.Merge(FOldLayout) - then FSavedLayoutJS := FTouchLayout.Save(False) - else FSavedLayoutJS := FNewLayoutJS; - finally - FOldLayout.Free; - end; - end - else - FSavedLayoutJS := FNewLayoutJS; - end; - finally - FTouchLayout.Free; + FSavedLayoutJS := FNewLayoutJS; end; - finally - RegisterSource; - if (FFileName <> '') and (FState <> '') then - modWebHttpServer.AppSource.RegisterSource(FFilename + '#state', FState, True); + FTouchLayout.Free; end; + if (FFileName <> '') and modWebHttpServer.AppSource.IsSourceRegistered(FFileName) then + begin + // If two .kmn files are loaded which both reference the same + // .keyman-touch-layout file, it's safest to just block it. This is a rare + // scenario, as most keyboard projects have a single .kmn, and it usually + // indicates a project may be in a bit of chaos anyway. + ShowMessage( + 'The touch layout is already opened for editing in another keyboard '+ + 'editor. Please close the other keyboard editor before opening this one '+ + 'again.'); + + // We want to prevent this window unregistering the sources it doesn't own + // when it is destroyed immediately after this, which we can do by blanking + // the filename. + FFileName := ''; + Exit(False); + end; + + RegisterSources(FState); + try DoLoad; except diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas index da4b1952fe3..21c1540adab 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas @@ -1027,7 +1027,7 @@ function TProject.CanUpgrade: Boolean; var i: Integer; Path: string; - SourcePath: string; + SourcePath, SourceFile: string; begin if FOptions.Version = pv20 then begin @@ -1040,6 +1040,7 @@ function TProject.CanUpgrade: Boolean; // Things that block upgrade: // 1. invalid paths in Options // 2. contained file paths outside the project folder (primary files only) + // 3. primary source files in different folders if Options.BuildPath.Contains('$SOURCEPATH') then begin @@ -1053,6 +1054,7 @@ function TProject.CanUpgrade: Boolean; end; SourcePath := '?'; + SourceFile := ''; for i := 0 to Files.Count - 1 do begin @@ -1068,11 +1070,13 @@ function TProject.CanUpgrade: Boolean; begin if SourcePath = '?' then begin - SourcePath := ExtractFileDir(Path) + SourcePath := ExtractFileDir(Path); + SourceFile := Path; end else if not SameFileName(SourcePath, ExtractFileDir(Path)) then begin - FUpgradeMessages.Add('File '+Files[i].FileName+' is not in the same folder as at least one other source file. All primary source files must be in the same folder.'); + FUpgradeMessages.Add('File '+Files[i].FileName+' is not in the same folder as '+SourceFile+ + '. All primary source files must be in the same folder.'); Result := False; end; end; @@ -1083,7 +1087,8 @@ function TProject.CanUpgrade: Boolean; Continue; end; - FUpgradeMessages.Add('File '+Files[i].FileName+' is outside the project folder. All primary source files must be in the same folder as the project file, or in a subfolder.'); + FUpgradeMessages.Add('File '+Files[i].FileName+' is outside the project folder ('+ExtractFileDir(Filename)+ + '). All primary source files must be in the same folder as the project file, or in the same subfolder.'); Result := False; end; end; diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFile.pas b/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFile.pas index cbba63398df..cf775694c4c 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFile.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFile.pas @@ -77,6 +77,7 @@ implementation uses System.Variants, + System.Win.ComObj, Keyman.Developer.System.Project.Project, PackageInfo; @@ -117,6 +118,12 @@ procedure TkpsProjectFile.GetFileParameters; end; on E:DOMException do begin + // ignore errors in the xml; reduce metadata visible to the user + Exit; + end; + on E:EOleException do + begin + // ignore errors in interpreting xml; reduce metadata visible to the user Exit; end; end; diff --git a/developer/src/tike/tike.dpr b/developer/src/tike/tike.dpr index b4bcd29e1f4..d3a1935315c 100644 --- a/developer/src/tike/tike.dpr +++ b/developer/src/tike/tike.dpr @@ -225,7 +225,7 @@ uses Keyman.System.HttpServer.Base in '..\..\..\common\windows\delphi\web\Keyman.System.HttpServer.Base.pas', Keyman.Developer.System.HttpServer.AppSource in 'http\Keyman.Developer.System.HttpServer.AppSource.pas', Keyman.UI.FontUtils in 'main\Keyman.UI.FontUtils.pas', - Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter in '..\kmconvert\Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter.pas', + Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter in '..\kmconvert\Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter.pas', Keyman.Developer.System.Project.kmnProjectFileAction in 'project\Keyman.Developer.System.Project.kmnProjectFileAction.pas', Keyman.Developer.System.Project.kpsProjectFileAction in 'project\Keyman.Developer.System.Project.kpsProjectFileAction.pas', Keyman.Developer.UI.Project.UfrmNewProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas' {frmNewProjectParameters}, diff --git a/developer/src/tike/tike.dproj b/developer/src/tike/tike.dproj index b3cd83bdfef..b343fc80375 100644 --- a/developer/src/tike/tike.dproj +++ b/developer/src/tike/tike.dproj @@ -468,7 +468,7 @@ - + diff --git a/developer/src/tike/xml/help/contexthelp.xml b/developer/src/tike/xml/help/contexthelp.xml index 00e4cf525b4..66140f0ae97 100644 --- a/developer/src/tike/xml/help/contexthelp.xml +++ b/developer/src/tike/xml/help/contexthelp.xml @@ -2,6 +2,225 @@ + + + +

The store() statement lets you store a string of characters or keys in a buffer for + use elsewhere in the keyboard source file. The store() statement can be used with any() + and index() to reduce sets of similar rules down to a single rule.



+

Syntax

+
+ store(storeName) value +
+
+ + +

The &name store lets you give a more descriptive name to your keyboard than just the + file name. If &name isn't specified in the keyboard file, Keyman will use the filename + of the keyboard, excluding the extension, so the &name statement is optional.



+

Syntax

+
+ store(&name) "nameText" +
+
+ + +

The &version store allows Keyman to distinguish what version of Keyman the keyboard + was written for and handle it accordingly. The &version store is optional. For most keyboards, + it should be omitted. +



+ If the &version store is omitted, the compiler will infer the lowest version that + supports the full set of features used in the keyboard, and assign that version number + value to this store internally. The compiler will also report on the version it infers + in the compile process.



+

Syntax

+
+ store(&version) "version" +
+
+ + +

The begin rule tells Keyman which groups should be entry points to the keyboard.



+

Syntax

+
+ begin [entrypoint] > use(startGroup) +
+
+ + +

The use() statement tells Keyman to switch processing to a new group; after Keyman + has gone through the new group, and any other nested groups, it will return to the + previous one. The use() statement can be used with the match and nomatch rules; it will + work the same way.



+

Syntax

+
+ begin > use(groupName)

+ ... > use(groupName) +
+
+ + +

The outs() statement simply copies the store 'storeName' into the position in which it + has been inserted. This can be used in stores, in the context and the output. If the + store to be emitted is a single character or virtual key, it can also be used in the + key part of the rule.



+

Syntax

+
+ store(store1) ... outs(storeName) ...

+ ... outs(storeName) ... + ... > ...

+ ... > ... outs(storeName) ... +
+
+ + +

The any() statement will match any character that is in the store 'inputStore'. This + statement is only valid on the left side of a rule; the index() statement is used to match + again or output the character matched by the any() statement in the output. The any() + statement remembers the offset in the store where the match occurred for later use with + the index() statement.



+

Syntax

+
+ any(inputStore) +
+
+ + +

The index() statement works together with any() to map an array of characters in + 'inputStore' to a corresponding array in 'outputStore'. index() can be used in the context + and output sections of a rule. If used in the context section, the offset parameter must + be less than the offset of the index() statement in the context.



+

Syntax

+
+ any(inputStore) > index(outputStore,offset) +
+
+ + +

The group() statement tells Keyman to start a new set of rules. Keyman supports + three sorts of groups: key processing groups, read-only groups, and context processing + groups.



+

Syntax

+
+ group(groupName)

+ group(groupName) using keys

+ group(groupName) readonly +
+
+ + +

The context statement reproduces the context stored from the rule match, or a single + character of it, into the output. Use the context statement as much as possible as it is + significantly faster than using the index statement.



+

Syntax

+
+ ... > context

+ ... > context(offset)

+ ... context(offset) > ... +
+
+ + +

The beep statement produces a beep with the system speaker when the rule is matched. + In Keyman Desktop, beep will produce the sound specified by "Asterisk" in Windows Sounds + configuration.



+

Syntax

+
+ beep +
+
+ + +

The platform() statement allows rules to match based on the device on which the + Keyman keyboard is running.



+

Syntax

+
+ platform("platformConstraint") ... > ...

+ if(&platform = "platformConstraint") ... > ... +
+
+ + +

The nul statement has two purposes: in the output of a rule, it signifies deleting + context and keystroke, and at the start of the context it signifies that the context + buffer in the application must be empty (or no longer than the context of the rule) in + order for the rule to match.



+

Syntax

+
+ nul [...] [+ key] > output

+ [...] [+ key] > nul +
+
+ + +

The &copyright store allows a keyboard author to embed a copyright statement in + a keyboard when it is compiled.



+

Syntax

+
+ store(&copyright) "message" +
+
+ + +

This is a generic message, such as a shareware notice that you can display when the + keyboard is installed. This statement is optional.The MESSAGE statement is deprecated + and the &message store should be used instead.



+

Syntax

+
+ store(&message) "messageText" +
+
+ + +

The &targets store specifies the target platforms for which a keyboard should be built.



+

Syntax

+
+ store(&targets) "target [target ...]" +
+
+ + +

The &layoutfile store specifies a touch layout .JSON file to incorporate in the Keyman + keyboard. If a touch layout is not specified, then Keyman will build one from the visual + keyboard description file referenced in the &visualkeyboard store. If neither is specified, + then Keyman builds a touch layout based on the US English desktop keyboard layout. +



+

Syntax

+
+ store(&layoutfile) "layoutFilename" +
+
+ + +

The &keyboardversion store documents the version of the keyboard. Keyman uses this on + touch platforms to check for updated versions of the keyboard and update them + automatically.



+

Syntax

+
+ store(&keyboardversion) "version" +
+
+ + +

The bitmap can be in two different formats: .bmp or .ico. A .bmp file must be a 16x16 + image. Keyman supports .ico files with multiple image sizes, and the appropriate size + will be used if available.



+

Syntax

+
+ store(&bitmap) "filename" +
+
+ + +

The &visualkeyboard store specifies a .kvks file to reference in the compiled Keyman + keyboard. The .kvks (XML format) is compiled into a .kvk binary On Screen Keyboard which + needs to be distributed with the .kmx.



+

Syntax

+
+ store(&visualkeyboard) "visualkeyboardname" +
+
+ +

The Filter allows a user to reduce the number of characters displayed in the character map. The standard filter options used are by font name or block name.

@@ -12,63 +231,63 @@
-

The Filter allows a user to reduce the number of characters displayed in the +

The Filter allows a user to reduce the number of characters displayed in the character map. The standard filter options used are by font name or block name.

-

The filter format for a range is: [U+]XXXX-[U+]YYYY, where U+ is optional, +

The filter format for a range is: [U+]XXXX-[U+]YYYY, where U+ is optional, XXXX is the starting Unicode value and YYYY is the finishing Unicode value.

-

The filter format for a range is: [U+]XXXX-[U+]YYYY, where U+ is optional, +

The filter format for a range is: [U+]XXXX-[U+]YYYY, where U+ is optional, XXXX is the starting Unicode value and YYYY is the finishing Unicode value.

-

The filter format for a range is: [U+]XXXX-[U+]YYYY, where U+ is optional, +

The filter format for a range is: [U+]XXXX-[U+]YYYY, where U+ is optional, XXXX is the starting Unicode value and YYYY is the finishing Unicode value.

-

The Filter allows a user to reduce the number of characters displayed in the +

The Filter allows a user to reduce the number of characters displayed in the character map. The standard filter options used are by font name or block name.

-

">" placed at the start of an entry will only show characters in the currently - selected Character Map font. This is helpful when trying to determine which characters +

">" placed at the start of an entry will only show characters in the currently + selected Character Map font. This is helpful when trying to determine which characters a given font supports.



Example: >LAO



finds all characters with names starting in "LAO" in the current font

-

"<" placed at the start of an entry will search Unicode block names instead of - character names. This is helpful when searching for characters within related +

"<" placed at the start of an entry will search Unicode block names instead of + character names. This is helpful when searching for characters within related blocks



Example: <Thai



finds the Thai Unicode block

-

Using "*" in an entry serves as a wildcard for any number of places in that entry. - For example, searching for "greek*alpha" will find characters whose Unicode names begin - with the word "Greek" and contain the word "Alpha" any number of places later. - This is helpful when searching for characters that share a common element in +

Using "*" in an entry serves as a wildcard for any number of places in that entry. + For example, searching for "greek*alpha" will find characters whose Unicode names begin + with the word "Greek" and contain the word "Alpha" any number of places later. + This is helpful when searching for characters that share a common element in their names (e.g. capital).

-

Using "?" anywhere in an entry serves as a wildcard for that single place in the entry. - For example, searching for "s???e" will return both the SPACE and the SMILE characters, +

Using "?" anywhere in an entry serves as a wildcard for that single place in the entry. + For example, searching for "s???e" will return both the SPACE and the SMILE characters, among others.



Example: 1000-119F



-

finds all characters between U+1000 and U+119F (inclusive) - +

finds all characters between U+1000 and U+119F (inclusive) - the Myanmar alphabet in this case

@@ -78,7 +297,7 @@ -

"$" placed at the end of an entry will match from the end of a Unicode character name. +

"$" placed at the end of an entry will match from the end of a Unicode character name. This option works best when used with "*" or "?".



Example: LATIN * LETTER A$



finds only "a" and "A"

@@ -98,6 +317,18 @@ ======================================================================== --> + +

Keyboard Editor page consists of: +

    +
  • Details
  • +
  • Layout
  • +
  • Icon
  • +
  • On-Screen
  • +
  • Touch Layout
  • +
  • Build
  • +
+

+
@@ -135,6 +366,24 @@

This corresponds to the following source line:

store(&message) 'Here is a message about a keyboard'
+ + +

The Keyboard Version documents the version of the keyboard.

+

A keyboard version should be updated whenever there are changes to a keyboard. The good principles to follow are: +

    +
  • Increment the major version number for a + keyboard that has significant new functionality.
  • +
  • Increment the minor version number for changes that impact functionality but not + in a significant manner.
  • +
  • Optionally, use a third number for bug fixes.
  • +
+

+

This corresponds to the following source line:

+
store(&keyboardversion) '1.1.2'
+

Note: there is a difference between &keyboardversion, which documents the keyboard version, and &version, + which determines which version of Keyman a keyboard will run with.

+
+

In this field, enter information about the keyboard for your own reference. These comments will only be visible in the source file, and not to users of your keyboard.

@@ -178,6 +427,55 @@

Tests your KeymanWeb keyboard in an Internet Explorer embedded window

+ +

Touch Layout tab defines the functionality of the keyboard for mobile devices: +

    +
  • Top bar Controls
  • +
  • Keyboard area
  • +
  • Middle bar controls
  • +
  • Long press area
  • +
+

+
+ + +

Creates a package (.kps) file to prepare distribution of the keyboard

+
+ + +

The Features grid controls which additional file components are included in + the keyboard. Each of the features relates to a system store. Here are the file + components: +

    +
  • Embedded JavaScript
  • +
  • Embedded CSS
  • +
  • Web Help
  • +
  • Include Codes
  • +
  • Desktop On-Screen Keyboard (auto-included if Targets is any)
  • +
  • Touch-Optimised Keyboard (auto-included if Targets is any)
  • +
+ Icon will be automatically included when a new keyboard project is created. +

+
+ + +

This will open a selection dialog allowing you to choose a feature to add to + the keyboard project. Adding a feature will add an extra tab to the editor, + and add the corresponding store to the keyboard source

+
+ + +

Depending on what is included in the Feature Grid, you can select a feature from + the grid then click on Edit... This will take you to the corresponding tab and + let you make changes. +

+
+ + +

Removing a feature will not delete the component file, + but will just remove the store from the keyboard source. +

+
@@ -193,8 +491,40 @@

The toolbox allows for the changing of the colours, addition of text and shapes. It also allows for the moving of the icon around the canvas and also a preview of the icon is displayed.

+ + +

Click on a colour from the box to apply the colour onto the Keyboard's + icon. You can see the Foreground Color displays the colour you chose. To deselect + the colour, click on the X mark on the corner left of the colour box, or choose + another colour. +

+
+ +

This tab allows you to edit the visual representation of your keyboard + layout. The content on this tab is stored in the .kvks file associated with + your keyboard. The visual representation is used only in desktop and desktop + web; however if no touch layout is defined, this layout will be synthesized + into a touch layout automatically. +

+

An On-Screen keyboard is optional but in most keyboards is recommended. + The On-Screen keyboard may not always match the actual layout identically, + because you may choose to hide some of the details of encoding from the + interface presented to the user. +

+

+ This keyboard layout can also be printed or included in HTML or other documentation. + The editor allows you to export the file to HTML, PNG or BMP formats. +

+
+ + +

If this option is checked, when the Fill from layout button is clicked, + then keys without corresponding rules in the Layout will be filled with the + base layout character. +

+
@@ -327,33 +657,185 @@

The debugger can be used without the debug information by clicking on Test without debugger.

- + +

Enter the name for your new keyboard. Click on the Browse button to change the location of the new keyboard.

- + +
- -

You can include an image file that will be displayed to the left of the install details when the package is installed. This image should be 140 pixels wide and 250 pixels high.

+ +

A keyboard package is most likely to have 7 tabs: +

    +
  • Files
  • +
  • Keyboards
  • +
  • Lexical Models
  • +
  • Details
  • +
  • Shortcuts
  • +
  • Source
  • +
  • Build
  • +
+

- -

The readme file can be displayed after the installation of the keyboard package, but can also be accessed from the keyboard folder at a later time. The file must be loaded under the Files tab, add option before being able to be selected from the drop down list.

+ + + +

Usually, there is only one keyboard listed in the box, and by clicking on it + will show the keyboard information.

- - -

This option will allow you to create a folder on the Start menu when the keyboard package is installed.

+ +

When font files are added to the package, this dropdown tells the Keyman + apps which font to use when rendering the On Screen Keyboard touch keyboard. +

+
+ +

When font files are added to the package, this dropdown tells the Keyman + apps for iOS and Android which font to use in edit fields. It only applies + within the Keyman app and apps that support this functionality.

+
+ +

Each language listed here is a BCP 47 language tag and every keyboard must have a + minimum of one language. When Keyman installs the keyboard package, it will associate the + keyboard with the language(s) you select.

+
+ +

This button will open up the BCP 47 Tag window to add the keyboard's language tag, script tag, + region tag, and language name.

+
+ +

Select on a language then click on Remove to delete the language tag.

+
+ +

Click on Edit... then the BCP 47 Tag window will pop up once again with the language information.

+
+ + + +

This is the location of the Lexical Model (Predictive text) for the keyboard in the keyboard + package.

+
+ +

Add button brings up the “Select BCP 47 Tag” + window, thus allowing the input of a language for the Lexical Model (Predictive text).

+
+ +

By ticking this, it will set the text direction of the Lexical Model (Predictive text) to Right-to-left.

+
+ +

A short or long text related to the Lexical Model (Predictive text) is encouraged to be added here but it is optional.

+
+ + + +

Choose from the list to specifies which welcome file is suitable to display when they install the keyboard package. + However, this is optional, most keyboard package uses the default option which is (none).

+
+ +

Choose from the list if there is a License file to specified to the keyboard package. + However, this is optional, most keyboard package uses the default option which is (none) and we + only accept an open-license keyboard package.

+
+ +

Enter the name of the author or authors of the keyboard package.

+
+ +

Enter the copyright details of the keyboard package This information will be displayed with the version, author and message information when the package is installed.

+
+ +

Enter the contact email address for the keyboard package.

+
+ +

Package Name is the name that will be displayed when the package is installed. It should be a descriptive name, in any language, but remember that some applications may use a font that does not include the language you are writing the keyboard name in. Don't include a version number, help information or hotkey in the name.

+
+ +

The version number allows the user to check whether they have the latest version of the keyboard. The format should be 'major.minor[.subversion]'. Each number should be an integer, and you should avoid non-integer version strings. See help for more details.

+
+ +

The website details for the keyboard package if available.

+
+ +

Specifies an independant version for the Lexical Model.

+
+ +

By ticking this, the Lexical Model (Predictive text) version will receive a version bump alongside + the keyboard, even when the Model does not receive an update.

+
+ +

A keyboard package's description about the language, wonderful community, script, or any related information + that would help users once they install the package.

+
+ +

If a keyboard package is intended to replace an existing keyboard, or if there are related packages, + then the identifiers for these packages should be listed here.

+
+ +

Add the Package ID and specify Deprecated or Non-deprecated if a keyboard package is intended to + replace an existing keyboard, or if there are related packages.

+
+ +

Selects the package and click Edit... to add any changes to the current information.

+
+ +

A keyboard package's description about the language, community, script, or any related information + that would help users when they see once installing the package.

+
+ + + +

Enter the path of the start menu to be displayed when the keyboard package is installed.

+
+ +

This is a list of Start menu entries for the keyboard package.

The uninstall shortcut will be added automatically to the shortcut menu list when the package is installed.

- -

This option allows the user to add new files to the package.

+ +

This option will allow you to create a folder on the Start menu when the keyboard package is installed.

+
+ +

To add a new menu item to the list of shortcuts to be displayed in the Start menu folder created when the keyboard package is installed.

To delete the selected shortcut menu item from the Start menu folder.

+ +

The text to be displayed for the selected file as a menu item in the Start up folder that is created when the package is installed.

+
+ + + + + + +

Starts the Keyman Developer Web Server for the keyboard package. + This will list the various IP addresses and hostnames that Keyman Developer is listening on.

+
+ +

A list of available servers to test the keyboard package.

+
+ +

Starts your default browser with the selected address to allow testing of the keyboard package + directly.

+
+ +

Specifies the location of the Keyman MSI file. As of Keyman Developer 17, bundled executable package + installers for Keyman for Windows can be created using kmc, but cannot be created within the IDE.

+
+ + +

You can include an image file that will be displayed to the left of the install details when the package is installed. This image should be 140 pixels wide and 250 pixels high.

+
+ +

The readme file can be displayed after the installation of the keyboard package, but can also be accessed from the keyboard folder at a later time. The file must be loaded under the Files tab, add option before being able to be selected from the drop down list.

+
+ + +

This option allows the user to add new files to the package.

+

This allows for the insertion of a copyright symbol if needed.

@@ -371,8 +853,6 @@ installed correctly. If you can, try installing your package on several different machines.

- -

Displays the output path and filename of the file when the package is compiled.

@@ -400,9 +880,6 @@

This option will install the package on the computer. A message will be displayed as to the success of the install.

- -

To add a new menu item to the list of shortcuts to be displayed in the Start menu folder created when the keyboard package is installed.

-

Opens the source folder of the selected file.

@@ -428,33 +905,6 @@

File Type

Enter the details of the file type being added to the keyboard package.

- -

Enter the name of the author or authors of the keyboard package.

-
- -

Enter the copyright details of the keyboard package This information will be displayed with the version, author and message information when the package is installed.

-
- -

Enter the contact email address for the keyboard package.

-
- -

Package Name is the name that will be displayed when the package is installed. It should be a descriptive name, in any language, but remember that some applications may use a font that does not include the language you are writing the keyboard name in. Don't include a version number, help information or hotkey in the name.

-
- -

The version number allows the user to check whether they have the latest version of the keyboard. The format should be 'major.minor[.subversion]'. Each number should be an integer, and you should avoid non-integer version strings. See help for more details.

-
- -

The website details for the keyboard package if available.

-
- - -

The text to be displayed for the selected file as a menu item in the Start up folder that is created when the package is installed.

-
- - -

Start Menu Path

-

Enter the path of the start menu to be displayed when the keyboard package is installed.

-

This will display all the files that have been added to the keyboard package. It allows for the addition and removal of files, the entering of file details and the editing of any of the files listed if the appropriate editor is available.

@@ -463,6 +913,7 @@
+

The Project Manager allows you to manage all the files related to a keyboard layout in a single location.

@@ -616,18 +1067,18 @@ -

The name of the developer of the keyboard. This is either your full name or +

The name of the developer of the keyboard. This is either your full name or the organization you're creating a model for.

-

We recommend the name of the language, dialect, or community that this model is +

We recommend the name of the language, dialect, or community that this model is intended for. The name must be written in all the Latin letters or Arabic numerals.

-

Who owns the rights to this model and its data? Typically, - you can use the automatically generated default value: © 2024 Your Full Name or +

Who owns the rights to this model and its data? Typically, + you can use the automatically generated default value: © 2024 Your Full Name or Your Organization.

@@ -636,18 +1087,18 @@
-

If this is the first time you've created a lexical model for you language, you should - leave the version as 1.0. Otherwise, your version number must conform to the following +

If this is the first time you've created a lexical model for you language, you should + leave the version as 1.0. Otherwise, your version number must conform to the following rules: A version string made of major revision number.minor revision number.

-

Specifies the default BCP 47 language tags which will be added to the package +

Specifies the default BCP 47 language tags which will be added to the package metadata and project metadata.

-

To add a language tag, click the Add button to bring up the “Select BCP 47 Tag” +

To add a language tag, click the Add button to bring up the “Select BCP 47 Tag” dialog box.

@@ -669,7 +1120,7 @@ -

An Author ID is a unique identifier used to distinguish you from others +

An Author ID is a unique identifier used to distinguish you from others who have the same or similar names.

@@ -678,13 +1129,13 @@ -

Enter a unique name of the model. You can use the name of the language, dialect, +

Enter a unique name of the model. You can use the name of the language, dialect, or community that this model is intended for.

-

Keyman automatically generates a model ID for you, given all the - information already filled out. Model ID helps Keyman sorts and organizes +

Keyman automatically generates a model ID for you, given all the + information already filled out. Model ID helps Keyman sorts and organizes different lexical models.

@@ -701,17 +1152,18 @@
+
-

Wordlist tabs have two views: Design, and Code. Changes to one view are reflected - immedaitely in the other view. Wordlist files should be stored in UTF-8 encoding +

Wordlist tabs have two views: Design, and Code. Changes to one view are reflected + immedaitely in the other view. Wordlist files should be stored in UTF-8 encoding (preferably without BOM), and tab-separated format.

-

Every line of the tab-separated format file is shown here, and can be edited directly. - For most wordlists, it will be more effective to use an external dictionary tool, - such as SIL Fieldworks or SIL PrimerPrep to generate the wordlist from a text corpus, +

Every line of the tab-separated format file is shown here, and can be edited directly. + For most wordlists, it will be more effective to use an external dictionary tool, + such as SIL Fieldworks or SIL PrimerPrep to generate the wordlist from a text corpus, and use this tab just to preview the contents of the file.

@@ -720,24 +1172,27 @@ -

The Sort by frequency button has no effect on the functioning of the wordlist, +

The Sort by frequency button has no effect on the functioning of the wordlist, but can help you, the editor, by showing more common words earlier in the list.

+
-

Editor windows in Keyman Developer supports standard Windows editing keystrokes. - Many file formats, including .kmn, .kps, .xml, .html, .js and .json, support syntax - highlighting. The text editor in Keyman uses the Monaco component from Visual Studio Code, +

Editor windows in Keyman Developer supports standard Windows editing keystrokes. + Many file formats, including .kmn, .kps, .xml, .html, .js and .json, support syntax + highlighting. The text editor in Keyman uses the Monaco component from Visual Studio Code, so all the functionality available in that editor is also available here.

+
+
-

Attempt to identify the fonts on your system that will support the - characters. You can quickly change fonts by clicking on a font name in the grid of +

Attempt to identify the fonts on your system that will support the + characters. You can quickly change fonts by clicking on a font name in the grid of identified fonts.

@@ -746,49 +1201,51 @@
+
-

The message window appears at the bottom of the screen, or floating in a toolbar - window. It contains a list of error and warning messages returned from a compilation +

The message window appears at the bottom of the screen, or floating in a toolbar + window. It contains a list of error and warning messages returned from a compilation session. You can undock and dock the window by dragging its title bar.

+
-

The debugger input window is used for typing input to test the keyboard. - In the top half of this window, input you type while testing your keyboard will be - displayed, exactly the same as in use, with one exception: deadkeys will be shown +

The debugger input window is used for typing input to test the keyboard. + In the top half of this window, input you type while testing your keyboard will be + displayed, exactly the same as in use, with one exception: deadkeys will be shown visually with an OBJ symbol.

-

The lower half of the window shows a grid of the characters to the virtual left - of the insertion point, or the selected characters if you make a selection. Deadkeys - will be identified in the grid. The grid will show characters in right-to-left scripts - in backing store order, from left to right. If there are more characters in your text +

The lower half of the window shows a grid of the characters to the virtual left + of the insertion point, or the selected characters if you make a selection. Deadkeys + will be identified in the grid. The grid will show characters in right-to-left scripts + in backing store order, from left to right. If there are more characters in your text than can fit on the screen, then only those that fit will be shown in the grid.

-

The idea in regression testing is to record a sequence of keystrokes and the - output the keyboard produced, in order to test for the same behaviour when you +

The idea in regression testing is to record a sequence of keystrokes and the + output the keyboard produced, in order to test for the same behaviour when you make changes to the keyboard.

-

Use Start Log/Stop Log to record the input and output. You can then use Start Test - to run the test again, or go the Options menu to clear the log, or save or load a test, +

Use Start Log/Stop Log to record the input and output. You can then use Start Test + to run the test again, or go the Options menu to clear the log, or save or load a test, or use the batch mode to run several tests in a row.



-

If the output produced while running a test is different to that stored when recording it, - Keyman will halt the test on the line where the failure occurred, and activate +

If the output produced while running a test is different to that stored when recording it, + Keyman will halt the test on the line where the failure occurred, and activate Single Step mode.

-

In the Options menu, you can clear the log, save or load a test, +

In the Options menu, you can clear the log, save or load a test, or use the batch mode to run several tests in a row.

@@ -800,43 +1257,45 @@
-

This window shows the current keystroke state, and the sequence of +

This window shows the current keystroke state, and the sequence of keystrokes that were typed to arrive at this state.

-

This shows the elements that make up rule currently being processed: the context, - the key, and also what the output will be. If the rule uses stores, the contents of +

This shows the elements that make up rule currently being processed: the context, + the key, and also what the output will be. If the rule uses stores, the contents of the store will be shown in the right-hand column, with the matched letter in red.

-

Here all the lines that have been processed to this point are shown in a list. - You can double-click on any entry in the list to display the line in the +

Here all the lines that have been processed to this point are shown in a list. + You can double-click on any entry in the list to display the line in the keyboard source.

-

This lists all the deadkeys that are currently in the context. - You can select one from the list to see it highlighted in the debug input box. - This information can also be seen in the character grid in the lower half of +

This lists all the deadkeys that are currently in the context. + You can select one from the list to see it highlighted in the debug input box. + This information can also be seen in the character grid in the lower half of the debugger input window.

+
-

The About dialog displays copyright and registration information for Keyman Developer, +

The About dialog displays copyright and registration information for Keyman Developer, and has a link to the Keyman website.

+

This dialog lets you check the virtual key code for any key combination (except Window reserved key combinations such as Alt + Tab). You can then insert the virtual key code into the last active edit window at the current cursor position.



@@ -851,4 +1310,155 @@

To close the dialog, click the Close button or press Shift + Esc.

-
+ + +
+ +

The only required option is the Language tag, which is an ISO 639-1 or ISO 639-3 code. + ISO 639-1 tags are a two-letter code. ISO 639-3 tags are a three-letter code. First, try to find your + language on the list of two-letter ISO 639-1 codes.

+

If you can't find a two-letter code, you'll need to find the closest three-letter code. You can use Glottolog + to search for your language, and it will give you an appropriate code.

+

The Language tag is conventionally written in lower case.

+
+ +

The Script tag allows you to specify the writing system used in your language model or keyboard. + If your language only uses one writing system, omit the Script subtag. he Script subtag is conventionally + written in title case - first letter capitalized.

+
+ +

The Region tag allows you to specify the region your language or + dialect is spoken in. If your language is only spoken in one region, omit the Region subtag. + Alphabetic region will show up in upper case once the region tag is selected.

+
+ +

This is the BCP 47 Code forms from the language, script, and region tag which were previously + selected.

+
+ +

If lefts as default, a language name will be assigned automatically. Feel free to change the name + if you must. +

+
+ +

Reset the Language name to what it was assigned to.

+
+ +

Once everything is set, click OK to confirm.

+
+ +

Exits out of the "Select BCP 47 Tag".

+
+ +

Learn more about the BCP 47 Tag over at w3.org.

+
+
+ + +
+ +

The descriptive name of the keyboard. This will be set in the &Name store in the keyboard, + in the package name, and where appropriate in documentation and metadata.

+
+ +

A brief or long description about the keyboard's functionality, background, script, language, community, + or any information related to showcase to users.

+
+ +

The name of the developer of the keyboard. This is either your full name or + the organization you're creating the keyboard for.

+
+ +

This field should contain the word "Copyright", the copyright symbol "©", and the full name of the rights + owner. Do not put the year of copyright in this field (see Full Copyright). Typically, + you can use the automatically generated default value: "Copyright © Your Full Name or + Your Organization".

+

A copyright string for the keyboard. This will be set in the &Copyright store in the keyboard, + in the package metadata, and where appropriate in documentation and metadata.

+
+ +

Who owns the rights to this keyboard? This field should + contain the word "Copyright", the copyright symbol "©", the first year of copyright, + and the full name of the rights owner. Do include the year of copyright in this field. + Typically, you can use the automatically generated default value: + "Copyright © Current-Year Your-Full Name or Your Organization".

+
+ +

If this is the first time you've created a keyboard for the language, you should + leave the version as 1.0. Otherwise, your version number must conform to the following + rules: A version string made of major revision number.minor revision number.

+
+ +

+ Specifies the default deployment targets for the keyboard, set in the &Targets store in the keyboard, + and controls the files added to the package initially. This also is reflected in documentation and metadata. +

+
+ +

+ Specifies the base path where the project folder will be created. The project folder name will be the keyboard ID. + If the folder already exists, then you will be prompted before Keyman Developer overwrites files inside it. +

+
+ +

+ Specifies a different path to store the project folder. +

+
+ +

+ The base filename of the keyboard, project and package. This must conform to the Keyman + keyboard identifier rules, using the characters a-z, 0-9 and _ (underscore) only. +

+
+ +

+ A Keyboard Project (.kpj) file is store inside the base path, this cannot be changed to anywhere else. +

+
+ +

This button will open up the BCP 47 Tag window to add the keyboard's language tag, script tag, + region tag, and language name.

+
+ +

+ Click on Edit... to open up the BCP 47 Tag window with the language information. +

+
+ +

+ Delete the selected language. +

+
+ +

+ Once everything is ready, click OK and enjoy developing the keyboard. +

+
+ +

Click Cancel to close the dialog and reset the process entirely.

+
+
+ + +
+ +

+ Creates a new Keyman Keyboard, LDML Keyboard, or a Wordlist Lexical Model, + or by importing from another source. Details are written on individual icons; clicking on + them shows a detailed explanation of each keyboard project. +

+
+ +

+ Once decided on a project to create, click OK will take you to the next step. +

+
+ +

+ Cancels creating a project. +

+
+
+ + \ No newline at end of file diff --git a/developer/src/tike/xml/help/help.xsl b/developer/src/tike/xml/help/help.xsl index 2416fdff16f..5f1eb19fa0f 100644 --- a/developer/src/tike/xml/help/help.xsl +++ b/developer/src/tike/xml/help/help.xsl @@ -53,6 +53,7 @@ +
Context help is not available for . @@ -66,6 +67,18 @@ + +
+ language/reference/ +
+ + +
+
+
- diff --git a/developer/src/tike/xml/layoutbuilder/builder.xsl b/developer/src/tike/xml/layoutbuilder/builder.xsl index 3b75cd81a3e..d43d90c5745 100644 --- a/developer/src/tike/xml/layoutbuilder/builder.xsl +++ b/developer/src/tike/xml/layoutbuilder/builder.xsl @@ -271,6 +271,10 @@
+
+

All available platforms have already been added.

+
+
diff --git a/developer/src/tike/xml/layoutbuilder/platform-controls.js b/developer/src/tike/xml/layoutbuilder/platform-controls.js index 965513e69ce..bccb99717c5 100644 --- a/developer/src/tike/xml/layoutbuilder/platform-controls.js +++ b/developer/src/tike/xml/layoutbuilder/platform-controls.js @@ -5,14 +5,20 @@ $(function() { for (var platform in KVKL) { platforms[platform] = 0; } + let nPlatforms = 0; for (platform in platforms) { if (platforms[platform]) { var opt = document.createElement('option'); $(opt).text(platform); $('#selAddPlatform').append(opt); + nPlatforms++; } } - $('#addPlatformDialog').dialog('open') + if(nPlatforms == 0) { + $('#addPlatformDialogNoPlatformsToAdd').dialog('open') + } else { + $('#addPlatformDialog').dialog('open') + } }); $('#btnDelPlatform').click(function () { @@ -69,6 +75,22 @@ $(function() { } }); + // + // Platform dialog -- no platforms to add + // + + $('#addPlatformDialogNoPlatformsToAdd').dialog({ + autoOpen: false, + height: 150, + width: 350, + modal: true, + buttons: { + "OK": function () { + $(this).dialog('close'); + } + } + }); + // // Platform Properties Dialog // diff --git a/developer/src/tike/xml/project/globalwelcome.xsl b/developer/src/tike/xml/project/globalwelcome.xsl index aecd70d90ff..5ef8c4a7e1f 100644 --- a/developer/src/tike/xml/project/globalwelcome.xsl +++ b/developer/src/tike/xml/project/globalwelcome.xsl @@ -18,8 +18,8 @@

Version

-
Created by SIL International - +
Created by SIL Global +
diff --git a/developer/src/tike/xml/project/project.css b/developer/src/tike/xml/project/project.css index c9486e4ac6e..cd29ef360f7 100644 --- a/developer/src/tike/xml/project/project.css +++ b/developer/src/tike/xml/project/project.css @@ -281,7 +281,8 @@ k\:menuitem.down { #header-sil img { vertical-align: top; - margin-left: 4px; + margin-left: -6px; + margin-top: 7px; } #header-sil > div { diff --git a/developer/src/tike/xml/project/sil.png b/developer/src/tike/xml/project/sil.png index 08a4291d5e8..b65c2a4e086 100644 Binary files a/developer/src/tike/xml/project/sil.png and b/developer/src/tike/xml/project/sil.png differ diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 7902ce7a682..dcf54ce78cc 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -3,30 +3,29 @@ # # @darcywong00 @mcdurdin @ermshiperete @rc-swag @SabineSIL @sgschantz -/android/ @darcywong00 @sgschantz +/android/ @darcywong00 @sgschantz -/common/ @mcdurdin @rc-swag -/core/ @mcdurdin @rc-swag -/common/lexical-model-types/ @jahorton @mcdurdin -/common/models/ @jahorton @mcdurdin -/common/predictive-text/ @jahorton @mcdurdin -/common/schemas/ @mcdurdin @jahorton -/common/test/ @mcdurdin @ermshiperete -/common/web/ @jahorton @mcdurdin +/common/ @mcdurdin @rc-swag +/core/ @mcdurdin @rc-swag +/common/lexical-model-types/ @jahorton @mcdurdin +/common/schemas/ @mcdurdin @jahorton +/common/test/ @mcdurdin @ermshiperete +/common/web/ @jahorton @mcdurdin -/developer/ @mcdurdin @darcywong00 -/docs/ @mcdurdin @jahorton -/ios/ @sgschantz @jahorton -/linux/ @ermshiperete @darcywong00 -/mac/ @sgschantz @SabineSIL +/developer/ @mcdurdin @darcywong00 +/docs/ @mcdurdin @jahorton +/ios/ @sgschantz @jahorton +/linux/ @ermshiperete @darcywong00 +/mac/ @sgschantz @SabineSIL -/oem/firstvoices/android/ @darcywong00 @sgschantz -/oem/firstvoices/common/ @mcdurdin @rc-swag -/oem/firstvoices/ios/ @sgschantz @jahorton -/oem/firstvoices/windows/ @rc-swag @ermshiperete -/resources/ @mcdurdin @jahorton +/oem/firstvoices/android/ @darcywong00 @sgschantz +/oem/firstvoices/common/ @mcdurdin @rc-swag +/oem/firstvoices/ios/ @sgschantz @jahorton +/oem/firstvoices/windows/ @rc-swag @ermshiperete +/resources/ @mcdurdin @jahorton # Web is currently shared between Eberhard and Joshua: -/web/ @ermshiperete @jahorton +/web/ @ermshiperete @jahorton +/web/src/engine/predictive-text/ @jahorton @mcdurdin -/windows/ @rc-swag @ermshiperete +/windows/ @rc-swag @ermshiperete diff --git a/docs/build/linux-ubuntu.md b/docs/build/linux-ubuntu.md index 00aaeef0b30..a45d9390ba5 100644 --- a/docs/build/linux-ubuntu.md +++ b/docs/build/linux-ubuntu.md @@ -53,23 +53,43 @@ sudo mk-build-deps --install linux/debian/control #### Node.js -Node.js v18 is required for Core builds, Web builds, and Developer command line tool builds and usage. +Node.js is required for Core builds, Web builds, and Developer command line tool builds and usage. -Follow the instructions on the [NodeSource Distributions](https://github.com/nodesource/distributions#table-of-contents) page. +Our recommended way to install node.js is with +[nvm](https://github.com/nvm-sh/nvm). This makes it easy to switch between +versions of node.js. + +See [node.md](node.md) for more information. #### Emscripten -You'll also have to install `emscripten` (version 3.1.44 is known to work): +You'll also have to install `emscripten`: ```shell git clone https://github.com/emscripten-core/emsdk.git cd emsdk -./emsdk install 3.1.44 -./emsdk activate 3.1.44 -export EMSCRIPTEN_BASE=$(pwd)/upstream/emscripten +./emsdk install 3.1.58 +./emsdk activate 3.1.58 +export EMSCRIPTEN_BASE="$(pwd)/upstream/emscripten" +echo "export EMSCRIPTEN_BASE=\"$EMSCRIPTEN_BASE\"" >> .bashrc ``` -**NOTE:** Don't put EMSDK on the path, i.e. don't source `emsdk_env.sh`. +> ![WARNING] +> Don't put EMSDK on the path, i.e. don't source `emsdk_env.sh`. +> +> Emscripten very unhelpfully overwrites `JAVA_HOME`, and adds its own +> versions of Python, Node and Java to the `PATH`. For best results, restart +> your shell after installing Emscripten so that you don't end up with the +> wrong versions. + +**Optional environment variables**: + +To let the Keyman build scripts control the version of Emscripten installed on +your computer: + +```shell +export KEYMAN_USE_EMSDK=1 +``` ## Keyman Core diff --git a/docs/build/macos.md b/docs/build/macos.md index 673739379e3..25b0c1f82ae 100644 --- a/docs/build/macos.md +++ b/docs/build/macos.md @@ -77,16 +77,52 @@ PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH" ## KeymanWeb Dependencies -* node.js 18+, emscripten 3.1.46 or later, openjdk 8 +* node.js, emscripten 3.1.46 or later -```shell -brew install node emscripten openjdk@8 +### node.js + +Our recommended way to install node.js is with +[nvm](https://github.com/nvm-sh/nvm). This makes it easy to switch between +versions of node.js. + +Alternatively, you can install node.js with `brew`. + +See [node.md](node.md) for more information. + +### emscripten + +The recommended way to install emscripten is with the official method, as that +then allows the build scripts to select the appropriate version automatically. + +To install emscripten, `cd` to an appropriate path, and then: + +```bash +git clone https://github.com/emscripten-core/emsdk +cd emsdk +emsdk install 3.1.58 +emsdk activate 3.1.58 +export EMSCRIPTEN_BASE="$(pwd)/upstream/emscripten" +echo "export EMSCRIPTEN_BASE=\"$EMSCRIPTEN_BASE\"" >> .bashrc ``` -Note: if you install emscripten with brew on macOS, only emscripten binaries are -added to the path via symlinks. This makes it reasonably safe to have emscripten -on the path, unlike on other platforms where emscripten also ends up adding its -versions of node, python, and other binaries to the path. +You will want to add `EMSCRIPTEN_BASE` to your .bashrc. + +> ![WARNING] +> Don't put EMSDK on the path, i.e. don't source `emsdk_env.sh`. +> +> Emscripten very unhelpfully overwrites `JAVA_HOME`, and adds its own +> versions of Python, Node and Java to the `PATH`. For best results, restart +> your shell after installing Emscripten so that you don't end up with the +> wrong versions. + +**Optional environment variables**: + +To let the Keyman build scripts control the version of Emscripten installed on +your computer: + +```shell +export KEYMAN_USE_EMSDK=1 +``` ## Keyman for iOS Dependencies diff --git a/docs/build/node.md b/docs/build/node.md new file mode 100644 index 00000000000..da0a96e1fd4 --- /dev/null +++ b/docs/build/node.md @@ -0,0 +1,71 @@ +# Node use in the Keyman project + +For a given release cycle, we select and pin to a specific version of node.js, +and try to use that throughout. The version in use can be found in +`package.json/engines.node`. + +If we encounter a blocking issue, we will upgrade to a known-good release, but +this is exceptional. + +Generally, as a developer, if you use the same major version as is found in +package.json, you probably won't have significant issues. However, if you are +switching between stable branches and alpha branches, you may find the required +node version changes. + +## Automatic node version selection + +The Keyman build system can be configured to manage the node.js version for you. +This does have several caveats, so it is not enabled by default. + +When this is enabled, the build system will download the required node version +and make it available on the PATH, and it will also stop the build if the node +version mismatches. This happens in `build.sh configure` steps for any +Typescript/Javascript project. + +To enable automatic node version selection, add the variable `KEYMAN_USE_NVM=1` +to your environment. + +### Caveats + +nvm (for macOS/Linux) and nvm-windows use somewhat different paradigms, so the +caveats vary per platform. + +### Caveats on macOS/Linux + +On macOS/Linux, nvm is provided as a shell function that modifies the current +environment PATH to make a specific node version available -- and so this does +not affect unrelated processes. + +1. You must use [nvm](https://github.com/nvm-sh/nvm) (macOS/Linux) to install + and manage Node versions. +2. The Keyman build environment will create a symlink at `~/.keyman/node` + pointing to the version of node selected by nvm. +3. You should add `$HOME/.keyman/node` to the front of your `PATH` variable, + e.g. in `~/.bash_profile`. This means that the node version _will_ be set for + the entire system, unlike with standard nvm usage. This allows build scripts + to run without calling `nvm` for each invocation. + +### Caveats on Windows + +On Windows, nvm-windows creates a symlink to the current node version, and +this symlink is what is on the system PATH, so it affects all processes on the +system. + +1. You must use [nvm-windows](https://github.com/coreybutler/nvm-windows) to + install and manage Node versions. +2. By default on Windows, creating symlinks requires elevation. While + nvm-windows does this for you, it can be irritating to have a build script + pause for elevation. In Local Group Policy Editor, `Computer Configuration`, + `Windows Settings`, `Security Settings`, `Local Policies`, + `User Rights Assignment`, you can add your username to the + `Create symbolic links` policy (and reboot), to avoid elevation. + +## Build Agents + +The Keyman build agents use nvm as described above, including the caveats. + +## Implementation + +See `_select_node_version_with_nvm()` in +`/resources/build/shellHelperFunctions.sh`, and +`/resources/build/_builder_nvm.sh`. \ No newline at end of file diff --git a/docs/build/windows.md b/docs/build/windows.md index 99f1b44e281..3168bcd0ae0 100644 --- a/docs/build/windows.md +++ b/docs/build/windows.md @@ -6,13 +6,13 @@ On Windows, you can build the following projects: * [Keyman for Android](#keyman-for-android) * [Keyman for Windows](#keyman-for-windows) -* [Keyman Developer](#keyman-developer) (together with Keyman for Windows) +* [Keyman Developer](#keyman-developer) * [KeymanWeb](#keymanweb) The following libraries can also be built: -* Keyman Core (Windows, wasm targets) (aka core) -* Common/Web +* Keyman Core (Windows, wasm targets) +* Common libraries The following projects **cannot** be built on Windows: @@ -63,10 +63,6 @@ Dependencies: * [Base](#base-dependencies) * [Windows Platform](#windows-platform-dependencies) -**Note**: Keyman for Windows is currently built together with Keyman Developer. -We are working on splitting these projects. For now, you will need the Keyman -Developer dependencies as well. - Building: * [Building Keyman for Windows](../../windows/src/README.md) @@ -75,7 +71,7 @@ Building: Dependencies: * [Base](#base-dependencies) * [Web](#web-dependencies) -* [Windows Platform](#windows-platform-dependencies) +* [Windows Platform](#windows-platform-dependencies) (optional, for Windows-only components) Building: * [Building Keyman Developer](../../windows/src/README.md) @@ -94,42 +90,19 @@ Building: **Dependencies**: * [Base](#base-dependencies) * [Web](#web-dependencies) - -**Additional requirements**: -* Android SDK -* Android Studio -* Ant -* Gradle -* Maven -* Optional: OpenJDK 11 (https://learn.microsoft.com/en-us/java/openjdk/download) - -```ps1 -# Elevated PowerShell -choco install android-sdk android-studio ant gradle maven -# optionally install sdk images -sdkmanager "system-images;android-33;google_apis;armeabi-v7a" -sdkmanager --update -sdkmanager --licenses -``` - -* Run Android Studio once after installation to install additional components - such as emulator images and SDK updates. - -**Required environment variables**: -* [`JAVA_HOME`](#java_home) - -**Optional environment variables**: -* [`JAVA_HOME_11`](#java_home) +* [Android](#android-dependencies) Building: * [Building Keyman for Android](../../android/README.md) -## Prerequisites +--- + +## Dependencies and Prerequisites Many dependencies are only required for specific projects. We prefer [Chocolatey](https://chocolatey.org/install) at present for -installation of dependencies. Chocolatey should be run in an elevated +installation of most dependencies. Chocolatey should be run in an elevated PowerShell. ### Base Dependencies @@ -150,28 +123,36 @@ PowerShell. # for *much* faster download, hide progress bar (PowerShell/PowerShell#2138) $ProgressPreference = 'SilentlyContinue' choco install git jq python ninja pandoc meson -refreshenv ``` **Environment variables**: -* [`KEYMAN_ROOT`](#keyman_root) -* `PATH`: add your Python scripts folder to your path: it will normally be `%appdata%\Python\Python310\Scripts`. + +If you pull the entire `keyman.git` repo to `c:\keyman`, then the paths by +default will work without changes. Otherwise, you will need to set an +environment variable `KEYMAN_ROOT` to the root path of the Keyman repo. For +example: ```bat -SET KEYMAN_ROOT=c:\Projects\keyman\keyman -SET PATH=%path%;%appdata%\Python\Python310\Scripts +SETX KEYMAN_ROOT "c:\Projects\keyman\keyman" ``` -To check whether environment variables are set, run `SET ` in command -prompt. - -You can use Windows Settings to add these environment variables permanently: - -1. In Windows Search, type "environment" and select "Edit System Environment - Variables" -2. Click `Environment Variables...` -3. You can add or edit variables in either User or System settings, as you - prefer. +> [!NOTE] +> The `SETX` command will set persistent environment variables but they do not +> impact the current shell environment. Start a new shell to see the variables. + +> [!TIP] +> +> To check whether environment variables are set, run `SET ` in command +> prompt. +> +> You can alternatively use Windows Settings to add these environment variables +> permanently: +> +> 1. In Windows Search, type "environment" and select "Edit System Environment +> Variables" +> 2. Click `Environment Variables...` +> 3. You can add or edit variables in either User or System settings, as you +> prefer. ### Web Dependencies @@ -181,42 +162,70 @@ You can use Windows Settings to add these environment variables permanently: * KeymanWeb **Requirements**: -* emscripten 3.1.46 or later -* node.js 18+ -* [openjdk 11](https://learn.microsoft.com/en-us/java/openjdk/download#openjdk-11)+ +* Emscripten +* node.js -```ps1 -# Elevated PowerShell +#### Emscripten -# for *much* faster download, hide progress bar (PowerShell/PowerShell#2138) -$ProgressPreference = 'SilentlyContinue' +In bash, run the following commands: -choco install emscripten --version 3.1.46 +```bash +cd /c/Projects/keyman +git clone https://github.com/emscripten-core/emsdk +cd emsdk +emsdk install 3.1.58 +emsdk activate 3.1.58 ``` -Note: emscripten very unhelpfully overwrites JAVA_HOME, and adds its own -versions of Python, Node and Java to the PATH. For best results, go ahead -and remove those paths from your PATH variable before continuing. +> ![WARNING] +> Emscripten very unhelpfully overwrites `JAVA_HOME`, and adds its own +> versions of Python, Node and Java to the `PATH`. For best results, restart +> your shell after installing Emscripten so that you don't end up with the +> wrong versions. There is no need to add emscripten to the path in order to build Keyman. -However, you should set the EMSCRIPTEN_BASE variable to the path where `emcc` -can be found, but always in the upstream\emscripten subdirectory where you -installed emsdk (most likely %LocalAppData%\emsdk\upstream\emscripten) +However, you should set the `EMSCRIPTEN_BASE` variable to the path where `emcc` +can be found, in the `upstream\emscripten` subdirectory of where you installed +emsdk. **Environment variables**: -* `EMSCRIPTEN_BASE`: `\upstream\emscripten` -After installing emscripten, you'll need to install node.js and openjdk: +```bat +SETX EMSCRIPTEN_BASE "\upstream\emscripten" +``` -```ps1 -# Elevated PowerShell +**Optional environment variables**: -# for *much* faster download, hide progress bar (PowerShell/PowerShell#2138) -$ProgressPreference = 'SilentlyContinue' -choco install nodejs -choco install openjdk +To let the Keyman build scripts control the version of Emscripten +installed on your computer: + +```bat +SETX KEYMAN_USE_EMSDK 1 +``` + +#### node.js + +Our recommended way to install node.js is to use +[nvm-windows](https://github.com/coreybutler/nvm-windows). This makes it +easy to switch between versions of node.js. + +```bat +nvm install 20.16.0 +nvm use 20.16.0 ``` +**Optional environment variables**: + +To let the Keyman build scripts control the version of node.js installed +and active on your computer: + +```bat +SETX KEYMAN_USE_NVM 1 +```` + +See [node.md](node.md) for more information, including automatic selection +of appropriate node versions during builds. + ### Windows Platform Dependencies **Projects**: @@ -238,8 +247,7 @@ choco install openjdk Start Delphi IDE once after installation as it will create various environment files and take you through required registration. - * Note: It is possible to build all components that do _not_ require Delphi by - adding the environment variable `NODELPHI=1` before starting the build. + * Note: It is possible to build all components that do _not_ require Delphi. Currently many components are Delphi-based, but if you are working just in Keyman Core, the compiler, or Keyman Engine's C++ components, you may be able to get away without building them. In this situation, we recommend @@ -251,10 +259,12 @@ choco install openjdk ```ps1 choco install visualstudio2019community visualstudio2019-workload-nativedesktop visualstudio2019buildtools ``` + * Verify required build tools are installed * Run `Visual Studio Installer` * Check the `Individual components` tab - * Verify `MSVC v142 - VS 2019 c++ x64/x86 build tools (Latest)` is installed. If not, install it. + * Verify `MSVC v142 - VS 2019 c++ x64/x86 build tools (Latest)` is installed. + If not, install it. Recommended: configure Visual Studio to use two-space tab stops: 1. Open the options dialog: Tools > Options. @@ -262,20 +272,36 @@ choco install openjdk 3. Change 'Tab size' to 2 and 'Indent size' to 2. 4. Select 'Insert spaces'. +* Windows SDK (C++ Desktop Development) + + https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/ + **Required environment variables**: * `PATH` - * Add the C:\Projects\keyman\keyman\windows\lib folder in the Keyman + * Add the `C:\Projects\keyman\keyman\windows\lib` folder in the Keyman repository to your `PATH` environment variable. This is required for Keyman's design-time packages to load in Delphi. -* [`KEYMAN_CEF4DELPHI_ROOT`](#keyman_cef4delphi_root) -**Optional environment variables**: -* [`GIT_BASH_FOR_KEYMAN`](#git_bash_for_keyman) -* [`USERDEFINES`](#userdefines) +### KEYMAN_CEF4DELPHI_ROOT + +Keyman and Keyman Developer use Chromium Embedded Framework. The source repo is +at https://github.com/keymanapp/CEF4Delphi. In order to build the installers, we +need to source the binary files from the +https://github.com/keymanapp/CEF4Delphi_binary repo. The +`KEYMAN_CEF4DELPHI_ROOT` environment variable should be set to the root of this +repo on your local machine. + +The version of CEF in use is determined by CEF_VERSION.md. This maps to a branch +prefixed with `v` e.g. `v89.0.18` in the CEF4Delphi_binary repository. During a +release build, the common/windows/cef-checkout.sh script will checkout the correct +branch of the repository automatically and extract any compressed files found in +it. + +The [`KEYMAN_CEF4DELPHI_ROOT`](#keyman_cef4delphi_root) variable is +used to specify the path to the CEF4Delphi binaries. ```bat -SET KEYMAN_CEF4DELPHI_ROOT=c:\Projects\keyman\CEF4Delphi_Binary -SET GIT_BASH_FOR_KEYMAN="C:\Program Files\Git\bin\bash.exe" --init-file "c:\Program Files\Git\etc\profile" -l +SETX KEYMAN_CEF4DELPHI_ROOT "c:\Projects\keyman\CEF4Delphi_Binary" ``` **Additional requirements for release builds**: @@ -292,75 +318,63 @@ choco install wixtoolset --version=3.11.1 git clone https://github.com/keymanapp/CEF4Delphi_Binary C:\Projects\keyman\CEF4Delphi_Binary ``` -## Certificates +### Android dependencies -In order to make a release build, you need to sign all the executables. See -[windows/src/README.md#Certificates](../../windows/src/README.md#Certificates) -for details on how to create test code signing certificates or specify your own -certificates for the build. - -## Notes on Environment Variables - -### KEYMAN_ROOT - -If you pull the entire `keyman.git` repo to `c:\keyman`, then the paths by -default will work without changes. Otherwise, you will need to set an -environment variable `KEYMAN_ROOT` to the root path of the Keyman repo. For -example: - -```bat -SET KEYMAN_ROOT=c:\projects\keyman\keyman -``` - -### KEYMAN_CEF4DELPHI_ROOT +**Projects**: +* Keyman for Android -Keyman and Keyman Developer use Chromium Embedded Framework. The source repo is -at https://github.com/keymanapp/CEF4Delphi. In order to build the installers, we -need to source the binary files from the -https://github.com/keymanapp/CEF4Delphi_binary repo. The -`KEYMAN_CEF4DELPHI_ROOT` environment variable should be set to the root of this -repo on your local machine. +**Requirements**: +* Android SDK +* Android Studio +* Ant +* Gradle +* Maven +* JDK 11 (Temurin11) -The version of CEF in use is determined by CEF_VERSION.md. This maps to a branch -prefixed with `v` e.g. `v89.0.18` in the CEF4Delphi_binary repository. During a -release build, the common/windows/cef-checkout.sh script will checkout the correct -branch of the repository automatically and extract any compressed files found in -it. +#### JDK 11 -### GIT_BASH_FOR_KEYMAN +Use Powershell + Chocolatey to install JDK 11: -This environment variable is optional: the build will run bash in a separate -window in order to build KeymanWeb if it isn't present, but you'll lose logging -and have the annoyance of a window popping up halfway through the build. To -resolve both of those issues, set the environment variable to: +```ps1 +# Elevated PowerShell -```bat -SET GIT_BASH_FOR_KEYMAN="C:\Program Files\Git\bin\bash.exe" --init-file "c:\Program Files\Git\etc\profile" -l +# for *much* faster download, hide progress bar (PowerShell/PowerShell#2138) +$ProgressPreference = 'SilentlyContinue' +choco install temurin11 ``` -You should verify the install location of Git on your computer as it may vary. +**Multiple versions of Java:** If you need to build Keyman for Android 16.0 or +older versions, you can set `JAVA_HOME_11` to the JDK 11 path and +`JAVA_HOME` to the JDK 8 path. This will build both versions correctly +from command line. But note that you do need to update your `JAVA_HOME` env +var to the associated version before opening Android Studio and loading any +Android projects. `JAVA_HOME_11` is mostly used by CI. -### USERDEFINES +#### Android Studio and friends -You can specify defines that will not be added to the git repository and will be -used in the build in the UserDefines.mak file in the root folder. This is used -mostly for code signing certificates. If not specified, a test certificate will -be used to sign executables when you build a release. +```ps1 +# Elevated PowerShell +choco install androidstudio ant gradle maven android-sdk +``` -To include UserDefines.mak in the build, use the command line parameter -`-DUSERDEFINES`. You can also set an environment variable `USERDEFINES=1` to get -the same result. +Start a new shell to get the new paths and then update Android SDKs: -### JAVA_HOME +```ps1 +# optionally install sdk images +sdkmanager --update +# sdkmanager "system-images;android-33;google_apis;armeabi-v7a" +sdkmanager --licenses +``` -This environment variable tells Gradle what version of Java to use for building Keyman for Android. +* Run Android Studio once after installation to install additional components + such as emulator images and SDK updates. -**Multiple versions of Java:** If you need to build Keyman for Android 16.0 or older versions, you can set `JAVA_HOME_11` to the OpenJDK 11 path and `JAVA_HOME` to the OpenJDK 8 path. This will build both versions correctly from command line. But note that you do need to update your `JAVA_HOME` env var to the associated version before opening Android Studio and loading any Android projects. `JAVA_HOME_11` is mostly used by CI. +## Certificates -```bat -SET JAVA_HOME="path to OpenJDK 8" -SET JAVA_HOME_11="path to OpenJDK 11" -``` +In order to make a release build, you need to sign all the executables. See +[windows/src/README.md#Certificates](../../windows/src/README.md#Certificates) +for details on how to create test code signing certificates or specify your own +certificates for the build. ## Optional Tools diff --git a/docs/file-formats/kmx-file-format.md b/docs/file-formats/kmx-file-format.md new file mode 100644 index 00000000000..67288bc123c --- /dev/null +++ b/docs/file-formats/kmx-file-format.md @@ -0,0 +1,361 @@ +# KMX Binary Format + +This document describes the binary format for KMX, compiled from Keyman .kmn +source files. + +## Principles + +- All integer values are unsigned 32-bit little-endian unless otherwise + specified. +- All offsets are 32-bit little-endian values. All offsets are relative to the + beginning of the data (normally start of file). A zero offset indicates that + the value is not present, equivalent to `NULL` -- no offset can ever point to + the start of the file. +- All strings are UTF-16LE unless otherwise specified and null terminated. + Strings are referenced by offset (see above). +- The file format has a header section followed by data. There is no specific + defined order for data in the file. Empty space is permitted but should be + zeroed. Note that `COMP_KEYBOARD_KMXPLUS` must follow immediately after + `COMP_KEYBOARD` with no empty space (that is at byte offset 64). + +A .kmx file is essentially a 32-bit memory mapped file. In a 32-bit process, the +file can be loaded into a memory buffer, and (assuming the file is valid), all +offset values in the file can be rewritten into pointers by adding the base of +the memory buffer to them. (In a 64-bit process, a fixup is needed to make space +for 64-bit pointers.) + +## `COMP_KEYBOARD`: KMX header + +The very first section is a header that describes the remainder of the file. + +| ∆ | Bits | Name | Description | +|---|------|----------------------|-----------------------------------------------------------------------------------| +| 0 | 32 | `dwIdentifier` | `KXTS` 0x5354584B, `kmx.FILEID_COMPILED` | +| 4 | 32 | `dwFileVersion` | minimum Keyman version that can read the file, `KMX_Version` enum | +| 8 | 32 | `dwCheckSum` | deprecated in 16.0, `0x0`; CRC32 checksum of entire file | +|12 | 32 | `KeyboardID` | deprecated in 10.0, `0x0`; Windows LANGID for keyboard | +|16 | 32 | `IsRegistered` | deprecated in 10.0, `0x1`; registration status of compiler | +|20 | 32 | `version` | deprecated, `0x0`; was version of keyboard, see `TSS_KEYBOARDVERSION` | +|24 | 32 | `cxStoreArray` | number of `COMP_STORE` entries in `COMP_KEYBOARD.dpStoreArray` | +|28 | 32 | `cxGroupArray` | number of `COMP_GROUP` entries in `COMP_KEYBOARD.dpGroupArray` | +|32 | 32 | `dpStoreArray` | offset of first `COMP_STORE` entry | +|36 | 32 | `dpGroupArray` | offset of first `COMP_GROUP` entry | +|40 | 32 | `StartGroup_ANSI` | deprecated; index of starting non-Unicode `COMP_GROUP`, `0xFFFFFFFF` means unused | +|44 | 32 | `StartGroup_Unicode` | index of starting Unicode `COMP_GROUP`, `0xFFFFFFFF` means unused | +|48 | 32 | `dwFlags` | global flags for the keyboard, see description below | +|52 | 32 | `dwHotKey` | default hotkey for keyboard, from [`&Hotkey`], see description below | +|56 | 32 | `dpBitmapOffset` | offset of keyboard icon | +|60 | 32 | `dwBitmapSize` | size in bytes of keyboard icon, `0x0` if not present | + +This structure is present at the start of every .kmx file. The `KXTS` identifier +can be used as a 'magic' to determine whether a binary file is a .kmx file. + +Note that `dpBitmapOffset` should be ignored if `dwBitmapSize` is `0x0` -- it may be a non-zero value. + +### `dwFileVersion`: Minimum version of Keyman required to read the file + +The `dwFileVersion` property should be checked before attempting to read any +other data in the file. If it is not a recognized version, then no further +attempt should be made to read the file. The recognized versions are listed in +the `KMX_Version` enum: + +| Value | Identifier | Description | +|--------------|---------------|------------------------------------------------------------------| +| `0x00000300` | `VERSION_30` | Keyman 3.0. Non-Unicode, Windows 3.1 only, not supported | +| `0x00000301` | `VERSION_31` | Keyman 3.1. Non-Unicode, Windows 3.1 only, not supported | +| `0x00000302` | `VERSION_32` | Keyman 3.2. Non-Unicode, Windows 3.1, 95, 98 only, not supported | +| `0x00000400` | `VERSION_40` | Keyman 4.0. Non-Unicode, Windows 3.1, 95, 98 only, not supported | +| `0x00000500` | `VERSION_50` | Keyman 5.0. Unicode, Windows only | +| `0x00000501` | `VERSION_501` | Keyman 5.1, added support for `call()` statement | +| `0x00000600` | `VERSION_60` | Keyman 6.0 | +| `0x00000700` | `VERSION_70` | Keyman 7.0 | +| `0x00000800` | `VERSION_80` | Keyman 8.0 | +| `0x00000900` | `VERSION_90` | Keyman 9.0 | +| `0x00000A00` | `VERSION_100` | Keyman 10.0, first true multi-platform version | +| `0x00000E00` | `VERSION_140` | Keyman 14.0 (versions 11.0-13.0 had no binary format changes) | +| `0x00000F00` | `VERSION_150` | Keyman 15.0 | +| `0x00001000` | `VERSION_160` | Keyman 16.0 | +| `0x00001100` | `VERSION_170` | Keyman 17.0 | + +### `StartGroup` + +Keyboards may have up to 4 starting groups, defined by [`begin`] statements in +the source .kmn file.. The `begin ANSI` group and `begin Unicode` groups are +defined in the `COMP_KEYBOARD` header, however as they were introduced into the +format later, the `begin NewContext` and `begin PostKeystroke` groups are +defined as system stores. + +### `dwFlags` + +The following flags are defined for .kmx keyboards: + +| Bit value | Name | Description | +|--------------|-----------------------|---------------------------------------------------------------------------------------------| +| `0x00000001` | `KF_SHIFTFREESCAPS` | Pressing Shift releases Caps Lock (see [`&ShiftFreesCaps`]) | +| `0x00000002` | `KF_CAPSONONLY` | Caps Lock switches on only (see [`&CapsOnOnly`]) | +| `0x00000004` | `KF_CAPSALWAYSOFF` | Caps Lock state is disabled (see [`&CapsAlwaysOff`]) | +| `0x00000008` | `KF_LOGICALLAYOUT` | Unused, should never be set | +| `0x00000010` | `KF_AUTOMATICVERSION` | The compiler determined the minimum version of Keyman automatically (see [`&Version`]) | +| `0x00000020` | `KF_KMXPLUS` | 16.0+: A `COMP_KEYBOARD_KMXPLUSINFO` structure is present immediately after `COMP_KEYBOARD` | + +### `dwHotKey`: keyboard hotkeys + +This field is `0x0` if no default hotkey is specified. The value is a bitmask: + +| Bit mask | Name | Description | +|--------------|-------------|------------------------------------------| +| `0x000000FF` | virtual key | [Keyman virtual key] value of the hotkey | +| `0x00010000` | `HK_ALT` | Set if the hotkey uses Alt | +| `0x00020000` | `HK_CTRL` | Set if the hotkey uses Ctrl | +| `0x00040000` | `HK_SHIFT` | Set if the hotkey uses Shift | + +### Keyboard icon + +The `COMP_KEYBOARD.dpBitmapOffset` and `COMP_KEYBOARD.dwBitmapSize` members +describe the keyboard icon, if present. If not present, both members should have +the value `0x0`. The keyboard icon may be a 16x16 Windows .bmp format file, 4, +8, 24, or 32 bit color, or a Windows .ico format file, which may have multiple +icons present at different bit depths and resolutions. Refer to the Windows API +documentation for .bmp and .ico binary formats. + +By convention, the compiler stores the bitmap image at the end of the KMX +data, but this is not a requirement of the file format. + +## `COMP_KEYBOARD_KMXPLUSINFO`: KMX+ secondary header + +| ∆ | Bits | Name | Description | +|---|------|-----------------|----------------------------| +| 0 | 32 | `dpKMXPlus` | offset of KMX+ data | +| 4 | 32 | `dwKMXPlusSize` | size in bytes of KMX+ data | + +The `COMP_KEYBOARD_KMXPLUSINFO` structure is present only if +`COMP_KEYBOARD.dwFlags` flag has bit `KF_KMXPLUS` (`0x20`) set. If present, it +must be located immediately after the `COMP_KEYBOARD` structure, that is, at +byte offset 64. + +See [KMX+ File Format] for details of the KMX+ file format. + +## `COMP_STORE` + +| ∆ | Bits | Name | Description | +|---|------|--------------|------------------------------------------------------------------------| +| 0 | 32 | `dwSystemID` | `0x0` for a normal store, or a system store value | +| 4 | 32 | `dpName` | offset to name of the store from keyboard source, `0x0` if not present | +| 8 | 32 | `dpString` | offset to value of the store | + +The `COMP_STORE` structure describes a [`store`] in the keyboard source file. +`COMP_STORE` structures are also used for additional metadata such as compiler +version. + +Store names are compiled into keyboards if debug information is included (see +[Debug information for keyboards](#debug-information-for-keyboards)), or if the store is used for [keyboard +options]. If the store name is not included, `dpName` will have a `0x0` value. + +### System Stores + +If `dwSystemID` is non-zero, then it will be one of the following values: + +| Value | Identifier | Count | Version | Description | +|-------|---------------------------------|-------|---------|------------------------------------------------------------------------------------------| +| 0 | `TSS_NONE` | 0+ | 5.0 | (Not a system store, normal keyboard store data) | +| 1 | `TSS_BITMAP` | 0-1 | 5.0 | [`&Bitmap`] store, used during compile only; see `COMP_KEYBOARD.dpBitmapOffset` | +| 2 | `TSS_COPYRIGHT` | 0-1 | 5.0 | [`&Copyright`] store | +| 3 | `TSS_HOTKEY` | 0-1 | 5.0 | [`&Hotkey`] store (see also `COMP_KEYBOARD.dwHotkey`) | +| 4 | `TSS_LANGUAGE` | 0-1 | 5.0 | Deprecated, [`&Language`] store | +| 5 | `TSS_LAYOUT` | 0-1 | 5.0 | Deprecated, [`LAYOUT`] statement | +| 6 | `TSS_MESSAGE` | 0-1 | 5.0 | [`&Message`] store | +| 7 | `TSS_NAME` | 0-1 | 5.0 | [`&Name`] store, public name of keyboard | +| 8 | `TSS_VERSION` | 0-1 | 5.0 | [`&Version`] store; use `COMP_KEYBOARD.dwFileVersion` | +| 9 | `TSS_CAPSONONLY` | 0-1 | 5.0 | [`&CapsOnOnly`] store; use `TF_CAPSONONLY` | +| 10 | `TSS_CAPSALWAYSOFF` | 0-1 | 5.0 | [`&CapsAlwaysOff`] store; use `TF_CAPSALWAYSOFF` | +| 11 | `TSS_SHIFTFREESCAPS` | 0-1 | 5.0 | [`&ShiftFreesCaps`] store; use `TF_SHIFTFREESCAPS` | +| 12 | `TSS_LANGUAGENAME` | 0-1 | 5.0 | Deprecated, `LANGUAGENAME` statement | +| 13 | `TSS_CALLDEFINITION` | 0+ | 5.0 | Definition for an IMX call | +| 14 | `TSS_CALLDEFINITION_LOADFAILED` | 0 | 5.0 | Deprecated, unused, was used internally in KMX processor | +| 15 | `TSS_ETHNOLOGUECODE` | 0-1 | 5.0 | Deprecated, [`&EthnologueCode`] store | +| 16 | `TSS_DEBUG_LINE` | 0+ | 5.0 | [Debug information for keyboards](#debug-information-for-keyboards) | +| 17 | `TSS_MNEMONIC` | 0-1 | 5.0 | [`&MnemonicLayout`] store, `'0'` or `'1'` | +| 18 | `TSS_INCLUDECODES` | 0-1 | 5.0 | [`&IncludeCodes`] store, used during compile only | +| 19 | `TSS_OLDCHARPOSMATCHING` | 0-1 | 5.0 | Deprecated, [`&OldCharPosMatching`] store | +| 20 | `TSS_COMPILEDVERSION` | 0-1 | 5.0 | Version of compiler used, may be omitted if `--no-compiler-version` passed to kmc | +| 21 | `TSS_KEYMANCOPYRIGHT` | 0-1 | 5.0 | Copyright message from compiler, may be omitted if `--no-compiler-version` passed to kmc | +| 22 | `TSS_CUSTOMKEYMANEDITION` | 1 | 5.0 | Deprecated, always `'0'`, always present | +| 23 | `TSS_CUSTOMKEYMANEDITIONNAME` | 1 | 5.0 | Deprecated, always `'Keyman'`, always present | +| 24 | `TSS_VISUALKEYBOARD` | 0-1 | 7.0 | [`&VisualKeyboard`] store | +| 25 | `TSS_KMW_RTL` | 0-1 | 7.0 | [`&KMW_RTL`] store, `'0'` or `'1'` | +| 26 | `TSS_KMW_HELPFILE` | 0-1 | 7.0 | [`&KMW_HelpFile`] store, used during compile only | +| 27 | `TSS_KMW_HELPTEXT` | 0-1 | 7.0 | [`&KMW_HelpText`] store, used during compile only | +| 28 | `TSS_KMW_EMBEDJS` | 0-1 | 7.0 | [`&KMW_EmbedJS`] store, used during compile only | +| 29 | `TSS_WINDOWSLANGUAGES` | 0-1 | 7.0 | Deprecated, [`&WindowsLanguages`] store | +| 30 | `TSS_COMPARISON` | 0+ | 8.0 | Generated by compiler for use in `if()` statements | +| 31 | `TSS_PLATFORM` | 0-1 | 9.0 | Cannot be used in `COMP_STORE`; used for [`if(&platform)`]. [`platform()`] only | +| 32 | `TSS_BASELAYOUT` | 0-1 | 9.0 | Cannot be used in `COMP_STORE`; used for [`if(&baselayout)`], [`baselayout()`] only | +| 33 | `TSS_LAYER` | 0 | 9.0 | Cannot be used in `COMP_STORE`; used for [`if(&layer)`] and [`set(&layer)`] | +| 34 | `TSS_VKDICTIONARY` | 0-1 | 9.0 | Dictionary of `T_` and `U_` virtual key names for touch layouts, generated by compiler | +| 35 | `TSS_LAYOUTFILE` | 0-1 | 9.0 | Name of touch layout file, used during compile only | +| 36 | `TSS_KEYBOARDVERSION` | 0-1 | 9.0 | [`&KeyboardVersion`] store | +| 37 | `TSS_KMW_EMBEDCSS` | 0-1 | 9.0 | [`&KMW_EmbedCSS`] store, used during compile only | +| 38 | `TSS_TARGETS` | 0-1 | 9.0 | [`&Targets`] store | +| 39 | `TSS_CASEDKEYS` | 0-1 | 14.0 | [`&CasedKeys`] store, used during compile only | +| 40 | `TSS_BEGIN_NEWCONTEXT` | 0-1 | 15.0 | zero-based index of group referenced in [`begin NewContext`] | +| 41 | `TSS_BEGIN_POSTKEYSTROKE` | 0-1 | 15.0 | zero-based index of group referenced in [`begin PostKeystroke`] | +| 42 | `TSS_NEWLAYER` | 0 | 15.0 | Cannot be used in `COMP_STORE`; used for testing layer switch events | +| 43 | `TSS_OLDLAYER` | 0 | 15.0 | Cannot be used in `COMP_STORE`; used for testing layer switch events | +| 44 | `TSS_DISPLAYMAP` | 0-1 | 17.0 | [`&DisplayMap`] store | + +## `COMP_GROUP` + +| ∆ | Bits | Name | Description | +|----|------|--------------|-----------------------------------------------------------------------------| +| 0 | 32 | `dpName` | offset to name of the group from keyboard source, `0x0` if not present | +| 4 | 32 | `dpKeyArray` | offset to array of `COMP_KEY` entries | +| 8 | 32 | `dpMatch` | offset to string for the [`match`] statement output, `0x0` if not present | +| 12 | 32 | `dpNoMatch` | offset to string for the [`nomatch`] statement output, `0x0` if not present | +| 16 | 32 | `cxKeyArray` | number of `COMP_KEY` entries in `COMP_GROUP.dpKeyArray` | +| 20 | 32 | `fUsingKeys` | `0x1` if group is `using keys`, `0x0` otherwise | + +The `COMP_GROUP` structure describes a [`group`] in the keyboard source file. +Note that the `readonly` property of a group is only applicable in the source +file and is never written to the binary format. + +## `COMP_KEY` + +| ∆ | Bits | Name | Description | +|----|------|--------------|------------------------------------------------------------------------------------------------| +| 0 | 16 | `Key` | [Keyman virtual key], virtual character key, or character, for the rule, 0 if unused | +| 2 | 16 | (unused) | padding, reserved | +| 4 | 32 | `Line` | Line number for the rule, 0 if not compiled with debug information | +| 8 | 32 | `ShiftFlags` | Modifier flags for the key, see description below | +| 12 | 32 | `dpOutput` | offset to string for the output of the rule, always present even for zero-length output | +| 16 | 32 | `dpContext` | offset to string for the context part of the rule, always present even for zero-length context | + +The `COMP_KEY` structure describes a single rule in a keyboard source file. The +compiler expands `any()` statements found in the key part of a rule into +multiple rules -- so the key part of the rule is always only ever matching a +single key combination. + +`ShiftFlags` contains a set of modifier and state key flags, and some additional +flags that determine how to interpret `Key`. + +### Modifier and state key flags + +The modifier and state key flags are: + +| Bit mask | Name | Description | +|--------------|------------------|-----------------------------------------------------------| +| `0x00000001` | `LCTRLFLAG` | Left Control key | +| `0x00000002` | `RCTRLFLAG` | Right Control key | +| `0x00000004` | `LALTFLAG` | Left Alt key (Option on macOS) | +| `0x00000008` | `RALTFLAG` | Right Alt key | +| `0x00000010` | `K_SHIFTFLAG` | Either Shift key | +| `0x00000020` | `K_CTRLFLAG` | Either Ctrl key | +| `0x00000040` | `K_ALTFLAG` | Either Alt key | +| `0x00000080` | `K_METAFLAG` | Either Meta key. Reserved, not for use in .kmx | +| `0x00000100` | `CAPITALFLAG` | Caps lock on | +| `0x00000200` | `NOTCAPITALFLAG` | Caps lock NOT on | +| `0x00000400` | `NUMLOCKFLAG` | Num lock on | +| `0x00000800` | `NOTNUMLOCKFLAG` | Num lock NOT on | +| `0x00001000` | `SCROLLFLAG` | Scroll lock on | +| `0x00002000` | `NOTSCROLLFLAG` | Scroll lock NOT on | + +* The chiral (left/right) modifier flags should not be used together with the + non-chiral flags. +* If neither `CAPITALFLAG` nor `NOTCAPITALFLAG` are set, then the state of the + Caps Lock key is ignored, and the same principle applies for Num lock and + Scroll lock. +* The Meta key may be the Windows key on Windows, or the + Command key on macOS. This flag is reserved for internal use, and + is not valid in a .kmx file. + +### Additional flags + +The meaning of `Key` is determined by the flags `ISVIRTUALKEY` and +`VIRTUALCHARKEY` in `ShiftFlags` (mask `0x0000C000`): + +* `0`: `Key` is a character on a key cap (US English unless the keyboard is a + mnemonic layout). All other shift flags are ignord +* `ISVIRTUALKEY`: `Key` is a US English virtual key +* `VIRTUALCHARKEY`: `Key` is a character on a key cap, combined with the shift + flags (US English unless the keyboard is a mnemonic layout) + +| Bit mask | Name | Description | +|--------------|------------------|-----------------------------------------------| +| `0x00004000` | `ISVIRTUALKEY` | `Key` is a virtual key | +| `0x00008000` | `VIRTUALCHARKEY` | Keyman 6.0+: `Key` is a Virtual character key | + +Note that the shift flags for key rules do not use the same values as hotkeys +for historical reasons. + +## Debug information for keyboards + +When debug information is present, it will be found in four places in compiled +keyboards: + +* `COMP_STORE.dpName`: the name of the store from the .kmn file +* `COMP_GROUP.dpName`: the name of the group from the .kmn file +* `COMP_KEY.Line`: the line number for the rule +* System stores with `COMP_STORE.dwSystemID` value of `TSS_DEBUG_LINE` + +When a keyboard is compiled with debug data, the `TSS_DEBUG_LINE` system store +contains debug information about each source line in the file. In each entry, +`COMP_STORE.dpName` contains debug metadata, and `COMP_STORE.dpString` contains +only the stringified .kmn source line number where the metadata is found. + +The name of the `TSS_DEBUG_LINE` system store starts with a single character +which describes the rest of the debug metadata in that store: + +| Character | Debug data type | Metadata | +|-----------|-----------------------|----------------------------------------------------------| +| `'B'` | [`begin`] statement | `Unicode` or `ANSI` | +| `'M'` | [`match`] statement | Stringified index of containing group, space, group name | +| `'N'` | [`nomatch`] statement | Stringified index of containing group, space, group name | +| `'G'` | [`group`] statement | Stringified index of group, space, group name | +| `'D'` | [`deadkey`] statement | Stringified index of deadkey, space, deadkey name | + + + + + +[KMX+ File Format]: kmx-plus-file-format.md +[`begin`]: https://help.keyman.com/developer/language/reference/begin +[`deadkey`]: https://help.keyman.com/developer/language/reference/deadkey +[`group`]: https://help.keyman.com/developer/language/reference/group +[`match`]: https://help.keyman.com/developer/language/reference/match +[`nomatch`]: https://help.keyman.com/developer/language/reference/nomatch +[`store`]: https://help.keyman.com/developer/language/reference/store +[Keyman virtual key]: https://help.keyman.com/developer/language/guide/virtual-keys +[keyboard options]: https://help.keyman.com/developer/language/guide/variable-stores +[`&Bitmap`]: https://help.keyman.com/developer/language/reference/bitmap +[`&Copyright`]: https://help.keyman.com/developer/language/reference/copyright +[`&Hotkey`]: https://help.keyman.com/developer/language/reference/hotkey +[`&Language`]: https://help.keyman.com/developer/language/reference/language +[`LAYOUT`]: https://help.keyman.com/developer/language/reference/layout +[`&Message`]: https://help.keyman.com/developer/language/reference/message +[`&Name`]: https://help.keyman.com/developer/language/reference/name +[`&Version`]: https://help.keyman.com/developer/language/reference/version +[`&CapsOnOnly`]: https://help.keyman.com/developer/language/reference/caps +[`&CapsAlwaysOff`]: https://help.keyman.com/developer/language/reference/caps +[`&ShiftFreesCaps`]: https://help.keyman.com/developer/language/reference/caps +[`&EthnologueCode`]: https://help.keyman.com/developer/language/reference/ethnologuecode +[`&MnemonicLayout`]: https://help.keyman.com/developer/language/reference/mnemoniclayout +[`&IncludeCodes`]: https://help.keyman.com/developer/language/reference/includecodes +[`&OldCharPosMatching`]: https://help.keyman.com/developer/language/reference/oldcharposmatching +[`if(&layer)`]: https://help.keyman.com/developer/language/reference/if +[`set(&layer)`]: https://help.keyman.com/developer/language/reference/set +[`&VisualKeyboard`]: https://help.keyman.com/developer/language/reference/visualkeyboard +[`&KMW_RTL`]: https://help.keyman.com/developer/language/reference/kmw_rtl +[`&KMW_HelpFile`]: https://help.keyman.com/developer/language/reference/kmw_helpfile +[`&KMW_HelpText`]: https://help.keyman.com/developer/language/reference/kmw_helptext +[`&KMW_EmbedJS`]: https://help.keyman.com/developer/language/reference/kmw_embedjs +[`&WindowsLanguages`]: https://help.keyman.com/developer/language/reference/windowslanguages +[`if(&platform)`]: https://help.keyman.com/developer/language/reference/if +[`platform()`]: https://help.keyman.com/developer/language/reference/platform +[`if(&baselayout)`]: https://help.keyman.com/developer/language/reference/if +[`baselayout()`]: https://help.keyman.com/developer/language/reference/baselayout +[`&KeyboardVersion`]: https://help.keyman.com/developer/language/reference/keyboardversion +[`&KMW_EmbedCSS`]: https://help.keyman.com/developer/language/reference/kmw_embedcss +[`&Targets`]: https://help.keyman.com/developer/language/reference/targets +[`&CasedKeys`]: https://help.keyman.com/developer/language/reference/casedkeys +[`begin NewContext`]: https://help.keyman.com/developer/language/reference/begin +[`begin PostKeystroke`]: https://help.keyman.com/developer/language/reference/begin +[`&DisplayMap`]: https://help.keyman.com/developer/language/reference/displaymap diff --git a/core/src/ldml/C7043_ldml.md b/docs/file-formats/kmx-plus-file-format.md similarity index 95% rename from core/src/ldml/C7043_ldml.md rename to docs/file-formats/kmx-plus-file-format.md index b1d2223d556..a7b7e190a6e 100644 --- a/core/src/ldml/C7043_ldml.md +++ b/docs/file-formats/kmx-plus-file-format.md @@ -2,7 +2,7 @@ - copied from -- Authors: MD SL +- Authors: MD SRL ## C7043.0 Introduction @@ -13,7 +13,10 @@ Draft spec PR: ## C7043.1 Principles -- The data described here is located at byte offset `dpKMXPlus`. +- When embedded in a .kmx file, the data described here is located at byte + offset `COMP_KEYBOARD_KMXPLUS.dpKMXPlus`. See + [KMX file format](kmx-file-format.md) for a full description of the .kmx + format. - All integer values are unsigned 32-bit little-endian unless otherwise specified. - All strings are UTF-16LE unless otherwise specified. (See the `strs` section.) @@ -21,7 +24,7 @@ Draft spec PR: the `strs` table. - All offsets are 32-bit little-endian values. For all sections except for the `'sect'` section (which see), offsets are relative to the beginning of each - section. + section and must fall within the `size` of the section. ## C7043.2 Sections @@ -39,8 +42,8 @@ Draft spec PR: The very first section is a table of contents listing the rest of the sections. The table of contents does not list itself. -This is the only section where all byte offsets are relative to the value of -`dpKMXPlus`. +Unlike all other sections, all byte offsets are relative to start of the `sect` +section and do not have to be located within the `size` of the section. | ∆ | Bits | Name | Description | |---|------|---------|------------------------------------------| @@ -170,8 +173,8 @@ Then for each string: | ∆ | Bits | Name | Description | |---|------|---------------|-----------------------------------------------| -|16+| 32 | offset | off: Offset to string | -|20+| 32 | length | int: Length of string in UTF-16LE code units | +|12+| 32 | offset | off: Offset to string | +|16+| 32 | length | int: Length of string in UTF-16LE code units | After the string offset table comes the actual UTF-16LE data. There is a null (\u0000) after each string, which is _not_ included in the string length. @@ -359,14 +362,14 @@ For each element: | ∆ | Bits | Name | Description | |---|------|---------|------------------------------------------| -|32+| 32 | to | str: to string | -|36+| 32 | id | str: id string | -|40+| 32 | display | str: output display string | +|16+| 32 | to | str: to string | +|20+| 32 | id | str: id string | +|24+| 32 | display | str: output display string | Either `to` or `id` must be set, not both. Entries with an `to` field are sorted in a binary codepoint sort on the `to` field, -followed by entries with an `id` field set sorted in a binary codepoint sort on the `id` field. - +followed by entries with an `id` field set sorted in a binary codepoint sort +on the `id` field. ### C7043.2.15 `key2`—Extended keybag @@ -399,7 +402,8 @@ For each key: |28+| 32 | multiTap | list: index into `list` section with multiTap key id list or 0 | |32+| 32 | flicks | int: index into `key2.flicks` subtable | -- `id`: The original string id from XML. This may be 0 to save space (i.e. omit the string id). +- `id`: The original string id from XML. This may be 0 to save space (i.e. + omit the string id). - `flags`: Flags is a 32-bit bitfield defined as below: | Bit position | Meaning | Description | @@ -409,7 +413,8 @@ For each key: - `to`: If `extend` is 0, `to` is a UTF-32LE codepoint. If `extend` is 1, `to` is a 32 bit index into the `strs` table. The string may be zero-length. -- `longPress`, `longPressDefault`, and `multiTap` refer to key ids or lists of key ids in this same `key2.keys` subtable. +- `longPress`, `longPressDefault`, and `multiTap` refer to key ids or lists + of key ids in this same `key2.keys` subtable. #### `key2.flicks` flick list subtable @@ -418,24 +423,28 @@ For each flicks in the flick list: | ∆ | Bits | Name | Description | |---|------|---------------- |----------------------------------------------------------| | 0+| 32 | count | int: number of flick elements in this flick | -|12+| 32 | flick | int: index into `flick` subtable for first flick element | -|16+| 32 | id | str: flick id | +| 4+| 32 | flick | int: index into `flick` subtable for first flick element | +| 8+| 32 | id | str: flick id | -- `id`: The original string id from XML. This may be 0 to save space (i.e. omit the string id). +- `id`: The original string id from XML. This may be 0 to save space (i.e. omit + the string id). Elements are ordered by the string id. -If this section is present, it must have a 'flicks' in the list at position zero with count=0, index=0 and id=0 meaning 'no flicks'. +If this section is present, it must have a 'flicks' in the list at position zero +with count=0, index=0 and id=0 meaning 'no flicks'. + #### `key2.flick` flick element subtable For each flick element: | ∆ | Bits | Name | Description | |---|------|---------------- |----------------------------------------------------------| -| 0+| 32 | directions | list: index into `list` section with direction list | -| 8+| 32 | keyId | str: id of key | +| 0+| 32 | directions | list: index into `list` section with direction sequence | +| 4+| 32 | keyId | str: id of key | -If this section is present, it must have a 'flick element' at position zero with directions=0, flags=0, and to=0 meaning 'no flick'. +If this section is present, it must have a 'flick element' at position zero with +directions=0, flags=0, and to=0 meaning 'no flick'. There is not a 'null' flick element at the end of each list. @@ -453,7 +462,7 @@ For each key: |---|------|---------|------------------------------------------| |16+| 32 | vkey | int: vkey ID | |20+| 32 | mod | int: modifier key flags | -|24+| 32 | key | int: index into `key` sibling subtable | +|24+| 32 | key | int: index into `keys` sibling subtable | - `vkey`: If this is 0-255, it is the resolved standard/predefined vkey (K_A, etc.). It is resolved because the `vkeyMap` from LDML has already been diff --git a/docs/linux/ibus-keyman.md b/docs/linux/ibus-keyman.md index ffc7b06c1cf..2b6241ccf0a 100644 --- a/docs/linux/ibus-keyman.md +++ b/docs/linux/ibus-keyman.md @@ -6,7 +6,7 @@ Source code for Keyman engine for IBus is in [linux/ibus-keyman](../../linux/ibu ## Requirements -meson (>= 0.57) +meson (>= 1.0) ## Building diff --git a/docs/linux/keyman-config.md b/docs/linux/keyman-config.md index f5750248325..93ed3ffd8bf 100644 --- a/docs/linux/keyman-config.md +++ b/docs/linux/keyman-config.md @@ -38,8 +38,7 @@ pip3 install sentry-sdk Copy and compile the GSettings schema: ```bash -cd keyman_config -sudo cp com.keyman.gschema.xml /usr/share/glib-2.0/schemas +sudo cp linux/keyman-config/resources/com.keyman.gschema.xml /usr/share/glib-2.0/schemas sudo glib-compile-schemas /usr/share/glib-2.0/schemas ``` diff --git a/docs/minimum-versions.md b/docs/minimum-versions.md index adb4edc687d..cce04050500 100644 --- a/docs/minimum-versions.md +++ b/docs/minimum-versions.md @@ -49,7 +49,6 @@ https://help.keyman.com/developer/engine/android/latest-version/ | KEYMAN Variable | Value | |-----------------------------------|--------------| | KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER | noble | -| KEYMAN_MAX_VERSION_EMSCRIPTEN | 3.1.58 | | KEYMAN_MIN_TARGET_VERSION_ANDROID | 5 | | KEYMAN_MIN_TARGET_VERSION_CHROME | 95.0 | | KEYMAN_MIN_TARGET_VERSION_IOS | 12.2 | @@ -58,11 +57,17 @@ https://help.keyman.com/developer/engine/android/latest-version/ | KEYMAN_MIN_TARGET_VERSION_WINDOWS | 10 | | KEYMAN_MIN_VERSION_ANDROID_SDK | 21 | | KEYMAN_MIN_VERSION_CPP | 17 | -| KEYMAN_MIN_VERSION_EMSCRIPTEN | 3.1.44 | +| KEYMAN_MIN_VERSION_EMSCRIPTEN | 3.1.58 | | KEYMAN_MIN_VERSION_MESON | 1.0.0 | -| KEYMAN_MIN_VERSION_NODE_MAJOR | 18 | +| KEYMAN_MIN_VERSION_NODE_MAJOR | 20 | | KEYMAN_MIN_VERSION_NPM | 10.5.1 | | KEYMAN_MIN_VERSION_VISUAL_STUDIO | 2019 | +| KEYMAN_VERSION_CLDR | 45 | +| KEYMAN_VERSION_ICU | 73.1 | +| KEYMAN_VERSION_ISO639_3 | 2024-05-22 | | KEYMAN_VERSION_JAVA | 11 | +| KEYMAN_VERSION_LANGTAGS | 2024-05-22 | +| KEYMAN_VERSION_LANGTAGS_SUBTAG_REGISTRY | 2024-05-16 | +| KEYMAN_VERSION_UNICODE | 16.0.0 | -> ### This file is auto-generated by min-ver.sh. Do not modify +> ### This file is auto-generated by publish-minimum-versions.sh. Do not modify diff --git a/docs/npm-packages.md b/docs/npm-packages.md index 36e5d9a6de7..494bec00448 100644 --- a/docs/npm-packages.md +++ b/docs/npm-packages.md @@ -19,13 +19,13 @@ Packages are **never** published from a developer's machine. * `@keymanapp/kmc-model` -- located at `developer/src/kmc-model` * `@keymanapp/kmc-model-info` -- located at `developer/src/kmc-model-info` * `@keymanapp/kmc-package` -- located at `developer/src/kmc-package` -* `@keymanapp/models-types` -- located at `common/models/types` ### Deprecated npm packages * `@keymanapp/developer-lexical-model-compiler` -- replaced by `@keymanapp/kmc` * `@keymanapp/lexical-model-compiler` -- replaced by `@keymanapp/kmc` -* `@keymanapp/lexical-model-types` -- replaced by `@keymanapp/models-types` +* `@keymanapp/lexical-model-types` -- replaced by `@keymanapp/common-types` +* `@keymanapp/models-types` -- replaced by `@keymanapp/common-types` * `@keymanapp/lmlayer-cli` -- replaced by `@keymanapp/kmc` * `@keymanapp/models-wordbreakers` -- published accidentally diff --git a/docs/websites/README.md b/docs/websites/README.md index 5fc84ffa596..62fc3f7388c 100644 --- a/docs/websites/README.md +++ b/docs/websites/README.md @@ -53,6 +53,8 @@ This maps the local directory to the the Docker image. For sites that use Composer dependencies, this step also creates a link in the Docker image from /var/www/vendor/ to /var/www/html/vendor. The link file also appears locally. +Some sites have assets in `/cdn/dev/` used to generate CDN in `/dev/deploy/`. To avoid confusion during development, you can skip generating CDN with `./build.sh start --debug`. + ##### Port lookup table After this, you can access the website at the following ports: diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 9dc49f1739e..d1c711368f1 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -60,7 +60,6 @@ C0324B8F1F8750B700AF3785 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0324B8E1F8750B700AF3785 /* TextView.swift */; }; C0324B911F8763E100AF3785 /* TextViewDelegateProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0324B901F8763E100AF3785 /* TextViewDelegateProxy.swift */; }; C0324B931F87689B00AF3785 /* KeymanURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0324B921F87689B00AF3785 /* KeymanURLProtocol.swift */; }; - C040E50E1F85FF8A00901EE4 /* KeyPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E50D1F85FF8A00901EE4 /* KeyPreviewView.swift */; }; C040E5101F8606E300901EE4 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E50F1F8606E300901EE4 /* TextField.swift */; }; C040E5121F86107E00901EE4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E5111F86107E00901EE4 /* AppDelegate.swift */; }; C040E5141F86108900901EE4 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E5131F86108900901EE4 /* MainViewController.swift */; }; @@ -76,7 +75,6 @@ C06D37341F81F5C300F61AE0 /* HTTPDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0ED71B41F6BBFAF002A2FD6 /* HTTPDownloader.swift */; }; C06D37351F81F5C300F61AE0 /* HTTPDownloadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0ED71B21F6BB0B1002A2FD6 /* HTTPDownloadRequest.swift */; }; C06D37361F81F5C300F61AE0 /* KeyboardMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C034BBBD1F7CCE7D00021FF5 /* KeyboardMenuView.swift */; }; - C06D37381F81F5C400F61AE0 /* SubKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07B42AE1F7CEE2700ECA97F /* SubKeysView.swift */; }; C06D37421F81F5C400F61AE0 /* KeyboardPickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C620F1F67BD4500268D03 /* KeyboardPickerButton.swift */; }; C06D37431F81F5C400F61AE0 /* KeyboardPickerBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C620B1F6792A200268D03 /* KeyboardPickerBarButtonItem.swift */; }; C06D37441F81F5C400F61AE0 /* KeyboardNameTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C62121F67C31100268D03 /* KeyboardNameTableViewCell.swift */; }; @@ -378,7 +376,6 @@ C0324B901F8763E100AF3785 /* TextViewDelegateProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewDelegateProxy.swift; sourceTree = ""; }; C0324B921F87689B00AF3785 /* KeymanURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanURLProtocol.swift; sourceTree = ""; }; C034BBBD1F7CCE7D00021FF5 /* KeyboardMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardMenuView.swift; sourceTree = ""; }; - C040E50D1F85FF8A00901EE4 /* KeyPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPreviewView.swift; sourceTree = ""; }; C040E50F1F8606E300901EE4 /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; C040E5111F86107E00901EE4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C040E5131F86108900901EE4 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; @@ -395,7 +392,6 @@ C06D372B1F81F4E100F61AE0 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C06D372E1F81F4E100F61AE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C075EB051F8EFF870041F4BD /* String+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Helpers.swift"; sourceTree = ""; }; - C07B42AE1F7CEE2700ECA97F /* SubKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubKeysView.swift; sourceTree = ""; }; C082CE141F90AFD400860F02 /* Collection+SafeAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+SafeAccess.swift"; sourceTree = ""; }; C08C620B1F6792A200268D03 /* KeyboardPickerBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardPickerBarButtonItem.swift; sourceTree = ""; }; C08C620F1F67BD4500268D03 /* KeyboardPickerButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardPickerButton.swift; sourceTree = ""; }; @@ -497,6 +493,9 @@ CEA9670A24BEC8030035AACF /* EngineStateBundler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EngineStateBundler.swift; sourceTree = ""; }; CEA9670C24BEEFF80035AACF /* khmer_angkor update-base.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "khmer_angkor update-base.bundle"; sourceTree = ""; }; CEA9670E24BEF05A0035AACF /* Updates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updates.swift; sourceTree = ""; }; + CEAC2A222C2A503600C99ABD /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/ResourceInfoView.strings; sourceTree = ""; }; + CEAC2A232C2A505700C99ABD /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = ""; }; + CEAC2A242C2A508000C99ABD /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; CEACC90125F07C81006EAB45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/ResourceInfoView.strings; sourceTree = ""; }; CEACC90325F07CAF006EAB45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; CEACC90425F07CB6006EAB45 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/Localizable.strings; sourceTree = ""; }; @@ -796,9 +795,7 @@ CE79B24823C711FF007E72AE /* KeyboardScaleMap.swift */, C0EF3E7A1F95B65300CE9BD4 /* KeymanWebDelegate.swift */, C0C16A881FA8146300F090BA /* KeymanWebViewController.swift */, - C040E50D1F85FF8A00901EE4 /* KeyPreviewView.swift */, C0A5FF361F6682EB00BE740C /* PopoverView.swift */, - C07B42AE1F7CEE2700ECA97F /* SubKeysView.swift */, ); path = Keyboard; sourceTree = ""; @@ -1211,6 +1208,7 @@ uk, ru, es, + el, ); mainGroup = F243887314BBD43000A3E055; productRefGroup = F243887F14BBD43000A3E055 /* Products */; @@ -1454,7 +1452,6 @@ CE7A26DB23CEEF640005955C /* Colors.swift in Sources */, 9A9CB08022416E5400231FB9 /* LexicalModelPickerViewController.swift in Sources */, 9A079E3D223B5FAF00581263 /* InstallableLexicalModel.swift in Sources */, - C06D37381F81F5C400F61AE0 /* SubKeysView.swift in Sources */, C0A93A541F8B21240079948B /* Manager.swift in Sources */, CE99350E24D7CCFC00202DA3 /* LanguagePickAssociator.swift in Sources */, C024C9981FA6F2340060583B /* NotificationObserver.swift in Sources */, @@ -1468,7 +1465,6 @@ 9A3E832522EAC14A00D22D2A /* KeyboardSwitcherViewController.swift in Sources */, C0324B911F8763E100AF3785 /* TextViewDelegateProxy.swift in Sources */, C0B09EAE1FCFD10F002F39AF /* FontManager.swift in Sources */, - C040E50E1F85FF8A00901EE4 /* KeyPreviewView.swift in Sources */, CEC0C6772410E049003E1BCD /* SentryManager.swift in Sources */, 9A079E3A223AFF3C00581263 /* KeyboardKeymanPackage.swift in Sources */, C0CB633C1FA6F61500617CDB /* Notifications.swift in Sources */, @@ -1610,6 +1606,7 @@ 298566D32980D39F004ACA95 /* uk */, 298566DF2980E48C004ACA95 /* ru */, CEFFECD72A417F2E00D58C36 /* es */, + CEAC2A232C2A505700C99ABD /* el */, ); name = Localizable.stringsdict; sourceTree = ""; @@ -1638,6 +1635,7 @@ 298566D22980D39A004ACA95 /* uk */, 298566DE2980E486004ACA95 /* ru */, CEFFECD62A417F2900D58C36 /* es */, + CEAC2A242C2A508000C99ABD /* el */, ); name = Localizable.strings; sourceTree = ""; @@ -1667,6 +1665,7 @@ 298566D12980D390004ACA95 /* uk */, 298566DD2980E477004ACA95 /* ru */, CEFFECD52A417F1300D58C36 /* es */, + CEAC2A222C2A503600C99ABD /* el */, ); name = ResourceInfoView.xib; sourceTree = ""; diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/InputViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/InputViewController.swift index d8e3bd705ae..003a4e15a8f 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/InputViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/InputViewController.swift @@ -388,10 +388,6 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate { } func insertText(_ keymanWeb: KeymanWebViewController, numCharsToDelete: Int, newText: String) { - if keymanWeb.isSubKeysMenuVisible { - return - } - if isInputClickSoundEnabled { UIDevice.current.playInputClick() diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeyPreviewView.swift b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeyPreviewView.swift deleted file mode 100644 index e8fd307a474..00000000000 --- a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeyPreviewView.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// KeyPreviewView.swift -// KeymanEngine -// -// Created by Gabriel Wong on 2017-10-05. -// Copyright © 2017 SIL International. All rights reserved. -// - -import UIKit - -class KeyPreviewView: UIView { - private let keyFrame: CGRect - private let strokeWidth: CGFloat = 1.0 - private let borderRadius: CGFloat = 5.0 - private let adjX: CGFloat - private let adjY: CGFloat - - private let borderColor = Colors.popupBorder - private var bgColor = Colors.keyboardBackground - private let bgColor2 = Colors.keyboardBackground - private let label: UILabel - - override init(frame: CGRect) { - let isPortrait: Bool - if Manager.shared.isSystemKeyboard { - isPortrait = InputViewController.isPortrait - } else { - isPortrait = UIDevice.current.orientation.isPortrait - } - - keyFrame = frame - let viewWidth = keyFrame.width * (isPortrait ? 1.6 : 1.3) - let viewHeight = keyFrame.size.height * 2.1 - - var viewPosX = keyFrame.origin.x - (viewWidth - keyFrame.width) / 2.0 - let viewPosY = keyFrame.origin.y - (viewHeight - keyFrame.height) - - adjY = 0 - - if viewPosX < 0 { - adjX = keyFrame.origin.x - viewPosX - viewPosX = keyFrame.origin.x - } else if (viewPosX + viewWidth) > UIScreen.main.bounds.width { - adjX = viewPosX - keyFrame.origin.x - viewPosX = keyFrame.origin.x + 2 * adjX - } else { - adjX = 0 - } - - let m = strokeWidth * 2 - let fontSize = viewHeight * 0.35 - let labelFrame = CGRect(x: m, y: m, width: viewWidth - 2 * m, height: viewHeight * 0.5 - m) - label = UILabel(frame: labelFrame) - label.numberOfLines = 1 - label.minimumScaleFactor = 0.25 - label.adjustsFontSizeToFitWidth = true - label.textAlignment = .center - label.font = UIFont.systemFont(ofSize: fontSize) - label.shadowColor = UIColor.lightText - label.shadowOffset = CGSize(width: 1.0, height: 1.0) - label.backgroundColor = UIColor.clear - label.textColor = Colors.keyText - label.text = "" - - super.init(frame: CGRect(x: viewPosX, y: viewPosY, width: viewWidth, height: viewHeight)) - - super.backgroundColor = UIColor.clear - isUserInteractionEnabled = false - addSubview(label) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func draw(_ rect: CGRect) { - drawKeyPreviewUp(rect) - } - - private func drawKeyPreviewUp(_ rect: CGRect) { - let keyWidth = keyFrame.width - let viewWidth = rect.width - let viewHeight = rect.height - - guard let context = UIGraphicsGetCurrentContext() else { - return - } - UIGraphicsPushContext(context) - context.setLineJoin(.round) - context.setLineWidth(strokeWidth * 2) - context.setStrokeColor(borderColor.cgColor) - context.setFillColor(bgColor.cgColor) - - let keyLeft = (viewWidth - keyWidth) / 2.0 + strokeWidth - adjX - let keyRight = (viewWidth - keyWidth) / 2.0 + keyWidth - strokeWidth - adjX - let midY1 = viewHeight * 3.0 / 5.0 - let midY2 = viewHeight / 2.0 - let viewLeft = strokeWidth - let viewRight = viewWidth - strokeWidth - let viewTop = strokeWidth - let viewBottom = viewHeight - strokeWidth * 1.25 - let viewMid: CGFloat = viewWidth / 2.0 - let r0 = borderRadius - strokeWidth - let r1 = borderRadius - strokeWidth - let r2 = borderRadius * 1.25 - strokeWidth - - context.beginPath() - context.move(to: CGPoint(x: viewMid, y: viewTop)) - context.addArc(tangent1End: CGPoint(x: viewLeft, y: viewTop), - tangent2End: CGPoint(x: viewLeft, y: midY2), radius: r1) - context.addArc(tangent1End: CGPoint(x: viewLeft, y: midY2), - tangent2End: CGPoint(x: keyLeft, y: midY1), radius: r2) - context.addArc(tangent1End: CGPoint(x: keyLeft, y: midY1), - tangent2End: CGPoint(x: keyLeft, y: viewBottom), radius: r2) - context.addArc(tangent1End: CGPoint(x: keyLeft, y: viewBottom), - tangent2End: CGPoint(x: keyRight, y: viewBottom), radius: r0) - context.addArc(tangent1End: CGPoint(x: keyRight, y: viewBottom), - tangent2End: CGPoint(x: keyRight, y: midY1), radius: r0) - context.addArc(tangent1End: CGPoint(x: keyRight, y: midY1), - tangent2End: CGPoint(x: viewRight, y: midY2), radius: r2) - context.addArc(tangent1End: CGPoint(x: viewRight, y: midY2), - tangent2End: CGPoint(x: viewRight, y: viewTop), radius: r2) - context.addArc(tangent1End: CGPoint(x: viewRight, y: viewTop), - tangent2End: CGPoint(x: viewMid, y: viewTop), radius: r1) - context.closePath() - context.drawPath(using: .stroke) - - context.beginPath() - context.move(to: CGPoint(x: viewMid, y: viewTop)) - context.addArc(tangent1End: CGPoint(x: viewLeft, y: viewTop), - tangent2End: CGPoint(x: viewLeft, y: midY2), radius: r1) - context.addArc(tangent1End: CGPoint(x: viewLeft, y: midY2), - tangent2End: CGPoint(x: keyLeft, y: midY1), radius: r2) - context.addArc(tangent1End: CGPoint(x: keyLeft, y: midY1), - tangent2End: CGPoint(x: keyLeft, y: viewBottom), radius: r2) - context.addArc(tangent1End: CGPoint(x: keyLeft, y: viewBottom), - tangent2End: CGPoint(x: keyRight, y: viewBottom), radius: r0) - context.addArc(tangent1End: CGPoint(x: keyRight, y: viewBottom), - tangent2End: CGPoint(x: keyRight, y: midY1), radius: r0) - context.addArc(tangent1End: CGPoint(x: keyRight, y: midY1), - tangent2End: CGPoint(x: viewRight, y: midY2), radius: r2) - context.addArc(tangent1End: CGPoint(x: viewRight, y: midY2), - tangent2End: CGPoint(x: viewRight, y: viewTop), radius: r2) - context.addArc(tangent1End: CGPoint(x: viewRight, y: viewTop), - tangent2End: CGPoint(x: viewMid, y: viewTop), radius: r1) - context.closePath() - context.clip() - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradientColors = [bgColor.cgColor, bgColor2.cgColor] as CFArray - let gradientLocations: [CGFloat] = [0, 1] - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: gradientLocations)! - - let startPoint = CGPoint(x: rect.midX, y: rect.minY) - let endPoint = CGPoint(x: rect.midX, y: rect.maxY) - context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: []) - UIGraphicsPopContext() - } - - func setLabelText(_ text: String) { - label.text = text - } - - func setLabelFont(_ fontName: String?) { - let fontSize = label.font.pointSize - if let fontName = fontName { - label.font = UIFont(name: fontName, size: fontSize) - } else { - label.font = UIFont.systemFont(ofSize: fontSize) - } - } - - override var backgroundColor: UIColor? { - get { - return super.backgroundColor - } - - set(color) { - super.backgroundColor = UIColor.clear - if let color = color { - bgColor = color - } - } - } -} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebDelegate.swift b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebDelegate.swift index a176a2d1ed0..9a3ed80b9aa 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebDelegate.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebDelegate.swift @@ -21,23 +21,6 @@ protocol KeymanWebDelegate: class { /// - Parameters: func beep(_ keymanWeb: KeymanWebViewController) - /// - Parameters: - /// - keyFrame: The frame of the anchor key. - /// - preview: The text to preview. - func showKeyPreview(_ keymanWeb: KeymanWebViewController, keyFrame: CGRect, preview: String) - - func dismissKeyPreview(_ keymanWeb: KeymanWebViewController) - - /// - Parameters: - /// - keyFrame: The frame of the anchor key. - /// - subkeyIDs: The IDs of the subkeys. - /// - subkeyTexts: The user-displayable texts of the subkeys. - /// - useSpecialFont: Use the Keyman Web OSK font. - func showSubkeys(_ keymanWeb: KeymanWebViewController, - keyFrame: CGRect, - subkeyIDs: [String], - subkeyTexts: [String], - useSpecialFont: Bool) func menuKeyDown(_ keymanWeb: KeymanWebViewController) func menuKeyUp(_ keymanWeb: KeymanWebViewController) func menuKeyHeld(_ keymanWeb: KeymanWebViewController) @@ -47,13 +30,6 @@ extension KeymanWebDelegate { func keyboardLoaded(_ keymanWeb: KeymanWebViewController) {} func insertText(_ keymanWeb: KeymanWebViewController, numCharsToDelete: Int, newText: String) {} func beep(_ keymanWeb: KeymanWebViewController) {} - func showKeyPreview(_ keymanWeb: KeymanWebViewController, keyFrame: CGRect, preview: String) {} - func dismissKeyPreview(_ keymanWeb: KeymanWebViewController) {} - func showSubkeys(_ keymanWeb: KeymanWebViewController, - keyFrame: CGRect, - subkeyIDs: [String], - subkeyTexts: [String], - useSpecialFont: Bool) {} func menuKeyDown(_ keymanWeb: KeymanWebViewController) {} func menuKeyUp(_ keymanWeb: KeymanWebViewController) {} func menuKeyHeld(_ keymanWeb: KeymanWebViewController) {} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift index 3d91398ab76..50a22e6fe45 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift @@ -45,8 +45,6 @@ class KeymanWebViewController: UIViewController { var webView: KeymanWebView? var activeModel: Bool = false private var helpBubbleView: PopoverView? - private var keyPreviewView: KeyPreviewView? - private var subKeysView: SubKeysView? private var keyboardMenuView: KeyboardMenuView? // Arrays @@ -155,12 +153,6 @@ class KeymanWebViewController: UIViewController { view = webView - // Set UILongPressGestureRecognizer to show sub keys -// let hold = UILongPressGestureRecognizer(target: self, action: #selector(self.holdAction)) -// hold.minimumPressDuration = 0.5 -// hold.delegate = self -// view.addGestureRecognizer(hold) - NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), @@ -494,80 +486,6 @@ extension KeymanWebViewController: WKScriptMessageHandler { let newText = String(s).stringFromUTF16CodeUnits() ?? "" insertText(self, numCharsToDelete: numCharsToDelete, newText: newText) delegate?.insertText(self, numCharsToDelete: numCharsToDelete, newText: newText) - } else if fragment.hasPrefix("#showKeyPreview-") { - let xKey = fragment.range(of: "+x=")! - let yKey = fragment.range(of: "+y=")! - let wKey = fragment.range(of: "+w=")! - let hKey = fragment.range(of: "+h=")! - let tKey = fragment.range(of: "+t=")! - - let x = CGFloat(Float(fragment[xKey.upperBound.. Void) - NSObject.cancelPreviousPerformRequests(withTarget: self, selector: dismissKeyPreview, object: nil) - perform(dismissKeyPreview, with: nil, afterDelay: 0.1) - clearSubKeyArrays() - } - - func showSubkeys(_ view: KeymanWebViewController, - keyFrame: CGRect, - subkeyIDs: [String], - subkeyTexts: [String], - useSpecialFont: Bool) { - dismissHelpBubble() - Manager.shared.isKeymanHelpOn = false - dismissSubKeys() - dismissKeyboardMenu() - - subKeyAnchor = keyFrame - subKeyIDs = subkeyIDs - subKeyTexts = subkeyTexts - self.useSpecialFont = useSpecialFont - } - - func menuKeyUp(_ view: KeymanWebViewController) { - dismissHelpBubble() - Manager.shared.isKeymanHelpOn = false - if Util.isSystemKeyboard { - let userData = UserDefaults.standard - userData.set(true, forKey: Key.keyboardPickerDisplayed) - userData.synchronize() - } - } - func hideKeyboard(_ view: KeymanWebViewController) { dismissHelpBubble() - dismissSubKeys() dismissKeyboardMenu() } } -// MARK: - UIGestureRecognizerDelegate -extension KeymanWebViewController: UIGestureRecognizerDelegate { - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } - - // An adaptation of holdAction, just for direct taps. - @objc func tapAction(_ sender: UITapGestureRecognizer) { - switch sender.state { - case .ended: - // Touch Ended - guard let subKeysView = subKeysView else { - return - } - - let touchPoint = sender.location(in: subKeysView.containerView) - var buttonClicked = false - for button in subKeys { - if button.frame.contains(touchPoint) { - button.isEnabled = true - button.isHighlighted = true - button.backgroundColor = subKeyColorHighlighted - button.sendActions(for: .touchUpInside) - - buttonClicked = true - } else { - button.isHighlighted = false - button.isEnabled = false - button.backgroundColor = subKeyColor - } - } - - if !buttonClicked { - clearSubKeyArrays() - } - default: - return - } - } - - @objc func holdAction(_ sender: UILongPressGestureRecognizer) { - switch sender.state { - case .ended: - // Touch Ended - if let subKeysView = subKeysView { - subKeysView.removeFromSuperview() - subKeysView.subviews.forEach { $0.removeFromSuperview() } - self.subKeysView = nil - setPopupVisible(false) - } - var buttonClicked = false - for button in subKeys where button.isHighlighted { - button.isHighlighted = false - button.backgroundColor = subKeyColor - button.isEnabled = false - button.sendActions(for: .touchUpInside) - buttonClicked = true - break - } - if !buttonClicked { - clearSubKeyArrays() - } - case .began: -// // Touch & Hold Began -// let touchPoint = sender.location(in: sender.view) -// // Check if touch was for language menu button -// languageMenuPosition { keyFrame in -// if keyFrame.contains(touchPoint) { -// self.delegate?.menuKeyHeld(self) -// return -// } -// self.touchHoldBegan() -// } - return - default: - // Hold & Move - guard let subKeysView = subKeysView else { - return - } - let touchPoint = sender.location(in: subKeysView.containerView) - for button in subKeys { - if button.frame.contains(touchPoint) { - button.isEnabled = true - button.isHighlighted = true - button.backgroundColor = subKeyColorHighlighted - } else { - button.isHighlighted = false - button.isEnabled = false - button.backgroundColor = subKeyColor - } - } - } - } - - private func touchHoldBegan() { - // Is also called for banner longpresses. Will need a way to properly differentiate. - let isPad = UIDevice.current.userInterfaceIdiom == .pad - let fontSize = isPad ? UIFont.buttonFontSize * 2 : UIFont.buttonFontSize - - let oskFontName = Manager.shared.oskFontNameForKeyboard(withFullID: Manager.shared.currentKeyboardID!) - ?? Manager.shared.fontNameForKeyboard(withFullID: Manager.shared.currentKeyboardID!) - - if subKeyIDs.isEmpty { - subKeys = [] - return - } - - subKeys = subKeyTexts.enumerated().map { i, subKeyText in - let button = UIButton(type: .custom) - button.tag = i - button.backgroundColor = subKeyColor - button.setRoundedBorder(withRadius: 4.0, borderWidth: 1.0, color: .gray) - button.setTitleColor(Colors.keyText, for: .disabled) - button.setTitleColor(Colors.keyText, for: .highlighted) - button.setTitleColor(Colors.keyText, for: .normal) - - if let oskFontName = oskFontName { - button.titleLabel?.font = UIFont(name: oskFontName, size: fontSize) - } else { - button.titleLabel?.font = UIFont.systemFont(ofSize: fontSize) - } - - if useSpecialFont{ - if FontManager.shared.registerFont(at: Storage.active.specialOSKFontURL), - let fontName = FontManager.shared.fontName(at: Storage.active.specialOSKFontURL) { - button.titleLabel?.font = UIFont(name: fontName, size: fontSize) - } - button.setTitleColor(.gray, for: .disabled) - } - - button.addTarget(self, action: #selector(subKeyButtonClick), for: .touchUpInside) - - // Detect the text width for subkeys. The 'as Any' silences an inappropriate warning from Swift. - let textSize = subKeyText.size(withAttributes: [NSAttributedString.Key.font: button.titleLabel?.font! as Any]) - var displayText = subKeyText - - if textSize.width <= 0 && subKeyText.count > 0 { - // It's probably a diacritic in need of a combining character! - // Also check if the language is RTL! - if Manager.shared.currentKeyboard?.isRTL ?? false { - displayText = "\u{200f}\u{25cc}" + subKeyText - } else { - displayText = "\u{25cc}" + subKeyText - } - } - - button.setTitle(displayText, for: .normal) - button.tintColor = Colors.popupKeyTint - button.isEnabled = false - return button - } - - dismissKeyPreview() - subKeysView = SubKeysView(keyFrame: subKeyAnchor, subKeys: subKeys) - view.addSubview(subKeysView!) - - let tap = UITapGestureRecognizer(target: self, action: #selector(self.tapAction)) - subKeysView!.addGestureRecognizer(tap) - - let hold = UILongPressGestureRecognizer(target: self, action: #selector(self.holdAction)) - hold.minimumPressDuration = 0.01 - hold.delegate = self - subKeysView!.addGestureRecognizer(hold) - setPopupVisible(true) - } - - @objc func menuKeyHeld(_ keymanWeb: KeymanWebViewController) { - self.delegate?.menuKeyHeld(self) - } - - @objc func subKeyButtonClick(_ sender: UIButton) { - let keyIndex = sender.tag - if keyIndex < subKeyIDs.count && keyIndex < subKeyTexts.count { - let subKeyID = subKeyIDs[keyIndex] - let subKeyText = subKeyTexts[keyIndex] - executePopupKey(id: subKeyID, text: subKeyText) - } - subKeys.removeAll() - subKeyIDs.removeAll() - subKeyTexts.removeAll() - } -} - // MARK: - Manage views extension KeymanWebViewController { // MARK: - Sizing public var keyboardHeight: CGFloat { return keyboardSize.height } + + @objc func menuKeyHeld(_ keymanWeb: KeymanWebViewController) { + self.delegate?.menuKeyHeld(self) + } func constraintTargetHeight(isPortrait: Bool) -> CGFloat { return KeyboardScaleMap.getDeviceDefaultKeyboardScale(forPortrait: isPortrait)?.keyboardHeight ?? 216 // default for ancient devices @@ -1089,8 +771,6 @@ extension KeymanWebViewController { } func resetKeyboardState() { - dismissSubKeys() - dismissKeyPreview() dismissKeyboardMenu() resizeKeyboard() } @@ -1217,33 +897,6 @@ extension KeymanWebViewController { helpBubbleView = nil } - @objc func dismissKeyPreview() { - keyPreviewView?.removeFromSuperview() - keyPreviewView = nil - } - - var isSubKeysMenuVisible: Bool { - return subKeysView != nil - } - - func dismissSubKeys() { - if let subKeysView = subKeysView { - subKeysView.removeFromSuperview() - subKeysView.subviews.forEach { $0.removeFromSuperview() } - self.subKeysView = nil - setPopupVisible(false) - } - clearSubKeyArrays() - } - - func clearSubKeyArrays() { - if subKeysView == nil { - subKeys.removeAll() - subKeyIDs.removeAll() - subKeyTexts.removeAll() - } - } - func showHelpBubble(afterDelay delay: TimeInterval) { helpBubbleView?.removeFromSuperview() let showHelpBubble = #selector(self.showHelpBubble as () -> Void) @@ -1277,8 +930,6 @@ extension KeymanWebViewController { // MARK: - Keyboard Notifications extension KeymanWebViewController { @objc func keyboardWillShow(_ notification: Notification) { - dismissSubKeys() - dismissKeyPreview() resizeKeyboard() if Manager.shared.isKeymanHelpOn { @@ -1288,7 +939,5 @@ extension KeymanWebViewController { @objc func keyboardWillHide(_ notification: Notification) { dismissHelpBubble() - dismissSubKeys() - dismissKeyPreview() } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/SubKeysView.swift b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/SubKeysView.swift deleted file mode 100644 index 5bfa45a7a99..00000000000 --- a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/SubKeysView.swift +++ /dev/null @@ -1,272 +0,0 @@ -// -// SubKeysView.swift -// KMEI -// -// Created by Gabriel Wong on 2017-09-28. -// Copyright © 2017 SIL International. All rights reserved. -// - -import UIKit - -class SubKeysView: UIView { - private let borderRadius: CGFloat = 5.0 - private let strokeWidth: CGFloat = 1.0 - private var baseKeyYDelta: CGFloat = 0.0 - - private var keyFrame = CGRect.zero - private var adjX: CGFloat = 0.0 - private var adjY: CGFloat = 0.0 - private var rows: Int = 0 - - private let scale = UIScreen.main.scale - private let bgColor = Colors.keyboardBackground - private let bgColor2 = Colors.keyboardBackground - private let borderColor = Colors.popupBorder - - let containerView: UIView - - init(keyFrame frame: CGRect, subKeys: [UIButton]) { - let isSystemKeyboard = Manager.shared.isSystemKeyboard - var isPortrait = true - if isSystemKeyboard { - isPortrait = InputViewController.isPortrait - } else { - isPortrait = UIDevice.current.orientation.isPortrait - } - - let screenRect = UIScreen.main.bounds - let screenWidth = screenRect.width - let screenHeight = screenRect.height - - keyFrame = frame.insetBy(dx: 0, dy: 0) - var buttonWidth: CGFloat - var buttonHeight: CGFloat - let marginX: CGFloat = 4.0 - let marginY: CGFloat = 3.0 - - let maxButtonSize = CGSize(width: keyFrame.width * 0.9, height: keyFrame.height * 0.9) - let minButtonSize: CGSize - if UIDevice.current.userInterfaceIdiom == .pad { - if isPortrait { - minButtonSize = CGSize(width: keyFrame.width * 0.65, height: keyFrame.height * 0.65) - } else { - minButtonSize = CGSize(width: keyFrame.width * 0.55, height: keyFrame.height * 0.55) - } - } else { - if isPortrait { - minButtonSize = CGSize(width: keyFrame.width * 0.75, height: keyFrame.height * 0.75) - } else { - minButtonSize = CGSize(width: keyFrame.width * 0.675, height: keyFrame.height * 0.675) - } - } - - let kbHeight = Manager.shared.inputViewController.kmwHeight - let maxContainerHeight = (screenHeight - kbHeight) + keyFrame.origin.y - strokeWidth - - var columns = Int((screenWidth - marginX) / (maxButtonSize.width + marginX)) - rows = subKeys.count / columns - if subKeys.count % columns != 0 { - rows += 1 - } - - if subKeys.count % rows == 0 { - columns = subKeys.count / rows - } else { - let s = (columns * rows - subKeys.count) / 2 - columns -= s / (rows - 1) - } - - buttonWidth = maxButtonSize.width - buttonHeight = (maxContainerHeight - CGFloat(rows + 1) * marginY) / CGFloat(rows) - buttonHeight = CGFloat.minimum(buttonHeight, maxButtonSize.height) - buttonHeight = CGFloat.maximum(buttonHeight, minButtonSize.height) - - let cx: CGFloat = 1.5 - let containerWidth = (columns < 2 ? cx : CGFloat(columns)) * (buttonWidth + marginX) + marginX - let containerHeight = (CGFloat(rows) * (buttonHeight + marginY)) + marginY - let containerFrame = CGRect(x: strokeWidth, y: strokeWidth, width: containerWidth, height: containerHeight) - let viewWidth = containerWidth + 2 * strokeWidth - let baseHeight = keyFrame.size.height + marginY - let viewHeight = baseHeight + containerHeight - marginY + strokeWidth - var viewPosX = keyFrame.origin.x - (viewWidth - keyFrame.size.width) / 2.0 - var viewPosY = keyFrame.origin.y - (viewHeight - keyFrame.size.height + strokeWidth) - - // Ensure that the system keyboard doesn't try to display off the top of the keyboard's view area. - if viewPosY < 0 && isSystemKeyboard { - baseKeyYDelta = viewPosY // We'll need this to adjust the visualization. - viewPosY = 0 - } - - adjX = 0 - adjY = 0 - - if viewPosX < 0 { - if (keyFrame.origin.x - borderRadius * 1.25) < 0 { - adjX = keyFrame.origin.x - viewPosX - viewPosX = keyFrame.origin.x - } else { - adjX = -viewPosX - viewPosX = 0 - } - } else if (viewPosX + viewWidth) > screenWidth { - if ((keyFrame.origin.x + keyFrame.size.width) + borderRadius * 1.25) > screenWidth { - adjX = viewPosX - keyFrame.origin.x - viewPosX = keyFrame.origin.x + 2 * adjX - } else { - adjX = (screenWidth - viewWidth) - viewPosX - viewPosX = adjX + viewPosX - } - } - containerView = UIView(frame: containerFrame) - containerView.backgroundColor = UIColor.clear - - super.init(frame: CGRect(x: viewPosX, y: viewPosY, width: viewWidth, height: viewHeight)) - - super.backgroundColor = UIColor.clear - //isUserInteractionEnabled = false - - addSubview(containerView) - let fontSize = buttonHeight * (UIDevice.current.userInterfaceIdiom == .pad ? 0.4 : 0.5) - - for button in subKeys { - let i = button.tag - let buttonRow = CGFloat(i / columns) - let buttonFrame = CGRect(x: marginX + CGFloat(i % columns) * (buttonWidth + marginX), - y: (buttonRow * buttonHeight) + (buttonRow + 1) * marginY, - width: buttonWidth, height: buttonHeight) - button.frame = buttonFrame - if subKeys.count == 1 { - button.center = containerView.center - } - - button.titleLabel?.minimumScaleFactor = 0.25 - button.titleLabel?.adjustsFontSizeToFitWidth = true - button.titleLabel?.font = button.titleLabel?.font.withSize(fontSize) - containerView.addSubview(button) - } - - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func draw(_ rect: CGRect) { - drawKeyPreviewUp(rect) - } - - private func drawKeyPreviewUp(_ rect: CGRect) { - let keyWidth = keyFrame.size.width - let keyHeight = keyFrame.size.height + adjY - let viewWidth = rect.size.width - let viewHeight = rect.size.height - - guard let context = UIGraphicsGetCurrentContext() else { - return - } - UIGraphicsPushContext(context) - context.setLineJoin(.round) - context.setLineWidth(strokeWidth * 2) - context.setStrokeColor(borderColor.cgColor) - context.setFillColor(bgColor.cgColor) - - let keyLeftUnrounded = (viewWidth - keyWidth) / 2.0 + strokeWidth - adjX - let keyRightUnrounded = (viewWidth - keyWidth) / 2.0 + keyWidth - strokeWidth - adjX - var keyLeft = (keyLeftUnrounded * 10).rounded(.up) / 10 - var keyRight = (keyRightUnrounded * 10).rounded(.down) / 10 - - let midY = viewHeight - keyHeight - var viewLeft = strokeWidth - var viewRight = viewWidth - strokeWidth - let viewTop = strokeWidth - - var viewBottom = viewHeight - strokeWidth * 0.75 - (CGFloat(rows - 1) * 0.25) - viewBottom += baseKeyYDelta - - let showBaseKeyPopup = (viewBottom > midY) - - if scale == 3.0 { - viewBottom -= CGFloat(3 - rows) * 0.4 - } else if scale == 2.0 { - viewBottom -= CGFloat(3 - rows) * 0.1 - } - - let viewMid = viewWidth / 2.0 - let r0 = borderRadius - strokeWidth - let r1 = borderRadius - strokeWidth - let r2 = borderRadius * 1.25 - strokeWidth - - if viewLeft.rounded() == keyLeft.rounded() || viewLeft.rounded(.down) == keyLeft.rounded(.down) { - keyLeft = CGFloat.minimum(viewLeft, keyLeft) - viewLeft = keyLeft - } - - if viewRight.rounded() == keyRight.rounded() || viewRight.rounded(.down) == keyRight.rounded(.down) { - keyRight = CGFloat.maximum(viewRight, keyRight) - viewRight = keyRight - } - - context.beginPath() - context.move(to: CGPoint(x: viewMid, y: viewTop)) - context.addArc(tangent1End: CGPoint(x: viewLeft, y: viewTop), - tangent2End: CGPoint(x: viewLeft, y: midY), radius: r1) - if showBaseKeyPopup { - context.addArc(tangent1End: CGPoint(x: viewLeft, y: midY), - tangent2End: CGPoint(x: keyLeft, y: midY), radius: r1) - context.addArc(tangent1End: CGPoint(x: keyLeft, y: midY), - tangent2End: CGPoint(x: keyLeft, y: viewBottom), radius: r2) - context.addArc(tangent1End: CGPoint(x: keyLeft, y: viewBottom), - tangent2End: CGPoint(x: keyRight, y: viewBottom), radius: r0) - context.addArc(tangent1End: CGPoint(x: keyRight, y: viewBottom), - tangent2End: CGPoint(x: keyRight, y: midY), radius: r0) - context.addArc(tangent1End: CGPoint(x: keyRight, y: midY), - tangent2End: CGPoint(x: viewRight, y: midY), radius: r2) - } else { - context.addArc(tangent1End: CGPoint(x: viewLeft, y: midY), - tangent2End: CGPoint(x: viewRight, y: midY), radius: r0) - } - context.addArc(tangent1End: CGPoint(x: viewRight, y: midY), - tangent2End: CGPoint(x: viewRight, y: viewTop), radius: r1) - context.addArc(tangent1End: CGPoint(x: viewRight, y: viewTop), - tangent2End: CGPoint(x: viewMid, y: viewTop), radius: r1) - context.closePath() - context.drawPath(using: .stroke) - - context.beginPath() - context.move(to: CGPoint(x: viewMid, y: viewTop)) - context.addArc(tangent1End: CGPoint(x: viewLeft, y: viewTop), - tangent2End: CGPoint(x: viewLeft, y: midY), radius: r1) - if showBaseKeyPopup { - context.addArc(tangent1End: CGPoint(x: viewLeft, y: midY), - tangent2End: CGPoint(x: keyLeft, y: midY), radius: r1) - context.addArc(tangent1End: CGPoint(x: keyLeft, y: midY), - tangent2End: CGPoint(x: keyLeft, y: viewBottom), radius: r2) - context.addArc(tangent1End: CGPoint(x: keyLeft, y: viewBottom), - tangent2End: CGPoint(x: keyRight, y: viewBottom), radius: r0) - context.addArc(tangent1End: CGPoint(x: keyRight, y: viewBottom), - tangent2End: CGPoint(x: keyRight, y: midY), radius: r0) - context.addArc(tangent1End: CGPoint(x: keyRight, y: midY), - tangent2End: CGPoint(x: viewRight, y: midY), radius: r2) - } else { - context.addArc(tangent1End: CGPoint(x: viewLeft, y: midY), - tangent2End: CGPoint(x: viewRight, y: midY), radius: r0) - } - context.addArc(tangent1End: CGPoint(x: viewRight, y: midY), - tangent2End: CGPoint(x: viewRight, y: viewTop), radius: r1) - context.addArc(tangent1End: CGPoint(x: viewRight, y: viewTop), - tangent2End: CGPoint(x: viewMid, y: viewTop), radius: r1) - context.closePath() - context.clip() - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradientColors = [bgColor.cgColor, bgColor2.cgColor] as CFArray - let gradientLocations: [CGFloat] = [0, 1] - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: gradientLocations)! - - let startPoint = CGPoint(x: rect.midX, y: rect.minY) - let endPoint = CGPoint(x: rect.midX, y: rect.maxY) - - context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: []) - UIGraphicsPopContext() - } -} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index 5b77f79307e..d1d134b2494 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -831,12 +831,22 @@ public class Manager: NSObject, UIGestureRecognizerDelegate { shared.copyUserDefaults(to: nonShared, withKeys: keysToCopy, shouldOverwrite: true) do { try shared.copyFiles(to: nonShared) - FontManager.shared.registerCustomFonts() } catch { let message = ("Failed to copy from shared container: \(error)") os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message) SentryManager.capture(error, message:message) } + + // This operation has a surprisingly high cost and isn't critical + // for getting the keyboard loaded and available to the OS. + // + // We certainly want it done _soon_... but we don't want it to cause + // the primary, synchronous initialization call from the OS to run so + // long that the keyboard fails to start due to its enforced time + // constraints. + DispatchQueue.main.async { + FontManager.shared.registerCustomFonts() + } } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Data/FontManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Data/FontManager.swift index d64019cc27c..6070de2c96f 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Data/FontManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Data/FontManager.swift @@ -26,6 +26,17 @@ public class FontManager { fonts[url] = RegisteredFont(name: name, isRegistered: false) return name } + + private func cachedFont(at url: URL) -> RegisteredFont? { + if let font = fonts[url] { + return font + } + guard let name = readFontName(at: url) else { + return nil + } + fonts[url] = RegisteredFont(name: name, isRegistered: false) + return fonts[url] + } /// Registers all new fonts found in the font path. Call this after you have preloaded all your font files /// with `preloadFontFile(atPath:shouldOverwrite:)` @@ -33,8 +44,50 @@ public class FontManager { guard let keyboardDirs = Storage.active.keyboardDirs else { return } + + var fontSet: Set = [] for dir in keyboardDirs { - registerFonts(in: dir) + fontSet = fontSet.union(listFonts(in: dir)) + } + + registerListedFonts(fontSet) + } + + /** + * Iterates across all listed fonts, registering those not yet registered. + * Checks for, and filters out, any fonts already registered on the system. + * Also initializes the registration cache per URL if needed. + */ + private func registerListedFonts(_ initialFontSet: Set) { + // If we are unable to read the font file's properties sufficiently, + // skip it. We also don't need to register anything already registered or + // that cannot be registered due to loading/parsing errors. + // + // Calls to `cachedFont` after the `.filter` below may be assumed to have + // non-nil return values. + var fontSet = initialFontSet.filter { !(cachedFont(at: $0)?.isRegistered ?? true) } + + // The prior line filters out any entries where cachedFont(at: $0) would be nil. + // Batch-lookups all fonts lacking cache-confirmation of prior registration. + var fontNamesToRegister = missingFonts(from: Set(fontSet.map { cachedFont(at: $0)!.name })) + + for fontUrl in fontSet { + let fontName = cachedFont(at: fontUrl)!.name + + guard fontNamesToRegister.contains(fontName) else { + let message = "Did not register font at \(fontUrl) because font name \(fontName) is already registered" + os_log("%{public}s", log:KeymanEngineLogger.resources, type: .info, message) + continue + } + + let didRegister = _registerFont(at: fontUrl) + fonts[fontUrl] = RegisteredFont(name: fontName, isRegistered: didRegister) + + // We no longer need to register a font with this name, so drop it from + // the set to register. + if didRegister { + fontNamesToRegister.remove(fontName) + } } } @@ -43,6 +96,8 @@ public class FontManager { guard let keyboardDirs = Storage.active.keyboardDirs else { return } + // This doesn't use the expensive looped lookup operation seen in missingFonts, + // so there's no need to batch similar operations here. for dir in keyboardDirs { unregisterFonts(in: dir) } @@ -63,44 +118,29 @@ public class FontManager { } return name as String } + + private func _registerFont(at url: URL) -> Bool { + var errorRef: Unmanaged? + let fontName = fontName(at: url)! + let didRegister = CTFontManagerRegisterFontsForURL(url as CFURL, .none, &errorRef) + let error = errorRef?.takeRetainedValue() // Releases errorRef + if !didRegister { + let message = "Failed to register font \(fontName) at \(url) reason: \(String(describing: error))" + os_log("%{public}s", log:KeymanEngineLogger.resources, type: .error, message) + } else { + let message = "Registered font \(fontName) at \(url)" + os_log("%{public}s", log:KeymanEngineLogger.resources, type: .info, message) + } + + return didRegister + } /// - Parameters: /// - url: URL of the font to register /// - Returns: Font is registered. public func registerFont(at url: URL) -> Bool { - let fontName: String - if let font = fonts[url] { - if font.isRegistered { - return true - } - fontName = font.name - } else { - guard let name = readFontName(at: url) else { - return false - } - fontName = name - } - - let didRegister: Bool - if !fontExists(fontName) { - var errorRef: Unmanaged? - didRegister = CTFontManagerRegisterFontsForURL(url as CFURL, .none, &errorRef) - let error = errorRef?.takeRetainedValue() // Releases errorRef - if !didRegister { - let message = "Failed to register font \(fontName) at \(url) reason: \(String(describing: error))" - os_log("%{public}s", log:KeymanEngineLogger.resources, type: .error, message) - } else { - let message = "Registered font \(fontName) at \(url)" - os_log("%{public}s", log:KeymanEngineLogger.resources, type: .info, message) - } - } else { - didRegister = false - let message = "Did not register font at \(url) because font name \(fontName) is already registered" - os_log("%{public}s", log:KeymanEngineLogger.resources, type: .info, message) - } - let font = RegisteredFont(name: fontName, isRegistered: didRegister) - fonts[url] = font - return didRegister + registerListedFonts([url]) + return fonts[url]?.isRegistered ?? false } /// - Parameters: @@ -133,32 +173,53 @@ public class FontManager { return font.isRegistered } - - public func registerFonts(in directory: URL) { + + private func listFonts(in directory: URL) -> [URL] { guard let urls = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else { let message = "Could not list contents of directory \(directory)" os_log("%{public}s", log:KeymanEngineLogger.resources, type: .error, message) - return - } - for url in urls where url.lastPathComponent.hasFontExtension { - _ = registerFont(at: url) + return [] } + return urls.filter { $0.lastPathComponent.hasFontExtension } + } + + public func registerFonts(in directory: URL) { + let fontsToRegister = listFonts(in: directory) + registerListedFonts(Set(fontsToRegister)) } public func unregisterFonts(in directory: URL, fromSystemOnly: Bool = true) { - guard let urls = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else { - let message = "Could not list contents of directory \(directory)" - os_log("%{public}s", log:KeymanEngineLogger.resources, type: .error, message) - return - } - for url in urls where url.lastPathComponent.hasFontExtension { + let fontsToUnregister = listFonts(in: directory) + for url in fontsToUnregister { _ = unregisterFont(at: url, fromSystemOnly: fromSystemOnly) } } - private func fontExists(_ fontName: String) -> Bool { - return UIFont.familyNames.contains { familyName in - UIFont.fontNames(forFamilyName: familyName).contains(fontName) + /** + * Queries the system for existing registrations for the specified fonts with a single batch run. + * Only fonts that could not be found will be returned within the result set. + */ + private func missingFonts(from fontNames: Set) -> Set { + var fontsToFind = fontNames + + UIFont.familyNames.forEach { familyName in + if fontsToFind.count == 0 { + return + } + + let familyFonts = UIFont.fontNames(forFamilyName: familyName) + + for font in familyFonts { + if fontsToFind.contains(font) { + fontsToFind.remove(font) + } + + if fontsToFind.count == 0 { + break + } + } } + + return fontsToFind } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/el.lproj/ResourceInfoView.strings b/ios/engine/KMEI/KeymanEngine/Classes/el.lproj/ResourceInfoView.strings new file mode 100644 index 00000000000..6288e1237c7 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/el.lproj/ResourceInfoView.strings @@ -0,0 +1,2 @@ +/* Class = "UILabel"; text = "Scan this code to load this keyboard on another device"; ObjectID = "z2O-MT-IoV"; */ +"z2O-MT-IoV.text" = "Σαρῶστε αὐτὸν τὸν κωδικό γιὰ νὰ φορτώσετε αὐτὸ τὸ πληκτρολόγιο σὲ ἄλλη συσκευή"; diff --git a/ios/engine/KMEI/KeymanEngine/cs.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/cs.lproj/Localizable.strings index 86f138adffe..0653ed3aeb0 100644 --- a/ios/engine/KMEI/KeymanEngine/cs.lproj/Localizable.strings +++ b/ios/engine/KMEI/KeymanEngine/cs.lproj/Localizable.strings @@ -125,7 +125,7 @@ "kmp-error-missing-resource" = "Tento balíček neobsahuje požadovanou klávesnici ani slovník."; /* Error installing a Keyman package with a version of Keyman that does not support it */ -"kmp-error-unsupported-keyman-version" = "This package requires a newer version of Keyman."; +"kmp-error-unsupported-keyman-version" = "Tento balíček vyžaduje novější verzi Keyman."; /* Error opening a Keyman package - cannot parse contents */ "kmp-error-no-metadata" = "Tento balíček nebyl správně sestaven - obsah není znám."; diff --git a/ios/engine/KMEI/KeymanEngine/el.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/el.lproj/Localizable.strings new file mode 100644 index 00000000000..1bd8c7713fd --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/el.lproj/Localizable.strings @@ -0,0 +1,257 @@ +/* A descriptive message used for errors when the app is already busy downloading */ +"alert-download-error-busy" = "Λήψη ἤδη σὲ ἐξέλιξη."; + +/* A descriptive message used when a download fails */ +"alert-download-error-detail" = "Παρουσιάσθηκε σφάλμα κατὰ τὴν λήψη ἢ ἐγκατάσταση."; + +/* Title for a "download failed" alert */ +"alert-download-error-title" = "Σφάλμα κατὰ τὴν λήψη"; + +/* Title for a general alert about errors */ +"alert-error-title" = "Σφάλμα"; + +/* A descriptive message used when no internet connection is detected */ +"alert-no-connection-detail" = "Ἀδυναμία προσβάσεως στὸν διακομιστὴ Κῆμαν. Παρακαλοῦμε ξαναδοκιμάστε ἀργότερα."; + +/* Title for a "no connection" alert */ +"alert-no-connection-title" = "Σφάλμα συνδέσεως"; + +/* Short text for a 'back' navigational command. Used to return to the previous screen */ +"command-back" = "Πίσω"; + +/* Short text for a 'cancel' command. Used to back out of a menu or install process without making changes */ +"command-cancel" = "Ἀκυρῶστε"; + +/* Short text for a 'done' command. Used to back out of settings menus after changes have been made */ +"command-done" = "Ἕτοιμοι"; + +/* Short text for an 'install' command. Used to confirm installation of a package. */ +"command-install" = "Ἐγκαταστῆστε"; + +/* Short text for a 'next' navigational command. Used to advance to the next screen */ +"command-next" = "Ἑπόμενο"; + +/* Short text for an 'OK' command. Used for some error alerts */ +"command-ok" = "OK"; + +/* Short text for confirmining 'uninstall' commands. */ +"command-uninstall" = "Ἀπεγκαταστῆστε"; + +/* Text for the command to uninstall a keyboard */ +"command-uninstall-keyboard" = "Ἀπεγκαταστῆστε πληκτρολόγιο"; + +/* Confirmation text to display before uninstalling a keyboard */ +"command-uninstall-keyboard-confirm" = "Θὰ θέλατε νὰ ἀπεγκαταστήσετε αὐτὸ τὸ πληκτρολόγιο;"; + +/* Text for the command to uninstall a lexical model */ +"command-uninstall-lexical-model" = "Ἀπεγκαταστῆστε λεξικό"; + +/* Confirmation text to display before uninstalling a lexical model */ +"command-uninstall-lexical-model-confirm" = "Θὰ θέλατε νὰ ἀπεγκαταστήσετε αὐτὸ τὸ λεξικό;"; + +/* Text for error when a keyboard cannot load properly */ +"error-loading-keyboard" = "Ἀδυναμία φορτώσεως τοῦ πληκτρολογίου ποὺ ζητήσατε"; + +/* Text for error when a lexical model cannot load properly */ +"error-loading-lexical-model" = "Ἀδυναμία φορτώσεως τοῦ λεξικοῦ ποὺ ζητήσατε"; + +/* Text for error when an installed file is unexpectedly missing */ +"error-missing-file" = "Δὲν βρέθηκε κάποιο ἀπαιτούμενο ἀρχεῖο."; + +/* Text for error when an installed file is unexpectedly missing */ +"error-missing-file-critical" = "Κάποιο σημαντικὸ ἀρχεῖο δὲν εὑρέθη. Παρακαλοῦμε προσπαθῆστε νὰ ἐγκαταστήσετε ἐκ νέου τὴν ἐφαρμογή."; + +/* Text for errors in data received from search queries */ +"error-query-decoding" = "Αὐτὴν τὴν στιγμὴ ὑπάρχουν τεχνικὰ προβλήματα στὸν διακομιστή"; + +/* A descriptive message used when a download fails */ +"error-query-general" = "Σημειώθηκε σφάλμα κατὰ τὴν ἐπικοινωνία μὲ τὸν διακομιστή"; + +/* Text for error when a search query is unexpectedly empty */ +"error-query-no-data" = "Ὁ διακομιστὴς δὲν ἀποκρίθηκε"; + +/* Text for general errors where not much information is known */ +"error-unknown" = "Σημειώθηκε ἀπροσδόκητο σφάλμα"; + +/* Text for error when updating a resource: cannot download because no source is available */ +"error-update-no-link" = "Δὲν ὑπάρχει πηγαῖος κώδικας πρὸς ἐνημέρωσιν αὐτοῦ τοῦ πακέτου"; + +/* Text for error when updating a resource: Keyman does not know how to update the resource */ +"error-update-not-managed" = "Ἀδυναμία ἐνημερώσεως πόρου μὴ διαχειριζομένου ἀπὸ μηχανὴ Keyman"; + +/* Text for the command to open the help page of a keyboard, as shown on resource information views */ +"info-command-help-keyboard" = "Βοήθεια γιὰ τὸ πληκτρολόγιο"; + +/* Text for the command to open the help page of a lexical model, as shown on resource information views */ +"info-command-help-lexical-model" = "Βοήθεια γιὰ τὸ λεξικό"; + +/* Text label for displaying a keyboard's version, as shown on resource information views */ +"info-label-version-keyboard" = "Ἔκδοση πληκτρολογίου"; + +/* Text label for displaying a lexical model's version, as shown on resource information views */ +"info-label-version-lexical-model" = "Ἔκδοση λεξικοῦ"; + +/* Label used for the "package info" / readme tab with the package installation prompt on phones. */ +"installer-label-package-info" = "Πληροφορίες πακέτου"; + +/* Label used for the language-selection tab with the package installation prompt on phones. */ +"installer-label-select-languages" = "Ἐπιλέξτε γλῶσσα(-ες)"; + +/* Label used with a package's version, as seen within the package installation prompt. Example: "Version: 14.0.0" */ +"installer-label-version" = "Ἔκδοση: %@"; + +/* Section header for languages supported by a package, as seen within the package installation prompt */ +"installer-section-available-languages" = "Διαθέσιμες γλῶσσες"; + +/* Text for the help popup for changing keyboards with the globe key */ +"keyboard-help-change" = "Κτυπῆστε ἐδῶ γιὰ νὰ ἀλλάξετε πληκτρολόγιο"; + +/* Text for the command to exit the Keyman keyboard in favor of other keyboards installed on the system */ +"keyboard-menu-exit" = "Κλεῖστε τὸ %@"; + +/* Error installing a Keyman package - could not allocate a location to install it */ +"kmp-error-file-system" = "Σφάλμα κατά τὴν ἐγκατάσταση πακέτου - σφάλμα συστήματος ἀρχείων"; + +/* Error installing a Keyman package - could not copy package files */ +"kmp-error-file-copying" = "Σφάλμα κατά τὴν ἐγκατάσταση πακέτου - ἀδυναμία ἀντιγραφῆς ἀπαραιτήτων ἀρχείων"; + +/* Error opening a Keyman package - package is not valid */ +"kmp-error-invalid" = "Κατεστραμμένο ἀρχεῖο πακέτου."; + +/* Error opening a Keyman package - it does not exist / the specified location is wrong */ +"kmp-error-missing" = "Τὸ πακέτο ποὺ ὁρίσατε δὲν ὑπάρχει."; + +/* Error installing a Keyman package - expected resource (keyboard or dictionary) is missing */ +"kmp-error-missing-resource" = "Αὐτὸ τὸ πακέτο δὲν περιέχει τὸ πληκτρολόγιο ἢ λεξικὸ ποὺ ζητήσατε."; + +/* Error installing a Keyman package with a version of Keyman that does not support it */ +"kmp-error-unsupported-keyman-version" = "Αὐτὸ τὸ πακέτο ἀπαιτεῖ νεώτερη ἔκδοση τοῦ Κῆμαν."; + +/* Error opening a Keyman package - cannot parse contents */ +"kmp-error-no-metadata" = "Αὐτὸ τὸ πακέτο δὲν ἔχει κατασκευασθεῖ σωστά - ὑπάρχουν ἄγνωστα περιεχόμενα."; + +/* Error opening a Keyman package - package's contents are for desktop platforms only */ +"kmp-error-unsupported" = "Αὐτὸ τὸ πακέτο δὲν συμπεριλαμβάνει ὑποστήριξη γιὰ τὴν συσκευή σας."; + +/* Error opening a Keyman package - package contains unexpected resource (keyboard or dictionary) type */ +"kmp-error-wrong-type" = "Αὐτὸ τὸ πακέτο δὲν περιέχει τὸν ἀναμενόμενο τύπο πόρου."; + +/* Title for the Installed Languages menu */ +"menu-installed-languages-title" = "Ἐγκατεστημένες γλῶσσες"; + +/* Section header for lexical models within a language-specific settings menu */ +"menu-langsettings-label-lexical-models" = "Λεξικά"; + +/* Section header for keyboards within a language-specific settings menu */ +"menu-langsettings-section-keyboards" = "Πληκτρολόγια"; + +/* Section header for the settings toggles within a language-specific settings menu */ +"menu-langsettings-section-settings" = "Ρυθμίσεις γλώσσας"; + +/* Title for the language-specific settings menus */ +"menu-langsettings-title" = "Ρυθμίσεις %@"; + +/* Label for the toggle that enables corrections that is displayed within a language-specific settings menu */ +"menu-langsettings-toggle-correct" = "Ἐνεργοποιῆστε τὶς διορθώσεις"; + +/* Label for the toggle that enables predictions that is displayed within a language-specific settings menu */ +"menu-langsettings-toggle-predict" = "Ἐνεργοποιῆστε τὶς προβλέψεις κειμένου"; + +/* Help message for a prompt that appears for confirming a lexical model download: language (1): lexical model (dictionary) name (2) */ +"menu-lexical-model-install-message" = "Θὰ θέλατε νὰ ἐγκαταστήσετε αὐτὸ τὸ λεξικό;"; + +/* Title for a prompt that appears for confirming a lexical model download: language (1): lexical model (dictionary) name (2) */ +"menu-lexical-model-install-title" = "%1$@: %2$@"; + +/* Text for an info alert indicating that no lexical models are available */ +"menu-lexical-model-none-message" = "Δὲν ὑπάρχουν διαθέσιμα λεξικά"; + +/* Title for the lexical model menu, a submenu of the language-specific settings menus */ +"menu-lexical-model-title" = "%@ Λεξικά"; + +/* Title for the keyboard picker */ +"menu-picker-title" = "Πληκτρολόγια"; + +/* Primary text for the Settings menu option to report keyboard crashes */ +"menu-settings-error-report" = "Ἐπιτρέψτε ἀναφορὲς σφαλμάτων"; + +/* Secondary text for the Settings menu option to report keyboard crashes */ +"menu-settings-error-report-description" = "Μπορεῖ νὰ ἀπαιτηθεῖ \"πλήρης πρόσβαση\""; + +/* Primary text for the Settings menu local-file package installation option */ +"menu-settings-install-from-file" = "Ἐγκαταστῆστε ἀπὸ ἀρχεῖο"; + +/* Secondary text for the Settings menu local-file package installation option */ +"menu-settings-install-from-file-description" = "Ψάξτε ἀρχεῖα .kmp"; + +/* Primary text for the Settings menu option that displays the iOS system menu options for the app's keyboard */ +"menu-settings-system-keyboard-menu" = "Ρυθμίσεις πληκτρολογίου συστήματος"; + +/* Label for the "Show Banner" toggle on the main settings screen */ +"menu-settings-show-banner" = "Νὰ ἐμφανίζεται Banner"; + +/* Label for the "Get Started" automatic display toggle seen in the Settings menu */ +"menu-settings-startup-get-started" = "Νὰ ἐμφανίζεται ἡ ἐναρκτήρια ὀθόνη κατὰ τὴν ἐκκίνηση"; + +/* Title for the main Settings menu */ +"menu-settings-title" = "Ρυθμίσεις Κῆμαν"; + +/* Secondary text showing current setting for spacebar caption - blank */ +"menu-settings-spacebar-hint-blank" = "Καμμία λεζάντα στὸ πλῆκτρο διαστήματος"; + +/* Secondary text showing current setting for spacebar caption - keyboard */ +"menu-settings-spacebar-hint-keyboard" = "Ὄνομα πληκτρολογίου στὸ πλῆκτρο διαστήματος"; + +/* Secondary text showing current setting for spacebar caption - language */ +"menu-settings-spacebar-hint-language" = "Ὄνομα γλώσσας στὸ πλῆκτρο διαστήματος"; + +/* Secondary text showing current setting for spacebar caption - language + keyboard */ +"menu-settings-spacebar-hint-languageKeyboard" = "Ὄνομα πληκτρολογίου καὶ γλώσσας στὸ πλῆκτρο διαστήματος"; + +/* Label for the "Spacebar Caption" item on the main settings screen */ +"menu-settings-spacebar-text" = "Λεζάντα Πλήκτρου Διαστήματος"; + +/* Title for the "Spacebar Caption" settings screen */ +"menu-settings-spacebar-title" = "Λεζάντα Πλήκτρου Διαστήματος"; + +/* Text showing name of spacebar caption - blank */ +"menu-settings-spacebar-item-blank" = "Κενό"; + +/* Text showing name of spacebar caption - keyboard */ +"menu-settings-spacebar-item-keyboard" = "Πληκτρολόγιο"; + +/* Text showing name of spacebar caption - language */ +"menu-settings-spacebar-item-language" = "Γλῶσσα"; + +/* Text showing name of spacebar caption - language + keyboard */ +"menu-settings-spacebar-item-languageKeyboard" = "Γλῶσσα καὶ πληκτρολόγιο"; + +/* Short text for notification: download failure for keyboard */ +"notification-download-failure-keyboard" = "Ἡ μεταφόρτωση τοῦ πληκτρολογίου ἀπέτυχε"; + +/* Short text for notification: download failure for lexical model */ +"notification-download-failure-lexical-model" = "Ἡ μεταφόρτωση τοῦ λεξικοῦ ἀπέτυχε"; + +/* Short text for notification: download success for keyboard */ +"notification-download-success-keyboard" = "Τὸ πληκτρολόγιο μεταφορτώθηκε ἐπιτυχῶς"; + +/* Short text for notification: download success for lexical model */ +"notification-download-success-lexical-model" = "Τὸ λεξιλογικὸ μοντέλλο μεταφορτώθηκε ἐπιτυχῶς"; + +/* Short text for notification: downloading a keyboard */ +"notification-downloading-keyboard" = "Μεταφορτώνεται πληκτρολόγιο\U2026"; + +/* Short text for notification: downloading a lexical model */ +"notification-downloading-lexical-model" = "Μεταφορτώνεται λεξικό\U2026"; + +/* Short text for notification: an update is available */ +"notification-update-available" = "Διαθέσιμη Ἐνημέρωση"; + +/* Short text for notification: currently updating */ +"notification-update-processing" = "Ἐνημέρωση σὲ ἐξέλιξη\U2026"; + +/* Text indicating success at installing new keyboards or dictionaries */ +"success-install" = "Ἐπιτυχὴς ἐγκατάσταση."; + +/* A title to use in alerts indicating 'success' at whatever task the user requested */ +"success-title" = "Ἐπιτυχία"; diff --git a/ios/engine/KMEI/KeymanEngine/el.lproj/Localizable.stringsdict b/ios/engine/KMEI/KeymanEngine/el.lproj/Localizable.stringsdict new file mode 100644 index 00000000000..b0544d994e6 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/el.lproj/Localizable.stringsdict @@ -0,0 +1,118 @@ + + + + + menu-langsettings-lexical-model-count + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + Ἐγκατεστάθη %u λεξικό + other + Ἐγκατεστάθησαν %u λεξικά + + + notification-update-failed + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + %u ἐνημέρωση ἀπέτυχε + other + Ἀπέτυχαν %u ἐνημερώσεις + + + notification-update-success + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + %u ἐπιτυχὴς ἐνημέρωση + other + %u ἐπιτυχεῖς ἐνημερώσεις + + + settings-keyboards-installed-count + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + %u ἐγκατεστημένο πληκτρολόγιο + other + %u ἐγκατεστημένα πληκτρολόγια + + + settings-languages-installed-count + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + %u ἐγκατεστημένη γλῶσσα + other + %u ἐγκατεστημένες γλῶσσες + + + package-default-found-keyboards + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + Εὑρέθη %u πληκτρολόγιο στὸ πακέτο: + other + Εὑρέθησαν %u πληκτρολόγια στὸ πακέτο: + + + package-default-found-lexical-models + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + Εὑρέθη %u λεξικὸ στὸ πακέτο: + other + Εὑρέθησαν %u λεξικὰ στὸ πακέτο: + + + + diff --git a/ios/engine/KMEI/KeymanEngine/it.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/it.lproj/Localizable.strings index 262a99756db..bd12409118a 100644 --- a/ios/engine/KMEI/KeymanEngine/it.lproj/Localizable.strings +++ b/ios/engine/KMEI/KeymanEngine/it.lproj/Localizable.strings @@ -125,7 +125,7 @@ "kmp-error-missing-resource" = "Questo pacchetto non contiene la tastiera o il dizionario richiesto."; /* Error installing a Keyman package with a version of Keyman that does not support it */ -"kmp-error-unsupported-keyman-version" = "This package requires a newer version of Keyman."; +"kmp-error-unsupported-keyman-version" = "Questo pacchetto richiede una nuova versione di Keyman."; /* Error opening a Keyman package - cannot parse contents */ "kmp-error-no-metadata" = "Questo pacchetto non è stato costruito correttamente - contenuti sconosciuti."; diff --git a/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings index f12ba384c45..88dea57f709 100644 --- a/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings +++ b/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings @@ -125,7 +125,7 @@ "kmp-error-missing-resource" = "Este pacote não contém o teclado ou dicionário solicitados."; /* Error installing a Keyman package with a version of Keyman that does not support it */ -"kmp-error-unsupported-keyman-version" = "This package requires a newer version of Keyman."; +"kmp-error-unsupported-keyman-version" = "Este pacote requer uma nova versão do Keyman."; /* Error opening a Keyman package - cannot parse contents */ "kmp-error-no-metadata" = "Este pacote não foi construído corretamente - conteúdo desconhecido."; diff --git a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js index 93730c72ff4..bd323b970b4 100644 --- a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js +++ b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js @@ -131,10 +131,8 @@ function getOskWidth() { function getOskHeight() { var height = oskHeight; - if(keyman.osk && keyman.osk.banner._activeType != 'blank') { + if(keyman.osk.banner._activeType != 'blank') { height = height - keyman.osk.banner.height; - } else { - height = height - (bannerHeight ? bannerHeight : 0); } return height; } diff --git a/ios/engine/build.sh b/ios/engine/build.sh index 600b93154b2..f6687f0a00e 100755 --- a/ios/engine/build.sh +++ b/ios/engine/build.sh @@ -15,7 +15,7 @@ verify_on_mac builder_describe "Builds Keyman Engine for use on iOS devices - iPhone and iPad." \ "@/web/src/app/webview build" \ - "@/common/web/sentry-manager build" \ + "@/web/src/engine/sentry-manager build" \ "clean" \ "configure" \ "build" \ @@ -109,7 +109,7 @@ function update_bundle ( ) { cp "$KMW_PRODUCT/keymanweb-webview.js" "$BUNDLE_PATH/keymanweb-webview.js" cp "$KEYMAN_ROOT/node_modules/@sentry/browser/build/bundle.min.js" "$BUNDLE_PATH/sentry.min.js" - cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "$BUNDLE_PATH/keyman-sentry.js" + cp "$KEYMAN_ROOT/web/src/engine/sentry-manager/build/lib/index.js" "$BUNDLE_PATH/keyman-sentry.js" } # First things first - update our dependencies. @@ -140,4 +140,4 @@ function build_engine() { builder_run_action clean do_clean builder_run_action configure do_configure -builder_run_action build build_engine \ No newline at end of file +builder_run_action build build_engine diff --git a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj index fdeae450f0d..6211f265734 100644 --- a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj +++ b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -332,6 +332,7 @@ CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageBrowserViewController.swift; sourceTree = ""; }; CE80AD32257F2B4A008D2150 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CE98E6BD2615593300F3F2C0 /* ff */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ff; path = ff.lproj/Localizable.strings; sourceTree = ""; }; + CEAC2A272C2A50E100C99ABD /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; CEACC90E25F07D58006EAB45 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; CEACC90F25F07D5A006EAB45 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/Localizable.strings; sourceTree = ""; }; CEACC91225F07D77006EAB45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; @@ -793,6 +794,7 @@ sv, uk, ru, + el, ); mainGroup = 98ABADA7176935E400B62590; productRefGroup = 98ABADB1176935E400B62590 /* Products */; @@ -1106,6 +1108,7 @@ 298566C42980C493004ACA95 /* sv */, 298566D02980D354004ACA95 /* uk */, 298566DC2980E416004ACA95 /* ru */, + CEAC2A272C2A50E100C99ABD /* el */, ); name = Localizable.strings; sourceTree = ""; diff --git a/ios/keyman/Keyman/Keyman/el.lproj/Localizable.strings b/ios/keyman/Keyman/Keyman/el.lproj/Localizable.strings new file mode 100644 index 00000000000..d5d0fcec496 --- /dev/null +++ b/ios/keyman/Keyman/Keyman/el.lproj/Localizable.strings @@ -0,0 +1,81 @@ +/* Title for the bookmarks menu within the embedded browser */ +"browser-bookmarks-add-title" = "Προσθέστε σελιδοδείκτη"; + +/* Title for the bookmarks menu within the embedded browser */ +"browser-bookmarks-none" = "Δὲν ὑπάρχουν σελιδοδεῖκτες"; + +/* Title for the bookmarks menu within the embedded browser */ +"browser-bookmarks-title" = "Σελιδοδεῖκτες"; + +/* Used to confirm a user's desire to install a package */ +"confirm-install" = "Ἐγκαταστῆστε"; + +/* The label for the toggle to stop automatically showing the "Get Started" tutorial popup. */ +"disable-get-started" = "Νὰ μὴν ξαναεμφανισθεῖ"; + +/* Indicates an error opening a web page requested by a user */ +"error-opening-page" = "Ἀδυναμία ἀνοίγματος σελίδας"; + +/* Long-form used when installing a font in order to display a language properly. */ +"font-install-description" = "Πατῆστε τὸ πλῆκτρο ἐγκατάστασης γιὰ νὰ ἐμφανίζεται ἡ %@ σωστὰ σὲ ὅλες τὶς ἐφαρμογές σας"; + +/* The name of the iOS Settings menu option for keyboards */ +"ios-settings-keyboards" = "Πληκτρολόγια"; + +/* The name of the iOS Settings menu option for giving the keyboard full access. (The menu entry +underneath the app's name within the app-specific keyboard menu.) */ +"ios-settings-allow-full-access" = "Ἐπιτρέψτε πλήρη πρόσβαση"; + +/* Short-form used when installing a font in order to display a language properly. */ +"language-for-font" = "Γραμματοσειρά %@"; + +/* Used for 'add' options within menus */ +"menu-add" = "Προσθήκη"; + +/* Used to indicate a sequence of menu options that a user needs to copy. May be chained. Example: "Keyboards (1) > Enable Keyman (2)" */ +"menu-breadcrumbing" = "%1$@ > %2$@"; + +/* Used to exit a menu without choosing an option */ +"menu-cancel" = "Ἀκύρωση"; + +/* Menu option that erases all previously-typed text. */ +"menu-clear-text" = "Διαγραφὴ κειμένου"; + +/* Menu option that displays a list designed to help users start using the app */ +"menu-get-started" = "Ξεκινῆστε"; + +/* Menu option that displays help for the app */ +"menu-help" = "Πληροφορίες"; + +/* Menu option that displays the main screen's drop-down menu */ +"menu-more" = "Περισσότερα"; + +/* Menu option used to edit keyboard and dictionary settings */ +"menu-settings" = "Ρυθμίσεις"; + +/* Menu option that displays the iOS Share menu */ +"menu-share" = "Μοιρασθῆτε"; + +/* Menu option that displays an embedded web browser. */ +"menu-show-browser" = "Φυλλομετρητής"; + +/* Menu option used to control in-app font size. */ +"menu-text-size" = "Μέγεθος γραμματοσειρᾶς"; + +/* Used to describe the current font size */ +"text-size-label" = "Μέγεθος γραμματοσειρᾶς: %i"; + +/* Text to indicate that a user should set a toggle within iOS Settings to 'active'. */ +"toggle-to-enable" = "Ἐνεργοποίηση %@"; + +/* First option on the Get Started tutorial - Add a keyboard for your language */ +"tutorial-add-keyboard" = "Προσθέστε πληκτρολόγιο γιὰ τὴν γλῶσσα σας"; + +/* Third option on the Get Started tutorial - Displays app help. */ +"tutorial-show-help" = "Περισσότερες πληροφορίες"; + +/* Second option on the Get Started tutorial - Set up Keyman as system-wide keyboard */ +"tutorial-system-keyboard" = "Ὁρίστε τὸ Κῆμαν ὡς πληκτρολόγιο ὁλοκλήρου τοῦ συστήματος"; + +/* Used to display app version (as in \"Version: 1.0.2\" */ +"version-label" = "Ἔκδοση: %@"; diff --git a/linux/.pbuilderrc b/linux/.pbuilderrc deleted file mode 100644 index 226a267fe6c..00000000000 --- a/linux/.pbuilderrc +++ /dev/null @@ -1,82 +0,0 @@ -PBUILDERSATISFYDEPENDSCMD="/usr/lib/pbuilder/pbuilder-satisfydepends-gdebi" -PDEBUILD_PBUILDER="cowbuilder" - -# Codenames for Debian suites according to their alias. Update these when -# needed. -UNSTABLE_CODENAME="sid" -TESTING_CODENAME="buster" -STABLE_CODENAME="stretch" -STABLE_BACKPORTS_SUITE="$STABLE_CODENAME-backports" -OLDSTABLE_CODENAME="jessie" - -# List of Debian suites. -DEBIAN_SUITES=($UNSTABLE_CODENAME $TESTING_CODENAME $STABLE_CODENAME $STABLE_BACKPORTS_SUITE $OLDSTABLE_CODENAME - "experimental" "unstable" "testing" "stable") - -# List of Ubuntu suites. Update these when needed. -UBUNTU_SUITES=("noble" "mantic" "jammy" "focal") - -# Mirrors to use. Update these to your preferred mirror. -DEBIAN_MIRROR="deb.debian.org" -UBUNTU_MIRROR="archive.ubuntu.com" - -# Optionally use the changelog of a package to determine the suite to use if -# none set. -if [ -z "${DIST}" ] && [ -r "debian/changelog" ]; then - DIST=$(dpkg-parsechangelog --show-field=Distribution) -fi - -# Optionally set a default distribution if none is used. Note that you can set -# your own default (i.e. ${DIST:="unstable"}). -: ${DIST:="$(lsb_release --short --codename)"} - -# Optionally change Debian codenames in $DIST to their aliases. -case "$DIST" in - $UNSTABLE_CODENAME) - DIST="unstable" - ;; -# $TESTING_CODENAME) -# DIST="testing" -# ;; -# $STABLE_CODENAME) -# DIST="stable" -# ;; -esac - -# Optionally set the architecture to the host architecture if none set. Note -# that you can set your own default (i.e. ${ARCH:="i386"}). -#: ${ARCH:="$(dpkg --print-architecture)"} - -NAME="$DIST" -if [ -n "${ARCH}" ]; then - NAME="$NAME-$ARCH" - DEBOOTSTRAPOPTS=("--arch" "$ARCH" "${DEBOOTSTRAPOPTS[@]}") -fi -#BASETGZ="/var/cache/pbuilder/$NAME-base.tgz" -BASEPATH=/var/cache/pbuilder/base-$NAME.cow -DISTRIBUTION="$DIST" -BUILDRESULT="/var/cache/pbuilder/result/$NAME/" -APTCACHE="/var/cache/pbuilder/aptcache/$NAME/" -BUILDPLACE="/var/cache/pbuilder/build/" - -HOOKDIR="/var/cache/pbuilder/hook.d/$NAME/" -OTHERMIRROR="deb [trusted=yes] file:/var/cache/pbuilder/result/$NAME ./ " -BINDMOUNTS="/var/cache/pbuilder/result/$NAME" - -if $(echo ${DEBIAN_SUITES[@]} | grep -q $DIST); then - # Debian configuration - MIRRORSITE="http://$DEBIAN_MIRROR/debian/" - COMPONENTS="main contrib non-free" - if $(echo "$STABLE_CODENAME stable" | grep -q $DIST); then - OTHERMIRROR="$OTHERMIRROR | deb $MIRRORSITE $STABLE_BACKPORTS_SUITE $COMPONENTS" - fi -elif $(echo ${UBUNTU_SUITES[@]} | grep -q $DIST); then - # Ubuntu configuration - MIRRORSITE="http://$UBUNTU_MIRROR/ubuntu/" - COMPONENTS="main restricted universe multiverse" -else - echo "Unknown distribution: $DIST" - exit 1 -fi - -echo "DIST is $DIST" diff --git a/linux/Makefile b/linux/Makefile index a4ce57bbe04..0df264e43d3 100644 --- a/linux/Makefile +++ b/linux/Makefile @@ -25,9 +25,6 @@ install: # run as sudo uninstall: # run as sudo SUDOINSTALL="uninstall" ./scripts/install.sh -debnocow: - ./scripts/deb.sh - sources: ./scripts/dist.sh @@ -38,16 +35,7 @@ origdist: ./scripts/dist.sh origdist sourcepackage: reconf origdist - ./scripts/deb.sh sourcepackage - -cow: # it will prompt for sudo if required for cowbuilder - ./scripts/cow.sh - -deb: reconf origdist cow debnocow # it will prompt for sudo if required for cowbuilder - echo "Built dev Debian packages in builddebs/" - -releasedeb: reconf origdist cow debnocow # it will prompt for sudo if required for cowbuilder - echo "Built release Debian packages in builddebs/" + ./scripts/deb.sh nightlydist: reconf sources echo "Built nightly source release in dist/" diff --git a/linux/debian/changelog b/linux/debian/changelog index e5e2a939b6c..2f89e332c04 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,10 @@ +keyman (17.0.326-1) unstable; urgency=medium + + * New upstream release + * Re-release to Debian + + -- Eberhard Beilharz Mon, 03 Jun 2024 21:58:16 +0200 + keyman (17.0.295-1) unstable; urgency=medium * Remove ibus-keyman.post{inst,rm} (closes: #1034040) diff --git a/linux/debian/control b/linux/debian/control index 54b186c790e..fbe7066f5c2 100644 --- a/linux/debian/control +++ b/linux/debian/control @@ -19,13 +19,14 @@ Build-Depends: libjson-glib-dev (>= 1.4.0), liblocale-gettext-perl, libsystemd-dev, - meson (>= 0.53), + meson (>= 1.0), metacity, ninja-build, perl, pkgconf, python3-all (>= 3.5), python3-dbus, + python3-fonttools, python3-gi, python3-lxml, python3-magic, @@ -90,6 +91,7 @@ Depends: gir1.2-webkit2-4.1 | gir1.2-webkit2-4.0, keyman-engine, python3-bs4, + python3-fonttools, python3-gi, python3-packaging, python3-sentry-sdk (>= 1.1), diff --git a/linux/ibus-keyman/.gitignore b/linux/ibus-keyman/.gitignore new file mode 100644 index 00000000000..cb76b31565a --- /dev/null +++ b/linux/ibus-keyman/.gitignore @@ -0,0 +1 @@ +resources/ diff --git a/linux/ibus-keyman/build.sh b/linux/ibus-keyman/build.sh index 96679604716..231e9039eac 100755 --- a/linux/ibus-keyman/build.sh +++ b/linux/ibus-keyman/build.sh @@ -45,6 +45,15 @@ else MESON_COVERAGE= fi +# Import our standard compiler defines; this is copied from +# /resources/build/meson/standard.meson.build by build.sh, because meson doesn't +# allow us to reference a file outside its root. ${THIS_SCRIPT_PATH}/meson.build +# then includes `resources` as a subdir. +if builder_has_action configure; then + mkdir -p "${THIS_SCRIPT_PATH}/resources" + cp "${KEYMAN_ROOT}/resources/build/meson/standard.meson.build" "${THIS_SCRIPT_PATH}/resources/meson.build" +fi + configure_action() { # shellcheck disable=SC2086,SC2154 meson setup ${MESON_COVERAGE} --werror --buildtype $MESON_TARGET "${builder_extra_params[@]}" "$MESON_PATH" diff --git a/linux/ibus-keyman/meson.build b/linux/ibus-keyman/meson.build index 5a8958e983f..763a121245f 100644 --- a/linux/ibus-keyman/meson.build +++ b/linux/ibus-keyman/meson.build @@ -1,8 +1,13 @@ project('ibus-keyman', 'c', 'cpp', version: run_command('cat', '../../VERSION.md', check: true).stdout().strip(), license: 'GPL-2+', - meson_version: '>=0.53.0') -cc = meson.get_compiler('c') + meson_version: '>=1.0') + +# Import our standard compiler defines; this is copied from +# /resources/build/standard.meson.build by build.sh, because +# meson doesn't allow us to reference a file outside its root +subdir('resources') + conf = configuration_data() ibus = dependency('ibus-1.0', version: '>= 1.2.0') @@ -14,7 +19,7 @@ icu = dependency('icu-i18n') core_dir = meson.current_source_dir() / '../../core' common_dir = meson.current_source_dir() / '../../common' -keymancore_lib = cc.find_library( +keymancore_lib = c_compiler.find_library( 'keymancore', # meson will prefix 'lib' dirs: [ core_dir / 'build/arch' / get_option('buildtype') / 'src' ] ) @@ -26,7 +31,7 @@ endif env = find_program('env') # Check if we have patched ibus (https://github.com/ibus/ibus/pull/2440) -if cc.has_header_symbol('ibus.h', 'IBUS_CAP_PREFILTER', dependencies: [ibus], required: false) +if c_compiler.has_header_symbol('ibus.h', 'IBUS_CAP_PREFILTER', dependencies: [ibus], required: false) conf.set('IBUS_HAS_PREFILTER', 1) endif diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index d3491cc398f..4ec6cf002f9 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -94,6 +94,7 @@ struct _IBusKeymanEngine { IBusLookupTable *table; IBusProperty *status_prop; IBusPropList *prop_list; + void *settings; commit_queue_item commit_queue[MAX_QUEUE_SIZE]; commit_queue_item *commit_item; @@ -366,8 +367,8 @@ setup_environment(IBusKeymanEngine *keyman) g_assert(keyman); g_message("%s: setting up environment", __FUNCTION__); - // Allocate enough options for: 3 environments plus 1 pad struct of 0's - km_core_option_item environment_opts[4] = {0}; + // Allocate enough options for: 4 environments plus 1 pad struct of 0's + km_core_option_item environment_opts[5] = {0}; environment_opts[0].scope = KM_CORE_OPT_ENVIRONMENT; environment_opts[0].key = KM_CORE_KMX_ENV_PLATFORM; @@ -381,6 +382,9 @@ setup_environment(IBusKeymanEngine *keyman) environment_opts[2].key = KM_CORE_KMX_ENV_BASELAYOUTALT; environment_opts[2].value = get_base_layout(); // TODO: free when mnemonic layouts are to be supported + environment_opts[3].scope = KM_CORE_OPT_ENVIRONMENT; + environment_opts[3].key = KM_CORE_KMX_ENV_SIMULATEALTGR; + environment_opts[3].value = keyman_get_option_fromdconf(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR) ? u"1" : u"0"; km_core_status status = km_core_state_create(keyman->keyboard, environment_opts, &(keyman->state)); if (status != KM_CORE_STATUS_OK) { @@ -389,6 +393,17 @@ setup_environment(IBusKeymanEngine *keyman) return status; } +static void +on_dconf_settings_change(GSettings *settings, gchar *key, gpointer user_data) { + if (!user_data || g_strcmp0(key, KEYMAN_DCONF_OPTIONS_SIMULATEALTGR) != 0) { + // not the SimulateAltGr option... + return; + } + IBusKeymanEngine *keyman = (IBusKeymanEngine *)user_data; + km_core_state_dispose(keyman->state); + setup_environment(keyman); +} + void free_km_core_option_item(gpointer data) { if (!data) @@ -407,7 +422,7 @@ load_keyboard_options(IBusKeymanEngine *keyman) { // Retrieve keyboard options from DConf // TODO: May need unique packageID and keyboard ID g_message("%s: Loading options for kb_name: %s", __FUNCTION__, keyman->kb_name); - GQueue *queue_options = keyman_get_options_queue_fromdconf(keyman->kb_name, keyman->kb_name); + GQueue *queue_options = keyman_get_keyboard_options_queue_fromdconf(keyman->kb_name, keyman->kb_name); int num_options = g_queue_get_length(queue_options); if (num_options < 1) { g_queue_free_full(queue_options, free_km_core_option_item); @@ -515,6 +530,8 @@ ibus_keyman_engine_constructor( return NULL; } + keyman->settings = keyman_subscribe_option_changes(on_dconf_settings_change, keyman); + status = load_keyboard_options(keyman); if (status != KM_CORE_STATUS_OK) { ibus_keyman_engine_destroy(keyman); @@ -537,6 +554,11 @@ ibus_keyman_engine_destroy (IBusKeymanEngine *keyman) g_assert (engine_name); g_message("DAR: %s %s", __FUNCTION__, engine_name); + if (keyman->settings) { + keyman_unsubscribe_option_changes(keyman->settings, on_dconf_settings_change, keyman); + keyman->settings = NULL; + } + if (keyman->prop_list) { g_debug("DAR: %s: unref keyman->prop_list", __FUNCTION__); g_object_unref (keyman->prop_list); @@ -655,7 +677,7 @@ process_persist_action(IBusEngine *engine, km_core_option_item *persist_options) g_assert(option->key != NULL && option->value != NULL); g_message("%s: Saving keyboard option to DConf", __FUNCTION__); // Load the current keyboard options from DConf - keyman_put_options_todconf(keyman->kb_name, keyman->kb_name, (gchar *)option->key, (gchar *)option->value); + keyman_put_keyboard_options_todconf(keyman->kb_name, keyman->kb_name, (gchar *)option->key, (gchar *)option->value); } } diff --git a/linux/ibus-keyman/src/keymanutil.c b/linux/ibus-keyman/src/keymanutil.c index da9c27af8d2..80256f7d4b6 100644 --- a/linux/ibus-keyman/src/keymanutil.c +++ b/linux/ibus-keyman/src/keymanutil.c @@ -430,19 +430,21 @@ ibus_keyman_get_component (void) return component; } -// Obtain Keyboard Options list from DConf -// DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2'] -// -// Parameters: -// package_id (gchar *): Package ID -// keyboard_id (gchar *): Keyboard ID -// -// Returns a newly allocated gchar**; free with g_strfreev() +/** + * Obtain Keyboard Options list from DConf + * + * DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2'] + * + * @param package_id Package ID + * @param keyboard_id Keyboard ID + * @return A newly allocated gchar**; free with g_strfreev() + */ gchar** -keyman_get_options_fromdconf(gchar *package_id, - gchar *keyboard_id) -{ - g_message("keyman_get_options_fromdconf"); +keyman_get_keyboard_options_fromdconf( + const gchar *package_id, + const gchar *keyboard_id +) { + g_message(__FUNCTION__); // Obtain keyboard options from DConf g_autofree gchar *path = g_strdup_printf("%s%s/%s/", KEYMAN_DCONF_OPTIONS_PATH, package_id, keyboard_id); @@ -458,22 +460,23 @@ keyman_get_options_fromdconf(gchar *package_id, return options; } -// Obtain Keyboard Options from DConf and parse into a GQueue of struct km_core_option_item -// -// Parameters: -// package_id (gchar *): Package ID -// keyboard_id (gchar *): Keyboard ID -// -// Return a newly allocated GQueue; free with g_queue_free_full() +/** + * Obtain Keyboard Options from DConf and parse into a GQueue of struct km_core_option_item + * + * @param package_id Package ID + * @param keyboard_id Keyboard ID + * @return A newly allocated GQueue; free with g_queue_free_full() + */ GQueue* -keyman_get_options_queue_fromdconf(gchar *package_id, - gchar *keyboard_id) -{ - g_message("keyman_get_options_queue_fromdconf"); +keyman_get_keyboard_options_queue_fromdconf( + const gchar *package_id, + const gchar *keyboard_id +) { + g_message(__FUNCTION__); GQueue *queue_options = g_queue_new(); // Obtain keyboard options from DConf - g_auto(GStrv) options = keyman_get_options_fromdconf(package_id, keyboard_id); + g_auto(GStrv) options = keyman_get_keyboard_options_fromdconf(package_id, keyboard_id); // Parse options into queue_options if (options != NULL) @@ -500,69 +503,126 @@ keyman_get_options_queue_fromdconf(gchar *package_id, return queue_options; } -// Write new keyboard option to DConf. -// DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2'] -// If the option key already exists, the value is updated. Otherwise a new string -// 'option_key=option_value' is appended. -// -// Parameters: -// package_id (gchar *): Package ID -// keyboard_id (gchar *): Keyboard ID -// option_key (gchar *): Key for the new option -// option_value (gchar *): Value of the new option +/** + * Write new keyboard option to DConf + * + * DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2'] + * If the option key already exists, the value is updated. Otherwise a new string + * 'option_key=option_value' is appended. + * + * @param package_id Package ID + * @param keyboard_id Keyboard ID + * @param option_key Key for the new option + * @param option_value Value of the new option + */ void -keyman_put_options_todconf(gchar *package_id, - gchar *keyboard_id, - gchar *option_key, - gchar *option_value) -{ - g_message("keyman_put_options_todconf"); - if (package_id == NULL || keyboard_id == NULL || option_key == NULL || option_value == NULL) - { - return; +keyman_put_keyboard_options_todconf( + const gchar *package_id, + const gchar *keyboard_id, + const gchar *option_key, + const gchar *option_value +) { + g_message(__FUNCTION__); + if (package_id == NULL || keyboard_id == NULL || option_key == NULL || option_value == NULL) { + return; + } + + // Obtain keyboard options from DConf + g_auto(GStrv) options = keyman_get_keyboard_options_fromdconf(package_id, keyboard_id); + g_autofree gchar *needle = g_strdup_printf("%s=", option_key); + gchar *kvp = g_strdup_printf("%s=%s", option_key, option_value); + + g_assert(options != NULL); + + int index = 0; + gboolean option_updated = FALSE; + while (options[index] != NULL) { + // If option_key already exists, update value with option_value + if (g_strrstr(options[index], needle) != NULL) { + g_free(options[index]); + options[index] = kvp; + option_updated = TRUE; + break; } + index++; + } - // Obtain keyboard options from DConf - g_auto(GStrv) options = keyman_get_options_fromdconf(package_id, keyboard_id); - g_autofree gchar *needle = g_strdup_printf("%s=", option_key); - gchar *kvp = g_strdup_printf("%s=%s", option_key, option_value); + if (!option_updated) { + // Resize to add new option and null-terminate + int size = index + 2; // old size: index + 1, plus 1 new + options = g_renew(gchar *, options, size); + options[index] = kvp; + options[index + 1] = NULL; + } - g_assert(options != NULL); + // Write to DConf + g_autofree gchar *path = g_strdup_printf("%s%s/%s/", KEYMAN_DCONF_OPTIONS_PATH, package_id, keyboard_id); + g_autoptr(GSettings) child_settings = g_settings_new_with_path(KEYMAN_DCONF_OPTIONS_CHILD_NAME, path); + if (child_settings != NULL) { + g_message("writing keyboard options to DConf"); + g_settings_set_strv(child_settings, KEYMAN_DCONF_OPTIONS_KEY, (const gchar *const *)options); + } - int index = 0; - gboolean option_updated = FALSE; - while (options[index] != NULL) - { - // If option_key already exists, update value with option_value - if (g_strrstr(options[index], needle) != NULL) - { - g_free(options[index]); - options[index] = kvp; - option_updated = TRUE; - break; - } - index++; - } + // kvp got assigned to options[x] and so gets freed when options are freed +} - if (!option_updated) - { - // Resize to add new option and null-terminate - int size = index + 2; // old size: index + 1, plus 1 new - options = g_renew(gchar*, options, size); - options[index] = kvp; - options[index+1] = NULL; - } +/** + * Obtain (general) option value from DConf + * + * @param option_key Key of the option + * @return The current value of the option + */ +gboolean keyman_get_option_fromdconf(const gchar* option_key) { + g_autoptr(GSettings) settings = g_settings_new(KEYMAN_DCONF_OPTIONS_NAME); + return g_settings_get_boolean(settings, option_key); +} - // Write to DConf - g_autofree gchar *path = g_strdup_printf("%s%s/%s/", KEYMAN_DCONF_OPTIONS_PATH, package_id, keyboard_id); - g_autoptr(GSettings) child_settings = g_settings_new_with_path(KEYMAN_DCONF_OPTIONS_CHILD_NAME, path); - if (child_settings != NULL) - { - g_message("writing keyboard options to DConf"); - g_settings_set_strv(child_settings, KEYMAN_DCONF_OPTIONS_KEY, (const gchar *const *)options); - } +/** + * Write new (general) option to DConf + * + * @param option_key Key for the new option + * @param option_value Value of the new option + * @return TRUE if setting the key succeeded, FALSE if the key was not writable + */ +gboolean keyman_put_option_todconf(const gchar* option_key, gboolean option_value) { + g_autoptr(GSettings) settings = g_settings_new(KEYMAN_DCONF_OPTIONS_NAME); + return g_settings_set_boolean(settings, option_key, option_value); +} + +/** + * Subscribe to changes in the (general) Keyman options + * + * @param callback Callback method. Note that this should be different methods if + * this method gets called for different keys. + * @param user_data Custom data that will passed to callback + * @return A transparent settings object + */ +void* keyman_subscribe_option_changes(void* callback, gpointer user_data) { + GSettings *settings; + settings = g_settings_new(KEYMAN_DCONF_OPTIONS_NAME); + + g_signal_connect(settings, "changed", G_CALLBACK(callback), user_data); + return settings; +} - // kvp got assigned to options[x] and so gets freed when options are freed +/** + * Unsubscribe from changes in the (general) option settings + * + * @param settings The settings object + * @param callback The callback method used when subscribing + * @param user_data Custom data for callback + * @return > 0 if successfully unsubscribed, otherwise 0 + */ +guint keyman_unsubscribe_option_changes(void* settings, void* callback, gpointer user_data) { + if (!settings) { + return 0; + } + + guint retval = g_signal_handlers_disconnect_by_func(settings, callback, user_data); + if (retval > 0) { + g_object_unref(settings); + } + return retval; } static GPtrArray * diff --git a/linux/ibus-keyman/src/keymanutil.h b/linux/ibus-keyman/src/keymanutil.h index 8a3f4506faa..a4d5355bda0 100644 --- a/linux/ibus-keyman/src/keymanutil.h +++ b/linux/ibus-keyman/src/keymanutil.h @@ -67,6 +67,7 @@ // (https://docs.gtk.org/gio/class.Settings.html) (#9579) #define KEYMAN_DCONF_OPTIONS_PATH "/desktop/ibus/keyman/options/" #define KEYMAN_DCONF_OPTIONS_KEY "options" +#define KEYMAN_DCONF_OPTIONS_SIMULATEALTGR "simulate-altgr" #define KEYMAN_DCONF_ENGINE_NAME "com.keyman.engine" #define KEYMAN_DCONF_ENGINE_PATH "/com/keyman/engine/" @@ -78,43 +79,76 @@ void ibus_keyman_init (void); GList *ibus_keyman_list_engines (void); IBusComponent *ibus_keyman_get_component (void); -// Obtain Keyboard Options list from DConf -// DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2'] -// -// Parameters: -// package_id (gchar *): Package ID -// keyboard_id (gchar *): Keyboard ID -// -// Returns a newly allocated gchar**; free with g_strfreev() -gchar** keyman_get_options_fromdconf - (gchar *package_id, - gchar *keyboard_id); - -// Obtain Keyboard Options from DConf and parse into a GQueue of struct km_core_option_item -// -// Parameters: -// package_id (gchar *): Package ID -// keyboard_id (gchar *): Keyboard ID -// -// Return a newly allocated GQueue; free with g_queue_free_full() -GQueue* keyman_get_options_queue_fromdconf - (gchar *package_id, - gchar *keyboard_id); - -// Write new keyboard option to DConf. -// DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2'] -// If the option key already exists, the value is updated. Otherwise a new string 'option_key=option_value' is appended. -// -// Parameters: -// package_id (gchar *): Package ID -// keyboard_id (gchar *): Keyboard ID -// option_key (gchar *): Key for the new option -// option_value (gchar *): Value of the new option -void keyman_put_options_todconf - (gchar *package_id, - gchar *keyboard_id, - gchar *option_key, - gchar *option_value); +/** + * Obtain Keyboard Options list from DConf + * + * DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2'] + * + * @param package_id Package ID + * @param keyboard_id Keyboard ID + * @return A newly allocated gchar**; free with g_strfreev() + */ +gchar **keyman_get_keyboard_options_fromdconf(const gchar *package_id, const gchar *keyboard_id); + +/** + * Obtain Keyboard Options from DConf and parse into a GQueue of struct km_core_option_item + * + * @param package_id Package ID + * @param keyboard_id Keyboard ID + * @return A newly allocated GQueue; free with g_queue_free_full() + */ +GQueue *keyman_get_keyboard_options_queue_fromdconf(const gchar *package_id, const gchar *keyboard_id); + +/** + * Write new keyboard option to DConf + * + * DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2'] + * If the option key already exists, the value is updated. Otherwise a new string + * 'option_key=option_value' is appended. + * + * @param package_id Package ID + * @param keyboard_id Keyboard ID + * @param option_key Key for the new option + * @param option_value Value of the new option + */ +void keyman_put_keyboard_options_todconf(const gchar *package_id, const gchar *keyboard_id, const gchar *option_key, const gchar *option_value); + +/** + * Obtain (general) option value from DConf + * + * @param option_key Key of the option + * @return The current value of the option + */ +gboolean keyman_get_option_fromdconf(const gchar* option_key); + +/** + * Write new (general) option to DConf + * + * @param option_key Key for the new option + * @param option_value Value of the new option + * @return TRUE if setting the key succeeded, FALSE if the key was not writable + */ +gboolean keyman_put_option_todconf(const gchar* option_key, gboolean option_value); + +/** + * Subscribe to changes in the (general) Keyman options + * + * @param callback Callback method. Note that this should be different methods if + * this method gets called for different keys. + * @param user_data Custom data that will passed to callback + * @return A transparent settings object + */ +void* keyman_subscribe_option_changes(void* callback, gpointer user_data); + +/** + * Unsubscribe from changes in the (general) option settings + * + * @param settings The settings object + * @param callback The callback method used when subscribing + * @param user_data Custom data for callback + * @return > 0 if successfully unsubscribed, otherwise 0 + */ +guint keyman_unsubscribe_option_changes(void *settings, void *callback, gpointer user_data); G_END_DECLS diff --git a/linux/ibus-keyman/src/test/keymanutil_tests.c b/linux/ibus-keyman/src/test/keymanutil_tests.c index 17e89ed2a6c..13636857d86 100644 --- a/linux/ibus-keyman/src/test/keymanutil_tests.c +++ b/linux/ibus-keyman/src/test/keymanutil_tests.c @@ -10,27 +10,42 @@ #define TEST_FIXTURE "keymanutil-test" void -_delete_tst_options_key(gchar* testname) { +_delete_tst_keyboard_options_key(const gchar* testname) { g_autofree gchar *path = g_strdup_printf("%s%s/%s/", KEYMAN_DCONF_OPTIONS_PATH, TEST_FIXTURE, testname); g_autoptr(GSettings) settings = g_settings_new_with_path(KEYMAN_DCONF_OPTIONS_CHILD_NAME, path); g_settings_reset(settings, KEYMAN_DCONF_OPTIONS_KEY); } void -_set_tst_options_key(gchar* testname, gchar** options) { +_set_tst_keyboard_options_key(const gchar* testname, const gchar* const* options) { g_autofree gchar* path = g_strdup_printf("%s%s/%s/", KEYMAN_DCONF_OPTIONS_PATH, TEST_FIXTURE, testname); g_autoptr(GSettings) settings = g_settings_new_with_path(KEYMAN_DCONF_OPTIONS_CHILD_NAME, path); - g_settings_set_strv(settings, KEYMAN_DCONF_OPTIONS_KEY, (const gchar* const*)options); + g_settings_set_strv(settings, KEYMAN_DCONF_OPTIONS_KEY, options); } gchar** -_get_tst_options_key(gchar* testname) { +_get_tst_keyboard_options_key(const gchar* testname) { g_autofree gchar* path = g_strdup_printf("%s%s/%s/", KEYMAN_DCONF_OPTIONS_PATH, TEST_FIXTURE, testname); g_autoptr(GSettings) settings = g_settings_new_with_path(KEYMAN_DCONF_OPTIONS_CHILD_NAME, path); gchar** result = g_settings_get_strv(settings, KEYMAN_DCONF_OPTIONS_KEY); return result; } +void _reset_tst_option(const gchar* key) { + g_autoptr(GSettings) settings = g_settings_new(KEYMAN_DCONF_OPTIONS_NAME); + g_settings_reset(settings, key); +} + +void _set_tst_option(const gchar* key, gboolean value) { + g_autoptr(GSettings) settings = g_settings_new(KEYMAN_DCONF_OPTIONS_NAME); + g_settings_set_boolean(settings, key, value); +} + +gboolean _get_tst_option(const gchar* key) { + g_autoptr(GSettings) settings = g_settings_new(KEYMAN_DCONF_OPTIONS_NAME); + return g_settings_get_boolean(settings, key); +} + void _delete_tst_kbds_key() { g_autoptr(GSettings) settings = g_settings_new(KEYMAN_DCONF_ENGINE_NAME); @@ -126,58 +141,58 @@ _kmn_assert_cmpstrv(gchar** result, gchar** expected) { //---------------------------------------------------------------------------------------------- void -test_keyman_put_options_todconf__invalid() { +test_keyman_put_keyboard_options_todconf__invalid() { // Initialize - gchar* testname = "test_keyman_put_options_todconf__test_keyman_put_options_todconf__invalid"; - _delete_tst_options_key(testname); + const gchar* testname = __FUNCTION__; + _delete_tst_keyboard_options_key(testname); // Execute - keyman_put_options_todconf(TEST_FIXTURE, testname, "new_key", NULL); + keyman_put_keyboard_options_todconf(TEST_FIXTURE, testname, "new_key", NULL); // Verify - g_auto(GStrv) options = _get_tst_options_key(testname); + g_auto(GStrv) options = _get_tst_keyboard_options_key(testname); g_assert_nonnull(options); g_assert_null(options[0]); // Cleanup - _delete_tst_options_key(testname); + _delete_tst_keyboard_options_key(testname); } void -test_keyman_put_options_todconf__new_key() { +test_keyman_put_keyboard_options_todconf__new_key() { // Initialize - gchar* testname = "test_keyman_put_options_todconf__new_key"; - _delete_tst_options_key(testname); + const gchar* testname = __FUNCTION__; + _delete_tst_keyboard_options_key(testname); g_autofree gchar* value = g_strdup_printf("%d", g_test_rand_int()); // Execute - keyman_put_options_todconf(TEST_FIXTURE, testname, "new_key", value); + keyman_put_keyboard_options_todconf(TEST_FIXTURE, testname, "new_key", value); // Verify - g_auto(GStrv) options = _get_tst_options_key(testname); + g_auto(GStrv) options = _get_tst_keyboard_options_key(testname); g_autofree gchar* expected = g_strdup_printf("new_key=%s", value); g_assert_nonnull(options); g_assert_cmpstr(options[0], ==, expected); g_assert_null(options[1]); // Cleanup - _delete_tst_options_key(testname); + _delete_tst_keyboard_options_key(testname); } void -test_keyman_put_options_todconf__other_keys() { +test_keyman_put_keyboard_options_todconf__other_keys() { // Initialize - gchar* testname = "test_keyman_put_options_todconf__other_keys"; - _delete_tst_options_key(testname); - gchar* existingKeys[] = {"key1=val1", "key2=val2", NULL}; - _set_tst_options_key(testname, existingKeys); + const gchar* testname = __FUNCTION__; + _delete_tst_keyboard_options_key(testname); + const gchar* const existingKeys[] = {"key1=val1", "key2=val2", NULL}; + _set_tst_keyboard_options_key(testname, existingKeys); g_autofree gchar* value = g_strdup_printf("%d", g_test_rand_int()); // Execute - keyman_put_options_todconf(TEST_FIXTURE, testname, "new_key", value); + keyman_put_keyboard_options_todconf(TEST_FIXTURE, testname, "new_key", value); // Verify - g_auto(GStrv) options = _get_tst_options_key(testname); + g_auto(GStrv) options = _get_tst_keyboard_options_key(testname); g_autofree gchar* expected = g_strdup_printf("new_key=%s", value); g_assert_nonnull(options); g_assert_cmpstr(options[0], ==, "key1=val1"); @@ -186,23 +201,23 @@ test_keyman_put_options_todconf__other_keys() { g_assert_null(options[3]); // Cleanup - _delete_tst_options_key(testname); + _delete_tst_keyboard_options_key(testname); } void -test_keyman_put_options_todconf__existing_key() { +test_keyman_put_keyboard_options_todconf__existing_key() { // Initialize - gchar* testname = "test_keyman_put_options_todconf__existing_key"; - _delete_tst_options_key(testname); - gchar* existingKeys[] = {"key1=val1", "new_key=val2", NULL}; - _set_tst_options_key(testname, existingKeys); + const gchar* testname = __FUNCTION__; + _delete_tst_keyboard_options_key(testname); + const gchar* const existingKeys[] = {"key1=val1", "new_key=val2", NULL}; + _set_tst_keyboard_options_key(testname, existingKeys); g_autofree gchar* value = g_strdup_printf("%d", g_test_rand_int()); // Execute - keyman_put_options_todconf(TEST_FIXTURE, testname, "new_key", value); + keyman_put_keyboard_options_todconf(TEST_FIXTURE, testname, "new_key", value); // Verify - g_auto(GStrv) options = _get_tst_options_key(testname); + g_auto(GStrv) options = _get_tst_keyboard_options_key(testname); g_autofree gchar* expected = g_strdup_printf("new_key=%s", value); g_assert_nonnull(options); g_assert_cmpstr(options[0], ==, "key1=val1"); @@ -210,7 +225,215 @@ test_keyman_put_options_todconf__existing_key() { g_assert_null(options[2]); // Cleanup - _delete_tst_options_key(testname); + _delete_tst_keyboard_options_key(testname); +} + +//---------------------------------------------------------------------------------------------- +void test_keyman_put_option_todconf__set_to_true() { + // Initialize + gboolean prevValue = _get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + + // Execute + gboolean result = keyman_put_option_todconf(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, TRUE); + + // Verify + g_assert_true(result); + g_assert_true(_get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR)); + + // Cleanup + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, prevValue); +} + +void test_keyman_put_option_todconf__set_to_false() { + // Initialize + gboolean prevValue = _get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + + // Execute + gboolean result = keyman_put_option_todconf(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, FALSE); + + // Verify + g_assert_true(result); + g_assert_false(_get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR)); + + // Cleanup + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, prevValue); +} + +//---------------------------------------------------------------------------------------------- +void test_keyman_get_option_fromdconf__default() { + // Initialize + gboolean prevValue = _get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + _reset_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + + // Execute + gboolean result = keyman_get_option_fromdconf(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + + // Verify + g_assert_false(result); + + // Cleanup + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, prevValue); +} + +//---------------------------------------------------------------------------------------------- +static gint count = 0; +static gpointer data = NULL; + +void on_settings_change(GSettings* settings, gchar* key, gpointer user_data) { + count++; + data = user_data; +} + +void test_keyman_subscribe_option_changes__create() { + // Initialize + gboolean prevValue = _get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + void* settings = NULL; + count = 0; + data = NULL; + gchar* user_data = "foo"; + + // Execute + settings = keyman_subscribe_option_changes(on_settings_change, user_data); + + // Verify + g_assert_nonnull(settings); + + // Cleanup + keyman_unsubscribe_option_changes(settings, on_settings_change, user_data); + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, prevValue); +} + +void test_keyman_subscribe_option_changes__callback_called_init_false() { + // Initialize + gboolean prevValue = _get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, FALSE); + count = 0; + data = NULL; + gchar* user_data = "foo"; + void* settings = keyman_subscribe_option_changes(on_settings_change, user_data); + + // Execute + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, TRUE); + + // Verify + g_assert_cmpint(count, ==, 1); + g_assert_cmpstr(data, ==, user_data); + + // Cleanup + keyman_unsubscribe_option_changes(settings, on_settings_change, user_data); + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, prevValue); +} + +void test_keyman_subscribe_option_changes__callback_called_init_true() { + // Initialize + gboolean prevValue = _get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, TRUE); + count = 0; + data = NULL; + void* settings = keyman_subscribe_option_changes(on_settings_change, NULL); + + // Execute + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, FALSE); + + // Verify + g_assert_cmpint(count, ==, 1); + + // Cleanup + keyman_unsubscribe_option_changes(settings, on_settings_change, NULL); + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, prevValue); +} + +void test_keyman_subscribe_option_changes__callback_called_toggle() { + // Initialize + gboolean prevValue = _get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, TRUE); + count = 0; + data = NULL; + void* settings = keyman_subscribe_option_changes(on_settings_change, NULL); + + // Execute + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, FALSE); + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, TRUE); + + // Verify + g_assert_cmpint(count, ==, 2); + + // Cleanup + keyman_unsubscribe_option_changes(settings, on_settings_change, NULL); + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, prevValue); +} + +void test_keyman_subscribe_option_changes__callback_called_no_toggle() { + // Initialize + gboolean prevValue = _get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, TRUE); + count = 0; + data = NULL; + void* settings = keyman_subscribe_option_changes(on_settings_change, NULL); + + // Execute + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, TRUE); + + // Verify + g_assert_cmpint(count, ==, 1); + + // Cleanup + keyman_unsubscribe_option_changes(settings, on_settings_change, NULL); + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, prevValue); +} + +//---------------------------------------------------------------------------------------------- +void test_keyman_unsubscribe_option_changes__create() { + // Initialize + gboolean prevValue = _get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + count = 0; + data = NULL; + void* settings = keyman_subscribe_option_changes(on_settings_change, NULL); + + // Execute + guint retval = keyman_unsubscribe_option_changes(settings, on_settings_change, NULL); + + // Verify + g_assert_cmpint(count, ==, 0); + g_assert_cmpint(retval, ==, 1); + + // Cleanup + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, prevValue); +} + +void test_keyman_unsubscribe_option_changes__null_setting() { + // Initialize + gboolean prevValue = _get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + count = 0; + data = NULL; + void* settings = NULL; + + // Execute + guint retval = keyman_unsubscribe_option_changes(settings, on_settings_change, NULL); + + // Verify + g_assert_cmpint(retval, ==, 0); + + // Cleanup + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, prevValue); +} + +void test_keyman_unsubscribe_option_changes__null_callback() { + // Initialize + gboolean prevValue = _get_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR); + count = 0; + data = NULL; + void* settings = keyman_subscribe_option_changes(on_settings_change, NULL); + + // Execute + guint retval = keyman_unsubscribe_option_changes(settings, NULL, NULL); + + // Verify + g_assert_cmpint(retval, ==, 0); + + // Cleanup + keyman_unsubscribe_option_changes(settings, on_settings_change, NULL); + _set_tst_option(KEYMAN_DCONF_OPTIONS_SIMULATEALTGR, prevValue); } //---------------------------------------------------------------------------------------------- @@ -1107,10 +1330,25 @@ int main(int argc, char* argv[]) { } // Add tests - g_test_add_func("/keymanutil/keyman_put_options_todconf/invalid", test_keyman_put_options_todconf__invalid); - g_test_add_func("/keymanutil/keyman_put_options_todconf/new_key", test_keyman_put_options_todconf__new_key); - g_test_add_func("/keymanutil/keyman_put_options_todconf/other_keys", test_keyman_put_options_todconf__other_keys); - g_test_add_func("/keymanutil/keyman_put_options_todconf/existing_key", test_keyman_put_options_todconf__existing_key); + g_test_add_func("/keymanutil/keyman_put_keyboard_options_todconf/invalid", test_keyman_put_keyboard_options_todconf__invalid); + g_test_add_func("/keymanutil/keyman_put_keyboard_options_todconf/new_key", test_keyman_put_keyboard_options_todconf__new_key); + g_test_add_func("/keymanutil/keyman_put_keyboard_options_todconf/other_keys", test_keyman_put_keyboard_options_todconf__other_keys); + g_test_add_func("/keymanutil/keyman_put_keyboard_options_todconf/existing_key", test_keyman_put_keyboard_options_todconf__existing_key); + + g_test_add_func("/keymanutil/keyman_put_option_todconf/set_to_true", test_keyman_put_option_todconf__set_to_true); + g_test_add_func("/keymanutil/keyman_put_option_todconf/set_to_false", test_keyman_put_option_todconf__set_to_false); + + g_test_add_func("/keymanutil/keyman_get_option_fromdconf/default", test_keyman_get_option_fromdconf__default); + + g_test_add_func("/keymanutil/keyman_subscribe_option_changes/create", test_keyman_subscribe_option_changes__create); + g_test_add_func("/keymanutil/keyman_subscribe_option_changes/callback_called_init_false", test_keyman_subscribe_option_changes__callback_called_init_false); + g_test_add_func("/keymanutil/keyman_subscribe_option_changes/callback_called_init_true", test_keyman_subscribe_option_changes__callback_called_init_true); + g_test_add_func("/keymanutil/keyman_subscribe_option_changes/callback_called_toggle", test_keyman_subscribe_option_changes__callback_called_toggle); + g_test_add_func("/keymanutil/keyman_subscribe_option_changes/callback_called_no_toggle", test_keyman_subscribe_option_changes__callback_called_no_toggle); + + g_test_add_func("/keymanutil/keyman_unsubscribe_option_changes/create", test_keyman_unsubscribe_option_changes__create); + g_test_add_func("/keymanutil/keyman_unsubscribe_option_changes/null_setting", test_keyman_unsubscribe_option_changes__null_setting); + g_test_add_func("/keymanutil/keyman_unsubscribe_option_changes/null_callback", test_keyman_unsubscribe_option_changes__null_callback); g_test_add_func("/keymanutil/keyman_get_custom_keyboard_dictionary/values", test_keyman_get_custom_keyboard_dictionary__values); g_test_add_func("/keymanutil/keyman_get_custom_keyboard_dictionary/invalid", test_keyman_get_custom_keyboard_dictionary__invalid); diff --git a/linux/ibus-keyman/tests/meson.build b/linux/ibus-keyman/tests/meson.build index 73ce8d84d08..40d8b9713af 100644 --- a/linux/ibus-keyman/tests/meson.build +++ b/linux/ibus-keyman/tests/meson.build @@ -5,7 +5,7 @@ test_files = [ '../src/KeymanSystemServiceClient.cpp', ] -keymancore_tests_lib = cc.find_library( +keymancore_tests_lib = c_compiler.find_library( 'keymancore-tests', # meson will prefix 'lib' dirs: [ core_dir / 'build/arch' / get_option('buildtype') / 'tests/kmx_test_source' ] ) diff --git a/linux/ibus-keyman/tests/scripts/run-tests.sh b/linux/ibus-keyman/tests/scripts/run-tests.sh index d06d8898b11..012dee996f8 100755 --- a/linux/ibus-keyman/tests/scripts/run-tests.sh +++ b/linux/ibus-keyman/tests/scripts/run-tests.sh @@ -63,7 +63,7 @@ function run_tests() { G_TEST_BUILDDIR="$(dirname "$0")/../../../build/$(arch)/${CONFIG}/tests" - setup "$DISPLAY_SERVER" "$ENV_FILE" "$CLEANUP_FILE" "$PID_FILE" + setup "$DISPLAY_SERVER" "$ENV_FILE" "$CLEANUP_FILE" "$PID_FILE" --standalone echo "# NOTE: When the tests fail check /tmp/ibus-engine-keyman.log and /tmp/ibus-daemon.log!" echo "" diff --git a/linux/ibus-keyman/tests/scripts/test-helper.inc.sh b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh index 01626a04909..a32fef86890 100755 --- a/linux/ibus-keyman/tests/scripts/test-helper.inc.sh +++ b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh @@ -116,7 +116,7 @@ function _setup_init() { echo > "$CLEANUP_FILE" echo > "$PID_FILE" TEMP_DATA_DIR=$(mktemp --directory) - echo "rm -rf ${TEMP_DATA_DIR} || true" >> "$CLEANUP_FILE" + echo "rm -rf \"${TEMP_DATA_DIR}\" || true # TEMP_DATA_DIR" >> "${CLEANUP_FILE}" COMMON_ARCH_DIR= [ -d "${TOP_SRCDIR}"/../../core/build/arch ] && COMMON_ARCH_DIR=${TOP_SRCDIR}/../../core/build/arch @@ -170,7 +170,7 @@ function _setup_display_server() { # mutter-Message: 18:56:15.422: Using Wayland display name 'wayland-1' mutter --wayland --headless --no-x11 --virtual-monitor 1024x768 &> "$TMPFILE" & PID=$! - echo "kill -9 ${PID} || true" >> "$CLEANUP_FILE" + echo "kill -9 ${PID} || true # mutter" >> "$CLEANUP_FILE" echo "${PID} mutter" >> "${PID_FILE}" sleep 1s export WAYLAND_DISPLAY @@ -189,7 +189,7 @@ function _setup_display_server() { break fi done - echo "kill -9 ${PID} || true" >> "$CLEANUP_FILE" + echo "kill -9 ${PID} || true # Xvfb" >> "$CLEANUP_FILE" echo "${PID} Xvfb" >> "${PID_FILE}" while true; do echo "Starting Xephyr..." @@ -201,12 +201,12 @@ function _setup_display_server() { break fi done - echo "kill -9 ${PID} || true" >> "$CLEANUP_FILE" + echo "kill -9 ${PID} || true # Xephyr" >> "$CLEANUP_FILE" echo "${PID} Xephyr" >> "${PID_FILE}" echo "Starting metacity" metacity --display=:${DISP_XEPHYR} &> /dev/null & PID=$! - echo "kill -9 ${PID} || true" >> "$CLEANUP_FILE" + echo "kill -9 ${PID} || true # metacity" >> "$CLEANUP_FILE" echo "${PID} metacity" >> "${PID_FILE}" export DISPLAY=:${DISP_XEPHYR} @@ -241,16 +241,23 @@ function _setup_schema_and_gsettings() { } function _setup_ibus() { - local ENV_FILE CLEANUP_FILE PID_FILE PID + local ENV_FILE CLEANUP_FILE PID_FILE PID STANDALONE ENV_FILE=$1 CLEANUP_FILE=$2 PID_FILE=$3 + STANDALONE=${4:-} echo "Starting ibus-daemon..." #shellcheck disable=SC2086 ibus-daemon ${ARG_VERBOSE-} --daemonize --panel=disable --address=unix:abstract="${TEMP_DATA_DIR}/test-ibus" ${IBUS_CONFIG-} &> /tmp/ibus-daemon.log PID=$(pgrep -f "${TEMP_DATA_DIR}/test-ibus") - echo "kill -9 ${PID} || true" >> "$CLEANUP_FILE" + if [[ "${STANDALONE}" == "--standalone" ]]; then + # manual test run + echo "if kill -9 ${PID}; then ibus restart || ibus start; fi # ibus-daemon" >> "${CLEANUP_FILE}" + else + # test run as part of the build + echo "kill -9 ${PID} || true" >> "${CLEANUP_FILE}" + fi echo "${PID} ibus-daemon" >> "${PID_FILE}" sleep 1s @@ -263,17 +270,18 @@ function _setup_ibus() { #shellcheck disable=SC2086 "${TOP_BINDIR}/src/ibus-engine-keyman" --testing ${ARG_VERBOSE-} &> /tmp/ibus-engine-keyman.log & PID=$! - echo "kill -9 ${PID} || true" >> "$CLEANUP_FILE" + echo "kill -9 ${PID} || true # ibus-engine-keyman" >> "${CLEANUP_FILE}" echo "${PID} ibus-engine-keyman" >> "${PID_FILE}" sleep 1s } function setup() { - local DISPLAY_SERVER ENV_FILE CLEANUP_FILE PID_FILE TESTBASEDIR TESTDIR + local DISPLAY_SERVER ENV_FILE CLEANUP_FILE PID_FILE TESTBASEDIR TESTDIR STANDALONE DISPLAY_SERVER=$1 ENV_FILE=$2 CLEANUP_FILE=$3 PID_FILE=$4 + STANDALONE=${5:-} _setup_init "${ENV_FILE}" "${CLEANUP_FILE}" "${PID_FILE}" @@ -287,7 +295,7 @@ function setup() { _setup_test_dbus_server "${ENV_FILE}" "${CLEANUP_FILE}" _setup_display_server "${ENV_FILE}" "${CLEANUP_FILE}" "${PID_FILE}" "${DISPLAY_SERVER}" _setup_schema_and_gsettings "${ENV_FILE}" - _setup_ibus "${ENV_FILE}" "${CLEANUP_FILE}" "${PID_FILE}" + _setup_ibus "${ENV_FILE}" "${CLEANUP_FILE}" "${PID_FILE}" "${STANDALONE}" } function setup_display_server_only() { diff --git a/linux/ibus-keyman/tests/testfixture.cpp b/linux/ibus-keyman/tests/testfixture.cpp index ecf5241e794..dab206731d7 100644 --- a/linux/ibus-keyman/tests/testfixture.cpp +++ b/linux/ibus-keyman/tests/testfixture.cpp @@ -320,7 +320,7 @@ static void test_source(IBusKeymanTestsFixture *fixture, gconstpointer user_data if (option.type == km::tests::KOT_INPUT) { auto key = g_utf16_to_utf8((gunichar2 *)option.key.c_str(), option.key.length(), NULL, NULL, NULL); auto value = g_utf16_to_utf8((gunichar2 *)option.value.c_str(), option.value.length(), NULL, NULL, NULL); - keyman_put_options_todconf(data->test_name, data->test_name, key, value); + keyman_put_keyboard_options_todconf(data->test_name, data->test_name, key, value); } } g_settings_sync(); diff --git a/linux/keyman-config/keyman_config/__init__.py b/linux/keyman-config/keyman_config/__init__.py index 2bb3cdbaa25..26fbeddf7dd 100644 --- a/linux/keyman-config/keyman_config/__init__.py +++ b/linux/keyman-config/keyman_config/__init__.py @@ -1,5 +1,6 @@ import atexit import gettext +import importlib import logging import os import pathlib @@ -53,6 +54,14 @@ def initialize_sentry(): SentryErrorHandling().initialize_sentry() +def are_requirements_missing(): + try: + ttLib = importlib.import_module('fontTools.ttLib') + except ImportError: + return True + return False + + class FileCleanup(): """ Allow to register files that will be deleted when the process exits diff --git a/linux/keyman-config/keyman_config/convertico.py b/linux/keyman-config/keyman_config/convertico.py index fa6f7b39abd..ee5a265e826 100755 --- a/linux/keyman-config/keyman_config/convertico.py +++ b/linux/keyman-config/keyman_config/convertico.py @@ -11,8 +11,8 @@ ImageFile.LOAD_TRUNCATED_IMAGES = True -def changeblacktowhite(im): - data = np.array(im) # "data" is a height x width x 4 numpy array +def _changeblacktowhite(image): + data = np.array(image) # "data" is a height x width x 4 numpy array red, green, blue, alpha = data.T # Temporarily unpack the bands for readability # Replace black with white... (leaves alpha values alone...) @@ -50,15 +50,15 @@ def checkandsaveico(icofile): def _convert_ico_to_bmp(icofile, bmpfile): - with Image.open(icofile) as im: - with im.convert('RGBA') as im2: - num, colour = max(im.getcolors(im2.size[0] * im2.size[1])) + with Image.open(icofile) as image: + with image.convert('RGBA') as image2: + num, colour = max(image.getcolors(image2.size[0] * image2.size[1])) logging.debug(f"checkandsaveico maxcolour: num {num}: colour {colour}") if num > 160 and colour == (0, 0, 0, 0): logging.info(f"checkandsaveico:{icofile} mostly black so changing black to white") - im2.close() - im2 = changeblacktowhite(im) - im2.save(bmpfile) + image2.close() + image2 = _changeblacktowhite(image) + image2.save(bmpfile) def extractico(kmxfile): @@ -109,7 +109,7 @@ def extractico(kmxfile): return True -def main(argv): +def main(): if len(sys.argv) != 2: logging.error("convertico.py ") sys.exit(2) @@ -122,4 +122,4 @@ def main(argv): if __name__ == "__main__": - main(sys.argv[1:]) + main() diff --git a/linux/keyman-config/keyman_config/get_kmp.py b/linux/keyman-config/keyman_config/get_kmp.py index 796fb7ae3f5..3addda759bf 100755 --- a/linux/keyman-config/keyman_config/get_kmp.py +++ b/linux/keyman-config/keyman_config/get_kmp.py @@ -21,6 +21,7 @@ class InstallLocation(GObject.GEnum): User = 3 Unknown = 99 +can_use_cache = True def get_install_area_string(area): if area == InstallLocation.OS: @@ -32,6 +33,35 @@ def get_install_area_string(area): return _('Unknown') +def _install_cache(cache_name, backend, expire_after): + global can_use_cache + if can_use_cache: + try: + requests_cache.install_cache(cache_name=cache_name, backend=backend, expire_after=expire_after) + except AttributeError as e: + logging.debug(f'Got exception trying to use `install_cache`: {e}') + can_use_cache = False + + +def _uninstall_cache(): + global can_use_cache + if can_use_cache: + try: + requests_cache.uninstall_cache() + except AttributeError as e: + logging.debug(f'Got exception trying to use `uninstall_cache`: {e}') + can_use_cache = False + + +def _did_use_cache(response): + if can_use_cache: + try: + return response.from_cache + except AttributeError as e: + logging.debug(f'Got exception trying to access `response.from_cache`: {e}') + return False + + def get_package_download_data(packageID, weekCache=False): """ Get package download data from keyboards download api. @@ -52,12 +82,12 @@ def get_package_download_data(packageID, weekCache=False): else: expire_after = datetime.timedelta(days=1) os.chdir(cache_dir) - requests_cache.install_cache(cache_name='keyman_cache', backend='sqlite', expire_after=expire_after) + _install_cache(cache_name='keyman_cache', backend='sqlite', expire_after=expire_after) now = time.ctime(int(time.time())) response = requests.get(api_url) - logging.debug('Time: {0} / Used Cache: {1}'.format(now, response.from_cache)) + logging.debug('Time: {0} / Used Cache: {1}'.format(now, _did_use_cache(response))) os.chdir(current_dir) - requests_cache.uninstall_cache() + _uninstall_cache() if response.status_code == 200: return response.json() else: @@ -84,15 +114,15 @@ def get_keyboard_data(keyboardID, weekCache=False): else: expire_after = datetime.timedelta(days=1) os.chdir(cache_dir) - requests_cache.install_cache(cache_name='keyman_cache', backend='sqlite', expire_after=expire_after) + _install_cache(cache_name='keyman_cache', backend='sqlite', expire_after=expire_after) now = time.ctime(int(time.time())) try: response = requests.get(api_url) - except requests.exceptions.RequestException as e: # This is the correct syntax + except requests.exceptions.RequestException: # This is the correct syntax return None - logging.debug('Time: {0} / Used Cache: {1}'.format(now, response.from_cache)) + logging.debug('Time: {0} / Used Cache: {1}'.format(now, _did_use_cache(response))) os.chdir(current_dir) - requests_cache.uninstall_cache() + _uninstall_cache() if response.status_code == 200: return response.json() else: @@ -212,7 +242,7 @@ def download_kmp_file(url, kmpfile, cache=False): if not os.path.isdir(cache_dir): os.makedirs(cache_dir) os.chdir(cache_dir) - requests_cache.install_cache(cache_name='keyman_kmp_cache', backend='sqlite', expire_after=expire_after) + _install_cache(cache_name='keyman_kmp_cache', backend='sqlite', expire_after=expire_after) now = time.ctime(int(time.time())) try: @@ -222,9 +252,9 @@ def download_kmp_file(url, kmpfile, cache=False): return downloadfile if cache: - logging.debug('Time: {0} / Used Cache: {1}'.format(now, response.from_cache)) + logging.debug('Time: {0} / Used Cache: {1}'.format(now, _did_use_cache(response))) os.chdir(current_dir) - requests_cache.uninstall_cache() + _uninstall_cache() if response.status_code == 200: try: diff --git a/linux/keyman-config/keyman_config/install_kmp.py b/linux/keyman-config/keyman_config/install_kmp.py index fd8d93542fe..fed67952ca5 100755 --- a/linux/keyman-config/keyman_config/install_kmp.py +++ b/linux/keyman-config/keyman_config/install_kmp.py @@ -177,8 +177,8 @@ def _install_files(self, keyboards, files): # Special handling to convert kvk into LDML logging.info("Converting %s to LDML and installing both as as keyman file", f['name']) - ldml = convert_kvk_to_ldml(fpath) name, ext = os.path.splitext(f['name']) + ldml = convert_kvk_to_ldml(name, fpath) ldmlfile = os.path.join(self.packageDir, f"{name}.ldml") output_ldml(ldmlfile, ldml) elif ftype == KMFileTypes.KM_ICON: @@ -307,9 +307,9 @@ def extract_kmp(kmpfile, directory): raise InstallError(InstallStatus.Abort, e) from e -def process_keyboard_data(keyboardID, packageDir) -> None: +def process_keyboard_data(keyboardID, packageDir) -> bool: if not (kbdata := get_keyboard_data(keyboardID)): - return + return False if not os.path.isdir(packageDir) and os.access(os.path.join(packageDir, os.pardir), os.X_OK | os.W_OK): try: os.makedirs(packageDir) @@ -318,11 +318,13 @@ def process_keyboard_data(keyboardID, packageDir) -> None: if os.access(packageDir, os.X_OK | os.W_OK): try: - with open(os.path.join(packageDir, f'{keyboardID}.json'), 'w') as outfile: + with open(os.path.join(packageDir, f'{keyboardID}.json'), 'w', encoding='utf-8') as outfile: json.dump(kbdata, outfile) logging.info("Installing api data file %s.json as keyman file", keyboardID) + return True except Exception as e: logging.warning('Exception %s writing %s/%s.json %s', type(e), packageDir, keyboardID, e.args) + return False def install_kmp(inputfile, sharedarea=False, language=None): diff --git a/linux/keyman-config/keyman_config/keyman_option.py b/linux/keyman-config/keyman_config/keyman_option.py new file mode 100644 index 00000000000..791d799cb83 --- /dev/null +++ b/linux/keyman-config/keyman_config/keyman_option.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 +''' + Keyman is copyright (C) SIL International. MIT License. + + Implementation of the Keyman DConf options +''' +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gio, Gtk + + +class KeymanOption: + ''' + Methods to set and read Keyman options + ''' + def __new__(cls, option_name): + if not hasattr(cls, 'settings'): + cls.settings = Gio.Settings.new('com.keyman.options') # type: ignore + obj = super().__new__(cls) + obj.option_name = option_name + obj.settings = cls.settings + return obj + + def bind_checkbutton(self, button: Gtk.CheckButton, handler = None) -> None: + ''' + Bind the option to the a button, optionally also adding an additional handler. + ''' + self.settings.bind(self.option_name, button, 'active', Gio.SettingsBindFlags.NO_SENSITIVITY) + if handler: + self.settings.connect(f'changed::{self.option_name}', handler) + + def set(self, enabled: bool) -> None: + ''' + Set the value of the option + ''' + self.settings.set_boolean(self.option_name, enabled) + + def get(self) -> bool: + ''' + Get the value of the option + ''' + return self.settings.get_boolean(self.option_name) diff --git a/linux/keyman-config/keyman_config/kvk2ldml.py b/linux/keyman-config/keyman_config/kvk2ldml.py index 5b3ab85ae8b..ddef89241a3 100755 --- a/linux/keyman-config/keyman_config/kvk2ldml.py +++ b/linux/keyman-config/keyman_config/kvk2ldml.py @@ -1,11 +1,15 @@ #!/usr/bin/python3 import logging +import os import struct import sys +from fontTools import ttLib from lxml import etree +from keyman_config.kmpmetadata import parsemetadata + # .kvk file format # KVK files are variable length files with variable sized structures. @@ -164,43 +168,43 @@ class NKey: } -def bytecheck(value, check): +def _bytecheck(value, check): if bytes([value & check[0]]) == check: return True else: return False -def get_nkey(file, fileContent, offset): +def _get_nkey(file, fileContent, offset): nkey = NKey() data = struct.unpack_from(" 256: @@ -224,7 +228,7 @@ def get_nstring(file, fileContent, offset): return stringdata.decode('utf-16'), offset + 2 + (2 * stringlength[0]) -def get_nbitmap(file, fileContent, offset): +def _get_nbitmap(file, fileContent, offset): bitmap = None bitmaplength = struct.unpack_from(" None: label.set_halign(Gtk.Align.START) self.pack_start(label, False, False, 10) - (enabled, reason) = sentry.is_sentry_enabled() + (sentry_enabled, reason) = sentry.is_sentry_enabled() disabledByVariable = sentry.is_sentry_disabled_by_variable() self.errorReportingButton = Gtk.CheckButton(_("Automatically report errors to keyman.com")) - self.errorReportingButton.set_active(enabled) + self.errorReportingButton.set_active(sentry_enabled) self.errorReportingButton.set_sensitive(not disabledByVariable) sentry.bind_checkbutton(self.errorReportingButton) self.pack_start(self.errorReportingButton, False, False, 0) @@ -32,3 +33,10 @@ def __init__(self, sentry) -> None: label.set_padding(25, 0) label.set_sensitive(False) self.pack_start(label, False, False, 0) + + self.simulate_altgr_option = KeymanOption('simulate-altgr') + self.simulate_altgr_enabled = self.simulate_altgr_option.get() + self.simulateAltGrButton = Gtk.CheckButton(_("Simulate AltGr with Ctrl + Alt")) + self.simulateAltGrButton.set_active(sentry_enabled) + self.simulate_altgr_option.bind_checkbutton(self.simulateAltGrButton) + self.pack_start(self.simulateAltGrButton, False, False, 0) diff --git a/linux/keyman-config/keyman_config/sentry_handling.py b/linux/keyman-config/keyman_config/sentry_handling.py index 7585f88e43d..a16ba8016c9 100644 --- a/linux/keyman-config/keyman_config/sentry_handling.py +++ b/linux/keyman-config/keyman_config/sentry_handling.py @@ -1,4 +1,9 @@ #!/usr/bin/python3 +''' +Keyman is copyright (C) SIL International. MIT License. + +Implements the Sentry error handling +''' import getpass import hashlib import importlib @@ -7,6 +12,7 @@ import platform import sys import traceback +from keyman_config.keyman_option import KeymanOption from keyman_config.version import ( __version__, __versionwithtag__, @@ -26,7 +32,7 @@ class SentryErrorHandling: def __init__(self) -> None: - self.settings = Gio.Settings.new('com.keyman.options') + self.error_reporting_setting = KeymanOption('error-reporting') def initialize_sentry(self): (enabled, reason) = self.is_sentry_enabled() @@ -63,8 +69,7 @@ def is_sentry_disabled_by_variable(self): return self._get_environ_nosentry() or not __uploadsentry__ def bind_checkbutton(self, button: Gtk.CheckButton): - self.settings.bind("error-reporting", button, "active", Gio.SettingsBindFlags.NO_SENSITIVITY) - self.settings.connect("changed::error-reporting", self._on_sentry_reporting_toggled) + self.error_reporting_setting.bind_checkbutton(button, self._on_sentry_reporting_toggled) def set_enabled(self, enabled): assert not self.is_sentry_disabled_by_variable() @@ -92,13 +97,13 @@ def _handle_enabled(self, enabled): self._close_sentry() def _save_setting(self, enabled: bool): - self.settings.set_boolean('error-reporting', enabled) + self.error_reporting_setting.set(enabled) def _get_setting(self) -> bool: - return self.settings.get_boolean('error-reporting') + return self.error_reporting_setting.get() def _on_sentry_reporting_toggled(self, settings, key): - self._handle_enabled(self.settings.get_boolean('error-reporting')) + self._handle_enabled(self._get_setting()) def _close_sentry(self): from sentry_sdk import Hub diff --git a/linux/keyman-config/keyman_config/view_installed.py b/linux/keyman-config/keyman_config/view_installed.py index f5d8e5c76db..c8416dfb62b 100755 --- a/linux/keyman-config/keyman_config/view_installed.py +++ b/linux/keyman-config/keyman_config/view_installed.py @@ -16,12 +16,14 @@ from keyman_config import _ from keyman_config.accelerators import bind_accelerator, init_accel +from keyman_config.convertico import checkandsaveico from keyman_config.dbus_util import get_keyman_config_service from keyman_config.downloadkeyboard import DownloadKmpWindow from keyman_config.fcitx_util import is_fcitx_running from keyman_config.get_kmp import (InstallLocation, get_keyboard_dir, - get_install_area_string) + get_install_area_string, get_keyman_dir) from keyman_config.ibus_util import IbusDaemon, verify_ibus_daemon +from keyman_config.install_kmp import process_keyboard_data from keyman_config.install_window import InstallKmpWindow, find_keyman_image from keyman_config.keyboard_layouts_model import create_kbd_layouts_model from keyman_config.keyboard_layouts_widget import KeyboardLayoutsWidget @@ -180,6 +182,23 @@ def _add_app_buttons(self): bbox_hbox.pack_start(bbox_bottom, True, True, 12) return bbox_hbox + def _try_restore_icon_file(self, path, kmpdata): + if not os.access(path, os.X_OK | os.W_OK): + return '' + + file_name = os.path.join(path, kmpdata['packageID'] + '.bmp') + if not os.path.isfile(file_name): + file_name = os.path.join(path, kmpdata['packageID'] + '.ico') + if not os.path.isfile(file_name): + file_name = os.path.join(path, kmpdata['keyboardID'] + '.bmp') + if not os.path.isfile(file_name): + file_name = os.path.join(path, kmpdata['keyboardID'] + '.ico') + if not os.path.isfile(file_name): + return '' + checkandsaveico(file_name) + root, ext = os.path.splitext(file_name) + return root + '.bmp.png' + def _addlistitems(self, installed_kmp, store, install_area): bmppng = ".bmp.png" # Icon file extension @@ -198,7 +217,9 @@ def _addlistitems(self, installed_kmp, store, install_area): if not os.path.isfile(icofile_name): icofile_name = os.path.join(path, kmpdata['keyboardID'] + bmppng) if not os.path.isfile(icofile_name): - icofile_name = find_keyman_image("icon_kmp.png") + icofile_name = self._try_restore_icon_file(path, kmpdata) + if icofile_name == '': + icofile_name = find_keyman_image('icon_kmp.png') try: icofile = GdkPixbuf.Pixbuf.new_from_file_at_size(icofile_name, 16, 16) @@ -218,28 +239,28 @@ def _addlistitems(self, installed_kmp, store, install_area): welcome_file, options_file]) + def _try_restore_package_json(self, packageId, area): + packageDir = get_keyman_dir(area) + if not os.access(packageDir, os.X_OK | os.W_OK): + # we don't have write access to packageDir, so we can't restore the .json file + return False + return process_keyboard_data(packageId, os.path.join(packageDir, packageId)) + + def _add_keyboards_from_area(self, area): + kmps = get_installed_kmp(area) + for kmp in sorted(kmps): + kmpdata = kmps[kmp] + if not kmpdata['has_kbjson'] and not self._try_restore_package_json(kmpdata['packageID'], area): + self.incomplete_kmp.append(kmpdata) + self._addlistitems(kmps, self.store, area) + def refresh_installed_kmp(self): logging.debug("Refreshing listview") self.store.clear() self.incomplete_kmp = [] - user_kmp = get_installed_kmp(InstallLocation.User) - for kmp in sorted(user_kmp): - kmpdata = user_kmp[kmp] - if kmpdata["has_kbjson"] is False: - self.incomplete_kmp.append(kmpdata) - self._addlistitems(user_kmp, self.store, InstallLocation.User) - shared_kmp = get_installed_kmp(InstallLocation.Shared) - for kmp in sorted(shared_kmp): - kmpdata = shared_kmp[kmp] - if kmpdata["has_kbjson"] is False: - self.incomplete_kmp.append(kmpdata) - self._addlistitems(shared_kmp, self.store, InstallLocation.Shared) - os_kmp = get_installed_kmp(InstallLocation.OS) - for kmp in sorted(os_kmp): - kmpdata = os_kmp[kmp] - if kmpdata["has_kbjson"] is False: - self.incomplete_kmp.append(kmpdata) - self._addlistitems(os_kmp, self.store, InstallLocation.OS) + self._add_keyboards_from_area(InstallLocation.User) + self._add_keyboards_from_area(InstallLocation.Shared) + self._add_keyboards_from_area(InstallLocation.OS) if __name__ == '__main__': diff --git a/linux/keyman-config/km-config b/linux/keyman-config/km-config index f40ebeebb4b..641fb964706 100755 --- a/linux/keyman-config/km-config +++ b/linux/keyman-config/km-config @@ -9,7 +9,10 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -from keyman_config import __versionwithtag__, __pkgversion__, add_standard_arguments, initialize_logging, initialize_sentry, verify_dbus_running +from keyman_config import ( + _, __versionwithtag__, __pkgversion__, add_standard_arguments, + are_requirements_missing, initialize_logging, initialize_sentry, + verify_dbus_running) from keyman_config.handle_install import download_and_install_package from keyman_config.ibus_util import verify_ibus_daemon from keyman_config.view_installed import ViewInstalledWindow @@ -42,6 +45,18 @@ if __name__ == '__main__': initialize_sentry() verify_dbus_running() + if are_requirements_missing(): + if args.install or args.url: + logging.error('Missing requirements. Please install python3-fonttools.') + else: + dialog = Gtk.MessageDialog( + None, 0, Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, _("Missing requirements. Please install python3-fonttools.")) + dialog.run() + dialog.destroy() + sys.exit(1) + + logging.info('Keyman version %s %s', __versionwithtag__, __pkgversion__) verify_ibus_daemon(False) diff --git a/linux/keyman-config/km-kvk2ldml b/linux/keyman-config/km-kvk2ldml index 8a9adbb322c..2ddddc5becc 100755 --- a/linux/keyman-config/km-kvk2ldml +++ b/linux/keyman-config/km-kvk2ldml @@ -5,7 +5,9 @@ import logging import os import sys -from keyman_config import add_standard_arguments, initialize_logging, initialize_sentry, verify_dbus_running +from keyman_config import ( + are_requirements_missing, add_standard_arguments, initialize_logging, + initialize_sentry, verify_dbus_running) from keyman_config.kvk2ldml import parse_kvk_file, print_kvk, convert_ldml, output_ldml @@ -15,8 +17,8 @@ def main(): 'print the details of the kvk file.') parser.add_argument('-p', "--print", help='print kvk details', action="store_true") parser.add_argument('-k', "--keys", help='if printing also print all keys', action="store_true") - parser.add_argument('kvkfile', help='kvk file') parser.add_argument('-o', '--output', metavar='LDMLFILE', help='output LDML file location') + parser.add_argument('kvkfile', help='kvk file') add_standard_arguments(parser) args = parser.parse_args() @@ -25,6 +27,10 @@ def main(): initialize_sentry() verify_dbus_running() + if are_requirements_missing(): + logging.error('km-kvk2ldml: Missing requirements. Please install python3-fonttools.') + sys.exit(1) + name, ext = os.path.splitext(args.kvkfile) # Check if input file extension is kvk if ext != ".kvk": @@ -46,6 +52,8 @@ def main(): outputfile = args.output if args.output else f'{name}.ldml' dirname = os.path.dirname(outputfile) + if dirname == '': + dirname = '.' if not os.path.isdir(dirname): if os.path.exists(dirname): logging.error(f'km-kvk2ldml: error, `{dirname}` exists but is not a directory') @@ -54,7 +62,8 @@ def main(): try: with open(outputfile, 'wb') as ldmlfile: - ldml = convert_ldml(kvkData) + kmpJsonFilename = os.path.join(os.path.dirname(args.kvkfile), 'kmp.json') + ldml = convert_ldml(name, kvkData, kmpJsonFilename) output_ldml(ldmlfile, ldml) except PermissionError: logging.error(f'km-kvk2ldml: error, permission denied writing file `{outputfile}`') diff --git a/linux/keyman-config/locale/el_polyton.po b/linux/keyman-config/locale/el_polyton.po new file mode 100644 index 00000000000..fa9797acce3 --- /dev/null +++ b/linux/keyman-config/locale/el_polyton.po @@ -0,0 +1,332 @@ +msgid "" +msgstr "" +"Project-Id-Version: keyman\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-08-19 19:17+0200\n" +"PO-Revision-Date: 2024-08-06 22:57\n" +"Last-Translator: \n" +"Language-Team: Greek (Polytonic)\n" +"Language: el_polyton\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: keyman\n" +"X-Crowdin-Project-ID: 386703\n" +"X-Crowdin-Language: el-polyton\n" +"X-Crowdin-File: /master/linux/keyman-config.pot\n" +"X-Crowdin-File-ID: 504\n" + +#: keyman_config/__init__.py:68 +msgid "Neither sentry-sdk nor raven is available. Not enabling Sentry error reporting." +msgstr "Δὲν εἶναι διαθέσιμα οὔτε τὸ sentry-sdk οὔτε τὸ raven. Δὲν ἐνεργοποιεῖται ἡ ἀναφορὰ σφαλμάτων Sentry." + +#: keyman_config/downloadkeyboard.py:23 +msgid "Download Keyman keyboards" +msgstr "Μεταφόρτωση πληκτρολογίων Keyman" + +#: keyman_config/downloadkeyboard.py:37 keyman_config/keyboard_details.py:49 +#: keyman_config/keyboard_details.py:340 keyman_config/view_installed.py:205 +msgid "_Close" +msgstr "_Close" + +#: keyman_config/install_kmp.py:99 +msgid "You do not have permissions to install the keyboard files to the shared area /usr/local/share/keyman" +msgstr "Δὲν σᾶς ἐπιτρέπεται νὰ ἐγκαταστήσετε τὰ ἀρχεῖα τοῦ πληκτρολογίου στὴν κοινὴ περιοχὴ /usr/local/share/keyman" + +#: keyman_config/install_kmp.py:103 +msgid "You do not have permissions to install the documentation to the shared documentation area /usr/local/share/doc/keyman" +msgstr "Δὲν σᾶς ἐπιτρέπεται νὰ ἐγκαταστήσετε τὴν τεκμηρίωση στὴν κοινὴ περιοχὴ τεκμηρίωσης /usr/local/share/doc/keyman" + +#: keyman_config/install_kmp.py:107 +msgid "You do not have permissions to install the font files to the shared font area /usr/local/share/fonts" +msgstr "Δὲν σᾶς ἐπιτρέπεται νὰ ἐγκαταστήσετε τὰ ἀρχεῖα γραμματοσειρῶν στὴν κοινὴ περιοχὴ γραμματοσειρῶν /usr/local/share/fonts" + +#: keyman_config/install_kmp.py:179 +#, python-brace-format +msgid "install_kmp.py: error: No kmp.json or kmp.inf found in {package}" +msgstr "install_kmp.py: σφάλμα: Δὲν βρέθηκε kmp.json ἢ kmp.inf στὸ {package}" + +#: keyman_config/install_kmp.py:246 +#, python-brace-format +msgid "install_kmp.py: error: No kmp.json or kmp.inf found in {packageFile}" +msgstr "install_kmp.py: σφάλμα: Δὲν βρέθηκε kmp.json ἢ kmp.inf στὸ {packageFile}" + +#: keyman_config/install_window.py:54 +#, python-brace-format +msgid "Installing keyboard/package {keyboardid}" +msgstr "Ἐγκαθίσταται πληκτρολόγιο/πακέτο {keyboardid}" + +#: keyman_config/install_window.py:72 keyman_config/install_window.py:93 +msgid "Keyboard is installed already" +msgstr "Τὸ πληκτρολόγιο ἔχει ἤδη ἐγκατασταθεῖ" + +#: keyman_config/install_window.py:74 +#, python-brace-format +msgid "The {name} keyboard is already installed at version {version}. Do you want to uninstall then reinstall it?" +msgstr "Τὸ πληκτρολόγιο {name} ἔχει ἤδη ἐγκατασταθεῖ στὴν ἔκδοση {version}. Θέλετε νὰ τὸ ἀφαιρέσετε καὶ νὰ τὸ ἐγκαταστήσετε ἐκ νέου;" + +#: keyman_config/install_window.py:95 +#, python-brace-format +msgid "The {name} keyboard is already installed with a newer version {installedversion}. Do you want to uninstall it and install the older version {version}?" +msgstr "Τὸ πληκτρολόγιο {name} ἔχει ἤδη ἐγκατασταθεῖ μὲ τὴν νεώτερη ἔκδοση {installedversion}. Θέλετε νὰ τὸ ἀφαιρέσετε καὶ νὰ ἐγκαταστήσετε τὴν παλαιότερη ἔκδοση {version};" + +#: keyman_config/install_window.py:128 +msgid "Keyboard layouts: " +msgstr "Διατάξεις πληκτρολογίου: " + +#: keyman_config/install_window.py:147 +msgid "Fonts: " +msgstr "Γραμματοσειρές: " + +#: keyman_config/install_window.py:167 keyman_config/keyboard_details.py:96 +msgid "Package version: " +msgstr "Ἔκδοση πακέτου: " + +#: keyman_config/install_window.py:179 +msgid "Author: " +msgstr "Κατασκευαστής: " + +#: keyman_config/install_window.py:197 +msgid "Website: " +msgstr "Ἱστότοπος: " + +#: keyman_config/install_window.py:211 +msgid "Copyright: " +msgstr "Πνευματικὰ δικαιώματα: " + +#: keyman_config/install_window.py:245 +msgid "Details" +msgstr "Λεπτομέρειες" + +#: keyman_config/install_window.py:248 +msgid "README" +msgstr "README" + +#: keyman_config/install_window.py:256 keyman_config/view_installed.py:200 +msgid "_Install" +msgstr "_Install" + +#: keyman_config/install_window.py:260 +msgid "_Cancel" +msgstr "_Cancel" + +#: keyman_config/install_window.py:305 +#, python-brace-format +msgid "Keyboard {name} installed" +msgstr "Τὸ πληκτρολόγιο {name} ἐγκατεστάθη" + +#: keyman_config/install_window.py:310 keyman_config/install_window.py:315 +#, python-brace-format +msgid "Keyboard {name} could not be installed." +msgstr "Τὸ πληκτρολόγιο {name} δὲν μπόρεσε νὰ ἐγκατασταθεῖ." + +#: keyman_config/install_window.py:311 +msgid "Error Message:" +msgstr "Μήνυμα σφάλματος:" + +#: keyman_config/install_window.py:316 +msgid "Warning Message:" +msgstr "Προειδοποιητικὸ μήνυμα:" + +#: keyman_config/keyboard_details.py:37 +#, python-brace-format +msgid "{name} keyboard" +msgstr "πληκτρολόγιο {name}" + +#: keyman_config/keyboard_details.py:53 +msgid "ERROR: Keyboard metadata is damaged.\n" +"Please \"Uninstall\" and then \"Install\" the keyboard." +msgstr "ΣΦΑΛΜΑ: Ζημία σὲ μεταδεδομένα πληκτρολογίου.\n" +"Παρακαλοῦμε \"Ἀπεγκαταστῆστε\" καὶ κατόπιν \"Ἐγκαταστῆστε\" τὸ πληκτρολόγιο." + +#: keyman_config/keyboard_details.py:74 +msgid "Package name: " +msgstr "Ὄνομα πακέτου: " + +#: keyman_config/keyboard_details.py:85 +msgid "Package id: " +msgstr "Id πακέτου: " + +#: keyman_config/keyboard_details.py:108 +msgid "Package description: " +msgstr "Περιγραφὴ πακέτου: " + +#: keyman_config/keyboard_details.py:121 +msgid "Package author: " +msgstr "Κατασκευαστὴς πακέτου: " + +#: keyman_config/keyboard_details.py:133 +msgid "Package copyright: " +msgstr "Πνευματικὰ δικαιώματα πακέτου: " + +#: keyman_config/keyboard_details.py:174 +msgid "Keyboard filename: " +msgstr "Ὄνομα ἀρχείου πακέτου: " + +#: keyman_config/keyboard_details.py:187 +msgid "Keyboard name: " +msgstr "Ὄνομα πληκτρολογίου: " + +#: keyman_config/keyboard_details.py:198 +msgid "Keyboard id: " +msgstr "Id πληκτρολογίου: " + +#: keyman_config/keyboard_details.py:209 +msgid "Keyboard version: " +msgstr "Ἔκδοση πληκτρολογίου: " + +#: keyman_config/keyboard_details.py:221 +msgid "Keyboard author: " +msgstr "Σχεδιαστὴς πληκτρολογίου: " + +#: keyman_config/keyboard_details.py:232 +msgid "Keyboard license: " +msgstr "Ἄδεια χρήσης πληκτρολογίου: " + +#: keyman_config/keyboard_details.py:243 +msgid "Keyboard description: " +msgstr "Περιγραφὴ πληκτρολογίου: " + +#: keyman_config/keyboard_details.py:334 +#, python-brace-format +msgid "Scan this code to load this keyboard\n" +"on another device or share online" +msgstr "Σαρῶστε αὐτὸν τὸν κωδικὸ γιὰ νὰ φορτώσετε αὐτὸ τὸ πληκτρολόγιο\n" +"σὲ ἄλλη συσκευὴ ἢ μοιρασθῆτε το ὀνλάϊν" + +#: keyman_config/options.py:24 +#, python-brace-format +msgid "{packageId} Settings" +msgstr "Ρυθμίσεις {packageId}" + +#: keyman_config/view_installed.py:30 +msgid "Keyman Configuration" +msgstr "Παράμετροι Keyman" + +#: keyman_config/view_installed.py:60 +msgid "Choose a kmp file..." +msgstr "Ἐπιλέξτε ἀρχεῖο kmp..." + +#. i18n: file type in file selection dialog +#: keyman_config/view_installed.py:65 +msgid "KMP files" +msgstr "Ἀρχεῖα KMP" + +#. i18n: column header in table displaying installed keyboards +#: keyman_config/view_installed.py:141 +msgid "Icon" +msgstr "Εἰκονίδιο" + +#. i18n: column header in table displaying installed keyboards +#: keyman_config/view_installed.py:145 +msgid "Name" +msgstr "Ὄνομα" + +#. i18n: column header in table displaying installed keyboards +#: keyman_config/view_installed.py:148 +msgid "Version" +msgstr "Ἔκδοση" + +#: keyman_config/view_installed.py:161 +msgid "_Uninstall" +msgstr "_Uninstall" + +#: keyman_config/view_installed.py:162 keyman_config/view_installed.py:304 +msgid "Uninstall keyboard" +msgstr "Ἀπεγκαταστῆστε πληκτρολόγιο" + +#: keyman_config/view_installed.py:167 +msgid "_About" +msgstr "_About" + +#: keyman_config/view_installed.py:168 keyman_config/view_installed.py:306 +msgid "About keyboard" +msgstr "Περὶ τοῦ πληκτρολογίου" + +#: keyman_config/view_installed.py:173 +msgid "_Help" +msgstr "_Help" + +#: keyman_config/view_installed.py:174 keyman_config/view_installed.py:305 +msgid "Help for keyboard" +msgstr "Βοήθεια γιὰ τὸ πληκτρολόγιο" + +#: keyman_config/view_installed.py:179 +msgid "_Options" +msgstr "_Options" + +#: keyman_config/view_installed.py:180 keyman_config/view_installed.py:307 +msgid "Settings for keyboard" +msgstr "Ρυθμίσεις πληκτρολογίου" + +#: keyman_config/view_installed.py:190 +msgid "_Refresh" +msgstr "_Refresh" + +#: keyman_config/view_installed.py:191 +msgid "Refresh keyboard list" +msgstr "Ἀνανεῶστε τὸν κατάλογο πληκτρολογίων" + +#: keyman_config/view_installed.py:195 +msgid "_Download" +msgstr "_Download" + +#: keyman_config/view_installed.py:196 +msgid "Download and install a keyboard from the Keyman website" +msgstr "Κατεβάστε καὶ ἐγκαταστῆστε πληκτρολόγιο ἀπὸ τὸν ἱστότοπο Keyman" + +#: keyman_config/view_installed.py:201 +msgid "Install a keyboard from a file" +msgstr "Ἐγκαταστῆστε πληκτρολόγιο ἀπὸ ἀρχεῖο" + +#: keyman_config/view_installed.py:206 +msgid "Close window" +msgstr "Κλεῖστε τὸ παράθυρο" + +#: keyman_config/view_installed.py:278 +#, python-brace-format +msgid "Uninstall keyboard {package}" +msgstr "Ἀπεγκαταστῆστε πληκτρολόγιο {package}" + +#: keyman_config/view_installed.py:280 +#, python-brace-format +msgid "Help for keyboard {package}" +msgstr "Βοήθεια γιὰ πληκτρολόγιο {package}" + +#: keyman_config/view_installed.py:282 +#, python-brace-format +msgid "About keyboard {package}" +msgstr "Περὶ τοῦ πληκτρολογίου {package}" + +#: keyman_config/view_installed.py:284 +#, python-brace-format +msgid "Settings for keyboard {package}" +msgstr "Ρυθμίσεις πληκτρολογίου {package}" + +#: keyman_config/view_installed.py:349 +msgid "Uninstall keyboard package?" +msgstr "Θὰ ἀπεγκαταστήσετε τὸ πακέτο πληκτρολογίου;" + +#: keyman_config/view_installed.py:351 +#, python-brace-format +msgid "Are you sure that you want to uninstall the {keyboard} keyboard and its fonts?" +msgstr "Εἶσθε βέβαιοι ὅτι θέλετε νὰ ἀπεγκαταστήσετε τὸ πληκτρολόγιο {keyboard} καὶ τὶς γραμματοσειρές του;" + +#: keyman_config/welcome.py:22 +#, python-brace-format +msgid "{name} installed" +msgstr "Τὸ {name} ἐγκατεστάθη" + +#: keyman_config/welcome.py:40 +msgid "Open in _Web browser" +msgstr "Ἀνοῖξτε στὸν φυλλομετρητὴ _Web" + +#: keyman_config/welcome.py:42 +msgid "Open in the default web browser to do things like printing" +msgstr "Ἀνοῖξτε στὸν προεπιλεγμένο φυλλομετρητὴ γιὰ νὰ ἐκτελέσετε π.χ., μία ἐκτύπωση" + +#: keyman_config/welcome.py:45 +msgid "_OK" +msgstr "_OK" + diff --git a/linux/keyman-config/resources/com.keyman.gschema.xml b/linux/keyman-config/resources/com.keyman.gschema.xml index c286687f407..28262c62044 100644 --- a/linux/keyman-config/resources/com.keyman.gschema.xml +++ b/linux/keyman-config/resources/com.keyman.gschema.xml @@ -7,6 +7,12 @@ Automatic error reporting true to enable automatic reporting of errors to Sentry + + false + Simulate AltGr with Ctrl+Alt + true will allow Ctrl+Alt to be used for AltGr. If false + Ctrl+Alt will not be used to simulate the AltGr key. + diff --git a/linux/keyman-config/tests/test_kvk2ldml.py b/linux/keyman-config/tests/test_kvk2ldml.py new file mode 100644 index 00000000000..9f4c072874f --- /dev/null +++ b/linux/keyman-config/tests/test_kvk2ldml.py @@ -0,0 +1,57 @@ +import os +import shutil +import sys +import tempfile +import unittest + +from keyman_config.kvk2ldml import KVKData, NFont, convert_ldml + +class Kvk2LdmlTests(unittest.TestCase): + def _createKmpJson(self, packagedir): + kmpJsonFilename = os.path.join(packagedir, 'kmp.json') + with open(kmpJsonFilename, 'w') as file: + file.write('''{ + "system": { + "keymanDeveloperVersion": "18.0", + "fileVersion": "7.0" + }, + "files": [ { + "name": "khmer_angkor.kmx", + "description": "Keyboard Khmer Angkor" + }, { + "name": "kmp.json", + "description": "Package information (JSON)" + } ], + "keyboards": [ { + "name": "Khmer Angkor", + "id": "khmer_angkor", + "version": "1.5", + "oskFont": "keymanweb-osk.ttf", + "languages": [ { + "name": "Central Khmer (Khmer, Cambodia)", + "id": "km" + } ]} + ]}''') + return kmpJsonFilename + + def test_convert_ldml__adds_keymanFacename(self): + # Setup + keyboardName = 'khmer_angkor' + kvkData = KVKData() + kvkData.AssociatedKeyboard = keyboardName + kvkData.UnicodeFont = NFont() + kvkData.UnicodeFont.name='DontUseThis!' + + workdir = tempfile.TemporaryDirectory() + kmpJsonFilename = self._createKmpJson(workdir.name) + shutil.copy2(os.path.join(sys.path[0], '../../../common/resources/fonts/keymanweb-osk.ttf'), + workdir.name) + + # Execute + ldml = convert_ldml(keyboardName, kvkData, kmpJsonFilename) + + # Verify + self.assertEqual(ldml.get('locale'), "zzz-keyman") + self.assertEqual(ldml.get('keymanFacename'), 'SymChar') + + workdir.cleanup() diff --git a/linux/keyman-system-service/.gitignore b/linux/keyman-system-service/.gitignore new file mode 100644 index 00000000000..f4e74111e44 --- /dev/null +++ b/linux/keyman-system-service/.gitignore @@ -0,0 +1 @@ +resources/meson.build diff --git a/linux/keyman-system-service/build.sh b/linux/keyman-system-service/build.sh index bb3e1f94615..aa151830ebc 100755 --- a/linux/keyman-system-service/build.sh +++ b/linux/keyman-system-service/build.sh @@ -23,6 +23,15 @@ builder_describe \ builder_parse "$@" +# Import our standard compiler defines; this is copied from +# /resources/build/meson/standard.meson.build by build.sh, because meson doesn't +# allow us to reference a file outside its root. ${THIS_SCRIPT_PATH}/meson.build +# then includes `resources` as a subdir. +if builder_has_action configure; then + cp "${KEYMAN_ROOT}/resources/build/meson/standard.meson.build" "${THIS_SCRIPT_PATH}/resources/meson.build" + cat "${THIS_SCRIPT_PATH}/resources/meson.build.in" >> "${THIS_SCRIPT_PATH}/resources/meson.build" +fi + if builder_is_debug_build; then MESON_TARGET=debug export CPPFLAGS=-DG_MESSAGES_DEBUG diff --git a/linux/keyman-system-service/meson.build b/linux/keyman-system-service/meson.build index 66da8f98914..596ba48bcd4 100644 --- a/linux/keyman-system-service/meson.build +++ b/linux/keyman-system-service/meson.build @@ -1,7 +1,7 @@ project('keyman-system-service', 'c', 'cpp', version: run_command('cat', '../../VERSION.md', check: true).stdout().strip(), license: 'GPL-2+', - meson_version: '>=0.61') + meson_version: '>=1.0') evdev = dependency('libevdev', version: '>= 1.9') systemd = dependency('libsystemd') diff --git a/linux/keyman-system-service/resources/meson.build b/linux/keyman-system-service/resources/meson.build.in similarity index 84% rename from linux/keyman-system-service/resources/meson.build rename to linux/keyman-system-service/resources/meson.build.in index c28cb81e585..2743889f302 100644 --- a/linux/keyman-system-service/resources/meson.build +++ b/linux/keyman-system-service/resources/meson.build.in @@ -1,3 +1,4 @@ +# This file will be appended to meson.build by build.sh install_data('com.keyman.SystemService1.conf', install_dir: get_option('datadir') / 'dbus-1/system.d/') install_data('com.keyman.SystemService1.service', install_dir: get_option('datadir') / 'dbus-1/system-services/') install_data('systemd-keyman.service', install_dir: get_option('prefix') / 'lib/systemd/system/') diff --git a/linux/scripts/cow.sh b/linux/scripts/cow.sh deleted file mode 100755 index 5a14ebed807..00000000000 --- a/linux/scripts/cow.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# If needed set cowbuilder up for building Keyman Debian packages -# Then cowbuilder update - -distributions='focal jammy mantic noble' - -if ! dpkg-query -l cowbuilder; then - echo "installing pbuilder and cowbuilder" - sudo apt install pbuilder cowbuilder -else - echo "already have pbuilder and cowbuilder" -fi - -# set -e at this point to allow detecting the error of the dpkg-query -set -e - -if [ ! -e ~/.pbuilderrc ]; then - echo "linking .pbuilderrc to your ~/.pbuilderrc" - ln -s "$(pwd)/.pbuilderrc" ~/.pbuilderrc -else - echo "assuming you already have ~/.pbuilderrc set up as you want it" - echo "though you may like to look at this .pbuilderrc in case it is useful" -fi - -for dist in $distributions; do - sudo mkdir -p /var/cache/pbuilder/result/"${dist}" - if [ ! -e /var/cache/pbuilder/result/"${dist}"/Packages ]; then - sudo touch /var/cache/pbuilder/result/"${dist}"/Packages - fi - sudo mkdir -p /var/cache/pbuilder/hook.d/"${dist}" - if [ ! -e /var/cache/pbuilder/hook.d/"${dist}"/D70results ]; then - echo "#!/bin/bash" | sudo tee /var/cache/pbuilder/hook.d/"${dist}"/D70results - echo "cd /var/cache/pbuilder/result/${dist}" | sudo tee -a /var/cache/pbuilder/hook.d/"${dist}"/D70results - echo "/usr/bin/dpkg-scanpackages . /dev/null > /var/cache/pbuilder/result/${dist}/Packages" | sudo tee -a /var/cache/pbuilder/hook.d/"${dist}"/D70results - echo "/usr/bin/apt-get update" | sudo tee -a /var/cache/pbuilder/hook.d/"${dist}"/D70results - sudo chmod +x /var/cache/pbuilder/hook.d/"${dist}"/D70results - sudo ln -s /var/cache/pbuilder/hook.d/"${dist}"/D70results /var/cache/pbuilder/hook.d/"${dist}"/I70buildresults - fi - if [ ! -e /var/cache/pbuilder/hook.d/"${dist}"/D80no-man-db-rebuild ]; then - sudo ln -s /usr/share/doc/pbuilder/examples/D80no-man-db-rebuild /var/cache/pbuilder/hook.d/"${dist}"/D80no-man-db-rebuild - fi - if [ ! -d /var/cache/pbuilder/base-"${dist}".cow ]; then - echo "making ${dist} cowbuilder" - sudo DIST="${dist}" cowbuilder --create --distribution "${dist}" --basepath /var/cache/pbuilder/base-"${dist}".cow --hookdir /var/cache/pbuilder/hook.d/"${dist}" - else - echo "already have ${dist} cowbuilder" - fi - sudo DIST="${dist}" cowbuilder --update --distribution "${dist}" --basepath /var/cache/pbuilder/base-"${dist}".cow --override-config --othermirror="deb [trusted=yes] file:/var/cache/pbuilder/result/${dist} ./" --bindmounts /var/cache/pbuilder/result/"${dist}" --hookdir /var/cache/pbuilder/hook.d/"${dist}" -done diff --git a/linux/scripts/deb-packaging.sh b/linux/scripts/deb-packaging.sh index 67511862507..22840396d54 100755 --- a/linux/scripts/deb-packaging.sh +++ b/linux/scripts/deb-packaging.sh @@ -55,7 +55,7 @@ source_action() { echo "${END_STEP}" echo "${START_STEP}Make deb source${COLOR_RESET}" - ./scripts/deb.sh sourcepackage + ./scripts/deb.sh echo "${END_STEP}" mv builddebs/* "${OUTPUT_PATH:-..}" diff --git a/linux/scripts/deb.sh b/linux/scripts/deb.sh index d6b68a7de46..bcf17982e91 100755 --- a/linux/scripts/deb.sh +++ b/linux/scripts/deb.sh @@ -1,45 +1,13 @@ #!/bin/bash # Build Debian source packages of Keyman -# and optionally also binary packages with pbuilder # It must be run from the keyman/linux directory -# parameters: ./deb.sh [sourcepackage] [dist] -# sourcepackage = only create Debian source package -# dist = only build for this distribution +# parameters: ./deb.sh set -e -all_distributions="focal jammy mantic noble" -distributions="" -echo "all_distributions: ${all_distributions}" - -if [ "$1" == "sourcepackage" ]; then - only_sourcepackage=true - shift -else - only_sourcepackage=false -fi - -if [ "$1" != "" ]; then - for dist in ${all_distributions}; do - if [ "${dist}" == "$1" ]; then - distributions="$1" - fi - done - if [ "${distributions}" == "" ]; then - echo "distribution $1 does not exist" - exit 1 - fi -fi - -if [ "${distributions}" == "" ]; then - distributions="${all_distributions}" -fi - -echo "distributions: ${distributions}" - BASEDIR=$(pwd) #echo "basedir is $BASEDIR" @@ -64,20 +32,3 @@ debuild -d -S -sa -Zxz -us -uc cd "$BASEDIR/builddebs" rm -rf "keyman-${vers}" cd "$BASEDIR" - -if $only_sourcepackage; then - exit 0 -fi - -# build the packages with cowbuilder from the source package -cd builddebs -echo "keyman version ${vers}" -rm -rf "keyman-${vers}" -dpkg-source -x "keyman_${vers}-1.dsc" -cd "keyman-${vers}" -for dist in ${distributions}; do - dch -v "${vers}-1+${dist}" "local build for ${dist}" - echo "dist: $dist" - DIST=${dist} pdebuild --pbuilder cowbuilder --buildresult /var/cache/pbuilder/result/"${dist}" -- --basepath /var/cache/pbuilder/base-"${dist}".cow --distribution "${dist}" --override-config --othermirror="deb [trusted=yes] file:/var/cache/pbuilder/result/${dist} ./" --bindmounts /var/cache/pbuilder/result/"${dist}" --hookdir /var/cache/pbuilder/hook.d/"${dist}" -done -cd "${BASEDIR}" diff --git a/linux/scripts/dist.sh b/linux/scripts/dist.sh index bb49de50c6e..6ef0be7d8bb 100755 --- a/linux/scripts/dist.sh +++ b/linux/scripts/dist.sh @@ -30,28 +30,37 @@ cp -a debian ../ cd .. echo "3.0 (native)" > debian/source/format dch keyman --newversion "${VERSION}" --force-bad-version --nomultimaint -dpkg-source --tar-ignore=*~ --tar-ignore=.git --tar-ignore=.gitattributes \ - --tar-ignore=.gitignore --tar-ignore=experiments --tar-ignore=debian \ - --tar-ignore=.github --tar-ignore=.vscode --tar-ignore=android \ +dpkg-source --tar-ignore=*~ \ + --tar-ignore=.git \ + --tar-ignore=.gitattributes \ + --tar-ignore=.gitignore \ + --tar-ignore=experiments \ + --tar-ignore=debian \ + --tar-ignore=.github \ + --tar-ignore=.vscode \ + --tar-ignore=android \ --tar-ignore=.devcontainer \ --tar-ignore=artifacts \ \ --tar-ignore=common/models \ - --tar-ignore=common/predictive-text \ --tar-ignore=common/resources \ --tar-ignore=common/schemas \ --tar-ignore=common/test/keyboards/build.* \ - --tar-ignore=common/test/predictive-text \ --tar-ignore=common/test/resources \ --tar-ignore=common/web \ --tar-ignore=common/windows \ \ --tar-ignore=core/build \ - --tar-ignore=developer --tar-ignore=docs --tar-ignore=ios \ + --tar-ignore=developer \ + --tar-ignore=docs \ + --tar-ignore=ios \ --tar-ignore=linux/keyman-config/keyman_config/version.py \ - --tar-ignore=linux/keyman-config/buildtools/build-langtags.py --tar-ignore=__pycache__ \ + --tar-ignore=linux/keyman-config/buildtools/build-langtags.py \ + --tar-ignore=__pycache__ \ --tar-ignore=linux/help \ - --tar-ignore=mac --tar-ignore=node_modules --tar-ignore=oem \ + --tar-ignore=mac \ + --tar-ignore=node_modules \ + --tar-ignore=oem \ --tar-ignore=linux/build \ --tar-ignore=linux/builddebs \ --tar-ignore=linux/ibus-keyman/build \ @@ -60,11 +69,17 @@ dpkg-source --tar-ignore=*~ --tar-ignore=.git --tar-ignore=.gitattributes \ --tar-ignore=resources/environment.sh \ --tar-ignore=resources/git-hooks \ --tar-ignore=resources/scopes \ - --tar-ignore=resources/build/*.lua --tar-ignore=resources/build/jq* \ + --tar-ignore=resources/build/*.lua \ + --tar-ignore=resources/build/jq* \ --tar-ignore=results \ --tar-ignore=tmp \ - --tar-ignore=web --tar-ignore=windows --tar-ignore=keyman_1* \ - --tar-ignore=dist --tar-ignore=.pbuilderrc --tar-ignore=VERSION -Zgzip -b . + --tar-ignore=web \ + --tar-ignore=windows \ + --tar-ignore=keyman_1* \ + --tar-ignore=dist \ + --tar-ignore=VERSION \ + \ + -Zgzip -b . mv ../keyman_"${VERSION}".tar.gz linux/dist/keyman-"${VERSION}".tar.gz echo "3.0 (quilt)" > debian/source/format cd "$BASEDIR" diff --git a/linux/scripts/launchpad.sh b/linux/scripts/launchpad.sh index da0622b1ca8..d23d942fb18 100755 --- a/linux/scripts/launchpad.sh +++ b/linux/scripts/launchpad.sh @@ -33,7 +33,7 @@ else fi echo "ppa: ${ppa}" -distributions="${DIST:-focal jammy mantic noble}" +distributions="${DIST:-focal jammy noble oracular}" packageversion="${PACKAGEVERSION:-1~sil1}" BASEDIR=$(pwd) diff --git a/linux/scripts/upload-to-debian.sh b/linux/scripts/upload-to-debian.sh index 0d6d3252b87..85472a4ed19 100755 --- a/linux/scripts/upload-to-debian.sh +++ b/linux/scripts/upload-to-debian.sh @@ -95,18 +95,20 @@ function get_latest_stable_branch_name() { function push_to_github_and_create_pr() { local BRANCH=$1 local BASE=$2 - local COMMIT_MSG=$3 - local PR_MSG=$4 - local pr_number + local PR_TITLE=$3 + local PR_BODY=$4 if [[ -n "${PUSH}" ]]; then - ${NOOP} git push --force-with-lease origin "${BRANCH}" - pr_number=$(gh pr list --draft --search "${COMMIT_MSG}" --base "${BASE}" --json number --jq '.[].number') - if [[ -n ${pr_number} ]]; then - builder_echo "PR #${pr_number} already exists" - else - ${NOOP} gh pr create --draft --base "${BASE}" --title "${PR_MSG}" --body "@keymanapp-test-bot skip" - fi + ${NOOP} git push --force-with-lease origin "${BRANCH}" + PR_NUMBER=$(gh pr list --draft --search "${PR_TITLE}" --base "${BASE}" --json number --jq '.[].number') + if [[ -n ${PR_NUMBER} ]]; then + builder_echo "PR #${PR_NUMBER} already exists" + else + ${NOOP} gh pr create --draft --base "${BASE}" --title "${PR_TITLE}" --body "${PR_BODY}" + PR_NUMBER=$(gh pr list --draft --search "${PR_TITLE}" --base "${BASE}" --json number --jq '.[].number') + fi + else + PR_NUMBER="" fi } @@ -142,12 +144,14 @@ cp debianpackage/keyman-*/debian/changelog debian/ git add debian/changelog COMMIT_MESSAGE="chore(linux): Update debian changelog" git commit -m "${COMMIT_MESSAGE}" -push_to_github_and_create_pr chore/linux/changelog "${DEPLOY_BRANCH#origin/}" "${COMMIT_MESSAGE}" "${COMMIT_MESSAGE}" +push_to_github_and_create_pr chore/linux/changelog "${DEPLOY_BRANCH#origin/}" "${COMMIT_MESSAGE}" "@keymanapp-test-bot skip" # Create cherry-pick on master branch git checkout -B chore/linux/cherry-pick/changelog origin/master git cherry-pick -x chore/linux/changelog -push_to_github_and_create_pr chore/linux/cherry-pick/changelog master "${COMMIT_MESSAGE}" "${COMMIT_MESSAGE} 🍒" +push_to_github_and_create_pr chore/linux/cherry-pick/changelog master "${COMMIT_MESSAGE} 🍒" \ +"Cherry-pick-of: #${PR_NUMBER} +@keymanapp-test-bot skip" builder_heading "Finishing" git checkout "${CURRENT_BRANCH}" diff --git a/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m b/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m index 44f7cfda587..f6bf6c81fed 100644 --- a/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m +++ b/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m @@ -9,8 +9,6 @@ #import "AppDelegate.h" #import -static BOOL debugMode = YES; - BOOL isKeyMapEnabled; const unsigned short keyMapSize = 0x80; @@ -47,8 +45,6 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { NSArray *kvkFiles = [self KVKFiles]; for (NSString *path in kvkFiles){ KVKFile *kvkFile = [[KVKFile alloc] initWithFilePath:path]; - if (debugMode) - NSLog(@"%@", kvkFile); }*/ } @@ -116,7 +112,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef NSLog(@"AppDelegate eventTapFunction key down event: %@", event); // Key down event NSEvent *mEvent = [NSEvent eventWithCGEvent:event]; - KMEngine *kme = [[KMEngine alloc] initWithKMX:kmx context:contextBuffer verboseLogging:debugMode]; + KMEngine *kme = [[KMEngine alloc] initWithKMX:kmx context:contextBuffer]; CoreKeyOutput *coreKeyOutput = [kme processEvent:mEvent]; if (coreKeyOutput) { if (coreKeyOutput.hasTextToInsert) { @@ -227,18 +223,6 @@ - (void)comboBoxSelectionDidChange:(NSNotification *)notification { NSLog(@"%d: %@", index, kmStore); index++; } - - for (NSObject *gp in kmx.group) { - if (debugMode) { - NSLog(@"Group %@", gp); - //NSLog(@"match = %@", gp.match); - //NSLog(@"nomatch = %@", gp.noMatch); - /* - for (KMCompKey *kmKey in gp.keys) { - NSLog(@"\nKey: %@", kmKey); - }*/ - } - } } } @@ -259,9 +243,6 @@ - (void)setKMXList { if (!infoDict) continue; - //if (debugMode) - // NSLog(@"%@", infoDict); - //NSString *str = [NSString stringWithFormat:@"%@ (%@)", [infoDict objectForKey:kKMKeyboardNameKey], [infoDict objectForKey:kKMKeyboardVersionKey]]; NSString *str = [infoDict objectForKey:kKMKeyboardNameKey]; [kmxDesc addObject:str]; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index 35a362f2617..57ce828e4a5 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -7,12 +7,14 @@ objects = { /* Begin PBXBuildFile section */ + 29015ABD2C58D86F00CCBB94 /* KMDataRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = 29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */; }; 290BC680274B9DB1005CD1C3 /* KMPackageInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 290BC67F274B9DB1005CD1C3 /* KMPackageInfo.m */; }; 290BC75E274F3FD7005CD1C3 /* KMKeyboardInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 290BC75D274F3FD7005CD1C3 /* KMKeyboardInfo.m */; }; 2915DC512BFE35DB0051FC52 /* KMLogs.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B4A0D32BF7675A00682049 /* KMLogs.m */; }; 293EA3E627140D8100545EED /* KMAboutWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 293EA3E827140D8100545EED /* KMAboutWindowController.xib */; }; 293EA3EB27140DEC00545EED /* preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 293EA3ED27140DEC00545EED /* preferences.xib */; }; 293EA3F427181FDA00545EED /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 293EA3F627181FDA00545EED /* Localizable.strings */; }; + 296105232C8E91C7007BF6B7 /* KMInputMethodLifecycle.m in Sources */ = {isa = PBXBuildFile; fileRef = 296105222C8E91C7007BF6B7 /* KMInputMethodLifecycle.m */; }; 296FE2FC275DD21600F46898 /* KMPackageReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 296FE2FB275DD21600F46898 /* KMPackageReader.m */; }; 297A501728DF4D360074EB1B /* PrivacyWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 297A501228DF4D360074EB1B /* PrivacyWindowController.m */; }; 297A501828DF4D360074EB1B /* PrivacyWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 297A501328DF4D360074EB1B /* PrivacyWindowController.xib */; }; @@ -30,6 +32,8 @@ 29B42A602728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29B42A622728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib */; }; 29B4A0D52BF7675A00682049 /* KMLogs.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B4A0D32BF7675A00682049 /* KMLogs.m */; }; 29B6FB732BC39DD60074BF7F /* TextApiComplianceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B6FB722BC39DD60074BF7F /* TextApiComplianceTests.m */; }; + 29C1CDE22C5B2F8B003C23BB /* KMSettingsRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = D861B03E2C5747F70003675E /* KMSettingsRepository.m */; }; + 29C1CDE32C5B2F8B003C23BB /* KMDataRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = 29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */; }; 37A245C12565DFA6000BBF92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 37A245C02565DFA6000BBF92 /* Assets.xcassets */; }; 37AE5C9D239A7B770086CC7C /* qrcode.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 37AE5C9C239A7B770086CC7C /* qrcode.min.js */; }; 37C2B0CB25FF2C350092E16A /* Help in Resources */ = {isa = PBXBuildFile; fileRef = 37C2B0CA25FF2C340092E16A /* Help */; }; @@ -72,6 +76,7 @@ 98FE10631B4DEE5600525F54 /* KMInfoWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 98FE10611B4DEE5600525F54 /* KMInfoWindowController.m */; }; 9A3D6C5D221531B0008785A3 /* KMOSVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = 9A3D6C5C221531B0008785A3 /* KMOSVersion.m */; }; B90818AF7ED302187DE0E026 /* Pods_Keyman.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B644E63217FFE54B91C71C94 /* Pods_Keyman.framework */; }; + D861B03F2C5747F70003675E /* KMSettingsRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = D861B03E2C5747F70003675E /* KMSettingsRepository.m */; }; E211769D20E182DD00F8065D /* NoContextTestClient.m in Sources */ = {isa = PBXBuildFile; fileRef = E211769C20E182DD00F8065D /* NoContextTestClient.m */; }; E21176A020E18C5200F8065D /* AppleCompliantTestClient.m in Sources */ = {isa = PBXBuildFile; fileRef = E211769F20E18C5200F8065D /* AppleCompliantTestClient.m */; }; E211CCF620B600A500505C36 /* KeymanEngine4Mac.framework.dSYM in CopyFiles */ = {isa = PBXBuildFile; fileRef = E211CCF520B600A500505C36 /* KeymanEngine4Mac.framework.dSYM */; }; @@ -114,13 +119,13 @@ }; E211CCF420B5FE7A00505C36 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; + buildActionMask = 8; dstPath = ""; dstSubfolderSpec = 16; files = ( E211CCF620B600A500505C36 /* KeymanEngine4Mac.framework.dSYM in CopyFiles */, ); - runOnlyForDeploymentPostprocessing = 0; + runOnlyForDeploymentPostprocessing = 1; }; E27BA9CF202036BA00D273E7 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -135,6 +140,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 29015ABB2C58D86F00CCBB94 /* KMDataRepository.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMDataRepository.h; sourceTree = ""; }; + 29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMDataRepository.m; sourceTree = ""; }; 2901BA8A292332B3009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/KMAboutWindowController.strings"; sourceTree = ""; }; 2901BA8B292332B3009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/preferences.strings"; sourceTree = ""; }; 2901BA8C292332B3009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/KMInfoWindowController.strings"; sourceTree = ""; }; @@ -152,6 +159,8 @@ 293EA3EF27140DFA00545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/preferences.strings; sourceTree = ""; }; 293EA3F02714158600545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = ""; }; 293EA3F527181FDA00545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 296105212C8E91C7007BF6B7 /* KMInputMethodLifecycle.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodLifecycle.h; sourceTree = ""; }; + 296105222C8E91C7007BF6B7 /* KMInputMethodLifecycle.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodLifecycle.m; sourceTree = ""; }; 296FE2FA275DD21600F46898 /* KMPackageReader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMPackageReader.h; sourceTree = ""; }; 296FE2FB275DD21600F46898 /* KMPackageReader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMPackageReader.m; sourceTree = ""; }; 29781101297FB262007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/KMAboutWindowController.strings; sourceTree = ""; }; @@ -257,6 +266,12 @@ 29C1E1772800227700759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/KMKeyboardHelpWindowController.strings"; sourceTree = ""; }; 29C1E1782800227800759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/MainMenu.strings"; sourceTree = ""; }; 29C1E179280025A900759EDE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; + 29D470942C648D5100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/KMAboutWindowController.strings; sourceTree = ""; }; + 29D470952C648D5100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/preferences.strings; sourceTree = ""; }; + 29D470962C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/KMInfoWindowController.strings; sourceTree = ""; }; + 29D470972C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/KMKeyboardHelpWindowController.strings; sourceTree = ""; }; + 29D470982C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/MainMenu.strings; sourceTree = ""; }; + 29D470992C648D7100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; 29DD8400276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMAboutWindowController.strings; sourceTree = ""; }; 29DD8401276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/preferences.strings; sourceTree = ""; }; 29DD8402276C49E30066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMInfoWindowController.strings; sourceTree = ""; }; @@ -350,6 +365,8 @@ CEFFECDB2A417FEC00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/KMKeyboardHelpWindowController.strings; sourceTree = ""; }; CEFFECDC2A417FEC00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainMenu.strings; sourceTree = ""; }; CEFFECDD2A4180FD00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + D861B03D2C5747F70003675E /* KMSettingsRepository.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMSettingsRepository.h; sourceTree = ""; }; + D861B03E2C5747F70003675E /* KMSettingsRepository.m */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = KMSettingsRepository.m; sourceTree = ""; tabWidth = 2; }; E211769B20E1826800F8065D /* NoContextTestClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NoContextTestClient.h; sourceTree = ""; }; E211769C20E182DD00F8065D /* NoContextTestClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NoContextTestClient.m; sourceTree = ""; }; E211769E20E18C0B00F8065D /* AppleCompliantTestClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppleCompliantTestClient.h; sourceTree = ""; }; @@ -548,6 +565,10 @@ 29B4A0D32BF7675A00682049 /* KMLogs.m */, 299ABD6F29ECE75B00AA5948 /* KeySender.m */, 299ABD7029ECE75B00AA5948 /* KeySender.h */, + D861B03D2C5747F70003675E /* KMSettingsRepository.h */, + D861B03E2C5747F70003675E /* KMSettingsRepository.m */, + 29015ABB2C58D86F00CCBB94 /* KMDataRepository.h */, + 29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */, 297A501128DF4D360074EB1B /* Privacy */, 98FE105B1B4DE86300525F54 /* Categories */, 98D6DA791A799EE700B09822 /* Frameworks */, @@ -561,6 +582,8 @@ 98D6DA7D1A799FF400B09822 /* KMInputController.m */, 98A778C21A8C53BF00CF809D /* KMInputMethodAppDelegate.h */, 98A778C31A8C53BF00CF809D /* KMInputMethodAppDelegate.m */, + 296105212C8E91C7007BF6B7 /* KMInputMethodLifecycle.h */, + 296105222C8E91C7007BF6B7 /* KMInputMethodLifecycle.m */, E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */, E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */, 298D09F62A1F4533006B9DFE /* TextApiCompliance.h */, @@ -795,6 +818,7 @@ uk, ru, es, + el, ); mainGroup = 989C9BFF1A7876DE00A20425; productRefGroup = 989C9C091A7876DE00A20425 /* Products */; @@ -974,8 +998,11 @@ 29B4A0D52BF7675A00682049 /* KMLogs.m in Sources */, 98BF924F1BF02DC20002126A /* KMBarView.m in Sources */, E240F599202DED740000067D /* KMPackage.m in Sources */, + 296105232C8E91C7007BF6B7 /* KMInputMethodLifecycle.m in Sources */, + D861B03F2C5747F70003675E /* KMSettingsRepository.m in Sources */, 984B8F441AF1C3D900E096A8 /* OSKWindowController.m in Sources */, 9836B3711AE5F11D00780482 /* mztools.c in Sources */, + 29015ABD2C58D86F00CCBB94 /* KMDataRepository.m in Sources */, 9836B3701AE5F11D00780482 /* ioapi.c in Sources */, E21799051FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */, 98E6729F1B532F5E00DBDE2F /* KMDownloadKBWindowController.m in Sources */, @@ -1005,6 +1032,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 29C1CDE22C5B2F8B003C23BB /* KMSettingsRepository.m in Sources */, + 29C1CDE32C5B2F8B003C23BB /* KMDataRepository.m in Sources */, 2915DC512BFE35DB0051FC52 /* KMLogs.m in Sources */, 2992F4202A28482800E08929 /* PrivacyWindowController.m in Sources */, 2992F41F2A2847C900E08929 /* TextApiCompliance.m in Sources */, @@ -1053,6 +1082,7 @@ 298566D42980D47C004ACA95 /* uk */, 298566E02980E579004ACA95 /* ru */, CEFFECD82A417FEA00D58C36 /* es */, + 29D470942C648D5100224B4F /* el */, ); name = KMAboutWindowController.xib; sourceTree = ""; @@ -1079,6 +1109,7 @@ 298566D52980D47D004ACA95 /* uk */, 298566E12980E579004ACA95 /* ru */, CEFFECD92A417FEB00D58C36 /* es */, + 29D470952C648D5100224B4F /* el */, ); name = preferences.xib; path = Keyman4MacIM/KMConfiguration; @@ -1105,6 +1136,7 @@ 298566D92980D489004ACA95 /* uk */, 298566E52980E584004ACA95 /* ru */, CEFFECDD2A4180FD00D58C36 /* es */, + 29D470992C648D7100224B4F /* el */, ); name = Localizable.strings; sourceTree = ""; @@ -1131,6 +1163,7 @@ 298566D62980D47D004ACA95 /* uk */, 298566E22980E57A004ACA95 /* ru */, CEFFECDA2A417FEB00D58C36 /* es */, + 29D470962C648D5200224B4F /* el */, ); name = KMInfoWindowController.xib; path = Keyman4MacIM/KMInfoWindow; @@ -1158,6 +1191,7 @@ 298566D72980D47D004ACA95 /* uk */, 298566E32980E57A004ACA95 /* ru */, CEFFECDB2A417FEC00D58C36 /* es */, + 29D470972C648D5200224B4F /* el */, ); name = KMKeyboardHelpWindowController.xib; path = Keyman4MacIM/KMKeyboardHelpWindow; @@ -1185,6 +1219,7 @@ 298566D82980D47E004ACA95 /* uk */, 298566E42980E57A004ACA95 /* ru */, CEFFECDC2A417FEC00D58C36 /* es */, + 29D470982C648D5200224B4F /* el */, ); name = MainMenu.xib; sourceTree = ""; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Info.plist b/mac/Keyman4MacIM/Keyman4MacIM/Info.plist index 29451d4cce2..49f532055ec 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/Info.plist +++ b/mac/Keyman4MacIM/Keyman4MacIM/Info.plist @@ -36,7 +36,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - Keyman $(PRODUCT_VERSION) for macOS + $(PRODUCT_VERSION) CFBundleSignature ???? CFBundleURLTypes diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/el.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/el.lproj/KMAboutWindowController.strings new file mode 100644 index 00000000000..a6dc5d6122c --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/el.lproj/KMAboutWindowController.strings @@ -0,0 +1,8 @@ +/* button text to close the About window */ +"Kab-Up-fYH.title" = "Κλεῖστε"; + +/* text of link to the license agreement */ +"hYC-Bx-aoP.title" = "Ἄδεια χρήσης"; + +/* button text to open Configuration window */ +"vkh-b5-vO7.title" = "Παράμετροι..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib index c327ce4ad27..e6cc0173aac 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib @@ -1,21 +1,18 @@ - + - - + + - - - @@ -79,7 +76,7 @@ - + - - - @@ -254,7 +217,7 @@ - + @@ -305,9 +268,9 @@ - + - + YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m index 3e2ffe24100..202be65ef5e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m @@ -8,14 +8,12 @@ #import "KMConfigurationWindowController.h" #import "KMDownloadKBWindowController.h" +#import "KMDataRepository.h" #import "KMLogs.h" @interface KMConfigurationWindowController () @property (nonatomic, weak) IBOutlet NSTableView *tableView; @property (nonatomic, weak) IBOutlet WebView *webView; -@property (nonatomic, weak) IBOutlet NSButton *alwaysShowOSKCheckBox; -@property (nonatomic, weak) IBOutlet NSButton *useVerboseLoggingCheckBox; -@property (nonatomic, weak) IBOutlet NSTextField *verboseLoggingInfo; @property (nonatomic, weak) IBOutlet NSButton *supportBack; @property (nonatomic, weak) IBOutlet NSButton *supportForward; @property (nonatomic, weak) IBOutlet NSButton *supportHome; @@ -68,9 +66,6 @@ - (void)windowDidLoad { NSURL *homeUrl = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html" subdirectory:@"Help"]; [self.webView.mainFrame loadRequest:[NSURLRequest requestWithURL:homeUrl]]; - - [self.alwaysShowOSKCheckBox setState:(self.AppDelegate.alwaysShowOSK ? NSOnState : NSOffState)]; - [self.useVerboseLoggingCheckBox setState:(self.AppDelegate.useVerboseLogging ? NSOnState : NSOffState)]; } - (void)webView:(WebView *)sender decidePolicyForNewWindowAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request newFrameName:(NSString *)frameName decisionListener:(id)listener { @@ -134,8 +129,10 @@ - (NSArray *)tableContents { NSArray *pArray = (NSArray *)obj; NSString *packageFolder = [self packageFolderFromPath:[pArray objectAtIndex:0]]; NSString *packageName = [self.AppDelegate packageNameFromPackageInfo:packageFolder]; + os_log_debug([KMLogs uiLog], "tableContents, packageFolder: %{public}@, packageName: %{public}@", packageFolder, packageName); [_tableContents addObject:[NSDictionary dictionaryWithObjectsAndKeys:packageName, @"HeaderTitle", nil]]; for (NSString *path in pArray) { + os_log_debug([KMLogs uiLog], "tableContents, path = '%{public}@'", path); NSDictionary *info = [KMXFile keyboardInfoFromKmxFile:path]; if (!info) { info = [[NSDictionary alloc] initWithObjectsAndKeys: @@ -150,6 +147,7 @@ - (NSArray *)tableContents { } else { NSString *path = (NSString *)obj; + os_log_debug([KMLogs uiLog], "tableContents, path = '%{public}@'", path); NSDictionary *info = [KMXFile keyboardInfoFromKmxFile:path]; if (!info) { info = [[NSDictionary alloc] initWithObjectsAndKeys: @@ -241,22 +239,27 @@ - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn BOOL isHeader = (headerTitle != nil); BOOL isOthers = NO; NSString *kmxFilePath = [self kmxFilePathAtIndex:row]; - if (kmxFilePath != nil) + if (kmxFilePath != nil) { isOthers = [[self packageFolderFromPath:kmxFilePath] isEqualToString:@"Others"]; - else if (isHeader && [headerTitle isEqualToString:@"Others"]) + } + else if (isHeader && [headerTitle isEqualToString:@"Others"]) { isOthers = YES; - + } if ([identifier isEqualToString:@"Column1"]) { KMConfigColumn1CellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self]; - if (isHeader) + if (isHeader) { [cellView setHidden:YES]; + } else { [cellView setHidden:NO]; cellView.imageView.objectValue = [info objectForKey:kKMKeyboardIconKey]; [cellView.checkBox setTag:row]; [cellView.checkBox setAction:@selector(checkBoxAction:)]; - [cellView.checkBox setState:([self.activeKeyboards containsObject:[self kmxFilePathAtIndex:row]])?NSOnState:NSOffState]; + NSString *kmxFilePath = [self kmxFilePathAtIndex:row]; + NSString *partialPath = [KMDataRepository.shared trimToPartialPath:kmxFilePath]; + os_log_debug([KMLogs uiLog], "tableView:viewForTableColumn, kmxFilePath = %{public}@ for row %li, partialPath = %{public}@", kmxFilePath, (long)row, partialPath); + [cellView.checkBox setState:([self.activeKeyboards containsObject:partialPath])?NSOnState:NSOffState]; } return cellView; @@ -357,14 +360,16 @@ - (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id)info r - (void)checkBoxAction:(id)sender { NSButton *checkBox = (NSButton *)sender; NSString *kmxFilePath = [self kmxFilePathAtIndex:checkBox.tag]; + NSString *partialPath = [KMDataRepository.shared trimToPartialPath:kmxFilePath]; + os_log_debug([KMLogs uiLog], "checkBoxAction, kmxFilePath = %{public}@ for checkBox.tag %li, partialPath = %{public}@", kmxFilePath, checkBox.tag, partialPath); if (checkBox.state == NSOnState) { os_log_debug([KMLogs uiLog], "Adding active keyboard: %{public}@", kmxFilePath); - [self.activeKeyboards addObject:kmxFilePath]; + [self.activeKeyboards addObject:partialPath]; [self saveActiveKeyboards]; } else if (checkBox.state == NSOffState) { os_log_debug([KMLogs uiLog], "Disabling active keyboard: %{public}@", kmxFilePath); - [self.activeKeyboards removeObject:kmxFilePath]; + [self.activeKeyboards removeObject:partialPath]; [self saveActiveKeyboards]; } } @@ -398,8 +403,8 @@ - (void)helpAction:(id)sender { } - (void)removeAction:(id)sender { - NSButton *removeButton = (NSButton *)sender; - NSDictionary *info = [self.tableContents objectAtIndex:removeButton.tag]; + NSButton *deleteButton = (NSButton *)sender; + NSDictionary *info = [self.tableContents objectAtIndex:deleteButton.tag]; NSString *deleteKeyboardMessage = NSLocalizedString(@"message-confirm-delete-keyboard", nil); if ([info objectForKey:@"HeaderTitle"] != nil) @@ -407,10 +412,13 @@ - (void)removeAction:(id)sender { else [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, [info objectForKey:kKMKeyboardNameKey]]]; - [self.deleteAlertView beginSheetModalForWindow:self.window - modalDelegate:self - didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) - contextInfo:(__bridge void *)([NSNumber numberWithInteger:removeButton.tag])]; + [self.deleteAlertView beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { + if (returnCode == NSAlertFirstButtonReturn) { + os_log_debug([KMLogs uiLog], "confirm delete keyboard alert dismissed"); + [self deleteFileAtIndex:[NSNumber numberWithInteger:deleteButton.tag]]; + } + self.deleteAlertView = nil; + }]; } - (IBAction)downloadAction:(id)sender { @@ -425,38 +433,31 @@ - (IBAction)downloadAction:(id)sender { [self.AppDelegate.downloadKBWindow.window makeKeyAndOrderFront:nil]; } -- (IBAction)alwaysShowOSKCheckBoxAction:(id)sender { - NSButton *checkBox = (NSButton *)sender; - [self.AppDelegate setAlwaysShowOSK:(checkBox.state == NSOnState)]; -} - -- (IBAction)useVerboseLoggingCheckBoxAction:(id)sender { - NSButton *checkBox = (NSButton *)sender; - BOOL verboseLoggingOn = checkBox.state == NSOnState; - [self.AppDelegate setUseVerboseLogging:verboseLoggingOn]; - [self.verboseLoggingInfo setHidden:!verboseLoggingOn]; -} - - (void)handleRequestToInstallPackage:(KMPackage *) package { + os_log_debug([KMLogs dataLog], "handleRequestToInstallPackage"); NSString *keyboardInfoString = NSLocalizedString(@"info-install-keyboard-filename", nil); [self.confirmKmpInstallAlertView setInformativeText:[NSString localizedStringWithFormat:keyboardInfoString, package.getOrigKmpFilename]]; os_log_debug([KMLogs uiLog], "Asking user to confirm installation of %{public}@, KMP - temp file name: %{public}@", package.getOrigKmpFilename, package.getTempKmpFilename); - [self.confirmKmpInstallAlertView beginSheetModalForWindow:self.window - modalDelegate:self - didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) - contextInfo:(__bridge void *)(package)]; + [self.confirmKmpInstallAlertView beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { + os_log_debug([KMLogs uiLog], "confirm keyboard installation alert dismissed"); + if (returnCode == NSAlertFirstButtonReturn) { + [self installPackageFile: package.getTempKmpFilename]; + } + [package releaseTempKMPFile]; + self.confirmKmpInstallAlertView = nil; + }]; } - (void)installPackageFile:(NSString *)kmpFile { // kmpFile could be a temp file (in fact, it always is!), so don't display the name. - - os_log_debug([KMLogs dataLog], "KMP - Ready to unzip/install Package File: %{public}@", kmpFile); + os_log_debug([KMLogs dataLog], "kmpFile - ready to unzip/install Package File: %{public}@", kmpFile); BOOL didUnzip = [self.AppDelegate unzipFile:kmpFile]; if (!didUnzip) { + os_log_debug([KMLogs dataLog], "kmpFile, unzipFile failed"); NSAlert *failure = [[NSAlert alloc] init]; [failure addButtonWithTitle:NSLocalizedString(@"button-keyboard-file-unreadable", @"Alert button")]; @@ -465,13 +466,12 @@ - (void)installPackageFile:(NSString *)kmpFile { [failure setIcon:[[NSBundle mainBundle] imageForResource:@"logo.png"]]; [failure setAlertStyle:NSAlertStyleWarning]; - [failure beginSheetModalForWindow:self.window - modalDelegate:self - didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) - contextInfo:nil]; + [failure beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { + os_log_debug([KMLogs uiLog], "kmpFile, keyboard file unreadable alert dismissed with returnCode: %ld", (long)returnCode); + }]; } else { - os_log_debug([KMLogs dataLog], "Completed installation of KMP file."); + os_log_debug([KMLogs dataLog], "kmpFile, completed installation of KMP file"); } } @@ -547,28 +547,6 @@ - (NSAlert *)confirmKmpInstallAlertView { return _confirmKmpInstallAlertView; } -- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { - os_log_debug([KMLogs uiLog], "User responded to NSAlert"); - if (alert == _deleteAlertView) { - if (returnCode == NSAlertFirstButtonReturn) { // Delete - [self deleteFileAtIndex:(__bridge NSNumber *)contextInfo]; - } - - _deleteAlertView = nil; - } - else if (alert == _confirmKmpInstallAlertView) { - KMPackage *package = (__bridge KMPackage *)contextInfo; - os_log_debug([KMLogs uiLog], "KMP - Temp file: %{public}@", package.getTempKmpFilename); - if (returnCode == NSAlertFirstButtonReturn) { // Install - [self installPackageFile: package.getTempKmpFilename]; - } - - [package releaseTempKMPFile]; - _confirmKmpInstallAlertView = nil; - } - // else, just a message - nothing to do. -} - - (void)deleteFileAtIndex:(NSNumber *) n { NSInteger index = [n integerValue]; NSString *path2Remove = nil; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/cs.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/cs.lproj/preferences.strings index 305368c9ca1..9477b4c6378 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/cs.lproj/preferences.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/cs.lproj/preferences.strings @@ -17,7 +17,7 @@ "JOK-JV-n8w.title" = "Zpět"; /* Keyboards button text */ -"MPN-9N-wWc.label" = "Keyboards"; +"MPN-9N-wWc.label" = "Klávesnice"; /* text to explain verbose Console logging option */ "MrI-GM-7d6.title" = "Když je zapnuta možnost detailního logování, Keyman zaznamená akce, které mohou pomoci Keymanovi diagnostikovat problém. Program konzole může být použit k zobrazení protokolu zpráv od Keymana. V konzoli filtr zobrazuje všechny zprávy z procesu \"Klíčové\". Tento log může být exportován a v případě potřeby odeslán na podporu klíče."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/el.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/el.lproj/preferences.strings new file mode 100644 index 00000000000..6b750343d00 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/el.lproj/preferences.strings @@ -0,0 +1,38 @@ +/* Checkbox text to always show on-screen keyboard */ +"4UX-80-0Vo.title" = "Νὰ ἐμφανίζεται πάντοτε τὸ πληκτρολόγιο ὀθόνης"; + +/* Checkbox text to enable verbose Console logging */ +"7J6-zy-R20.title" = "Χρησιμοποιῆστε λεπτομερῆ καταγραφὴ μηνυμάτων κονσόλας"; + +/* Support button text */ +"93O-x6-RLF.label" = "Ὑποστήριξη"; + +/* Download Keyboard button text */ +"CTw-kf-WNS.title" = "Κατεβάστε πληκτρολόγιο..."; + +/* Keyman Configuration window title */ +"F0z-JX-Cv5.title" = "Παράμετροι Keyman"; + +/* button text to go back to previous page */ +"JOK-JV-n8w.title" = "Πίσω"; + +/* Keyboards button text */ +"MPN-9N-wWc.label" = "Πληκτρολόγια"; + +/* text to explain verbose Console logging option */ +"MrI-GM-7d6.title" = "Ὅταν εἶναι ἐνεργὴ ἡ λεπτομερὴς καταγραφὴ μηνυμάτων κονσόλας, τὸ Keyman θὰ καταγράφει ἐνέργειες ποὺ θὰ βοηθήσουν τὴν ὁμάδα ὑποστήριξης Keyman νὰ διαγνώσει τυχὸν πρόβλημα. Μὲ τὸ πρόγραμμα κονσόλας μπορεῖτε νὰ δεῖτε τὰ μηνύματα τοῦ Keyman σὲ ἀρχεῖο. Στὴν Κονσόλα φιλτράρετε καὶ προβάλετε ὅλα τὰ μηνύματα ἀπὸ τὴν διεργασία \"Keyman\". Ἂν χρειασθεῖ, αὐτὴ ἡ ἀναφορὰ μπορεῖ νὰ ἐξαχθεῖ καὶ νὰ ἀποσταλεῖ στὴν ὑποστήριξη τοῦ Keyman."; + +/* button text to move forward to the next page */ +"eXr-8V-h1g.title" = "Μπροστά"; + +/* button text to return to the home help page */ +"fXS-aC-CMH.title" = "Ἀρχική"; + +/* Options button text */ +"frd-No-seV.label" = "Ἐπιλογές"; + +/* Installed Keyboard column heading */ +"jex-Nd-Qzg.headerCell.title" = "Ἐγκατεστημένο Πληκτρολόγιο"; + +/* Verbose logging tooltip text */ +"uWx-3J-U0D.ibShadowedToolTip" = "Ἐνεργοποιῆστε ἂν ἔχετε προβλήματα μὲ ἕνα συγκεκριμένο πληκτρολόγιο ἢ μὲ τὸ Keyman γενικῶς."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/it.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/it.lproj/preferences.strings index e83bbe7c452..23333e41df1 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/it.lproj/preferences.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/it.lproj/preferences.strings @@ -17,7 +17,7 @@ "JOK-JV-n8w.title" = "Indietro"; /* Keyboards button text */ -"MPN-9N-wWc.label" = "Keyboards"; +"MPN-9N-wWc.label" = "Tastiere"; /* text to explain verbose Console logging option */ "MrI-GM-7d6.title" = "Quando l'opzione di registrazione dettagliata è attiva, Keyman registrerà le azioni che potrebbero aiutare Keyman Supporto diagnosticare un problema. Il programma Console può essere utilizzato per vedere un registro dei messaggi da Keyman. In Console, filtrare per mostrare tutti i messaggi dal processo \"Keyman\". Questo registro può essere esportato e inviato al supporto di Keyman, se necessario."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.h b/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.h new file mode 100644 index 00000000000..35d3f427e6f --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.h @@ -0,0 +1,28 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Created by Shawn Schantz on 2024-07-30. + * + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KMDataRepository : NSObject +// keymanDataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman' +@property (readonly) NSURL *keymanDataDirectory; + +// keymanKeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards' +@property (readonly) NSURL *keymanKeyboardsDirectory; + ++ (KMDataRepository *)shared; +- (void)createDataDirectoryIfNecessary; +- (void)createKeyboardsDirectoryIfNecessary; +- (BOOL)migrateData; +- (NSString*)buildFullPath:(NSString *)fromPartialPath; +- (NSString*)trimToPartialPath:(NSString *)fromFullPath; +- (NSString *)buildPartialPathFrom:(NSString *)keyboardSubdirectory keyboardFile:(NSString *)kmxFilename; +@end + +NS_ASSUME_NONNULL_END diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.m b/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.m new file mode 100644 index 00000000000..4722a86fec9 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.m @@ -0,0 +1,230 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Created by Shawn Schantz on 2024-07-30. + * + * Singleton object which serves as an abstraction for the reading and writing of Keyman data. + * The 'data' currently consists of keyman keyboards installed by the user. All data is saved locally on disk using NSFileManager. + * This is in contrast with the lighter weight Settings which is stored using UserDefaults and handled by KMSettingsRepository. + */ + +#import "KMDataRepository.h" +#import "KMLogs.h" + +@interface KMDataRepository () +@property (readonly) NSURL *applicationSupportSubDirectory; +@property (readonly) NSURL *documentsSubDirectory; +@property (readonly) NSURL *obsoleteKeymanKeyboardsDirectory; +@end + +@implementation KMDataRepository +/** + * Two directory trees are represented by the following properties, one in active use + * and one that is obsolete. + * The actively used directories, begin with the parent + * applicationSupportSubDirectory: '~/Library/Application Support' + * keymanDataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman' + * keymanKeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards' + * The obsolete directories, begin with the parent + * documentsSubDirectory: '~/Documents' + * obsoleteKeymanKeyboardsDirectory: '~/Documents/Keyman-Keyboards' + */ +@synthesize applicationSupportSubDirectory = _applicationSupportSubDirectory; +@synthesize keymanDataDirectory = _keymanDataDirectory; +@synthesize keymanKeyboardsDirectory = _keymanKeyboardsDirectory; +@synthesize documentsSubDirectory = _documentsSubDirectory; +@synthesize obsoleteKeymanKeyboardsDirectory = _obsoleteKeymanKeyboardsDirectory; + +NSString *const kKeyboardsDirectoryName = @"Keyman-Keyboards"; +/** + * The name of the subdirectory within '~/Library/Application Support'. + * We follow the convention of using the bundle identifier rather than our subsystem id. + * (Also, using the subsystem id, "com.keyman.app", is a poor choice because the API + * createDirectoryAtPath sees the .app extension and creates an application file.) + */ +NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman"; + ++ (KMDataRepository *)shared { + static KMDataRepository *shared = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + shared = [[KMDataRepository alloc] init]; + }); + return shared; +} + +- (NSURL *)documentsSubDirectory { + if (_documentsSubDirectory == nil) { + NSError *directoryError = nil; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *documentsUrl = [fileManager URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&directoryError]; + + if (directoryError) { + os_log_error([KMLogs dataLog], "error getting Documents subdirectory: '%{public}@'", directoryError.localizedDescription); + } else { + os_log_info([KMLogs dataLog], "Documents subdirectory: '%{public}@'", documentsUrl); + _documentsSubDirectory = documentsUrl; + } + } + return _documentsSubDirectory; +} + +- (NSURL *)applicationSupportSubDirectory { + if (_applicationSupportSubDirectory == nil) { + NSError *directoryError = nil; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *applicationSupportUrl = [fileManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&directoryError]; + + if (directoryError) { + os_log_error([KMLogs dataLog], "error getting Application Support subdirectory: '%{public}@'", directoryError.localizedDescription); + } else { + os_log_info([KMLogs dataLog], "Application Support subdirectory: '%{public}@'", applicationSupportUrl.path); + _applicationSupportSubDirectory = applicationSupportUrl; + } + } + return _applicationSupportSubDirectory; +} + +- (NSURL *)keymanDataDirectory { + if (_keymanDataDirectory == nil) { + NSURL *keymanDataUrl = [self.applicationSupportSubDirectory URLByAppendingPathComponent:kKeymanSubdirectoryName isDirectory: TRUE]; + _keymanDataDirectory = keymanDataUrl; + } + return _keymanDataDirectory; +} + +- (NSURL *)keymanKeyboardsDirectory { + if (_keymanKeyboardsDirectory == nil) { + NSURL *keyboardsUrl = [self.keymanDataDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE]; + _keymanKeyboardsDirectory = keyboardsUrl; + } + return _keymanKeyboardsDirectory; +} + +/** + * Creates Keyman data directory if it do not exist yet. This is the main data subdirectory: keyman.inputmethod.Keyman + */ +- (void)createDataDirectoryIfNecessary { + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL isDir; + BOOL exists = [fileManager fileExistsAtPath:self.keymanDataDirectory.path isDirectory:&isDir]; + + if (!exists) { + NSError *createError = nil; + os_log_info([KMLogs dataLog], "createDataDirectoryIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keymanDataDirectory.path); + [fileManager createDirectoryAtPath:self.keymanDataDirectory.path withIntermediateDirectories:YES attributes:nil error:nil]; + if (createError) { + os_log_error([KMLogs dataLog], "error creating Keyman data directory: '%{public}@'", createError.localizedDescription); + } else { + os_log_info([KMLogs dataLog], "created Keyman data directory: '%{public}@'", self.keymanDataDirectory.path); + } + } else { + os_log_info([KMLogs dataLog], "Keyman data directory already exists: '%{public}@'", self.keymanDataDirectory.path); + } +} + +/** + * Creates Keyman keyboard directory if it does not exist yet. This is the 'Keyman-Keyboards' directory. + * It should not be created until after migrating because its existence would block migrating data from the old location. + */ +- (void)createKeyboardsDirectoryIfNecessary { + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL isDir; + BOOL exists = [fileManager fileExistsAtPath:self.keymanKeyboardsDirectory.path isDirectory:&isDir]; + + if (!exists) { + NSError *createError = nil; + os_log_info([KMLogs dataLog], "createKeyboardsDirectoryIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keymanKeyboardsDirectory.path); + [fileManager createDirectoryAtPath:self.keymanKeyboardsDirectory.path withIntermediateDirectories:YES attributes:nil error:nil]; + if (createError) { + os_log_error([KMLogs dataLog], "error creating Keyman-Keyboards directory: '%{public}@'", createError.localizedDescription); + } else { + os_log_info([KMLogs dataLog], "created Keyman-Keyboards subdirectory: '%{public}@'", self.keymanKeyboardsDirectory.path); + } + } else { + os_log_info([KMLogs dataLog], "Keyman-Keyboards already exists: '%{public}@'", self.keymanKeyboardsDirectory.path); + } +} + +- (NSURL *)obsoleteKeymanKeyboardsDirectory { + if (_obsoleteKeymanKeyboardsDirectory == nil) { + NSURL *keymanUrl = [self.documentsSubDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE]; + _obsoleteKeymanKeyboardsDirectory = keymanUrl; + } + return _obsoleteKeymanKeyboardsDirectory; +} + +/** + * Only called from migrateData. + * Causes user to be prompted for permission to access ~/Documents, but they should already have it. + * otherwise we would not be attempting to migrate. + */ +- (BOOL)keyboardsExistInObsoleteDirectory { + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL isDir; + BOOL exists = ([fileManager fileExistsAtPath:self.obsoleteKeymanKeyboardsDirectory.path isDirectory:&isDir]); + return exists; +} + +/** + * Migrate the keyboards data from the old location in '~/Documents' to the new location '~/Application Support/keyman.inputmethod.Keyman/' + * This should only be called if the Keyman settings written to the UserDefaults indicates that we have data in the old location. + * If this is the case, then we expect the user to have already granted permission for Keyman to access the ~/Documents directory. + * If that permission has been removed for some reason, then calling this code will cause the user to be asked for permission again. + */ +- (BOOL)migrateData { + BOOL didMoveData = NO; + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL dataExistsInOldLocation = [self keyboardsExistInObsoleteDirectory]; + os_log_debug([KMLogs dataLog], "obsolete keyman keyboards directory exists: %@", dataExistsInOldLocation?@"YES":@"NO"); + + // only move data if there is something to move + if (dataExistsInOldLocation) { + NSError *moveError = nil; + didMoveData = [fileManager moveItemAtURL:self.obsoleteKeymanKeyboardsDirectory + toURL:self.keymanKeyboardsDirectory + error:&moveError]; + if (moveError) { + os_log_error([KMLogs dataLog], "data migration failed: '%{public}@'", moveError.localizedDescription); + } else { + os_log_info([KMLogs dataLog], "data migrated successfully to: '%{public}@'", self.keymanKeyboardsDirectory.path); + } + } + + return didMoveData; +} + +- (NSString*)buildFullPath:(NSString *)fromPartialPath { + NSString *fullPath = [self.keymanKeyboardsDirectory.path stringByAppendingString:fromPartialPath]; + os_log_debug([KMLogs dataLog], "buildFullPath: '%{public}@' fromPartialPath '%{public}@'", + fullPath, fromPartialPath); + return fullPath; +} + +- (NSString *)trimToPartialPath:(NSString *)fromFullPath { + NSString *partialPath = fromFullPath; + if(fromFullPath != nil) { + NSRange range = [fromFullPath rangeOfString:kKeyboardsDirectoryName]; + if (range.length > 0) { + partialPath = [fromFullPath substringFromIndex:range.location + range.length]; + os_log_debug([KMLogs dataLog], "trimToPartialPath: fromFullPath: '%{public}@' to partialPath: '%{public}@'", fromFullPath, partialPath); + } + } + return partialPath; +} + +- (NSString *)buildPartialPathFrom:(NSString *)keyboardSubdirectory keyboardFile:(NSString *)kmxFilename { + NSMutableArray *pathComponents = [[NSMutableArray alloc] initWithCapacity:0]; + [pathComponents addObject:@"/"]; + [pathComponents addObject:keyboardSubdirectory]; + [pathComponents addObject:kmxFilename]; + NSString *keyboardPartialPath = [NSString pathWithComponents:pathComponents]; + os_log_debug([KMLogs keyboardLog], "buildPartialPathFrom, keyboardSubdirectory: %{public}@, kmxFileName: %{public}@, keyboardPartialPath : %{public}@", + keyboardSubdirectory, kmxFilename, keyboardPartialPath); + return keyboardPartialPath; +} + + +@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/KMInfoWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/KMInfoWindowController.m index 6be446ddd28..32a8377b64d 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/KMInfoWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/KMInfoWindowController.m @@ -85,8 +85,6 @@ - (BOOL)tabView:(NSTabView *)tabView shouldSelectTabViewItem:(NSTabViewItem *)ta // TODO: refactor: any reason for this to be HTML? hard to read stringWithFormat applied to template with 16 arguments - (NSString *)detailsHtml { - NSString *errorString = NSLocalizedString(@"message-keyboard-file-unreadable", nil); - @try { NSString *htmlFormat = @"" diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/cs.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/cs.lproj/KMInfoWindowController.strings index f3fe6a722b1..4c9f26c0f84 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/cs.lproj/KMInfoWindowController.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/cs.lproj/KMInfoWindowController.strings @@ -1,5 +1,5 @@ /* Package Information window title */ -"F0z-JX-Cv5.title" = "Package Information"; +"F0z-JX-Cv5.title" = "Informace o balíčku"; /* button text to show Details */ "Fvy-XJ-s38.label" = "Detaily"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/el.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/el.lproj/KMInfoWindowController.strings new file mode 100644 index 00000000000..af78cc7f5f8 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/el.lproj/KMInfoWindowController.strings @@ -0,0 +1,8 @@ +/* Package Information window title */ +"F0z-JX-Cv5.title" = "Πληροφορίες πακέτου"; + +/* button text to show Details */ +"Fvy-XJ-s38.label" = "Λεπτομέρειες"; + +/* button text to show Read Me */ +"waA-IW-Qyn.label" = "Read Me"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/it.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/it.lproj/KMInfoWindowController.strings index 5d3902de438..3d2755066ca 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/it.lproj/KMInfoWindowController.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/it.lproj/KMInfoWindowController.strings @@ -1,5 +1,5 @@ /* Package Information window title */ -"F0z-JX-Cv5.title" = "Package Information"; +"F0z-JX-Cv5.title" = "Informazioni pacchetto"; /* button text to show Details */ "Fvy-XJ-s38.label" = "Dettagli"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/pt-PT.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/pt-PT.lproj/KMInfoWindowController.strings index 0efa2434460..65f53f48d5e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/pt-PT.lproj/KMInfoWindowController.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/pt-PT.lproj/KMInfoWindowController.strings @@ -1,5 +1,5 @@ /* Package Information window title */ -"F0z-JX-Cv5.title" = "Package Information"; +"F0z-JX-Cv5.title" = "Informação de pacote"; /* button text to show Details */ "Fvy-XJ-s38.label" = "Detalhes"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index f832abf27fd..e97be3e389c 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -1,39 +1,46 @@ -// -// KMInputController.m -// Keyman4MacIM -// -// Created by Serkan Kurt on 29/01/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Created by Serkan Kurt on 2015-01-29. + * + */ #import "KMInputController.h" #import "KMInputMethodEventHandler.h" #import "KMOSVersion.h" #include /* For kVK_ constants. */ +#import "KMSettingsRepository.h" #import "KMLogs.h" +#import "InputMethodKit/InputMethodKit.h" +#import "KMInputMethodLifecycle.h" + @implementation KMInputController KMInputMethodEventHandler* _eventHandler; -NSMutableArray *servers; -- (KMInputMethodAppDelegate *)AppDelegate { +- (KMInputMethodAppDelegate *)appDelegate { return (KMInputMethodAppDelegate *)[NSApp delegate]; } - (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)inputClient { - os_log_debug([KMLogs lifecycleLog], "Initializing Keyman Input Method for server with bundleID: %{public}@", server.bundle.bundleIdentifier); - + os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getClientApplicationId]); + self = [super initWithServer:server delegate:delegate client:inputClient]; if (self) { - servers = [[NSMutableArray alloc] initWithCapacity:2]; - self.AppDelegate.inputController = self; - if (self.AppDelegate.kvk != nil && self.AppDelegate.alwaysShowOSK) { - [self.AppDelegate showOSK]; - } + self.appDelegate.inputController = self; } + /** + * Register to receive the Deactivated and ChangedClient notification generated from KMInputMethodLifecycle so + * that the eventHandler can be changed. There is no need to receive the Activated notification because + * the InputController does it all it needs to when it receives the ChangedClient. + */ + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil]; + return self; } @@ -59,102 +66,44 @@ - (void)handleBackspace:(NSEvent *)event { } } -- (void)activateServer:(id)sender { - @synchronized(servers) { - [sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"]; - - [self.AppDelegate wakeUpWith:sender]; - [servers addObject:sender]; - - if (_eventHandler != nil) { - [_eventHandler deactivate]; - } - - NSRunningApplication *currApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; - NSString *clientAppId = [currApp bundleIdentifier]; - os_log_debug([KMLogs lifecycleLog], "activateServer, new active app: '%{public}@'", clientAppId); - - _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId client:sender]; - +/** + * The Keyman input method is deactivating because the user chose a different input method: notification from KMInputMethodLifecycle + */ +- (void)inputMethodDeactivated:(NSNotification *)notification { + os_log_debug([KMLogs lifecycleLog], "***KMInputController inputMethodDeactivated, deactivating eventHandler"); + if (_eventHandler != nil) { + [_eventHandler deactivate]; } } -- (void)deactivateServer:(id)sender { - if ([self.AppDelegate debugMode]) { - os_log_debug([KMLogs lifecycleLog], "deactivateServer, sender %{public}@", sender); - } - @synchronized(servers) { - for (int i = 0; i < servers.count; i++) { - if (servers[i] == sender) { - [servers removeObjectAtIndex:i]; - break; - } - } - if (servers.count == 0) { - os_log_debug([KMLogs lifecycleLog], "No known active server for Keyman IM. Starting countdown to sleep..."); - [self performSelector:@selector(timerAction:) withObject:sender afterDelay:0.7]; - } +/** + * The user has switched to a different text input client: notification from KMInputMethodLifecycle + */ +- (void)inputMethodChangedClient:(NSNotification *)notification { + os_log_debug([KMLogs lifecycleLog], "***KMInputController inputMethodChangedClient, deactivating old eventHandler and activating new one"); + if (_eventHandler != nil) { + [_eventHandler deactivate]; } -} + _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getClientApplicationId] client:self.client]; -- (void)timerAction:(id)lastServer { - @synchronized(servers) { - if (servers.count == 0) { - if (_eventHandler != nil) { - [_eventHandler deactivate]; - _eventHandler = nil; - } - [self.AppDelegate sleepFollowingDeactivationOfServer:lastServer]; - } - } } +- (void)activateServer:(id)sender { + [sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"]; + [KMInputMethodLifecycle.shared activateClient:sender]; +} -/* - - (NSDictionary *)modes:(id)sender { - if ([self.AppDelegate debugMode]) - os_log_debug([KMLogs lifecycleLog], "*** Modes ***"); - if (_kmModes == nil) { - NSDictionary *amhMode = [[NSDictionary alloc] initWithObjectsAndKeys:@"keyman.png", kTSInputModeAlternateMenuIconFileKey, - [NSNumber numberWithBool:YES], kTSInputModeDefaultStateKey, - [NSNumber numberWithBool:YES], kTSInputModeIsVisibleKey, - @"A", kTSInputModeKeyEquivalentKey, - [NSNumber numberWithInteger:4608], kTSInputModeKeyEquivalentModifiersKey, - [NSNumber numberWithBool:YES], kTSInputModeDefaultStateKey, - @"keyman.png", kTSInputModeMenuIconFileKey, - @"keyman.png", kTSInputModePaletteIconFileKey, - [NSNumber numberWithBool:YES], kTSInputModePrimaryInScriptKey, - @"smUnicodeScript", kTSInputModeScriptKey, - @"amh", @"TISIntendedLanguage", nil]; - - NSDictionary *hinMode = [[NSDictionary alloc] initWithObjectsAndKeys:@"keyman.png", kTSInputModeAlternateMenuIconFileKey, - [NSNumber numberWithBool:YES], kTSInputModeDefaultStateKey, - [NSNumber numberWithBool:YES], kTSInputModeIsVisibleKey, - @"H", kTSInputModeKeyEquivalentKey, - [NSNumber numberWithInteger:4608], kTSInputModeKeyEquivalentModifiersKey, - [NSNumber numberWithBool:YES], kTSInputModeDefaultStateKey, - @"keyman.png", kTSInputModeMenuIconFileKey, - @"keyman.png", kTSInputModePaletteIconFileKey, - [NSNumber numberWithBool:YES], kTSInputModePrimaryInScriptKey, - @"smUnicodeScript", kTSInputModeScriptKey, - @"hin", @"TISIntendedLanguage", nil]; - - NSDictionary *modeList = [[NSDictionary alloc] initWithObjectsAndKeys:amhMode, @"com.apple.inputmethod.amh", hinMode, @"com.apple.inputmethod.hin", nil]; - NSArray *modeOrder = [[NSArray alloc] initWithObjects:@"com.apple.inputmethod.amh", @"com.apple.inputmethod.hin", nil]; - _kmModes = [[NSDictionary alloc] initWithObjectsAndKeys:modeList, kTSInputModeListKey, - modeOrder, kTSVisibleInputModeOrderedArrayKey, nil]; - } - - return _kmModes; - } - */ +- (void)deactivateServer:(id)sender { + [KMInputMethodLifecycle.shared deactivateClient:sender]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} - (NSMenu *)menu { - return self.AppDelegate.menu; + return self.appDelegate.menu; } - (KMXFile *)kmx { - return self.AppDelegate.kmx; + return self.appDelegate.kmx; } - (void)menuAction:(id)sender { @@ -165,13 +114,15 @@ - (void)menuAction:(id)sender { [self showConfigurationWindow:sender]; } else if (itag == OSK_MENUITEM_TAG) { - [self.AppDelegate showOSK]; + [KMSettingsRepository.shared writeShowOskOnActivate:YES]; + os_log_debug([KMLogs oskLog], "menuAction OSK_MENUITEM_TAG, updating settings writeShowOsk to YES"); + [self.appDelegate showOSK]; } else if (itag == ABOUT_MENUITEM_TAG) { - [self.AppDelegate showAboutWindow]; + [self.appDelegate showAboutWindow]; } else if (itag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) { - [self.AppDelegate selectKeyboardFromMenu:itag]; + [self.appDelegate selectKeyboardFromMenu:itag]; } } @@ -183,7 +134,7 @@ - (void)showConfigurationWindow:(id)sender { if ([KMOSVersion Version_10_13_1] <= systemVersion && systemVersion <= [KMOSVersion Version_10_13_3]) // between 10.13.1 and 10.13.3 inclusive { os_log_info([KMLogs uiLog], "Input Menu: calling workaround instead of showPreferences (sys ver %x)", systemVersion); - [self.AppDelegate showConfigurationWindow]; // call our workaround + [self.appDelegate showConfigurationWindow]; // call our workaround } else { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h index 4bf463c177f..667e630f3cd 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h @@ -23,8 +23,6 @@ typedef void(^PostEventCallback)(CGEventRef eventToPost); -extern NSString *const kKMSelectedKeyboardKey; -extern NSString *const kKMActiveKeyboardsKey; extern NSString *const kKeymanKeyboardDownloadCompletedNotification; typedef struct { @@ -75,7 +73,6 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; @property (nonatomic, assign) NSEventModifierFlags currentModifierFlags; @property (nonatomic, assign) CFMachPortRef lowLevelEventTap; @property (nonatomic, assign) CFRunLoopSourceRef runLoopEventSrc; -@property (nonatomic, assign) BOOL sleeping; @property (nonatomic, assign) BOOL contextChangedByLowLevelEvent; @property (nonatomic, strong) OSKWindowController *oskWindow; @property (nonatomic, strong) NSString *keyboardName; @@ -92,21 +89,15 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; @property (nonatomic, strong) NSString *downloadFilename; @property (nonatomic, strong) NSMutableData *receivedData; @property (nonatomic, assign) NSUInteger expectedBytes; -@property (nonatomic, assign) BOOL alwaysShowOSK; -@property (nonatomic, assign) BOOL useVerboseLogging; @property (nonatomic, assign) BOOL useNullChar; -@property (nonatomic, assign) BOOL debugMode; - (NSMenu *)menu; - (void)saveActiveKeyboards; -- (void)readPersistedOptions; -- (void)writePersistedOptions:(NSString *)storeKey withValue:(NSString* )value; +- (void)applyPersistedOptions; - (void)showAboutWindow; - (void)showOSK; - (void)showConfigurationWindow; - (void)selectKeyboardFromMenu:(NSInteger)tag; -- (void)sleepFollowingDeactivationOfServer:(id)lastServer; -- (void)wakeUpWith:(id)newServer; - (void)handleKeyEvent:(NSEvent *)event; - (BOOL)unzipFile:(NSString *)filePath; - (NSWindowController *)downloadKBWindow_; @@ -126,7 +117,6 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; - (NSString *)oskWindowTitle; - (void)postKeyboardEventWithSource: (CGEventSourceRef)source code:(CGKeyCode) virtualKey postCallback:(PostEventCallback)postEvent; - (KeymanVersionInfo)versionInfo; -- (NSString *)keymanDataPath; - (void)registerConfigurationWindow:(NSWindowController *)window; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 0675a78adec..199721f8be3 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -6,16 +6,10 @@ // Copyright (c) 2017 SIL International. All rights reserved. // -// *** TO INVESTIGATE *** -// Keyman4MacIM[6245]: IMK Stall detected, *please Report* your user scenario in - (activateServerWithReply:) block performed very slowly (0.00 secs) -// Keyman4MacIM[6245]: IMK Stall detected, *please Report* your user scenario in - (deactivateServerWithReply:) block performed very slowly (0.00 secs) -// Keyman4MacIM[6245]: IMK Stall detected, *please Report* your user scenario in - (menusDictionaryWithClientAsync:reply:) block performed very slowly (0.00 secs) -// Keyman4MacIM[6245]: IMK Stall detected, *please Report* your user scenario in - (modesWithClientAsync:reply:) block performed very slowly (0.00 secs) -// Keyman4MacIM[6245]: IMK Stall detected, *please Report* your user scenario in - (commitCompositionWithReply:) block performed very slowly (0.00 secs) -// Keyman4MacIM[6245]: IMK Stall detected, *please Report* your user scenario in - (hidePalettes) block performed very slowly (0.00 secs) -// Keyman4MacIM[6245]: IMK Stall detected, *please Report* your user scenario in - (sessionFinished) block performed very slowly (0.00 secs) - #import "KMInputMethodAppDelegate.h" +#import "KMInputMethodLifecycle.h" +#import "KMSettingsRepository.h" +#import "KMDataRepository.h" #import "KMConfigurationWindowController.h" #import "KMDownloadKBWindowController.h" #import "ZipArchive.h" @@ -26,21 +20,6 @@ #import "KMLogs.h" @import Sentry; -/** NSUserDefaults keys */ -NSString *const kKMSelectedKeyboardKey = @"KMSelectedKeyboardKey"; -NSString *const kKMActiveKeyboardsKey = @"KMActiveKeyboardsKey"; -/** - The following constant "KMSavedStoresKey" is left here for documentation - though we have abandoned stores written to UserDefaults with this key because - they used a less-reliable numeric key prior to integration with Keyman Core. - It is replaced by the renamed "KMPersistedOptionsKey" which directly - represents what it is saving. - */ -NSString *const kKMDeprecatedPersistedOptionsKey = @"KMSavedStoresKey"; -NSString *const kKMPersistedOptionsKey = @"KMPersistedOptionsKey"; -NSString *const kKMAlwaysShowOSKKey = @"KMAlwaysShowOSKKey"; -NSString *const kKMUseVerboseLogging = @"KMUseVerboseLogging"; - NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification"; @implementation NSString (VersionNumbers) @@ -80,23 +59,12 @@ @implementation KMInputMethodAppDelegate @synthesize selectedKeyboard = _selectedKeyboard; @synthesize activeKeyboards = _activeKeyboards; @synthesize contextBuffer = _contextBuffer; -@synthesize alwaysShowOSK = _alwaysShowOSK; id _lastServerWithOSKShowing = nil; -NSString* _keymanDataPath = nil; - (id)init { self = [super init]; - if (self) { -#ifdef DEBUG - // If debugging, we'll turn it on by default, regardless of setting. If developer - // really wants it off, they can either change this line of code temporarily or - // go to the config screen and turn it off explicitly. - _debugMode = YES; -#else - _debugMode = self.useVerboseLogging; -#endif - + if (self) { // first notify user and request access to Accessibility/PostEvent permissions // pass block as completion handler to complete init with initCompletion [PrivacyConsent.shared requestPrivacyAccess:^void (void){ @@ -139,6 +107,45 @@ - (void)initCompletion { if (self.runLoopEventSrc && runLoop) { CFRunLoopAddSource(runLoop, self.runLoopEventSrc, kCFRunLoopDefaultMode); } + + // register to receive notifications generated from KMInputMethodLifecycle + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodActivated:) name:kInputMethodActivatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil]; + + // start Input Method lifecycle + [KMInputMethodLifecycle.shared startLifecycle]; +} + +/** + * When the input method is deactivated, hide the OSK and disable the low-level event tap + */ +- (void)inputMethodDeactivated:(NSNotification *)notification { + if ([self.oskWindow.window isVisible]) { + os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK"); + [self.oskWindow.window setIsVisible:NO]; + } else { + os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, OSK already hidden"); + } + + if (self.lowLevelEventTap) { + os_log_debug([KMLogs lifecycleLog], "***inputMethodDeactivated, disabling event tap"); + CGEventTapEnable(self.lowLevelEventTap, NO); + } +} + +/** + * When the input method is activated, show the OSK and enable the low-level event tap + */ +- (void)inputMethodActivated:(NSNotification *)notification { + if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) { + os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap..."); + CGEventTapEnable(self.lowLevelEventTap, YES); + } + + if (_kvk != nil && ([KMInputMethodLifecycle.shared shouldShowOskOnActivate])) { + os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK"); + [self showOSK]; + } } - (KeymanVersionInfo)versionInfo { @@ -167,19 +174,37 @@ - (KeymanVersionInfo)versionInfo { -(void)applicationDidFinishLaunching:(NSNotification *)aNotification { [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @YES }]; + [KMLogs reportLogStatus]; + [self startSentry]; + [self setDefaultKeymanMenuItems]; + [self updateKeyboardMenuItems]; + [self setPostLaunchKeymanSentryTags]; + // [SentrySDK captureMessage:@"Starting Keyman [test message]"]; +} + +- (void)startSentry { KeymanVersionInfo keymanVersionInfo = [self versionInfo]; NSString *releaseName = [NSString stringWithFormat:@"%@", keymanVersionInfo.versionGitTag]; - [KMLogs reportLogStatus]; - [SentrySDK startWithConfigureOptions:^(SentryOptions *options) { options.dsn = @"https://960f8b8e574c46e3be385d60ce8e1fea@o1005580.ingest.sentry.io/5983522"; options.releaseName = releaseName; options.environment = keymanVersionInfo.sentryEnvironment; - // options.debug = @YES; + //options.debug = YES; }]; - - // [SentrySDK captureMessage:@"Starting Keyman [test message]"]; +} + +- (void)setPostLaunchKeymanSentryTags { + NSString *keyboardFileName = [self.kmx.filePath lastPathComponent]; + os_log_info([KMLogs keyboardLog], "initial kmx set to %{public}@", keyboardFileName); + + NSString *hasAccessibility = [PrivacyConsent.shared checkAccessibility]?@"Yes":@"No"; + + // assign custom keyboard tag in Sentry to initial keyboard + [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { + [scope setTagValue:hasAccessibility forKey:@"accessibilityEnabled"]; + [scope setTagValue:keyboardFileName forKey:@"keyboard"]; + }]; } #ifdef USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE @@ -227,43 +252,12 @@ + (KMInputMethodAppDelegate *)AppDelegate { return (KMInputMethodAppDelegate *)[NSApp delegate]; } --(void) sleepFollowingDeactivationOfServer:(id)lastServer { - os_log_debug([KMLogs lifecycleLog], "Keyman no longer active IM."); - self.sleeping = YES; - if ([self.oskWindow.window isVisible]) { - os_log_debug([KMLogs oskLog], "sleepFollowingDeactivationOfServer, Hiding OSK."); - // Storing this ensures that if the deactivation is temporary, resulting from dropping down a menu, - // the OSK will re-display when that client application re-activates. - _lastServerWithOSKShowing = lastServer; - [self.oskWindow.window setIsVisible:NO]; - } - if (self.lowLevelEventTap) { - os_log_debug([KMLogs lifecycleLog], "sleepFollowingDeactivationOfServer, disabling event tap..."); - CGEventTapEnable(self.lowLevelEventTap, NO); - } -} - --(void) wakeUpWith:(id)newServer { - self.sleeping = NO; - if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) { - os_log_debug([KMLogs lifecycleLog], "wakeUpWith, Keyman is now the active IM. Re-enabling event tap..."); - CGEventTapEnable(self.lowLevelEventTap, YES); - } - // See note in sleepFollowingDeactivationOfServer. - if (_kvk != nil && (_alwaysShowOSK || _lastServerWithOSKShowing == newServer)) { - [self showOSK]; - } - - _lastServerWithOSKShowing = nil; -} - CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { KMInputMethodAppDelegate *appDelegate = [KMInputMethodAppDelegate AppDelegate]; if (appDelegate != nil) { if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) { - // kCGEventTapDisabledByUserInput most likely means we're "sleeping", in which case we want it to stay - // disabled until we get the wake-up call. - if (!appDelegate.sleeping) { + // kCGEventTapDisabledByUserInput most likely means we're "sleeping", in which case we want it to stay disabled until we get the wake-up call. + if ([KMInputMethodLifecycle.shared shouldEnableEventTap]) { // REVIEW: We might need to consider putting in some kind of counter/flag to ensure that the very next // event is not another disable so we don't end up in an endless cycle. os_log([KMLogs eventsLog], "Event tap disabled by %{public}@! Attempting to restart...", (type == kCGEventTapDisabledByTimeout ? @"timeout" : @"user")); @@ -381,7 +375,7 @@ - (NSMenu *)menu { - (KMEngine *)kme { if (_kme == nil) { - _kme = [[KMEngine alloc] initWithKMX:nil context:self.contextBuffer verboseLogging:self.debugMode]; + _kme = [[KMEngine alloc] initWithKMX:nil context:self.contextBuffer]; } return _kme; @@ -390,7 +384,6 @@ - (KMEngine *)kme { - (KMPackageReader *)packageReader { if (_packageReader == nil) { _packageReader = [[KMPackageReader alloc] init]; - [_packageReader setDebugMode:self.debugMode]; } return _packageReader; @@ -399,6 +392,14 @@ - (KMPackageReader *)packageReader { - (void)setKmx:(KMXFile *)kmx { _kmx = kmx; [self.kme setKmx:_kmx]; + + NSString *keyboardFileName = [kmx.filePath lastPathComponent]; + os_log_info([KMLogs keyboardLog], "setKmx to %{public}@", keyboardFileName); + + // assign custom keyboard tag in Sentry to default keyboard + [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { + [scope setTagValue:keyboardFileName forKey:@"keyboard"]; + }]; } - (void)setKvk:(KVKFile *)kvk { @@ -413,55 +414,17 @@ - (void)setKeyboardName:(NSString *)keyboardName { [_oskWindow.window setTitle:self.oskWindowTitle]; } -- (void)readPersistedOptions { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - NSDictionary *allPersistedOptions = [userData dictionaryForKey:kKMPersistedOptionsKey]; - if (!allPersistedOptions) { - return; - } - NSDictionary *persistedOptionsForSelectedKeyboard = [allPersistedOptions objectForKey:_selectedKeyboard]; - if (!persistedOptionsForSelectedKeyboard) { - os_log_info([KMLogs configLog], "no persisted options found in UserDefaults for keyboard %{public}@ ", _selectedKeyboard); - return; - } +- (void)applyPersistedOptions { + NSDictionary *selectedPersistedOptions = [[KMSettingsRepository shared] readOptionsForSelectedKeyboard]; // TODO: pass array instead of making repeated calls - for (NSString *key in persistedOptionsForSelectedKeyboard) { - NSString *value = [persistedOptionsForSelectedKeyboard objectForKey:key]; + for (NSString *key in selectedPersistedOptions) { + NSString *value = [selectedPersistedOptions objectForKey:key]; os_log_info([KMLogs configLog], "persisted options found in UserDefaults for keyboard %{public}@, key: %{public}@, value: %{public}@", _selectedKeyboard, key, value); [self.kme setCoreOptions:key withValue:value]; } } -- (void)writePersistedOptions:(NSString *)storeKey withValue:(NSString* )value { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - NSDictionary *allPersistedOptions = [userData dictionaryForKey:kKMPersistedOptionsKey]; - NSDictionary *persistedOptionsForSelectedKeyboard; - - if (allPersistedOptions) { - persistedOptionsForSelectedKeyboard = [allPersistedOptions objectForKey:_selectedKeyboard]; - } - - if (persistedOptionsForSelectedKeyboard) { - NSMutableDictionary *newSavedStores = [persistedOptionsForSelectedKeyboard mutableCopy]; - [newSavedStores setObject:value forKey:storeKey]; - persistedOptionsForSelectedKeyboard = newSavedStores; - } else { - persistedOptionsForSelectedKeyboard = [[NSDictionary alloc] initWithObjectsAndKeys:value, storeKey, nil]; - } - - if (allPersistedOptions) { - NSMutableDictionary *newAllSavedStores = [allPersistedOptions mutableCopy]; - [newAllSavedStores setObject:persistedOptionsForSelectedKeyboard forKey:_selectedKeyboard]; - allPersistedOptions = newAllSavedStores; - } else { - allPersistedOptions = [[NSDictionary alloc] initWithObjectsAndKeys:persistedOptionsForSelectedKeyboard, _selectedKeyboard, nil]; - } - - [userData setObject:allPersistedOptions forKey:kKMPersistedOptionsKey]; - [userData synchronize]; -} - - (NSString *)oskWindowTitle { if (_keyboardName == nil || !_keyboardName.length) return [NSString stringWithFormat:@"Keyman"]; @@ -469,62 +432,14 @@ - (NSString *)oskWindowTitle { return [NSString stringWithFormat:@"%@ - Keyman", _keyboardName]; } -- (void)setAlwaysShowOSK:(BOOL)alwaysShowOSK { - _alwaysShowOSK = alwaysShowOSK; - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setBool:alwaysShowOSK forKey:kKMAlwaysShowOSKKey]; - [userData synchronize]; -} - -- (void)setUseVerboseLogging:(BOOL)useVerboseLogging { - os_log_debug([KMLogs configLog], "Turning verbose logging %{public}@", useVerboseLogging ? @"on." : @"off."); - _debugMode = useVerboseLogging; - if (_kme != nil) - [_kme setUseVerboseLogging:useVerboseLogging]; - if (_packageReader != nil) { - [_packageReader setDebugMode:useVerboseLogging]; - } - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setBool:useVerboseLogging forKey:kKMUseVerboseLogging]; - [userData synchronize]; -} - -- (BOOL)alwaysShowOSK { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - _alwaysShowOSK = [userData boolForKey:kKMAlwaysShowOSKKey]; - return _alwaysShowOSK; -} - -- (BOOL)useVerboseLogging { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - return [userData boolForKey:kKMUseVerboseLogging]; -} - #pragma mark - Keyman Data /** - * Locate and create the Keyman data path; currently in ~/Documents/Keyman-Keyboards - */ -- (NSString *)keymanDataPath { - if(_keymanDataPath == nil) { - NSString *documentDirPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; - _keymanDataPath = [documentDirPath stringByAppendingPathComponent:@"Keyman-Keyboards"]; - - NSFileManager *fm = [NSFileManager defaultManager]; - if (![fm fileExistsAtPath:_keymanDataPath]) { - [fm createDirectoryAtPath:_keymanDataPath withIntermediateDirectories:YES attributes:nil error:nil]; - } - } - return _keymanDataPath; -} - -/** - * Returns the root folder where keyboards are stored; currently the same - * as the keymanDataPath, but may diverge in future versions (possibly a sub-folder) + * Returns the root folder where keyboards are stored */ - (NSString *)keyboardsPath { if (_keyboardsPath == nil) { - _keyboardsPath = [self keymanDataPath]; + _keyboardsPath = [KMDataRepository shared].keymanKeyboardsDirectory.path; } return _keyboardsPath; @@ -532,10 +447,12 @@ - (NSString *)keyboardsPath { - (NSArray *)kmxFileList { if (_kmxFileList == nil) { - NSArray *kmxFiles = [self KMXFiles]; + os_log_debug([KMLogs dataLog], "creating kmxFileList"); + NSArray *kmxFiles = [self getKmxFilesInKeyboardsDirectory]; _kmxFileList = [[NSMutableArray alloc] initWithCapacity:0]; NSMutableArray *others = nil; for (NSString *filePath in kmxFiles) { + os_log_debug([KMLogs dataLog], "kmxFileList, filePath: %{public}@", filePath); NSString *packageFolder = [self packageFolderFromPath:filePath]; NSInteger index = [self indexForPackageFolder:packageFolder]; if ([packageFolder isEqualToString:@"Others"]) { @@ -639,7 +556,7 @@ - (KMPackageInfo *)loadPackageInfo:(NSString *)path { - (NSString *)packageNameFromPackageInfo:(NSString *)packageFolder { NSString *packageName = nil; - NSString *path = [[self keymanDataPath] stringByAppendingPathComponent:packageFolder]; + NSString *path = [[self keyboardsPath] stringByAppendingPathComponent:packageFolder]; KMPackageInfo *packageInfo = [self.packageReader loadPackageInfo:path]; if (packageInfo) { @@ -650,8 +567,9 @@ - (NSString *)packageNameFromPackageInfo:(NSString *)packageFolder { } - (NSArray *)keyboardNamesFromFolder:(NSString *)packageFolder { + os_log_debug([KMLogs dataLog], "keyboardNamesFromFolder, folder = %{public}@", packageFolder); NSMutableArray *kbNames = [[NSMutableArray alloc] initWithCapacity:0];; - for (NSString *kmxFile in [self KMXFilesAtPath:packageFolder]) { + for (NSString *kmxFile in [self getKmxFilesAtPath:packageFolder]) { NSDictionary * infoDict = [KMXFile keyboardInfoFromKmxFile:kmxFile]; if (infoDict != nil) { NSString *name = [infoDict objectForKey:kKMKeyboardNameKey]; @@ -666,52 +584,56 @@ - (NSArray *)keyboardNamesFromFolder:(NSString *)packageFolder { - (NSString *)selectedKeyboard { if (_selectedKeyboard == nil) { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - _selectedKeyboard = [userData objectForKey:kKMSelectedKeyboardKey]; + _selectedKeyboard = [[KMSettingsRepository shared] readSelectedKeyboard]; } - + os_log_debug([KMLogs dataLog], "selectedKeyboard = %{public}@", _selectedKeyboard); + return _selectedKeyboard; } - (void)setSelectedKeyboard:(NSString *)selectedKeyboard { + [[KMSettingsRepository shared] writeSelectedKeyboard:selectedKeyboard]; _selectedKeyboard = selectedKeyboard; - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setObject:_selectedKeyboard forKey:kKMSelectedKeyboardKey]; - [userData synchronize]; } - (NSMutableArray *)activeKeyboards { if (!_activeKeyboards) { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - _activeKeyboards = [[userData arrayForKey:kKMActiveKeyboardsKey] mutableCopy]; - if (!_activeKeyboards) - _activeKeyboards = [[NSMutableArray alloc] initWithCapacity:0]; + os_log_debug([KMLogs dataLog], "initializing activeKeyboards"); + _activeKeyboards = [[KMSettingsRepository.shared readActiveKeyboards] mutableCopy]; } - + return _activeKeyboards; } - (void)saveActiveKeyboards { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setObject:_activeKeyboards forKey:kKMActiveKeyboardsKey]; - [userData synchronize]; + os_log_debug([KMLogs dataLog], "saveActiveKeyboards"); + [KMSettingsRepository.shared writeActiveKeyboards:_activeKeyboards]; [self resetActiveKeyboards]; [self updateKeyboardMenuItems]; } - (void)clearActiveKeyboards { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setObject:nil forKey:kKMActiveKeyboardsKey]; - [userData synchronize]; + [KMSettingsRepository.shared clearActiveKeyboards]; [self updateKeyboardMenuItems]; } +- (void)addActiveKeyboard:(NSString *) partialPath { + if (![self.activeKeyboards containsObject:partialPath]) { + os_log_debug([KMLogs keyboardLog], "addActiveKeyboard, adding '%{public}@' to list of active keyboards: ", partialPath); + [self.activeKeyboards addObject:partialPath]; + } +} + - (void)resetActiveKeyboards { // Remove entries with missing files NSMutableArray *pathsToRemove = [[NSMutableArray alloc] initWithCapacity:0]; for (NSString *path in self.activeKeyboards) { - if (![[NSFileManager defaultManager] fileExistsAtPath:path]) + NSString *fullPath = [KMDataRepository.shared buildFullPath:path]; + os_log_debug([KMLogs dataLog], "resetActiveKeyboards, checking fullPath: '%{public}@' for path: '%{public}@'", fullPath, path); + if (![[NSFileManager defaultManager] fileExistsAtPath:fullPath]) { + os_log_debug([KMLogs dataLog], "resetActiveKeyboards, need to remove non-existent path: %{public}@", fullPath); [pathsToRemove addObject:path]; + } } BOOL found = FALSE; @@ -734,9 +656,7 @@ - (void)resetActiveKeyboards { } if (found) { - NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setObject:_activeKeyboards forKey:kKMActiveKeyboardsKey]; - [userData synchronize]; + [KMSettingsRepository.shared writeActiveKeyboards:_activeKeyboards]; } } @@ -756,8 +676,23 @@ - (void)setContextBuffer:(NSMutableString *)contextBuffer { } - (void)awakeFromNib { - [self setDefaultKeymanMenuItems]; - [self updateKeyboardMenuItems]; + [self prepareStorage]; +} + +/** + * Prepare the app environment for all the things that need to be persisted: + * namely, the keyboard data on disk and the settings in UserDefaults + */ +- (void)prepareStorage { + [KMDataRepository.shared createDataDirectoryIfNecessary]; + + if ([KMSettingsRepository.shared dataMigrationNeeded]) { + [KMDataRepository.shared migrateData]; + [KMSettingsRepository.shared convertSettingsForMigration]; + } + + [KMDataRepository.shared createKeyboardsDirectoryIfNecessary]; + [KMSettingsRepository.shared setDataModelVersionIfNecessary]; } - (void)setDefaultKeymanMenuItems { @@ -784,6 +719,7 @@ - (void)updateKeyboardMenuItems { } - (int)calculateNumberOfKeyboardMenuItems { + os_log_debug([KMLogs uiLog], "calculateNumberOfKeyboardMenuItems, entered"); if (self.activeKeyboards.count == 0) { // if there are no active keyboards, then we will insert one placeholder menu item 'No Active Keyboards' return 1; @@ -805,18 +741,19 @@ - (void)removeDynamicKeyboardMenuItems { } - (void)addDynamicKeyboardMenuItems { + os_log_debug([KMLogs startupLog], "addDynamicKeyboardMenuItems, entered"); BOOL didSetSelectedKeyboard = NO; NSInteger itag = KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG; NSString *keyboardMenuName = @""; int menuItemIndex = KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX; - if (self.debugMode) { - os_log_info([KMLogs configLog], "*** populateKeyboardMenuItems, number of active keyboards=%lu", self.activeKeyboards.count); - } + os_log_debug([KMLogs configLog], "*** populateKeyboardMenuItems, number of active keyboards=%lu", self.activeKeyboards.count); // loop through the active keyboards list and add them to the menu for (NSString *path in self.activeKeyboards) { - NSDictionary *infoDict = [KMXFile keyboardInfoFromKmxFile:path]; + NSString *fullPath = [KMDataRepository.shared buildFullPath:path]; + os_log_debug([KMLogs dataLog], "addDynamicKeyboardMenuItems, path = '%{public}@', full path = '%{public}@'", path, fullPath); + NSDictionary *infoDict = [KMXFile keyboardInfoFromKmxFile:fullPath]; if (!infoDict) { continue; } @@ -846,10 +783,12 @@ - (void)addDynamicKeyboardMenuItems { - (void) setSelectedKeyboard:(NSString*)keyboardName inMenuItem:(NSMenuItem*) menuItem { KVKFile *kvk = nil; + NSString *fullPath = [KMDataRepository.shared buildFullPath:keyboardName]; + os_log_debug([KMLogs dataLog], "setSelectedKeyboard, keyboardName = '%{public}@', full path = '%{public}@'", keyboardName, fullPath); [menuItem setState:NSOnState]; - KMXFile *kmx = [[KMXFile alloc] initWithFilePath:keyboardName]; + KMXFile *kmx = [[KMXFile alloc] initWithFilePath:fullPath]; [self setKmx:kmx]; - NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:keyboardName]; + NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:fullPath]; NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; if (kvkFilename != nil) { NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename]; @@ -860,7 +799,7 @@ - (void) setSelectedKeyboard:(NSString*)keyboardName inMenuItem:(NSMenuItem*) me [self setKvk:kvk]; [self setKeyboardName:[kmxInfo objectForKey:kKMKeyboardNameKey]]; [self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; - [self readPersistedOptions]; + [self applyPersistedOptions]; } // defaults to the whatever keyboard happens to be first in the list @@ -901,10 +840,13 @@ - (void)selectKeyboardFromMenu:(NSInteger)tag { } NSString *path = [self.activeKeyboards objectAtIndex:tag-KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG]; - KMXFile *kmx = [[KMXFile alloc] initWithFilePath:path]; + NSString *fullPath = [KMDataRepository.shared buildFullPath:path]; + os_log_debug([KMLogs dataLog], "setSelectedKeyboard, keyboardName = '%{public}@', full path = '%{public}@'", path, fullPath); + + KMXFile *kmx = [[KMXFile alloc] initWithFilePath:fullPath]; [self setKmx:kmx]; KVKFile *kvk = nil; - NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:path]; + NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:fullPath]; NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey]; if (kvkFilename != nil) { NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename]; @@ -918,45 +860,46 @@ - (void)selectKeyboardFromMenu:(NSInteger)tag { [self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]]; [self setContextBuffer:nil]; [self setSelectedKeyboard:path]; - [self readPersistedOptions]; - if (kvk != nil && self.alwaysShowOSK) - [self showOSK]; + [self applyPersistedOptions]; } -- (NSArray *)KMXFiles { - return [self KMXFilesAtPath:self.keyboardsPath]; +- (NSArray *)getKmxFilesInKeyboardsDirectory { + return [self getKmxFilesAtPath:self.keyboardsPath]; } -- (NSArray *)KMXFilesAtPath:(NSString *)path { +- (NSArray *)getKmxFilesAtPath:(NSString *)path { + os_log_debug([KMLogs dataLog], "getKmxFilesAtPath, path: '%{public}@'", path); NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:path]; NSMutableArray *kmxFiles = [[NSMutableArray alloc] initWithCapacity:0]; NSString *filePath; while (filePath = (NSString *)[dirEnum nextObject]) { NSString *extension = [[filePath pathExtension] lowercaseString]; - if ([extension isEqualToString:@"kmx"]) + if ([extension isEqualToString:@"kmx"]) { [kmxFiles addObject:[path stringByAppendingPathComponent:filePath]]; + os_log_debug([KMLogs dataLog], "file = %{public}@", filePath); + } } return kmxFiles; } -- (NSArray *)KVKFiles { +- (NSArray *)getKvkFilesArray { NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:self.keyboardsPath]; - NSMutableArray *kvkFiles = [[NSMutableArray alloc] initWithCapacity:0]; + NSMutableArray *kvkFilesArray = [[NSMutableArray alloc] initWithCapacity:0]; NSString *filePath; while (filePath = (NSString *)[dirEnum nextObject]) { NSString *extension = [[filePath pathExtension] lowercaseString]; if ([extension isEqualToString:@"kvk"]) - [kvkFiles addObject:[self.keyboardsPath stringByAppendingPathComponent:filePath]]; + [kvkFilesArray addObject:[self.keyboardsPath stringByAppendingPathComponent:filePath]]; } - return kvkFiles; + return kvkFilesArray; } - (NSString *)kvkFilePathFromFilename:(NSString *)kvkFilename { NSString *kvkFilePath = nil; - NSArray *kvkFiles = [self KVKFiles]; - for (NSString *filePath in kvkFiles) { + NSArray *kvkFilesArray = [self getKvkFilesArray]; + for (NSString *filePath in kvkFilesArray) { if ([[filePath lastPathComponent] isEqualToString:kvkFilename]) { kvkFilePath = filePath; break; @@ -1012,6 +955,7 @@ - (NSWindowController *)configWindow { return _configWindow; } +// TODO: rewrite confusing pattern, multiple methods differing only by underscore - (NSWindowController *)aboutWindow_ { return _aboutWindow; } @@ -1094,14 +1038,19 @@ - (void)windowWillClose:(NSNotification *)notification { * TODO: this should really be refactored */ -- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { - NSButton *button = (NSButton *)[alert.buttons objectAtIndex:0]; - if (button.tag == -1) { +- (void)downloadComplete:(NSModalResponse) returnCode { + os_log_debug([KMLogs uiLog], "downloadComplete, NSModalResponse returnCode: %ld", (long)returnCode); + if (returnCode == NSModalResponseCancel) { + os_log_debug([KMLogs uiLog], "downloadComplete, returnCode == NSModalResponseCancel"); [_connection cancel]; } - else if (button.tag == 1) { + else if (returnCode == NSModalResponseOK) { + os_log_debug([KMLogs uiLog], "downloadComplete, returnCode == NSModalResponseOK"); + [_downloadKBWindow close]; + if (self.configWindow.window != nil) { + os_log_debug([KMLogs uiLog], "downloadComplete, self.configWindow.window != nil"); [self.configWindow.window makeKeyAndOrderFront:nil]; if (![[self.configWindow.window childWindows] containsObject:self.infoWindow.window]) { [self.configWindow.window addChildWindow:self.infoWindow.window ordered:NSWindowAbove]; @@ -1110,6 +1059,7 @@ - (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInf [self.infoWindow.window makeKeyAndOrderFront:nil]; } else { + os_log_debug([KMLogs uiLog], "downloadComplete, self.configWindow.window == nil"); [self.infoWindow.window centerInParent]; [self.infoWindow.window makeKeyAndOrderFront:nil]; [self.infoWindow.window setLevel:NSFloatingWindowLevel]; @@ -1127,6 +1077,7 @@ - (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInf } - (NSAlert *)downloadInfoView { + os_log_debug([KMLogs uiLog], "downloadInfoView"); if (_downloadInfoView == nil) { _downloadInfoView = [[NSAlert alloc] init]; [_downloadInfoView setMessageText:NSLocalizedString(@"message-keyboard-downloading", nil)]; @@ -1140,6 +1091,7 @@ - (NSAlert *)downloadInfoView { } - (NSProgressIndicator *)progressIndicator { + os_log_debug([KMLogs uiLog], "progressIndicator"); if (_progressIndicator == nil) { _progressIndicator = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(0, 0, 300, 20)]; [_progressIndicator setIndeterminate:NO]; @@ -1159,38 +1111,51 @@ - (void)downloadKeyboardFromKeyboardId:(NSString *)keyboardId { [self downloadKeyboardFromURL:url]; } + - (void)downloadKeyboardFromURL:(NSURL *)url { NSURL* downloadUrl = url; - + os_log_debug([KMLogs uiLog], "downloadKeyboardFromURL, url.path: %{public}@", url.path); + if (downloadUrl && _downloadFilename) { if (_infoWindow.window != nil) [_infoWindow close]; [self.downloadInfoView setInformativeText:self.downloadFilename]; + if (self.configWindow.window != nil) { + os_log_debug([KMLogs uiLog], "downloadKeyboardFromURL, self.configWindow.window != nil"); [self.configWindow.window makeKeyAndOrderFront:nil]; if (![[self.configWindow.window childWindows] containsObject:self.downloadKBWindow.window]) { [self.configWindow.window addChildWindow:self.downloadKBWindow.window ordered:NSWindowAbove]; } [self.downloadKBWindow.window centerInParent]; [self.downloadKBWindow.window makeKeyAndOrderFront:nil]; - [self.downloadInfoView beginSheetModalForWindow:self.downloadKBWindow.window - modalDelegate:self - didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) - contextInfo:nil]; + + /* + Open sheet off of config window, not the download window. + This is because, if the download is successful, + the download window will be closed by the sheet. + */ + [self.downloadInfoView beginSheetModalForWindow:self.configWindow.window completionHandler:^(NSModalResponse returnCode) { + [self downloadComplete:returnCode]; + }]; } else { + os_log_debug([KMLogs uiLog], "downloadKeyboardFromURL, self.configWindow.window == nil"); [self.downloadKBWindow.window centerInParent]; [self.downloadKBWindow.window makeKeyAndOrderFront:nil]; [self.downloadKBWindow.window setLevel:NSFloatingWindowLevel]; - [self.downloadInfoView beginSheetModalForWindow:self.downloadKBWindow.window - modalDelegate:self - didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) - contextInfo:nil]; + /* + Open sheet off of config window, same as above. + */ + [self.downloadInfoView beginSheetModalForWindow:self.configWindow.window completionHandler:^(NSModalResponse returnCode) { + [self downloadComplete:returnCode]; + }]; } if (_connection == nil) { - [_downloadInfoView setMessageText:NSLocalizedString(@"message-keyboard-downloading", nil)]; + os_log_debug([KMLogs uiLog], "downloadKeyboardFromURL, _connection == nil, set button title cancel downloading, tag = -1"); + [_downloadInfoView setMessageText:NSLocalizedString(@"message-keyboard-downloading", nil)]; NSButton *button = (NSButton *)[_downloadInfoView.buttons objectAtIndex:0]; [button setTitle:NSLocalizedString(@"button-cancel-downloading", nil)]; [button setTag:-1]; @@ -1199,7 +1164,6 @@ - (void)downloadKeyboardFromURL:(NSURL *)url { _receivedData = [[NSMutableData alloc] initWithLength:0]; _connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]; } - } } @@ -1232,6 +1196,7 @@ - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheRespo } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { + os_log_debug([KMLogs uiLog], "connectionDidFinishLoading"); NSString *filePath = [self.keyboardsPath stringByAppendingPathComponent:self.downloadFilename]; [self.receivedData writeToFile:filePath atomically:YES]; [self unzipFile:filePath]; @@ -1239,6 +1204,7 @@ - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [_downloadInfoView setMessageText:NSLocalizedString(@"message-keyboard-download-complete", nil)]; NSButton *button = (NSButton *)[_downloadInfoView.buttons objectAtIndex:0]; + os_log_debug([KMLogs uiLog], "connectionDidFinishLoading, set button title download complete, tag = 1"); [button setTitle:NSLocalizedString(@"button-download-complete", nil)]; [button setTag:1]; [[NSNotificationCenter defaultCenter] postNotificationName:kKeymanKeyboardDownloadCompletedNotification @@ -1305,7 +1271,9 @@ - (BOOL)unzipFile:(NSString *)filePath { NSError *error = nil; NSString *fileName = filePath.lastPathComponent; NSString *folderName = [fileName stringByDeletingPathExtension]; - + + os_log_debug([KMLogs keyboardLog], "unzipFile, folderName: %{public}@, fileName: %{public}@", folderName, fileName); + // First we unzip into a temp folder, and check kmp.json for the fileVersion // before we continue installation. We don't want to overwrite existing // package if it is there if the files are not compatible with the installed @@ -1316,9 +1284,9 @@ - (BOOL)unzipFile:(NSString *)filePath { ZipArchive *za = [[ZipArchive alloc] init]; if ([za UnzipOpenFile:filePath]) { - os_log_debug([KMLogs keyboardLog], "Unzipping %{public}@ to %{public}@", filePath, tempDestFolder); + os_log_debug([KMLogs keyboardLog], "unzipFile, Unzipping %{public}@ to %{public}@", filePath, tempDestFolder); if ([[NSFileManager defaultManager] fileExistsAtPath:tempDestFolder]) { - os_log_debug([KMLogs keyboardLog], "The temp destination folder already exists. Overwriting..."); + os_log_debug([KMLogs keyboardLog], "unzipFile, The temp destination folder already exists. Overwriting..."); } didUnzip = [za UnzipFileTo:tempDestFolder overWrite:YES]; @@ -1326,11 +1294,11 @@ - (BOOL)unzipFile:(NSString *)filePath { } if (!didUnzip) { - os_log_error([KMLogs keyboardLog], "Failed to unzip file: %{public}@", filePath); + os_log_error([KMLogs keyboardLog], "unzipFile, Failed to unzip file: %{public}@", filePath); return NO; } - os_log_debug([KMLogs keyboardLog], "Unzipped file: %{public}@", filePath); + os_log_debug([KMLogs keyboardLog], "unzipFile, Unzipped file: %{public}@", filePath); BOOL didInstall = [self verifyPackageVersionInTempFolder:tempDestFolder filePath:filePath]; @@ -1338,10 +1306,10 @@ - (BOOL)unzipFile:(NSString *)filePath { // Remove existing package if it exists if (didInstall && [[NSFileManager defaultManager] fileExistsAtPath:destFolder]) { - os_log_debug([KMLogs keyboardLog], "The destination folder already exists. Overwriting..."); + os_log_debug([KMLogs keyboardLog], "unzipFile, The destination folder already exists. Overwriting..."); [[NSFileManager defaultManager] removeItemAtPath:destFolder error:&error]; if (error != nil) { - os_log_error([KMLogs keyboardLog], "Unable to remove destination folder %{public}@", destFolder); + os_log_error([KMLogs keyboardLog], "unzipFile, Unable to remove destination folder %{public}@", destFolder); didInstall = NO; } } @@ -1354,7 +1322,7 @@ - (BOOL)unzipFile:(NSString *)filePath { if(didInstall) { [[NSFileManager defaultManager] moveItemAtPath:tempDestFolder toPath:destFolder error:&error]; if (error != nil) { - os_log_error([KMLogs keyboardLog], "Unable to move temp folder %{public}@ to dest folder %{public}@", tempDestFolder, destFolder); + os_log_error([KMLogs keyboardLog], "unzipFile, Unable to move temp folder %{public}@ to dest folder %{public}@", tempDestFolder, destFolder); didInstall = NO; } } @@ -1362,7 +1330,7 @@ - (BOOL)unzipFile:(NSString *)filePath { if(!didInstall) { [[NSFileManager defaultManager] removeItemAtPath:tempDestFolder error:&error]; if (error != nil) { - os_log_error([KMLogs keyboardLog], "Unable to remove temp folder %{public}@", tempDestFolder); + os_log_error([KMLogs keyboardLog], "unzipFile, Unable to remove temp folder %{public}@", tempDestFolder); } return NO; @@ -1371,11 +1339,12 @@ - (BOOL)unzipFile:(NSString *)filePath { // Package has installed, now scan for keyboards and fonts // TODO: we need to be reading the kmp.json data to determine keyboards to install NSString * keyboardFolderPath = [self.keyboardsPath stringByAppendingPathComponent:folderName]; + os_log_debug([KMLogs keyboardLog], "unzipFile, folderName: %{public}@, keyboardFolderPath: %{public}@", folderName, keyboardFolderPath); [self installFontsAtPath:keyboardFolderPath]; - for (NSString *kmxFile in [self KMXFilesAtPath:keyboardFolderPath]) { - os_log_debug([KMLogs keyboardLog], "Adding keyboard to list of active keyboards: %{public}@", kmxFile); - if (![self.activeKeyboards containsObject:kmxFile]) - [self.activeKeyboards addObject:kmxFile]; + + for (NSString *kmxFile in [self getKmxFilesAtPath:keyboardFolderPath]) { + NSString *partialPath = [KMDataRepository.shared buildPartialPathFrom:folderName keyboardFile:[kmxFile lastPathComponent]]; + [self addActiveKeyboard:partialPath]; } [self saveActiveKeyboards]; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index fa912ca67d5..c5b6f172b60 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -10,6 +10,7 @@ #import /* For kVK_ constants. */ #import "KeySender.h" #import "TextApiCompliance.h" +#import "KMSettingsRepository.h" #import "KMLogs.h" @import Sentry; @@ -44,16 +45,23 @@ - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender { _lowLevelBackspaceCount = 0; _queuedText = nil; - // In Xcode, if Keyman is the active IM and is in "debugMode" and "English plus Spanish" is - // the current keyboard and you type "Sentry force now", it will force a simulated crash to + BOOL forceSentryCrash = [KMSettingsRepository.shared readForceSentryError]; + + // In Xcode, if Keyman is the active IM and the settings include the + // forceSentryCrash flag and "English plus Spanish" is the current keyboard + // and you type "Sentry force now", it will force a simulated crash to // test reporting to sentry.keyman.com - if ([self.appDelegate debugMode] && [clientAppId isEqual: @"com.apple.dt.Xcode"]) { - os_log_debug([KMLogs testLog], "Sentry - Preparing to detect Easter egg."); + if (forceSentryCrash && [clientAppId isEqual: @"com.apple.dt.Xcode"]) { + os_log_debug([KMLogs testLog], "initWithClient, preparing to force Sentry crash."); _easterEggForSentry = [[NSMutableString alloc] init]; } else _easterEggForSentry = nil; + [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { + [scope setTagValue:clientAppId forKey:@"clientAppId"]; + }]; + return self; } @@ -61,9 +69,7 @@ - (void)deactivate { if (_generatedBackspaceCount > 0 || (_queuedText != nil && _queuedText.length > 0) || _keyCodeOfOriginalEvent != 0 || _sourceFromOriginalEvent != nil) { - if ([self.appDelegate debugMode]) { - os_log_error([KMLogs lifecycleLog], "ERROR: new app activated before previous app finished processing pending events! _generatedBackspaceCount: %lu, _queuedText: '%{public}@' _keyCodeOfOriginalEvent: %hu", _generatedBackspaceCount, _queuedText == nil ? @"(NIL)" : (NSString*)[self queuedText], _keyCodeOfOriginalEvent); - } + os_log_error([KMLogs lifecycleLog], "ERROR: new app activated before previous app finished processing pending events! _generatedBackspaceCount: %lu, _queuedText: '%{public}@' _keyCodeOfOriginalEvent: %hu", _generatedBackspaceCount, _queuedText == nil ? @"(NIL)" : (NSString*)[self queuedText], _keyCodeOfOriginalEvent); _keyCodeOfOriginalEvent = 0; _generatedBackspaceCount = 0; _queuedText = nil; @@ -412,11 +418,11 @@ -(void) persistOptions:(NSDictionary*)options{ for(NSString *key in options) { NSString *value = [options objectForKey:key]; if(key && value) { - os_log_debug([KMLogs keyLog], "applyNonTextualOutput calling writePersistedOptions, key: %{public}@, value: %{public}@", key, value); - [self.appDelegate writePersistedOptions:key withValue:value]; + os_log_debug([KMLogs keyLog], "persistOptions, key: %{public}@, value: %{public}@", key, value); + [[KMSettingsRepository shared] writeOptionForSelectedKeyboard:key withValue:value]; } else { - os_log_debug([KMLogs keyLog], "applyNonTextualOutput, invalid values in optionsToPersist, not writing to UserDefaults, key: %{public}@, value: %{public}@", key, value); + os_log_debug([KMLogs keyLog], "invalid values in persistOptions, not writing to UserDefaults, key: %{public}@, value: %{public}@", key, value); } } } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h new file mode 100644 index 00000000000..62a3aac35d1 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h @@ -0,0 +1,27 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Created by Shawn Schantz on 2024-09-09. + * + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kInputMethodActivatedNotification; +extern NSString *const kInputMethodDeactivatedNotification; +extern NSString *const kInputMethodClientChangeNotification; + +@interface KMInputMethodLifecycle : NSObject ++ (KMInputMethodLifecycle *)shared; ++ (NSString*)getClientApplicationId; +- (void)startLifecycle; +- (void)activateClient:(id)client; +- (void)deactivateClient:(id)client; +- (BOOL)shouldEnableEventTap; +- (BOOL)shouldShowOskOnActivate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m new file mode 100644 index 00000000000..9b877a530e7 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m @@ -0,0 +1,253 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Created by Shawn Schantz on 2024-09-09. + * + * This class is responsible for determining the state of the Keyman input method. + * It is called from the KMInputController (a subclass of IMKInputController), and + * shares changes in the state of the input method by synchronously posting + * notifications to NSNotificationCenter. + */ + +/** + * This class is needed because many activateServer and deactivateServer messages sent from macOS + * to KMInputController, but they are not particularly reliable. Keyman receives some messages when it + * is not active and should not become active. It also receives messages when it is active, but there is no + * need to change state. For example, when a menu is clicked with Keyman active, macOS will send a + * deactivateServer message followed by an activateServer message when the menu is released. + * The messages may also arrive in an unexpected order. + * + * Instead of relying on the information conveyed in these messages, this class interprets them as a notification + * that the input method state may have changed. For the actual state of the input method, it gets the current + * input source using the Carbon APIs TISCopyCurrentKeyboardInputSource and TISGetInputSourceProperty. + * If the result is equal to "keyman.inputmethod.Keyman", then Keyman is the active input method. If, for + * example, the U.S. keyboard were selected, then the result would be "com.apple.keylayout.US". + * + * The state of the text input client is discovered using the NSRunningApplication frontmostApplication API. + * Knowing the current input method and the current text input client enables us to determine whether the + * state has actually changed and how to adjust to the new state. + * + * It is important for state to be known so that the On-screen keyboard can be appropriately shown or hidden + * and the low-level event tap can be stopped or started. + */ + +#import "KMInputMethodLifecycle.h" +#import "KMLogs.h" +#import +#import "KMSettingsRepository.h" +#import + +NSString *const kInputMethodActivatedNotification = @"kInputMethodActivatedNotification"; +NSString *const kInputMethodDeactivatedNotification = @"kInputMethodDeactivatedNotification"; +NSString *const kInputMethodClientChangeNotification = @"kInputMethodClientChangeNotification"; +NSString *const keymanInputMethodName = @"keyman.inputmethod.Keyman"; +const double transitionDelay = 0.25; + +typedef enum { + Started, + Active, + Inactive +} LifecycleState; + +typedef enum { + None, + Activate, + Deactivate, + ChangeClients, +} TransitionType; + + +@interface KMInputMethodLifecycle() + +@property LifecycleState lifecycleState; +@property NSString *inputSourceId; +@property NSString *clientApplicationId; +@end + +@implementation KMInputMethodLifecycle + ++ (KMInputMethodLifecycle *)shared { + static KMInputMethodLifecycle *shared = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + shared = [[KMInputMethodLifecycle alloc] init]; + }); + + return shared; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _lifecycleState = Started; + _inputSourceId = @""; + _clientApplicationId = @""; + } + return self; +} + +/** + * called from Application Delgate during init + */ +- (void)startLifecycle { + _lifecycleState = Started; +} + +/** + * Use Carbon APIs to get the current input source or input method. Even though many Carbon APIs were deprecated and removed + * from the OS years ago, these and other low-level APIs are still supported (but apparently completely undocumented). + */ ++ (NSString*)getCurrentInputSourceId { + TISInputSourceRef inputSource = TISCopyCurrentKeyboardInputSource(); + NSString *inputSourceId = (__bridge NSString *)(TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)); + return inputSourceId; +} + +/** + * Get the bundle ID of the currently active text input client.. + */ ++ (NSString*)getClientApplicationId { + NSRunningApplication *currentApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; + NSString *clientAppId = [currentApp bundleIdentifier]; + return clientAppId; +} + +/** + * Based on the current lifecycleState and the input method state from the OS, determine how the state must transition. + */ +- (TransitionType)determineTransition:(NSString*)newInputSourceId withAppId:(NSString*)newClientAppId { + TransitionType transition = None; + BOOL inputSourceIsKeyman = [newInputSourceId isEqualTo:keymanInputMethodName]; + BOOL clientHasChanged = [self.clientApplicationId isNotEqualTo:newClientAppId]; + os_log_debug([KMLogs lifecycleLog], "determineTransition, current InputSourceId: %{public}@, new InputSourceId: %{public}@, current ClientAppId: %{public}@, new ClientAppId: %{public}@, inputSourceIsKeyman: %d, clientHasChanged: %d", self.inputSourceId, newInputSourceId, self.clientApplicationId, newClientAppId, inputSourceIsKeyman, clientHasChanged); + + switch (self.lifecycleState) { + case Started: + transition = Activate; + break; + case Active: + if (inputSourceIsKeyman) { + if (clientHasChanged) { + transition = ChangeClients; + } + } else { + transition = Deactivate; + } + break; + case Inactive: + if (inputSourceIsKeyman) { + transition = Activate; + } + break; + } + return transition; +} + +/** + * Update the input method state, consisting of the input source ID and the client application ID. + */ +- (void)saveNewInputMethodState:(NSString*)newInputSourceId withAppId:(NSString*)newClientAppId { + self.inputSourceId = newInputSourceId; + self.clientApplicationId = newClientAppId; +} + +/** + * Called when IMKInputController receives an activateServer or a deactivateServer message + */ +- (void)performTransition:(id)client { + NSString *currentInputSource = [KMInputMethodLifecycle getCurrentInputSourceId]; + NSString *currentClientAppId = [KMInputMethodLifecycle getClientApplicationId]; + + TransitionType transition = [self determineTransition:currentInputSource withAppId:currentClientAppId]; + [self saveNewInputMethodState:currentInputSource withAppId:currentClientAppId]; + + switch(transition) { + case None: + os_log_info([KMLogs lifecycleLog], "performTransition: None, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + break; + case Activate: + os_log_info([KMLogs lifecycleLog], "performTransition: Activate, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + /** + * Perform two actions when activating the input method. + * Change the client first which prepares the event handler. + * Then do the activate which starts the event loop and opens the OSK. + */ + [self changeClient]; + [self activateInputMethod]; + break; + case Deactivate: + os_log_info([KMLogs lifecycleLog], "performTransition: Deactivate, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + [self deactivateInputMethod]; + break; + case ChangeClients: + os_log_info([KMLogs lifecycleLog], "performTransition: ChangeClients, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + [self changeClient]; + break; + } +} + +/** + * Called when IMKInputController receives an activateServer message + */ +- (void)activateClient:(id)client { + os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle activateClient"); + + [self performSelector:@selector(performTransitionAfterDelay:) withObject:client afterDelay:transitionDelay]; +} + +/** + * Called when IMKInputController receives an deactivateServer message + */ +- (void)deactivateClient:(id)client { + os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle deactivateClient"); + + [self performSelector:@selector(performTransitionAfterDelay:) withObject:client afterDelay:transitionDelay]; +} + +- (void)performTransitionAfterDelay:(id)client { + os_log_debug([KMLogs lifecycleLog], "performTransitionAfterDelay: calling performTransition"); + [self performTransition:client]; +} + +/** + * Change lifecycleState to Active and send notification. + */ +- (void)activateInputMethod { + os_log_debug([KMLogs lifecycleLog], "activateInputMethod"); + _lifecycleState = Active; + [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodActivatedNotification object:self]; +} + +/** + * Change lifecycleState to Inactive and send notification. + */ +- (void)deactivateInputMethod { + os_log_debug([KMLogs lifecycleLog], "deactivateInputMethod"); + _lifecycleState = Inactive; + [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodDeactivatedNotification object:self]; +} + +/** + * Does not change lifecycleState, just fires notification so that InputController knows to change the event handler + */ +- (void)changeClient { + os_log_debug([KMLogs lifecycleLog], "changeClient"); + [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodClientChangeNotification object:self]; +} + +/** + * Returns true if Started or Active + */ +- (BOOL)shouldEnableEventTap { + return ((self.lifecycleState == Started) || (self.lifecycleState == Active)); +} + +/** + * Returns true if lifecycleState is Active and the Settings require us to show the OSK + */ +- (BOOL)shouldShowOskOnActivate { + return [KMSettingsRepository.shared readShowOskOnActivate] + && (self.lifecycleState == Active); +} + +@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMKeyboardHelpWindow/el.lproj/KMKeyboardHelpWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMKeyboardHelpWindow/el.lproj/KMKeyboardHelpWindowController.strings new file mode 100644 index 00000000000..ed234ea2e29 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMKeyboardHelpWindow/el.lproj/KMKeyboardHelpWindowController.strings @@ -0,0 +1,8 @@ +/* Keyboard Help window title */ +"F0z-JX-Cv5.title" = "Βοήθεια γιὰ τὸ πληκτρολόγιο"; + +/* OK button text */ +"Zla-QF-m0K.title" = "OK"; + +/* Print button text */ +"fYY-Uj-y4S.title" = "Ἐκτυπῶστε..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMPackageReader.h b/mac/Keyman4MacIM/Keyman4MacIM/KMPackageReader.h index 2c138d8215a..008f0c025e6 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMPackageReader.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMPackageReader.h @@ -19,8 +19,6 @@ NS_ASSUME_NONNULL_BEGIN @interface KMPackageReader : NSObject -@property (assign, nonatomic) BOOL debugMode; - - (instancetype)init; - (KMPackageInfo *)loadPackageInfo:(NSString *)path; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMPackageReader.m b/mac/Keyman4MacIM/Keyman4MacIM/KMPackageReader.m index 7406488a8a0..12c0b3c5645 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMPackageReader.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMPackageReader.m @@ -44,9 +44,6 @@ @implementation KMPackageReader - (instancetype)init { self = [super init]; - if (self) { - _debugMode = NO; - } return self; } @@ -69,15 +66,6 @@ - (KMPackageInfo *)loadPackageInfo:(NSString *)path { return packageInfo; } -- (NSString *)packageNameFromPackageInfo:(NSString *)path { - NSString *packageName = nil; - - KMPackageInfo *packageInfo = [self loadPackageInfo:path]; - packageName = packageInfo.packageName; - - return packageName; -} - /** * read JSON file and load it into KMPackageInfo object * returns nil if the file does not exist or it cannot be parsed as JSON @@ -251,14 +239,12 @@ - (KMPackageInfo *) loadPackageInfoFromInfFile:(NSString *)path { NSString *s = [line substringFromIndex:kName.length+1]; NSArray *vs = [s componentsSeparatedByString:@"\","]; NSString *v1 = [[vs objectAtIndex:0] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; - NSString *v2 = [[vs objectAtIndex:1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; builder.packageName = v1; } else if ([[line lowercaseString] hasPrefix:[kVersion lowercaseString]]) { NSString *s = [line substringFromIndex:kVersion.length+1]; NSArray *vs = [s componentsSeparatedByString:@"\","]; NSString *v1 = [[vs objectAtIndex:0] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; - NSString *v2 = [[vs objectAtIndex:1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; builder.packageVersion = v1; } else if ([[line lowercaseString] hasPrefix:[kAuthor lowercaseString]]) { @@ -273,14 +259,12 @@ - (KMPackageInfo *) loadPackageInfoFromInfFile:(NSString *)path { NSString *s = [line substringFromIndex:kCopyright.length+1]; NSArray *vs = [s componentsSeparatedByString:@"\","]; NSString *v1 = [[vs objectAtIndex:0] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; - NSString *v2 = [[vs objectAtIndex:1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; builder.copyright = v1; } else if ([[line lowercaseString] hasPrefix:[kWebSite lowercaseString]]) { NSString *s = [line substringFromIndex:kWebSite.length+1]; NSArray *vs = [s componentsSeparatedByString:@"\","]; NSString *v1 = [[vs objectAtIndex:0] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; - NSString *v2 = [[vs objectAtIndex:1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; builder.website = v1; } @@ -294,20 +278,17 @@ - (KMPackageInfo *) loadPackageInfoFromInfFile:(NSString *)path { NSString *s = [line substringFromIndex:x+2]; if ([[s lowercaseString] hasPrefix:[kFile lowercaseString]]) { NSArray *vs = [s componentsSeparatedByString:@"\","]; - NSString *v1 = [[[vs objectAtIndex:0] substringFromIndex:kFile.length+1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; NSString *fileName = [[vs objectAtIndex:1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; [files addObject:fileName]; } else if ([[s lowercaseString] hasPrefix:[kFont lowercaseString]]) { NSArray *vs = [s componentsSeparatedByString:@"\","]; NSString *fontName = [[[vs objectAtIndex:0] substringFromIndex:kFont.length+1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; - NSString *fontFileName = [[vs objectAtIndex:1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; [fontArray addObject:fontName]; } else if ([[s lowercaseString] hasPrefix:[kKeyboard lowercaseString]]) { NSArray *vs = [s componentsSeparatedByString:@"\","]; NSString *keyboardName = [[[vs objectAtIndex:0] substringFromIndex:kKeyboard.length+1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; - NSString *keyboardFileName = [[vs objectAtIndex:1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; KMKeyboardInfoBuilder *builder = [[KMKeyboardInfoBuilder alloc] init]; builder.name = keyboardName; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h new file mode 100644 index 00000000000..846b9e11c67 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h @@ -0,0 +1,32 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * KMSettingsRepository.h + * Keyman + * + * Created by Shawn Schantz on 2024-07-29. + * + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KMSettingsRepository : NSObject ++ (KMSettingsRepository *)shared; +- (BOOL)dataMigrationNeeded; +- (void)convertSettingsForMigration; +- (void)setDataModelVersionIfNecessary; +- (NSString *)readSelectedKeyboard; +- (void)writeSelectedKeyboard:(NSString *)selectedKeyboard; +- (NSArray *)readActiveKeyboards; +- (void)writeActiveKeyboards: (NSArray *) keyboards; +- (void)clearActiveKeyboards; +- (NSDictionary *)readOptionsForSelectedKeyboard; +- (void)writeOptionForSelectedKeyboard:(NSString *)key withValue:(NSString*)value; +- (BOOL)readShowOskOnActivate; +- (void)writeShowOskOnActivate:(BOOL)show; +- (BOOL)readForceSentryError; +@end + +NS_ASSUME_NONNULL_END diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m new file mode 100644 index 00000000000..120c26e2c41 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m @@ -0,0 +1,334 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Created by Shawn Schantz on 2024-07-29. + * + * Singleton object for reading and writing Keyman application settings. + * Serves as an abstraction to StandardUserDefaults which is currently used to store application settings. + */ + +#import "KMSettingsRepository.h" +#import "KMLogs.h" +#import "KMDataRepository.h" + +NSString *const kActiveKeyboardsKey = @"KMActiveKeyboardsKey"; +NSString *const kSelectedKeyboardKey = @"KMSelectedKeyboardKey"; +NSString *const kPersistedOptionsKey = @"KMPersistedOptionsKey"; +NSString *const kShowOskOnActivate = @"KMShowOskOnActivate"; +NSString *const kForceSentryError = @"KMForceSentryError"; + +/** + * The following constant "KMSavedStoresKey" is left here for documentation + * though we have abandoned stores written to UserDefaults with this key because + * they used a less-reliable numeric key prior to integration with Keyman Core. + * It is replaced by the renamed "KMPersistedOptionsKey" which directly + * represents what it is saving. + */ +NSString *const kKMDeprecatedPersistedOptionsKey = @"KMSavedStoresKey"; +/** + * The following constant "KMAlwaysShowOSKKey" is left here for documentation + * but the related UI has been removed according to issue #12342 + */ +NSString *const kAlwaysShowOSKKey = @"KMAlwaysShowOSKKey"; +/** + * The following constant "KMUseVerboseLogging" is left here for documentation + * but it is obsolete and removed issue #11525 + */ +NSString *const kUseVerboseLogging = @"KMUseVerboseLogging"; + +NSString *const kObsoletePathComponent = @"/Documents/Keyman-Keyboards"; +NSString *const kNewPathComponent = @"/Library/Application Support/keyman.inputmethod.Keyman/"; + +/** + * Store the version number of the data model in the UserDefaults with this key. + * The first version, 1, is defined to indicate that we are storing the data/keyboards in the Library + * directory instead of in the Documents directory. + */ +NSString *const kDataModelVersion = @"KMDataModelVersion"; +NSInteger const kVersionStoreDataInLibraryDirectory = 1; +NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirectory; + +@implementation KMSettingsRepository + ++ (KMSettingsRepository *)shared +{ + static KMSettingsRepository *shared = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + shared = [[KMSettingsRepository alloc] init]; + }); + return shared; +} + +- (void)setDataModelVersionIfNecessary { + if (![self dataModelWithKeyboardsInLibrary]) { + [[NSUserDefaults standardUserDefaults] setInteger:kVersionStoreDataInLibraryDirectory forKey:kDataModelVersion]; + } +} + +/** + * If the selectedKeyboard has not been set, then the settings have not been saved in the UserDefaults. + * If this method is called after applicationDidFinishLaunching, then it will always return true. + * If called from awakeFromNib, then it will return false when running for the first time. + */ +- (BOOL)settingsExist +{ + return ([[NSUserDefaults standardUserDefaults] objectForKey:kSelectedKeyboardKey] != nil); +} + +- (void)writeOptionForSelectedKeyboard:(NSString *)key withValue:(NSString*)value { + NSDictionary *optionsMap = [self readOptionsForSelectedKeyboard]; + NSDictionary *newOptionsMap = nil; + + // if we can read an existing options map, then add the specified key-value pair + if (optionsMap != nil) { + NSMutableDictionary *mutableOptionsMap = [optionsMap mutableCopy]; + [mutableOptionsMap setObject:value forKey:key]; + newOptionsMap = mutableOptionsMap; + } else { + // if no options map exists, create a new one add the specified key-value pair + newOptionsMap = [[NSDictionary alloc] initWithObjectsAndKeys:value, key, nil]; + } + + // write the options map for the selected keyboard to the dictionary of options + NSString *selectedKeyboard = [self readSelectedKeyboard]; + os_log_info([KMLogs dataLog], "writeOptionForSelectedKeyboard, adding options map: %{public}@, to keyboard %{public}@", newOptionsMap, selectedKeyboard); + [self writeKeyboardOptionsMap: selectedKeyboard withOptions:newOptionsMap]; +} + +- (void)writeKeyboardOptionsMap:(NSString *)keyboardName withOptions:(NSDictionary*) optionsMap { + NSMutableDictionary *newFullOptionsMap = nil; + os_log_debug([KMLogs dataLog], "writeKeyboardOptionsMap, adding options map: %{public}@, to keyboard %{public}@", optionsMap, keyboardName); + + NSDictionary *fullOptionsMap = [self readFullOptionsMap]; + // if we can read the existing full options map, then add for the specified keyboard + if (fullOptionsMap != nil) { + newFullOptionsMap = [fullOptionsMap mutableCopy]; + [newFullOptionsMap setObject:optionsMap forKey:keyboardName]; + } else { + // otherwise, create the full options map and add for the specified keyboard + newFullOptionsMap = [[NSMutableDictionary alloc] initWithObjectsAndKeys:optionsMap, keyboardName, nil]; + } + + [self writeFullOptionsMap:newFullOptionsMap]; +} + +/** + * For the first numbered version of the data model, the app stores the keyboards under the ~/Library directory + * For versions before version 1, the keyboards were stored under the ~/Documents directory. + */ +- (BOOL)dataModelWithKeyboardsInLibrary { + // [NSUserDefaults integerForKey] returns zero if the key does not exist + NSInteger dataModelVersion = [[NSUserDefaults standardUserDefaults] integerForKey:kDataModelVersion]; + + return dataModelVersion >= kVersionStoreDataInLibraryDirectory; +} + +/** + * Determines whether the keyboard data needs to be moved from the old location to the new location + * This is true if + * 1) the UserDefaults exist (indicating that this is not a new installation of Keyman) and + * 2) the value for kVersionStoreDataInLibraryDirectory is < 1, + */ +- (BOOL)dataMigrationNeeded { + BOOL keymanSettingsExist = [self settingsExist]; + os_log([KMLogs dataLog], "keyman settings exist: %{public}@", keymanSettingsExist ? @"YES" : @"NO" ); + + BOOL keyboardsStoredInLibrary = [self dataModelWithKeyboardsInLibrary]; + os_log([KMLogs dataLog], "settings indicate that keyboards are stored in ~/Library: %{public}@", keyboardsStoredInLibrary ? @"YES" : @"NO" ); + + BOOL migrationNeeded = keymanSettingsExist && !keyboardsStoredInLibrary; + os_log([KMLogs dataLog], "dataMigrationNeeded: %{public}@", migrationNeeded ? @"YES" : @"NO" ); + + return migrationNeeded; +} + +- (NSString *)readSelectedKeyboard { + return [[NSUserDefaults standardUserDefaults] objectForKey:kSelectedKeyboardKey]; +} + +- (void)writeSelectedKeyboard:(NSString *)selectedKeyboard { + NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; + [userData setObject:selectedKeyboard forKey:kSelectedKeyboardKey]; +} + +- (NSMutableArray *)activeKeyboards { + NSMutableArray * activeKeyboards = [[[NSUserDefaults standardUserDefaults] arrayForKey:kActiveKeyboardsKey] mutableCopy]; + + if (!activeKeyboards) { + activeKeyboards = [[NSMutableArray alloc] initWithCapacity:0]; + } + return activeKeyboards; +} + +- (NSArray *)readActiveKeyboards { + os_log_debug([KMLogs dataLog], "KMSettingsRepository readActiveKeyboards"); + NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; + NSArray *keyboardsArray = [userData arrayForKey:kActiveKeyboardsKey]; + + // if the kActiveKeyboardsKey does not exist, then create an empty array + if (!keyboardsArray) { + os_log_debug([KMLogs dataLog], "KMActiveKeyboardsKey key not found in NSUserDefualts"); + keyboardsArray = [[NSArray alloc] init]; + } + return keyboardsArray; +} + +- (void)writeActiveKeyboards: (NSArray *) keyboards { + os_log_debug([KMLogs dataLog], "KMSettingsRepository writeActiveKeyboards"); + NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; + [userData setObject:keyboards forKey:kActiveKeyboardsKey]; +} + +- (void)clearActiveKeyboards { + os_log_debug([KMLogs dataLog], "KMSettingsRepository clearActiveKeyboards"); + NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; + [userData setObject:nil forKey:kActiveKeyboardsKey]; +} + + +/** + * returns dictionary of persisted options for the single selected keyboard + */ +- (NSDictionary *)readOptionsForSelectedKeyboard { + NSDictionary *optionsMap = [self readFullOptionsMap]; + NSString *selectedKeyboard = [self readSelectedKeyboard]; + NSDictionary *selectedOptionsMap = [optionsMap objectForKey: selectedKeyboard]; + if (selectedOptionsMap == nil) { + os_log_info([KMLogs dataLog], "no persisted options found in UserDefaults for keyboard %{public}@ ", selectedKeyboard); + } else { + for (NSString *key in selectedOptionsMap) { + NSString *value = [selectedOptionsMap objectForKey:key]; + os_log_info([KMLogs dataLog], "option for keyboard %{public}@ key: %{public}@, value %{public}@", selectedKeyboard, key, value); + } + } + return selectedOptionsMap; +} + +/** + * returns dictionary of all persisted options for all keyboards + * (options are stored in UserDefaults as a map of maps) + */ +- (NSDictionary *)readFullOptionsMap { + NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; + return [userData dictionaryForKey:kPersistedOptionsKey]; +} + +- (void)writeFullOptionsMap:(NSDictionary *) fullOptionsMap { + NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; + [userData setObject:fullOptionsMap forKey:kPersistedOptionsKey]; +} + +- (void)removeAllOptions { + NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; + return [userData removeObjectForKey:kPersistedOptionsKey]; +} + +- (void)convertSettingsForMigration { + os_log_debug([KMLogs dataLog], "converting settings in UserDefaults for migration"); + [self convertSelectedKeyboardPathForMigration]; + [self convertActiveKeyboardArrayForMigration]; + [self convertOptionsPathsForMigration]; +} + +- (void)convertSelectedKeyboardPathForMigration { + NSString *selectedKeyboardPath = [self readSelectedKeyboard]; + if (selectedKeyboardPath != nil) { + NSString *newPathString = [self trimObsoleteKeyboardPath:selectedKeyboardPath]; + + if ([selectedKeyboardPath isNotEqualTo:newPathString]) { + [self writeSelectedKeyboard:newPathString]; + os_log_debug([KMLogs dataLog], "converted selected keyboard setting from '%{public}@' to '%{public}@'", selectedKeyboardPath, newPathString); + } + } +} + +/** + * To convert the keyboard path for the new location, just trim the parent directory from the path + * No need to repeatedly store the parent directory with the path of each keyboard + * If the old directory is not found in the string, then return the string unchanged + */ +- (NSString *)trimObsoleteKeyboardPath:(NSString *)oldPath { + NSString *newPath = oldPath; + if(oldPath != nil) { + NSRange range = [oldPath rangeOfString:kObsoletePathComponent]; + if (range.length > 0) { + newPath = [oldPath substringFromIndex:range.location + range.length]; + os_log_debug([KMLogs dataLog], "trimmed keyboard path from '%{public}@' to '%{public}@'", oldPath, newPath); + } + } + return newPath; +} + +- (void)convertActiveKeyboardArrayForMigration { + NSMutableArray *keyboards = [self activeKeyboards]; + NSMutableArray *convertedActiveKeyboards = [[NSMutableArray alloc] initWithCapacity:0]; + BOOL didConvert = NO; + + for (NSString *oldPath in keyboards) { + NSString *newPath = [self trimObsoleteKeyboardPath:oldPath]; + if ([oldPath isNotEqualTo:newPath]) { + [convertedActiveKeyboards addObject:newPath]; + os_log_debug([KMLogs dataLog], "converted active keyboard from old path '%{public}@' to '%{public}@'", oldPath, newPath); + // if we have adjusted at least one path, set flag + didConvert = YES; + } else { + // if, somehow, the path does not need converting then retain it in new array + [convertedActiveKeyboards addObject:oldPath]; + } + } + + // only write array to UserDefaults if we actually converted something + if (didConvert) { + [self writeActiveKeyboards:convertedActiveKeyboards]; + } +} + +- (void)convertOptionsPathsForMigration { + NSDictionary * optionsMap = [self readFullOptionsMap]; + NSMutableDictionary *mutableOptionsMap = nil; + BOOL optionsChanged = NO; + + if (optionsMap != nil) { + os_log_info([KMLogs configLog], "optionsMap != nil"); + mutableOptionsMap = [[NSMutableDictionary alloc] initWithCapacity:0]; + for(id key in optionsMap) { + os_log_info([KMLogs configLog], "persisted options found in UserDefaults with key = %{public}@", key); + } + for (NSString *key in optionsMap) { + NSString *newPathString = [self trimObsoleteKeyboardPath:key]; + NSDictionary *optionsValue = [optionsMap objectForKey:key]; + + if ([key isNotEqualTo:newPathString]) { + optionsChanged = YES; + + // insert options into new map with newly converted path as key + [mutableOptionsMap setObject:optionsValue forKey:newPathString]; + os_log_debug([KMLogs dataLog], "converted option key from '%{public}@' to '%{public}@'", key, newPathString); + } else { + // retain options that did not need converting + [mutableOptionsMap setObject:optionsValue forKey:key]; + } + } + if (optionsChanged) { + [self writeFullOptionsMap:mutableOptionsMap]; + } + } +} + +- (BOOL)readShowOskOnActivate { + NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; + return [userData boolForKey:kShowOskOnActivate]; +} + +- (void)writeShowOskOnActivate:(BOOL)show { + NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; + [userData setBool:show forKey:kShowOskOnActivate]; +} + +- (BOOL)readForceSentryError { + NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; + return [userData boolForKey:kForceSentryError]; +} + +@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m b/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m index 417475c777d..dc73edc11bd 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m @@ -1,9 +1,6 @@ /** * Keyman is copyright (C) SIL International. MIT License. * - * KeySender.m - * Keyman - * * Created by Shawn Schantz on 2023-04-17. * * Sends keydown events for the provided keycode to the frontmost application. @@ -56,15 +53,13 @@ - (void)postKeyboardEventWithSource: (CGEventSourceRef)source code:(CGKeyCode) v } /** - sendKeymanKeyCodeForEvent sends the kKeymanEventKeyCode to the - frontmost application to indicate that all the backspaces have been processed - and we can insert the queuedText to the client + * sendKeymanKeyCodeForEvent sends the kKeymanEventKeyCode to the + * frontmost application to indicate that all the backspaces have been processed + * and we can insert the queuedText to the client */ - (void)sendKeymanKeyCodeForEvent:(NSEvent *)event { os_log_debug([KMLogs keyLog], "KeySender sendKeymanKeyCodeForEvent"); - - ProcessSerialNumber psn; // Returns the frontmost app, which is the app that receives key events. NSRunningApplication *app = NSWorkspace.sharedWorkspace.frontmostApplication; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h index 4b98f633b85..fd3a953a858 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h @@ -9,7 +9,7 @@ #import #import -@interface OSKWindowController : NSWindowController +@interface OSKWindowController : NSWindowController @property (nonatomic, weak) IBOutlet OSKView *oskView; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m index 30d30004c45..33d7eda4062 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m @@ -8,6 +8,8 @@ #import "OSKWindowController.h" #import "KMInputMethodAppDelegate.h" +#import "KMInputMethodLifecycle.h" +#import "KMSettingsRepository.h" #import "KMLogs.h" @interface OSKWindowController () @@ -25,7 +27,6 @@ - (KMInputMethodAppDelegate *)AppDelegate { } - (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; [self stopTimer]; } @@ -55,7 +56,6 @@ - (void)awakeFromNib { - (void)windowDidLoad { os_log_debug([KMLogs oskLog], "OSKWindowController windowDidLoad"); [super windowDidLoad]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidResize:) name:NSWindowDidResizeNotification object:self.window]; [self.oskView setKvk:[self.AppDelegate kvk]]; [self startTimerWithTimeInterval:0.1]; // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file. @@ -66,6 +66,11 @@ - (void)windowDidResize:(NSNotification *)notification { [self.oskView resizeOSKLayout]; } +- (void)windowWillClose:(NSNotification *)notification { + [KMSettingsRepository.shared writeShowOskOnActivate:NO]; + os_log_debug([KMLogs oskLog], "OSKWindowController windowWillClose, updating settings writeShowOsk to NO"); +} + - (void)helpAction:(id)sender { NSString *kvkPath = [self AppDelegate].kvk.filePath; if (!kvkPath) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib index 753a59365a9..b753e438028 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib @@ -2,7 +2,7 @@ - + @@ -14,7 +14,7 @@ - + diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m b/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m index 057f401cbe4..89e63388523 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m @@ -1,4 +1,4 @@ -/** +/* * Keyman is copyright (C) SIL International. MIT License. * * PrivacyConsent.m @@ -7,8 +7,8 @@ * Created by Shawn Schantz on 2022-09-22. * * Used to determine if the user has provided consent to the services it needs - * and, if not, to request that consent. For versions of macOS prior to 10.15, - * Keyman requires Accessibility access. For 10.15 and later, Keyman requires + * and, if not, to request that consent. For versions of macOS prior to 11.0, + * Keyman requires Accessibility access. For 11.0 and later, Keyman requires * PostEvent access. Both are presented to the user as a need for Accessibility * when prompted by the system and are listed under Accessibility in the Privacy * tab in System Preferences, Security & Privacy. Both Accessibility and @@ -16,7 +16,11 @@ * that access is not explicitly needed as long as one of the others is provided * first. * - * Access this functionality through the shared instance. Call + * Note that Apple's documentation states that the PostEvent access APIs are + * available with macOS 10.15 (Catalina) but testing showed that it was only + * available with macOS 11.0 (Big Sur). This is documented with issue #12295. + * + * To access this functionality, used the shared instance. Call * requestPrivacyAccess which will check whether access has been granted. If * not, then a dialog will be presented to the user to inform them that * Accessibility is required. Dismissing this dialog will trigger the API call @@ -24,7 +28,7 @@ * only happen once. If the user was prompted earlier and did not grant access, * they can still go to System Preferences and enable it there. So it is useful * to prompt with a dialog from Keyman at startup. Without Accessibility, the - * shift layer and the OSK do not work -- so Keyman is basically useless. + * shift layer and the OSK do not work. */ #import "PrivacyConsent.h" @@ -43,7 +47,7 @@ + (PrivacyConsent *)shared } /** - * For macOS earlier than 10.15: check for Accessibility access. + * For macOS earlier than 11.0: check for Accessibility access. */ - (BOOL)checkAccessibility { @@ -66,7 +70,7 @@ - (void)requestPrivacyAccess:(void (^)(void))withCompletionHandler BOOL hasAccessibility = NO; // check if we already have accessibility - if (@available(macOS 10.15, *)) { + if (@available(macOS 11.0, *)) { hasAccessibility = [self checkPostEventAccess]; } else { hasAccessibility = [self checkAccessibility]; @@ -85,7 +89,7 @@ - (void)requestPrivacyAccess:(void (^)(void))withCompletionHandler } /** - * For macOS earlier than 10.15: request Accessibility access. + * For macOS earlier than 11.0: request Accessibility access. */ - (void)requestAccessibility { @@ -113,18 +117,18 @@ - (NSWindowController *)privacyDialog { */ - (void)configureDialogForOsVersion { - if (@available(macOS 10.15, *)) { - [self configureDialogForCatalinaAndLater]; + if (@available(macOS 11.0, *)) { + [self configureDialogForBigSurAndLater]; } else { - [self configureDialogForPreCatalina]; + [self configureDialogForPreBigSur]; } } /** - * Initialize privacy dialog for macOS versions prior to Catalina (10.15). + * Initialize privacy dialog for macOS versions prior to Big Sur (11.0). * In this case, request Accessibility access, not PostEvent. */ -- (void)configureDialogForPreCatalina { +- (void)configureDialogForPreBigSur { void (^consentPrompt)(void) = ^(void) { [self requestAccessibility]; @@ -136,10 +140,10 @@ - (void)configureDialogForPreCatalina { } /** - * Initialize privacy dialog for macOS versions of Catalina (10.15) or later. + * Initialize privacy dialog for macOS versions of Big Sur (11.0) or later. * In this case, request PostEvent access, not Accessibility. */ -- (void)configureDialogForCatalinaAndLater { +- (void)configureDialogForBigSurAndLater { void (^consentPrompt)(void) = ^(void) { [self requestPostEventAccess]; @@ -160,7 +164,7 @@ - (void)showPrivacyDialog { /** * Check whether the user has allowed ListenEvent access. - * Only available with macOS Catalina (10.15) or later. + * Only available with macOS Big Sur (11.0) or later. * * If user has granted Accessibility or PostEvent access, then * ListenEvent access is also granted. @@ -170,18 +174,18 @@ - (BOOL)checkListenEventAccess BOOL hasAccess = NO; // below checks for ListenEvent access - if (@available(macOS 10.15, *)) { + if (@available(macOS 11.0, *)) { hasAccess = CGPreflightListenEventAccess(); os_log([KMLogs privacyLog], "CGPreflightListenEventAccess() returned %@", hasAccess ? @"YES" : @"NO"); } else { - os_log([KMLogs privacyLog], "CGPreflightListenEventAccess not available before macOS version 10.15"); + os_log([KMLogs privacyLog], "CGPreflightListenEventAccess not available before macOS version 11.0"); } return hasAccess; } /** * Check whether the user has allowed PostEvent access. - * Only available with macOS Catalina (10.15) or later. + * Only available with macOS Big Sur (11.0) or later. * * If user has granted Accessibility, then PostEvent access is also granted. */ @@ -190,46 +194,46 @@ - (BOOL)checkPostEventAccess BOOL hasAccess = NO; // below checks for PostEvent access - if (@available(macOS 10.15, *)) { + if (@available(macOS 11.0, *)) { hasAccess = CGPreflightPostEventAccess(); os_log([KMLogs privacyLog], "CGPreflightPostEventAccess() returned %@", hasAccess ? @"YES" : @"NO"); } else { - os_log([KMLogs privacyLog], "CGPreflightPostEventAccess not available before macOS version 10.15"); + os_log([KMLogs privacyLog], "CGPreflightPostEventAccess not available before macOS version 11.0"); } return hasAccess; } /** * Request ListenEvent access. - * Only available with macOS Catalina (10.15) or later. + * Only available with macOS Big Sur (11.0) or later. */ - (BOOL)requestListenEventAccess { BOOL granted = NO; - if (@available(macOS 10.15, *)) { + if (@available(macOS 11.0, *)) { granted = CGRequestListenEventAccess(); os_log([KMLogs privacyLog], "CGRequestListenEventAccess() returned %@", granted ? @"YES" : @"NO"); } else { - os_log([KMLogs privacyLog], "CGRequestListenEventAccess not available before macOS version 10.15"); + os_log([KMLogs privacyLog], "CGRequestListenEventAccess not available before macOS version 11.0"); } return granted; } /** * Request PostEvent access. - * Only available with macOS Catalina (10.15) or later. + * Only available with macOS Big Sur (11.0) or later. */ - (BOOL)requestPostEventAccess { BOOL granted = NO; - if (@available(macOS 10.15, *)) { + if (@available(macOS 11.0, *)) { granted = CGRequestPostEventAccess(); os_log([KMLogs privacyLog], "CGRequestPostEventAccess() returned %@", granted ? @"YES" : @"NO"); } else { - os_log([KMLogs privacyLog], "CGRequestPostEventAccess not available before macOS version 10.15"); + os_log([KMLogs privacyLog], "CGRequestPostEventAccess not available before macOS version 11.0"); } return granted; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h index 6c80417faf3..81cc4c3b348 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h @@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly) NSString *clientApplicationId; -(instancetype)initWithClient:(id) client applicationId:(NSString *)appId; +-(NSString *)description; -(void)checkCompliance:(id) client; -(void) checkComplianceAfterInsert:(id) client delete:(NSString *)textToDelete insert:(NSString *)textToInsert; -(BOOL)isComplianceUncertain; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m index 0f62fa61a8a..e83439794b6 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m @@ -73,7 +73,7 @@ -(instancetype)initWithClient:(id) client applicationId:(NSString *)appId { -(NSString *)description { - return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelectionApi: %d, canReadText: %d, canReplaceText: %d, mustBackspaceUsingEvents: %d, clientAppId: %@, client: %@", self.complianceUncertain, self.hasCompliantSelectionApi, [self canReadText], [self canReplaceText], [self mustBackspaceUsingEvents], _clientApplicationId, _client]; + return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelectionApi: %d, canReadText: %d, canReplaceText: %d, mustBackspaceUsingEvents: %d, clientApplicationId: %@, client: %@", self.complianceUncertain, self.hasCompliantSelectionApi, [self canReadText], [self canReplaceText], [self mustBackspaceUsingEvents], _clientApplicationId, _client]; } /** test to see if the API selectedRange functions properly for the text input client */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/Localizable.strings index f192a323683..31b54577b28 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/cs.lproj/Localizable.strings @@ -26,10 +26,10 @@ "message-keyboard-file-unreadable" = "Nelze číst soubor Keymana '%@'."; /* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error loading keyboard)"; +"message-error-loading-keyboard" = "(chyba při načítání klávesnice)"; /* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "unknown"; +"message-error-unknown-metadata" = "neznámé"; /* Button text to acknowledge that .kmp file could not be read */ "button-keyboard-file-unreadable" = "OK"; @@ -50,22 +50,25 @@ "button-download-complete" = "Hotovo"; /* keyboards label in the Package Information window */ -"keyboards-label" = "Keyboards:"; +"keyboards-label" = "Klávesnice:"; /* fonts label in the Package Information window */ -"fonts-label" = "Fonts:"; +"fonts-label" = "Písma:"; /* package version label in the Package Information window */ -"package-version-label" = "Package Version:"; +"package-version-label" = "Verze balíčku:"; /* author label in the Package Information window */ -"author-label" = "Author:"; +"author-label" = "Autor:"; /* website label in the Package Information window */ -"website-label" = "Website:"; +"website-label" = "Webová stránka:"; /* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; +"copyright-label" = "Autorská práva:"; /* message displayed to alert user to need grant accessibility permission */ -"privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; +"privacy-alert-text" = "Ke správné funkci vyžaduje Keyman zpřístupnění:\n\nPovolte přístup v Předvolbách systému, Zabezpečení a soukromí.\nRestartujte systém."; + +/* Text of menu item in Input Menu when no Keyboards are configured -- include parentheses */ +"no-keyboard-configured-menu-placeholder" = "(Žádná klávesnice nenastavena)"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/Localizable.strings new file mode 100644 index 00000000000..006c96a99c2 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/Localizable.strings @@ -0,0 +1,74 @@ +/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ +"message-confirm-delete-keyboard" = "Εἶσθε βέβαιοι ὅτι θέλετε νὰ διαγράψετε τὸ πληκτρολόγιο '%@';"; + +/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ +"info-cannot-undo-delete-keyboard" = "Δὲν μπορεῖτε νὰ ἀναιρέσετε αὐτὴν τὴν ἐνέργεια."; + +/* Button text to cancel delete of the specified installed Keyman keyboard */ +"button-cancel-delete-keyboard" = "Ἀκυρῶστε"; + +/* Button text to confirm delete of the specified installed Keyman keyboard */ +"button-delete-keyboard" = "Διαγράψτε"; + +/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ +"message-confirm-install-keyboard" = "Θὰ ἐγκαταστήσετε πληκτρολόγιο Keyman;"; + +/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ +"info-install-keyboard-filename" = "Θέλετε ἡ ἐφαρμογὴ Keyman νὰ ἐγκαταστήσει τὸ πληκτρολόγιο ἀπὸ τὸ ἀρχεῖο '%@';"; + +/* Button text to cancel installation of the double-clicked Keyman file */ +"button-cancel-install-keyboard" = "Ἀκυρῶστε"; + +/* Button text to confirm installation of the double-clicked Keyman file */ +"button-install-keyboard" = "Ἐγκαταστῆστε"; + +/* Message displayed to inform user that .kmp file could not be read/unzipped */ +"message-keyboard-file-unreadable" = "Δὲν μπορέσαμε νὰ διαβάσομε τὸ ἀρχεῖο Keyman '%@'."; + +/* Message displayed in Configuration window when keyboard cannot be loaded */ +"message-error-loading-keyboard" = "(σφάλμα κατὰ τὴν φόρτωση πληκτρολογίου)"; + +/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ +"message-error-unknown-metadata" = "ἄγνωστο"; + +/* Button text to acknowledge that .kmp file could not be read */ +"button-keyboard-file-unreadable" = "OK"; + +/* label text to identify Keyman version */ +"version-label-text" = "Ἔκδοση: %@"; + +/* Status text for keyboard downloading */ +"message-keyboard-downloading" = "Μεταφόρτωση σὲ ἐξέλιξη..."; + +/* Status text for keyboard download complete */ +"message-keyboard-download-complete" = "Ὁλοκλήρωση φόρτωσης."; + +/* Button text to cancel downloading keyboard */ +"button-cancel-downloading" = "Ἀκυρῶστε"; + +/* Button text keyboard download complete */ +"button-download-complete" = "Ἕτοιμοι"; + +/* keyboards label in the Package Information window */ +"keyboards-label" = "Πληκτρολόγια:"; + +/* fonts label in the Package Information window */ +"fonts-label" = "Γραμματοσειρές:"; + +/* package version label in the Package Information window */ +"package-version-label" = "Ἔκδοση πακέτου:"; + +/* author label in the Package Information window */ +"author-label" = "Δημιουργός:"; + +/* website label in the Package Information window */ +"website-label" = "Ἱστότοπος:"; + +/* copyright label in the Package Information window */ +"copyright-label" = "Πνευματικὰ δικαιώματα:"; + +/* message displayed to alert user to need grant accessibility permission */ +"privacy-alert-text" = "Τὸ Keyman, γιὰ νὰ λειτουργήσει σωστά, ἀπαιτεῖ ὁρισμένες δυνατότητες προσπέλασης:\n\nἘπιτρέψτε τὴν πρόσβαση στὶς Προτιμήσεις Συστήματος, τὴν Ἀσφάλεια & Ἰδιωτικότητα.\nἘπανεκκινῆστε τὸ σύστημά σας."; + +/* Text of menu item in Input Menu when no Keyboards are configured -- include parentheses */ +"no-keyboard-configured-menu-placeholder" = "(Δὲν ἔχει ἐγκατασταθεῖ πληκτρολόγιο)"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/MainMenu.strings new file mode 100644 index 00000000000..dc5aae4d89c --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/el.lproj/MainMenu.strings @@ -0,0 +1,14 @@ +/* Configuration window menu text */ +"P1b-lE-yFw.title" = "Διαμόρφωση..."; + +/* Keyboards menu text */ +"bQa-j9-nHe.title" = "Πληκτρολόγια"; + +/* Keyboards menu text */ +"goP-aK-3WB.title" = "Πληκτρολόγια"; + +/* About menu text */ +"kb2-ww-RS3.title" = "Περὶ ἡμῶν"; + +/* On-Screen Keyboard menu text */ +"s96-JI-YDh.title" = "Πληκτρολόγιο ὀθόνης"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/Localizable.strings index adb2ea787e8..f1264dd4030 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/it.lproj/Localizable.strings @@ -26,10 +26,10 @@ "message-keyboard-file-unreadable" = "Impossibile leggere il file Keyman «%@»."; /* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error loading keyboard)"; +"message-error-loading-keyboard" = "(errore caricamento tastiera)"; /* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "unknown"; +"message-error-unknown-metadata" = "sconosciuto"; /* Button text to acknowledge that .kmp file could not be read */ "button-keyboard-file-unreadable" = "OK"; @@ -50,22 +50,25 @@ "button-download-complete" = "Fatto"; /* keyboards label in the Package Information window */ -"keyboards-label" = "Keyboards:"; +"keyboards-label" = "Tastiere:"; /* fonts label in the Package Information window */ -"fonts-label" = "Fonts:"; +"fonts-label" = "Caratteri:"; /* package version label in the Package Information window */ -"package-version-label" = "Package Version:"; +"package-version-label" = "Versione pacchetto:"; /* author label in the Package Information window */ -"author-label" = "Author:"; +"author-label" = "Autore:"; /* website label in the Package Information window */ -"website-label" = "Website:"; +"website-label" = "Sito web:"; /* copyright label in the Package Information window */ "copyright-label" = "Copyright:"; /* message displayed to alert user to need grant accessibility permission */ -"privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; +"privacy-alert-text" = "Per funzionare correttamente, Keyman richiede funzionalità di accessibilità:\n\nConsenti l'accesso nelle preferenze di sistema, Sicurezza e Privacy.\nRiavvia il sistema."; + +/* Text of menu item in Input Menu when no Keyboards are configured -- include parentheses */ +"no-keyboard-configured-menu-placeholder" = "(Nessuna tastiera configurata)"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/Localizable.strings index de298fb21b3..591f85f4f00 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/pt-PT.lproj/Localizable.strings @@ -26,10 +26,10 @@ "message-keyboard-file-unreadable" = "Não foi possível ler o arquivo Keyman '%@'."; /* Message displayed in Configuration window when keyboard cannot be loaded */ -"message-error-loading-keyboard" = "(error loading keyboard)"; +"message-error-loading-keyboard" = "(erro ao carregar teclado)"; /* Message displayed in Configuration window when keyboard metadata cannot be loaded */ -"message-error-unknown-metadata" = "unknown"; +"message-error-unknown-metadata" = "desconhecido"; /* Button text to acknowledge that .kmp file could not be read */ "button-keyboard-file-unreadable" = "OK"; @@ -50,22 +50,25 @@ "button-download-complete" = "Concluído"; /* keyboards label in the Package Information window */ -"keyboards-label" = "Keyboards:"; +"keyboards-label" = "Teclados:"; /* fonts label in the Package Information window */ -"fonts-label" = "Fonts:"; +"fonts-label" = "Tipos de letra:"; /* package version label in the Package Information window */ -"package-version-label" = "Package Version:"; +"package-version-label" = "Versão do pacote:"; /* author label in the Package Information window */ -"author-label" = "Author:"; +"author-label" = "Autor:"; /* website label in the Package Information window */ -"website-label" = "Website:"; +"website-label" = "Página web:"; /* copyright label in the Package Information window */ -"copyright-label" = "Copyright:"; +"copyright-label" = "Direitos de autor:"; /* message displayed to alert user to need grant accessibility permission */ -"privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; +"privacy-alert-text" = "Para funcionar corretamente, o Keyman requer recursos de acessibilidade:\n\nConceda acesso em Definições do Sistema, Segurança e Privacidade.\nReinício do sistema."; + +/* Text of menu item in Input Menu when no Keyboards are configured -- include parentheses */ +"no-keyboard-configured-menu-placeholder" = "(Nenhum teclado configurado)"; diff --git a/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m b/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m index 8eb9d974ceb..45e5e9855ae 100644 --- a/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m +++ b/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m @@ -13,25 +13,32 @@ #import "KMInputMethodEventHandler.h" #import "AppleCompliantTestClient.h" #import "TextApiCompliance.h" +#import "KMSettingsRepository.h" +#import "KMDataRepository.h" +#import KMInputMethodEventHandler *testEventHandler = nil; +id testClient = nil; @interface InputMethodTests : XCTestCase @end @interface KMInputMethodEventHandler (Testing) - +@property (nonatomic, retain) TextApiCompliance* apiCompliance; +@property (nonatomic, retain) NSString* clientApplicationId; +@property BOOL contextChanged; - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender; - (NSRange) calculateInsertRangeForDeletedText:(NSString*)textToDelete selectionRange:(NSRange) selection; +- (void)checkTextApiCompliance:(id)client; @end @implementation InputMethodTests - (void)setUp { - id client = [[AppleCompliantTestClient alloc] init]; + testClient = [[AppleCompliantTestClient alloc] init]; NSString *clientAppId = @"com.compliant.app"; - testEventHandler = [[KMInputMethodEventHandler alloc]initWithClient:clientAppId client:client]; + testEventHandler = [[KMInputMethodEventHandler alloc]initWithClient:clientAppId client:testClient]; } - (void)tearDown { @@ -85,4 +92,43 @@ - (void)testCalculateInsertRange_deleteOneBMPCharacterWithOneSelected_returnsRan XCTAssertTrue(correctResult, @"insert or replacement range expected to be {1,2}"); } +/** + * test compliance check of a KMInputMethodEventHandler with a nil client application ID + * not sure if this can ever happen, but the lifecycle of the input method is not 100% clear, + * and we would like this scenario, if it can occur, to not result in a crash + */ +- (void)testCheckCompliance_withUnknownApplicationId_createsComplianceObject { + id client = [[AppleCompliantTestClient alloc] init]; + KMInputMethodEventHandler *eventHandler = [[KMInputMethodEventHandler alloc]initWithClient:nil client:client]; + [eventHandler checkTextApiCompliance:client]; + XCTAssertNotNil(eventHandler.apiCompliance, @"apiCompliance object was not created"); +} + +- (void)testCheckCompliance_withNilComplianceObject_createsComplianceObject { + [testEventHandler checkTextApiCompliance:testClient]; + XCTAssertNotNil(testEventHandler.apiCompliance, @"apiCompliance object was not created"); +} + +- (void)testCheckCompliance_withChangedClientApplicationId_createsNewComplianceObject { + // first call causes textApiCompliance object to be created + [testEventHandler checkTextApiCompliance:testClient]; + TextApiCompliance *originalComplianceObject = testEventHandler.apiCompliance; + + testEventHandler.clientApplicationId = @"com.different.app"; + // second call causes new textApiCompliance object to be created due to stale application ID + [testEventHandler checkTextApiCompliance:testClient]; + XCTAssertNotEqualObjects(originalComplianceObject, testEventHandler.apiCompliance, @"New TextApiCompliance object not created for new client application ID"); +} + +- (void)testCheckCompliance_withContextChanged_createsNewComplianceObject { + // first call causes textApiCompliance object to be created + [testEventHandler checkTextApiCompliance:testClient]; + TextApiCompliance *originalComplianceObject = testEventHandler.apiCompliance; + + testEventHandler.contextChanged = YES; + // second call causes new textApiCompliance object to be created due to setting contextChanged flag + [testEventHandler checkTextApiCompliance:testClient]; + XCTAssertNotEqualObjects(originalComplianceObject, testEventHandler.apiCompliance, @"New TextApiCompliance object not created after contextChanged flag set"); +} + @end diff --git a/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.h b/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.h index fafd6f19057..8cb043d4105 100644 --- a/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.h +++ b/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.h @@ -22,7 +22,6 @@ typedef void(^PostEventCallback)(CGEventRef eventToPost); @property (nonatomic, assign) CFMachPortRef lowLevelEventTap; // Always nil for tests @property (nonatomic, assign) BOOL contextChangingEventDetected; @property (nonatomic, assign) BOOL useNullChar; -@property (nonatomic, assign) BOOL debugMode; @property (nonatomic, assign) CGKeyCode virtualKeyPosted; // Helper method diff --git a/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.m b/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.m index 4b247056af2..b2d1f859a2f 100644 --- a/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.m +++ b/mac/Keyman4MacIM/KeymanTests/TestAppDelegate.m @@ -16,7 +16,7 @@ @implementation TestAppDelegate - (KMEngine *)kme { if (_kme == nil) { - _kme = [[KMEngine alloc] initWithKMX:nil context:self.contextBuffer verboseLogging:self.debugMode]; + _kme = [[KMEngine alloc] initWithKMX:nil context:self.contextBuffer]; } return _kme; @@ -42,10 +42,6 @@ - (void)setContextBuffer:(NSMutableString *)contextBuffer { [self.kme setCoreContextIfNeeded:self.contextBuffer]; } -- (BOOL)debugMode { - return YES; -} - -(NSEvent *)keyStrokeEventForCharacter:(NSString *)character keyCode:(unsigned short) keyCode { return [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSZeroPoint modifierFlags:0 timestamp:NSTimeIntervalSince1970 windowNumber:0 context:nil characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode]; } diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.h index 0345ea6f31e..ba3e5031f3d 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.h @@ -16,13 +16,10 @@ extern UInt32 VirtualKeyMap[0x80]; @interface CoreHelper : NSObject -@property (assign, nonatomic) BOOL debugMode; - -(unichar const *) createUnicharStringFromNSString:(NSString *)string; -(NSString *) createNSStringFromUnicharString:(unichar const *)string; -(unsigned long long) unicharStringLength:(unichar const *)string; --(instancetype)initWithDebugMode:(BOOL)debugMode; -(unsigned short) macVirtualKeyToWindowsVirtualKey:(unsigned short) keyCode; -(UTF32Char)macToKeymanModifier:(NSEventModifierFlags)modifiers; -(NSString*)utf32ValueToString:(UTF32Char)scalarValue; diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m index 46ddbdeeb14..c7858e7b0d9 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m @@ -69,10 +69,9 @@ -(unsigned long) scalarValueStringLength:(UTF32Char const *)string { return length; } --(instancetype)initWithDebugMode:(BOOL)debugMode { +-(instancetype)init { self = [super init]; if (self) { - _debugMode = debugMode; [self initVirtualKeyMapping]; } return self; diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/Info.plist b/mac/KeymanEngine4Mac/KeymanEngine4Mac/Info.plist index 970606a26a0..3f42d3e63f7 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/Info.plist +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - Keyman Engine $(PRODUCT_VERSION) for macOS + $(PRODUCT_VERSION) CFBundleSignature ???? CFBundleVersion diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.h index c79b02ba32b..523415fd26a 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.h @@ -18,16 +18,14 @@ @interface KMEngine : NSObject @property (weak, nonatomic) KMXFile *kmx; -@property (assign, nonatomic) BOOL debugMode; -- (id)initWithKMX:(KMXFile *)kmx context:(NSString *)ctxBuf verboseLogging:(BOOL)enableDebugLogging; +- (id)initWithKMX:(KMXFile *)kmx context:(NSString *)ctxBuf; - (NSString *)getCoreContextDebug; - (void)clearCoreContext; - (void)setCoreContextIfNeeded:(NSString *)context; - (void)setCoreOptions:(NSString *)key withValue:(NSString *)value; - (CoreKeyOutput *)processEvent:(NSEvent *)event; -- (void)setUseVerboseLogging:(BOOL)useVerboseLogging; @end diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m index dd2b3247ede..4578472b6c5 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m @@ -25,16 +25,11 @@ @interface KMEngine () @implementation KMEngine -NSMutableString* _easterEggForSentry = nil; -const NSString* kEasterEggText = @"Sentrycrash#KME"; -const NSString* kEasterEggKmxName = @"EnglishSpanish.kmx"; - -- (id)initWithKMX:(KMXFile *)kmx context:(NSString *)contextString verboseLogging:(BOOL)enableDebugLogging { +- (id)initWithKMX:(KMXFile *)kmx context:(NSString *)contextString { self = [super init]; if (self) { - self.debugMode = enableDebugLogging; _kmx = kmx; - _coreHelper = [[CoreHelper alloc] initWithDebugMode:enableDebugLogging]; + _coreHelper = [[CoreHelper alloc] init]; if (kmx) { [self loadCoreWrapperFromKmxFile:self.kmx.filePath]; @@ -63,31 +58,6 @@ -(void)setKmx:(KMXFile*) kmxFile { } } -- (void)setUseVerboseLogging:(BOOL)useVerboseLogging { - self.debugMode = useVerboseLogging; - - if (self.coreHelper) { - self.coreHelper.debugMode = useVerboseLogging; - } - - if (useVerboseLogging) { - os_log_debug([KMELogs testLog], "KMEngine - Turning verbose logging on"); - // In Keyman Engine if "debugMode" is turned on (explicitly) with "English plus Spanish" as the current keyboard and you type "Sentrycrash#KME", - // it will force a simulated crash to test reporting to sentry.keyman.com. - NSString * kmxName = [[_kmx filePath] lastPathComponent]; - os_log_debug([KMELogs testLog], "Sentry - KME: _kmx name = %{public}@", kmxName); - if ([kEasterEggKmxName isEqualToString:kmxName]) { - os_log_debug([KMELogs testLog], "Sentry - KME: Preparing to detect Easter egg."); - _easterEggForSentry = [[NSMutableString alloc] init]; - } - else - _easterEggForSentry = nil; - } - else { - os_log_debug([KMELogs testLog], "KMEngine - Turning verbose logging off"); - } -} - - (NSString *)getCoreContextDebug { return self.coreWrapper.contextDebug; } @@ -115,43 +85,6 @@ - (CoreKeyOutput *)processEvent:(NSEvent *)event { return [self.coreWrapper processEvent:event]; } -- (void) processPossibleEasterEggCharacterFrom:(NSString *)characters { - NSUInteger len = [_easterEggForSentry length]; - os_log_debug([KMELogs keyLog], "Sentry - KME: Processing character(s): %{public}@", characters); - if ([characters length] == 1 && [characters characterAtIndex:0] == [kEasterEggText characterAtIndex:len]) { - NSString *characterToAdd = [kEasterEggText substringWithRange:NSMakeRange(len, 1)]; - os_log_debug([KMELogs keyLog], "Sentry - KME: Adding character to Easter Egg code string: %{public}@", characterToAdd); - [_easterEggForSentry appendString:characterToAdd]; - if ([kEasterEggText isEqualToString:_easterEggForSentry]) { - os_log_debug([KMELogs keyLog], "Sentry - KME: Forcing crash now!"); - // Both of the following approaches do throw an exception that causes control to exit this method, - // but at least in my debug builds locally, neither one seems to get picked up by Sentry in a - // way that results in a new report on sentry.keyman.com - -#ifndef USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE - //#1 - @throw ([NSException exceptionWithName:@"SentryForce" reason:@"Easter egg hit" userInfo:nil]); - - //#2 - // NSDecimalNumber *i = [NSDecimalNumber decimalNumberWithDecimal:[@(1) decimalValue]]; - // NSDecimalNumber *o = [NSDecimalNumber decimalNumberWithDecimal:[@(0) decimalValue]]; - // // Divide by 0 to throw an exception - // NSDecimalNumber *x = [i decimalNumberByDividingBy:o]; - -#else - //#3 The following DOES work, but it's really lame because the crash actually gets forced in the IM - // via this bogus call to a protocol method implemented in the IM's App Delegate just for the - // purpose of enabling the engine to force a crash. - [(NSObject *)[NSApp delegate] alertShowHelp:[NSAlert alertWithMessageText:@"Forcing an error" defaultButton:nil alternateButton:nil otherButton:nil informativeTextWithFormat:@"Forcing an Easter egg error from KME!"]]; -#endif - } - } - else if (len > 0) { - os_log_debug([KMELogs keyLog], "Sentry - KME: Clearing Easter Egg code string."); - [_easterEggForSentry setString:@""]; - } -} - - (NSString *)getCharsFromKeyCode:(UInt16)keyCode { TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardLayoutInputSource(); CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData); diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/KeyView.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/KeyView.m index c1dfa75a87a..5b31cf8ffaa 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/KeyView.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/KeyView.m @@ -41,7 +41,10 @@ @implementation KeyView - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { - os_log_debug([KMELogs oskLog], "KeyView initWithFrame: %{public}@, bounds: %{public}@, default clipsToBounds %{public}@", NSStringFromRect(frame), NSStringFromRect(self.bounds), self.clipsToBounds?@"YES":@"NO"); + /* + // usually too much logging, uncomment for debugging + os_log_debug([KMELogs oskLog], "KeyView initWithFrame: %{public}@, bounds: %{public}@, default clipsToBounds %{public}@", NSStringFromRect(frame), NSStringFromRect(self.bounds), self.clipsToBounds?@"YES":@"NO"); + */ self.clipsToBounds = true; CGSize size = frame.size; CGFloat x = size.width*0.05; @@ -70,7 +73,10 @@ - (id)initWithFrame:(NSRect)frame { } - (void)drawRect:(NSRect)rect { + /* + // usually too much logging, uncomment for debugging os_log_debug([KMELogs uiLog], "KeyView drawRect: %{public}@, bounds: %{public}@, keyCode: 0x%lx, caption: %{public}@, label: %{public}@", NSStringFromRect(rect), NSStringFromRect(self.bounds), self.keyCode, self.caption.stringValue, self.label.stringValue); + */ [[self getOpaqueColorWithRed:241 green:242 blue:242] setFill]; NSRectFillUsingOperation(rect, NSCompositingOperationSourceOver); diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKKey.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKKey.m index d81097d3e2f..b1144a17fa9 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKKey.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKKey.m @@ -14,7 +14,10 @@ @implementation OSKKey - (id)initWithKeyCode:(NSUInteger)keyCode caption:(NSString *)caption scale:(CGFloat)scale { self = [super init]; if (self) { - os_log_debug([KMELogs oskLog], "OSKKey initWithKeyCode: 0x%lx, caption: %{public}@, scale: %f", keyCode, caption, scale); + /* + // usually too much logging, uncomment for debugging + os_log_debug([KMELogs oskLog], "OSKKey initWithKeyCode: 0x%lx, caption: %{public}@, scale: %f", keyCode, caption, scale); + */ _keyCode = keyCode; if (caption == nil) diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.m index f5154fa76da..512920e0528 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/OSKView.m @@ -43,7 +43,7 @@ - (id)initWithFrame:(NSRect)frame { } - (void)drawRect:(NSRect)rect { - os_log_debug([KMELogs uiLog], "OSKView drawRect: %{public}@", NSStringFromRect(rect)); + os_log_debug([KMELogs oskLog], "OSKView drawRect: %{public}@", NSStringFromRect(rect)); CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] CGContext]; CGContextSetLineJoin(context, kCGLineJoinRound); diff --git a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreTestStaticHelperMethods.m b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreTestStaticHelperMethods.m index 0f0aaf8a830..9954f4e01da 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreTestStaticHelperMethods.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreTestStaticHelperMethods.m @@ -16,7 +16,7 @@ @implementation CoreTestStaticHelperMethods + (CoreHelper *)helper { static CoreHelper *coreHelper = nil; if (coreHelper == nil) { - coreHelper = [[CoreHelper alloc] initWithDebugMode:YES]; + coreHelper = [[CoreHelper alloc] init]; } return coreHelper; } diff --git a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m index 290f30f302b..2ebe487f8cb 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m @@ -32,7 +32,7 @@ - (void)tearDown { } - (void)testinitWithKMX_NilKmx_ProcessEventReturnsNil { - KMEngine *engine = [[KMEngine alloc] initWithKMX:nil context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:nil context:@""]; NSEvent *event = [[NSEvent alloc] init]; CoreKeyOutput *output = [engine processEvent:event]; XCTAssert(output == nil, @"Expected processEvent to return nil for nil kmx"); @@ -40,7 +40,7 @@ - (void)testinitWithKMX_NilKmx_ProcessEventReturnsNil { - (void)testinitWithKMX_ValidKmxEmptyContext_InitializedWithEmptyContext { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; XCTAssert(engine != nil, @"Expected non-nil engine"); // Note: relying on km_core_state_context_debug output format is just barely // acceptable for a unit test @@ -49,7 +49,7 @@ - (void)testinitWithKMX_ValidKmxEmptyContext_InitializedWithEmptyContext { - (void)testinitWithKMX_ValidKmxNonEmptyContext_InitializedWithContext { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"abc" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"abc"]; XCTAssert(engine != nil, @"Expected non-nil engine"); // Note: relying on km_core_state_context_debug output format is just barely // acceptable for a unit test @@ -58,7 +58,7 @@ - (void)testinitWithKMX_ValidKmxNonEmptyContext_InitializedWithContext { - (void)testsetCoreContextIfNeeded_NonEmptyContext_InitialContextUpdated { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"a" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"a"]; [engine setCoreContextIfNeeded:@"xyz"]; // Note: relying on km_core_state_context_debug output format is just barely // acceptable for a unit test @@ -69,7 +69,7 @@ - (void)testsetCoreContextIfNeeded_NonEmptyContext_InitialContextUpdated { /* - (void)testsetCoreContextIfNeeded_EmptyContext_InitialContextUpdated { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; [engine setCoreContextIfNeeded:@"xyz"]; // Note: relying on km_core_state_context_debug output format is just barely // acceptable for a unit test @@ -79,7 +79,7 @@ - (void)testsetCoreContextIfNeeded_EmptyContext_InitialContextUpdated { - (void)testprocessEvent_eventForCommandKey_ReturnsNilOutput { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:NSEventModifierFlagCommand timestamp:0 windowNumber:0 context:nil characters:@"a" charactersIgnoringModifiers:@"a" isARepeat:NO keyCode:kVK_ANSI_A]; CoreKeyOutput *output = [engine processEvent:event]; XCTAssert(output == nil, @"expected nil output from core"); @@ -87,7 +87,7 @@ - (void)testprocessEvent_eventForCommandKey_ReturnsNilOutput { - (void)testprocessEvent_eventWithoutKeycode_ReturnsNilOutput { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; NSEvent *event = [NSEvent mouseEventWithType:NSEventTypeMouseMoved location:NSMakePoint(29, 21) modifierFlags:NSEventModifierFlagShift timestamp:0 windowNumber:0 context:nil eventNumber:23 clickCount:0 pressure:0]; CoreKeyOutput *output = [engine processEvent:event]; XCTAssert(output == nil, @"nil CoreKeyOutput"); @@ -96,7 +96,7 @@ - (void)testprocessEvent_eventWithoutKeycode_ReturnsNilOutput { // TODO: shouldn't this return emitKeystroke = YES? - (void)testprocessEvent_eventForUnmappedKey_ReturnsNoActions { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"z" charactersIgnoringModifiers:@"z" isARepeat:NO keyCode:kVK_ANSI_Z]; CoreKeyOutput *output = [engine processEvent:event]; XCTAssert(output == nil, @"Expected nil array of actions"); @@ -104,7 +104,7 @@ - (void)testprocessEvent_eventForUnmappedKey_ReturnsNoActions { - (void)testprocessEvent_eventForLowercaseA_ReturnsCharacterActionWithExpectedCharacterBasedOnKmx { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"a" charactersIgnoringModifiers:@"a" isARepeat:NO keyCode:kVK_ANSI_A]; CoreKeyOutput *output = [engine processEvent:event]; os_log_debug([KMELogs testLog], "output = %{public}@", output); @@ -116,7 +116,7 @@ - (void)testprocessEvent_eventForLowercaseA_ReturnsCharacterActionWithExpectedCh /* -(void)testprocessEvent_eventForCtrlShiftNumeralWithCipherMusicKmx_ReturnsQstrActionForCorrectUnicodeSurrogatePair { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForCipherMusicTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile contextBuffer:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile contextBuffer:@""]; for (int i = 1; i <= 6; i++) { unsigned short ansiCode = kVK_ANSI_1 + i - 1; @@ -152,7 +152,7 @@ -(void)testprocessEvent_eventForCtrlShiftNumeralWithCipherMusicKmx_ReturnsQstrAc - (void)testprocessEvent_eventsForOpenCurlyBraceWithCipherMusicKmx_ReturnsCharacterForStartSlide { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForCipherMusicTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; UTF32Char expectedUtf32Char = 0x1D177; NSString * expectedStartSlideSurrogatePair = [[NSString alloc] initWithBytes:&expectedUtf32Char length:4 encoding:NSUTF32LittleEndianStringEncoding]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:NSEventModifierFlagShift timestamp:0 windowNumber:0 context:nil characters:@"{" charactersIgnoringModifiers:@"[" isARepeat:NO keyCode:kVK_ANSI_LeftBracket]; @@ -165,7 +165,7 @@ - (void)testprocessEvent_eventsForOpenCurlyBraceWithCipherMusicKmx_ReturnsCharac // TODO: fails with core, investigate - (void)testprocessEvent_eventForUnshiftedNumeralWithCipherMusicKmx_ReturnsCharacterActionToInsertNumeral { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForCipherMusicTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile contextBuffer:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile contextBuffer:@""]; for (int i = 1; i <= 9; i++) { unsigned short ansiCode = kVK_ANSI_1 + i - 1; @@ -185,7 +185,7 @@ - (void)testprocessEvent_eventForUnshiftedNumeralWithCipherMusicKmx_ReturnsChara // TODO: fails with core, returns CharacterAction - (void)testprocessEvent_eventForShiftNumeralsWithoutRulesInCipherMusicKmx_ReturnsNoAction { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForCipherMusicTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; for (int i = 0; i <= 9; i++) { if (i == 6) @@ -228,7 +228,7 @@ - (void)testprocessEvent_eventForShiftNumeralsWithoutRulesInCipherMusicKmx_Retur // TODO: fails with core, returns CharacterAction - (void)testprocessEvent_eventForPeriodWithCipherMusicKmx_ReturnsNoAction { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForCipherMusicTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"." charactersIgnoringModifiers:@"." isARepeat:NO keyCode:kVK_ANSI_Period]; CoreKeyOutput *output = [engine processEvent:event]; XCTAssert(!output.hasTextToInsert, @"expected no text to insert"); @@ -236,7 +236,7 @@ - (void)testprocessEvent_eventForPeriodWithCipherMusicKmx_ReturnsNoAction { - (void)testprocessEvent_eventForCtrl8WithCipherMusicKmx_ReturnsEmit { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForCipherMusicTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:NSEventModifierFlagControl timestamp:0 windowNumber:0 context:nil characters:@"8" charactersIgnoringModifiers:@"8" isARepeat:NO keyCode:kVK_ANSI_8]; CoreKeyOutput *output = [engine processEvent:event]; XCTAssert(output.emitKeystroke, @"emitKeystroke == YES"); @@ -279,7 +279,7 @@ - (void)testprocessEvent_eventForAWithModifiers_ReturnsCharacterActionWithExpect [KMEngineTests fillInNamesAndModifiersForAllChiralCombinations]; KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; for (i = 0; i < nCombinations; i++) { [engine clearCoreContext]; @@ -346,7 +346,7 @@ - (void)testprocessEvent_eventForSWithModifiers_ReturnsCharacterActionWithExpect [KMEngineTests fillInNamesAndModifiersForAllChiralCombinations]; KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; for (i = 0; i < nCombinations; i++) { [engine clearCoreContext]; @@ -419,7 +419,7 @@ - (void)testprocessEvent_eventForSWithModifiers_ReturnsCharacterActionWithExpect - (NSString *)checkPlatform_getOutputForKeystroke: (NSString*) character modifierFlags: (NSEventModifierFlags) flag keyCode:(unsigned short)code { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForPlatformTest]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; NSString *lcChar = [character lowercaseString]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:flag timestamp:0 windowNumber:0 context:nil characters:character charactersIgnoringModifiers:lcChar isARepeat:NO keyCode:code]; CoreKeyOutput *output = [engine processEvent:event]; @@ -516,7 +516,7 @@ - (void)testContextMatch_InvertedPlatformLogic_NotTouch { - (void)testEngine_ipaKeyboardAction_DoesNotCrash_Issue1892 { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForIndexOffsetTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"z" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"z"]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"a" charactersIgnoringModifiers:@"a" isARepeat:NO keyCode:kVK_ANSI_A]; CoreKeyOutput *output = [engine processEvent:event]; XCTAssert(output.hasCodePointsToDelete, @"output hasCodePointsToDelete == YES"); @@ -525,7 +525,7 @@ - (void)testEngine_ipaKeyboardAction_DoesNotCrash_Issue1892 { - (void)testCoreProcessEvent_eventForFWithElNuerKmx_ReturnsCorrectCharacter { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForElNuerTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"f" charactersIgnoringModifiers:@"f" isARepeat:NO keyCode:kVK_ANSI_F]; CoreKeyOutput *output = [engine processEvent:event]; XCTAssert([output.textToInsert isEqualToString:@"ɣ"], @"Expected output to be 'ɣ'."); @@ -535,7 +535,7 @@ - (void)testCoreProcessEvent_eventForFWithElNuerKmx_ReturnsCorrectCharacter { - (void)testArmenianMnemonic_triggerPersistOptions_ReturnsOptions { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForArmenianMnemonicTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"√" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"√"]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"w" charactersIgnoringModifiers:@"w" isARepeat:NO keyCode:kVK_ANSI_W]; CoreKeyOutput *output = [engine processEvent:event]; NSString *key = @"option_ligature_ew"; @@ -546,7 +546,7 @@ - (void)testArmenianMnemonic_triggerPersistOptions_ReturnsOptions { - (void)testCoreProcessEvent_backspaceElNuerEmptyContext_PassesThrough { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForElNuerTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@""]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"\b" charactersIgnoringModifiers:@"\b" isARepeat:NO keyCode:kVK_Delete]; CoreKeyOutput *output = [engine processEvent:event]; os_log_debug([KMELogs testLog], "output = %{public}@", output); @@ -557,7 +557,7 @@ - (void)testCoreProcessEvent_backspaceElNuerEmptyContext_PassesThrough { - (void)testCoreProcessEvent_backspaceElNuerWithContext_EmptiesContextReturnsDelete { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForElNuerTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"ɣ" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"ɣ"]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"\b" charactersIgnoringModifiers:@"\b" isARepeat:NO keyCode:kVK_Delete]; CoreKeyOutput *output = [engine processEvent:event]; os_log_debug([KMELogs testLog], "output = %{public}@", output); @@ -571,7 +571,7 @@ - (void)testCoreProcessEvent_backspaceElNuerWithContext_EmptiesContextReturnsDel - (void)testCoreProcessEvent_eventReturnWithElNuerKmx_EmitWithContextEmpty { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForElNuerTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"ɣ" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"ɣ"]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"\n" charactersIgnoringModifiers:@"\n" isARepeat:NO keyCode:kVK_Return]; CoreKeyOutput *output = [engine processEvent:event]; XCTAssert(output.emitKeystroke, @"Expected emitKeystroke==YES"); @@ -583,7 +583,7 @@ - (void)testCoreProcessEvent_eventReturnWithElNuerKmx_EmitWithContextEmpty { - (void)testCoreProcessEvent_eventTabWithElNuerKmx_EmitWithContextEmpty { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForElNuerTests]; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"ɣ" verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"ɣ"]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"\t" charactersIgnoringModifiers:@"\t" isARepeat:NO keyCode:kVK_Tab]; CoreKeyOutput *output = [engine processEvent:event]; XCTAssert(output.emitKeystroke, @"Expected emitKeystroke==YES"); @@ -596,7 +596,7 @@ - (void)testCoreProcessEvent_eventTabWithElNuerKmx_EmitWithContextEmpty { - (void)testCoreProcessEvent_eventSingleQuoteWithElNuerKmx_ReturnsDiacritic { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForElNuerTests]; NSString *context = @"ɛ"; - KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:context verboseLogging:YES]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:context]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"'" charactersIgnoringModifiers:@"'" isARepeat:NO keyCode:kVK_ANSI_Quote]; CoreKeyOutput *output = [engine processEvent:event]; XCTAssert(output.hasTextToInsert, @"returns text to insert"); diff --git a/oem/firstvoices/android/app/build.gradle b/oem/firstvoices/android/app/build.gradle index 0837c381ad0..fa5dc5a1273 100644 --- a/oem/firstvoices/android/app/build.gradle +++ b/oem/firstvoices/android/app/build.gradle @@ -73,12 +73,17 @@ java { } // how to configure the sentry android gradle plugin -sentry { - // Disables or enables the automatic configuration of Native symbols - uploadNativeSymbols = true - - // Does or doesn't include the source code of native code for Sentry - includeNativeSources = true +task publishSentry { + doLast { + println 'Publishing FirstVoices symbols to Sentry' + sentry { + // Disables or enables the automatic configuration of Native symbols + uploadNativeSymbols = true + + // Does or doesn't include the source code of native code for Sentry + includeNativeSources = true + } + } } String env_keys_json_file = System.getenv("oem_firstvoices_keys_json_file") diff --git a/oem/firstvoices/android/app/src/main/assets/setup/main.html b/oem/firstvoices/android/app/src/main/assets/setup/main.html index 331251981d9..d9cc9893da8 100644 --- a/oem/firstvoices/android/app/src/main/assets/setup/main.html +++ b/oem/firstvoices/android/app/src/main/assets/setup/main.html @@ -98,7 +98,7 @@

To use FirstVoices Keyboards

diff --git a/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/FVShared.java b/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/FVShared.java index 95158826ba5..9b75b3eea95 100644 --- a/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/FVShared.java +++ b/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/FVShared.java @@ -7,22 +7,25 @@ import android.net.Uri; import android.util.Log; import android.widget.Toast; +import com.keyman.engine.JSONParser; import com.keyman.engine.KMManager; import com.keyman.engine.data.Keyboard; import com.keyman.engine.packages.PackageProcessor; import com.keyman.engine.util.KMLog; -import java.io.BufferedReader; +import org.json.JSONArray; +import org.json.JSONObject; + import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -30,6 +33,8 @@ final class FVShared { private static FVShared instance = null; private boolean isInitialized = false; + // File containing keyboard+region info for each keyboard + private static final String FVKeyboards_JSON = "keyboards.json"; private static final String FVLoadedKeyboardList = "loaded_keyboards.dat"; // Keys from earlier versions of app, used only in the upgrade process @@ -107,7 +112,8 @@ public synchronized void initialize(Context context) { } this.context = context.getApplicationContext(); - this.regionList = loadRegionList(); + String keyboardsJSONPath = getPackagesDir() + FVDefault_PackageID + File.separator + FVKeyboards_JSON; + this.regionList = loadRegionList(keyboardsJSONPath); this.loadedKeyboards = loadLoadedKeyboardList(); isInitialized = true; @@ -123,54 +129,86 @@ public static FVShared getInstance() { return instance; } - private FVRegionList loadRegionList() { - FVRegionList list = new FVRegionList(); - try { - // At this point in initialization, fv_all.kmp hasn't been extracted, so - // we get all the keyboard info from keyboards.csv - InputStream inputStream = context.getAssets().open("keyboards.csv"); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - - reader.readLine(); // skip header row - String line = reader.readLine(); - - while (line != null) { - while (line.contains(",,")) - line = line.replace(",,", ", ,"); - - String[] values = line.split(","); - if (values != null && values.length > 0) { - // Read in column info - String kbId = values[1]; - String kbName = values[2]; - String regionName = values[3]; - String legacyId = values[4]; - String version = values[5]; - String lgId = values[6].toLowerCase(); // Normalize language ID - String lgName = values[7]; - - FVRegion region = list.findRegion(regionName); - if(region == null) { - region = new FVRegion(regionName); - list.add(region); - } - - FVKeyboard keyboard = new FVKeyboard(kbId, kbName, legacyId, version, lgId, lgName); - - region.keyboards.add(keyboard); - } + /** + * Parse keyboards.json file to associate region info for each keyboard + * @param keyboardsJSONPath - path to the keyboards.json file containing region info for each keyboard + * @return FVRegionList + */ + private FVRegionList loadRegionList(String keyboardsJSONPath) { + FVRegionList list = new FVRegionList(); + JSONParser parser = new JSONParser(); + File jsonFile = new File(keyboardsJSONPath); + if (!jsonFile.exists()) { + // Fatal error + throw new Error("keyboards.json file doesn't exist"); + } + try { + // At this point in initialization, fv_all.kmp is now extracted, so + // populate keyboard info from keyboards.json + JSONArray keyboardsArray = parser.getJSONObjectFromFile(jsonFile, JSONArray.class); + + if (keyboardsArray == null) { + KMLog.LogError(TAG, "Unable to parse keyboards.json"); + return list; + } - line = reader.readLine(); - } + for (int i=0; i r1.name.compareTo(r2.name)); - return list; - } + for (int i=0; i k1.name.compareTo(k2.name)); + } + return list; + } private FVLoadedKeyboardList loadLoadedKeyboardList() { FVLoadedKeyboardList data = new FVLoadedKeyboardList(); diff --git a/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/MainActivity.java b/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/MainActivity.java index f91657203bc..d38d5b9bc07 100644 --- a/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/MainActivity.java +++ b/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/MainActivity.java @@ -49,17 +49,17 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); + if (BuildConfig.DEBUG) { + KMManager.setDebugMode(true); + } + KMManager.initialize(getApplicationContext(), KMManager.KeyboardType.KEYBOARD_TYPE_INAPP); + FVShared.getInstance().initialize(this); FVShared.getInstance().upgradeTo12(); FVShared.getInstance().upgradeTo14(); FVShared.getInstance().preloadPackages(); - if (BuildConfig.DEBUG) { - KMManager.setDebugMode(true); - } - KMManager.initialize(getApplicationContext(), KMManager.KeyboardType.KEYBOARD_TYPE_INAPP); - /** * We need to set the default (fallback) keyboard to sil_euro_latin inside the fv_all package * rather than the normal default of sil_euro_latin inside the sil_euro_latin package. diff --git a/oem/firstvoices/android/build.sh b/oem/firstvoices/android/build.sh index 3e10a79ae66..260f8823687 100755 --- a/oem/firstvoices/android/build.sh +++ b/oem/firstvoices/android/build.sh @@ -25,6 +25,7 @@ builder_describe "Builds FirstVoices for Android app." \ "configure" \ "build" \ "test Runs lint and tests." \ + "publish Publishes symbols to Sentry and the APK to the Play Store." \ "--ci Don't start the Gradle daemon. For CI" # parse before describe_outputs to check debug flags @@ -61,13 +62,9 @@ if builder_start_action clean; then fi if builder_start_action configure; then - KEYBOARDS_CSV="$KEYMAN_ROOT/oem/firstvoices/keyboards.csv" - KEYBOARDS_CSV_TARGET="$KEYMAN_ROOT/oem/firstvoices/android/app/src/main/assets/keyboards.csv" - KEYBOARD_PACKAGE_ID="fv_all" KEYBOARDS_TARGET="$KEYMAN_ROOT/oem/firstvoices/android/app/src/main/assets/${KEYBOARD_PACKAGE_ID}.kmp" - cp "$KEYBOARDS_CSV" "$KEYBOARDS_CSV_TARGET" downloadKeyboardPackage "$KEYBOARD_PACKAGE_ID" "$KEYBOARDS_TARGET" builder_finish_action success configure @@ -89,3 +86,6 @@ if builder_start_action test; then builder_finish_action success test fi + +builder_run_action publish ./gradlew $DAEMON_FLAG publishSentry publishReleaseApk + diff --git a/oem/firstvoices/keyboards.csv b/oem/firstvoices/keyboards.csv index 02132efd4a6..d1f1b92180e 100644 --- a/oem/firstvoices/keyboards.csv +++ b/oem/firstvoices/keyboards.csv @@ -7,57 +7,62 @@ fv,fv_uummarmiutun,Uummarmiutun,Arctic,fv_uummarmiutun_kmw-9.0.js,9.1.1,ikt-Latn fv,fv_eastern_canadian_inuktitut,ᐃᓄᒃᑎᑐᑦ (Eastern Canadian Inuktitut),Arctic,fv_eastern_canadian_inuktitut_kmw-9.0.js,9.2.1,ike-Cans,Eastern Canadian Inuktitut (Unified Canadian Aboriginal Syllabics) fv,fv_migmaq,Mi'gmawi'simg / Mi'kmawi'simk,Atlantic,fv_migmaq_kmw-9.0.js,9.1.2,mic-Latn,Mi'kmaq (Latin) fv,fv_skicinuwatuwewakon,Skicinuwatuwewakon,Atlantic,fv_skicinuwatuwewakon_kmw-9.0.js,9.1.1,pqm-Latn,Malecite-Passamaquoddy (Latin) -fv,fv_uwikala,'Uwik̓ala,BC Coast,fv_uwikala_kmw-9.0.js,9.3,hei,Heiltsuk +fv,fv_uwikala,'Uwik̓ala,BC Coast,fv_uwikala_kmw-9.0.js,9.4,hei,Heiltsuk fv,fv_dexwlesucid,Dəxʷləšucid,BC Coast,fv_dexwlesucid_kmw-9.0.js,9.2.1,lut-Latn,Lushootseed (Latin) -fv,fv_diitiidatx,Diidiitidq,BC Coast,fv_diitiidatx_kmw-9.0.js,9.1.3,nuk-Latn,Nuu-chah-nulth (Latin) -fv,fv_gitsenimx,Gitsenimx̱,BC Coast,fv_gitsenimx_kmw-9.0.js,10.0.1,git,Gitxsan (Latin) -fv,fv_hailzaqvla,Haiɫzaqvla,BC Coast,fv_hailzaqvla_kmw-9.0.js,9.5.1,hei,Heiltsuk (Latin) -fv,fv_haisla,Haisla,BC Coast,fv_haisla.js,2.0.1,has-Latn,Haisla (Latin) -fv,fv_halqemeylem,Halq'eméylem,BC Coast,fv_halqemeylem_kmw-9.0.js,9.2,hur-Latn,Halkomelem (Latin) -fv,fv_henqeminem,Hǝn̓q̓ǝmin̓ǝm,BC Coast,fv_henqeminem_kmw-9.0.js,10.0.1,hur-Latn,Halkomelem (Latin) -fv,fv_klahoose,Homalco-Klahoose-Sliammon,BC Coast,fv_klahoose_kmw-9.0.js,10.1,coo,Comox -fv,fv_hulquminum,Hul’q’umi’num’,BC Coast,fv_hulquminum_kmw-9.0.js,9.1,hur-Latn,Halkomelem (Latin) -fv,fv_hulquminum_combine,Hul̓q̓umin̓um̓,BC Coast,fv_hulquminum_combine_kmw-9.0.js,1.1,hur-Latn,Halkomelem (Latin) -fv,fv_kwakwala_liqwala,Kʷak̓ʷala,BC Coast,fv_kwakwala_liqwala_kmw-9.0.js,9.2.5,kwk-Latn,Kwakiutl (Latin) -fv,fv_kwakwala,Kwak̕wala,BC Coast,fv_kwakwala_kmw-9.0.js,9.1.2,kwk-Latn,Kwakiutl (Latin) -fv,fv_nexwslayemucen,Nəxʷsƛ̓ay̓əmúcən,BC Coast,fv_nexwslayemucen_kmw-9.0.js,9.2.1,clm-Latn,Clallam (Latin) -fv,fv_nisgaa,Nisg̱a'a,BC Coast,fv_nisgaa_kmw-9.0.js,9.1.2,ncg-Latn,Nisga'a (Latin) -fv,fv_nuucaanul,Nuučaan̓uł,BC Coast,fv_nuucaanul_kmw-9.0.js,9.1.4,nuk-Latn,Nuu-chah-nulth (Latin) -fv,fv_nuxalk,Nuxalk,BC Coast,fv_nuxalk_kmw-9.0.js,10.0,blc-Latn,Bella Coola (Latin) -fv,fv_sencoten,SENĆOŦEN,BC Coast,fv_sencoten_kmw-9.0.js,9.2.1,str,Straits Salish -fv,fv_sguuxs,Sgüüx̱s,BC Coast,fv_sguuxs.js,1.0,tsi,Tsimshian -fv,fv_shashishalhem,Shashishalhem,BC Coast,fv_shashishalhem_kmw-9.0.js,9.1.3,sec-Latn,Sechelt (Latin) -fv,fv_skwxwumesh_snichim,Sḵwx̱wúmesh sníchim,BC Coast,fv_skwxwumesh_snichim_kmw-9.0.js,9.3,squ-Latn,Squamish (Latin) -fv,fv_smalgyax,Sm'algya̱x,BC Coast,fv_smalgyax_kmw-9.0.js,9.1.3,tsi-Latn,Tsimshian (Latin) -fv,fv_xaislakala,X̄a'ʼislak̓ala,BC Coast,fv_xaislakala_kmw-9.0.js,9.1.1,has-Latn,Haisla (Latin) -fv,fv_hlgaagilda_xaayda_kil,X̱aayda-X̱aad Kil,BC Coast,fv_hlgaagilda_xaayda_kil_kmw-9.0.js,9.3,hax,Southern Haida -fv,fv_dakelh,Dakelh,BC Interior,fv_dakelh_kmw-9.0.js,9.1.5,caf-Latn,Southern Carrier (Latin) -fv,fv_ktunaxa,Ktunaxa,BC Interior,fv_ktunaxa_kmw-9.0.js,9.1.3,kut-Latn,Kutenai (Latin) -fv,fv_kwadacha_tsekene,Kwadacha Tsek’ene,BC Interior,fv_kwadacha_tsekene_kmw-9.0.js,1.0,sek-Latn,Sekani -fv,fv_natwits,Nedut’en-Witsuwit'en,BC Interior,fv_natwits_kmw-9.0.js,9.1.3,caf-Latn,Southern Carrier (Latin) -fv,fv_nlekepmxcin,Nłeʔkepmxcin,BC Interior,fv_nlekepmxcin_kmw-9.0.js,9.2.3,thp-Latn,Thompson (Latin) -fv,fv_nlha7kapmxtsin,Nlha7kapmxtsin,BC Interior,fv_nlha7kapmxtsin_kmw-9.0.js,10.0,thp-Latn,Thompson (Latin) -fv,fv_nlakapamuxcheen,Nlakapamuxcheen,BC Interior,fv_nlakapamuxcheen_kmw-9.0.js,1.0,thp,Thompson -fv,fv_nsilxcen,Nsilxcən,BC Interior,fv_nsilxcen_kmw-9.0.js,9.3,oka,Okanagan -fv,fv_secwepemctsin,Secwepemctsín,BC Interior,fv_secwepemctsin_kmw-9.0.js,9.2,shs-Latn,Shuswap (Latin) -fv,fv_stlatlimxec,Sƛ̓aƛ̓imxəc,BC Interior,fv_stlatlimxec_kmw-9.0.js,9.2.3,lil-Latn,Lillooet (Latin) -fv,fv_statimcets,St̓át̓imcets,BC Interior,fv_statimcets_kmw-9.0.js,9.1.4,lil-Latn,Lillooet (Latin) -fv,fv_taltan,Tāłtān,BC Interior,fv_taltan_kmw-9.0.js,9.1.5,tht-Latn,Tahltan (Latin) -fv,fv_tsekehne,Tsek'ehne,BC Interior,fv_tsekehne_kmw-9.0.js,9.1.2,sek-Latn,Sekani (Latin) -fv,fv_tsilhqotin,Tŝilhqot'in,BC Interior,fv_tsilhqotin_kmw-9.0.js,9.1.3,clc-Latn,Chilcotin (Latin) -fv,fv_southern_carrier,ᑐᑊᘁᗕᑋᗸ (Southern Carrier),BC Interior,fv_southern_carrier_kmw-9.0.js,10.0.1,caf-Cans,Southern Carrier (Unified Canadian Aboriginal Syllabics) +fv,fv_diitiidatx,Diidiitidq,BC Coast,fv_diitiidatx_kmw-9.0.js,9.2.1,nuk-Latn,Nuu-chah-nulth (Latin) +fv,fv_gitsenimx,Gitsenimx̱,BC Coast,fv_gitsenimx_kmw-9.0.js,10.1.2,git,Gitxsan (Latin) +fv,fv_hailzaqvla,Haiɫzaqvla,BC Coast,fv_hailzaqvla_kmw-9.0.js,9.5.2,hei,Heiltsuk (Latin) +fv,fv_haisla,Haisla,BC Coast,fv_haisla.js,2.1.3,has-Latn,Haisla (Latin) +fv,fv_halqemeylem,Halq'eméylem,BC Coast,fv_halqemeylem_kmw-9.0.js,9.2.1,hur-Latn,Halkomelem (Latin) +fv,fv_henqeminem,Hǝn̓q̓ǝmin̓ǝm,BC Coast,fv_henqeminem_kmw-9.0.js,10.2.1,hur-Latn,Halkomelem (Latin) +fv,fv_klahoose,Homalco-Klahoose-Sliammon,BC Coast,fv_klahoose_kmw-9.0.js,10.2,coo,Comox +fv,fv_hulquminum,Hul’q’umi’num’,BC Coast,fv_hulquminum_kmw-9.0.js,9.1,hur,Halkomelem +fv,fv_hulquminum_combine,Hul̓q̓umin̓um̓,BC Coast,fv_hulquminum_combine_kmw-9.0.js,2.0.1,hur-Latn,Halkomelem (Latin) +fv,fv_kwakwala_liqwala,Kʷak̓ʷala,BC Coast,fv_kwakwala_liqwala_kmw-9.0.js,9.3,kwk-Latn,Kwakiutl (Latin) +fv,fv_kwakwala,Kwak̕wala,BC Coast,fv_kwakwala_kmw-9.0.js,9.2,kwk-Latn,Kwakiutl (Latin) +fv,fv_lekwungen,Lekwungen (Lək̓ʷəŋən),BC Coast,fv_lekwungen_kmw-9.0.js,1.0.1,str,Straits Salish +fv,fv_nexwslayemucen,Nəxʷsƛ̓ay̓əmúcən,BC Coast,fv_nexwslayemucen_kmw-9.0.js,11.0,clm,Clallam +fv,fv_nisgaa,Nisg̱a'a,BC Coast,fv_nisgaa_kmw-9.0.js,9.2.1,ncg-Latn,Nisga'a (Latin) +fv,fv_nuucaanul,Nuučaan̓uł,BC Coast,fv_nuucaanul_kmw-9.0.js,9.2,nuk-Latn,Nuu-chah-nulth (Latin) +fv,fv_nuxalk,Nuxalk,BC Coast,fv_nuxalk_kmw-9.0.js,10.0.1,blc-Latn,Bella Coola (Latin) +fv,fv_sencoten,SENĆOŦEN,BC Coast,fv_sencoten_kmw-9.0.js,9.2.3,str,Straits Salish +fv,fv_sguuxs,Sgüüx̱s,BC Coast,fv_sguuxs.js,1.1,tsi,Tsimshian +fv,fv_shashishalhem,Shashishalhem,BC Coast,fv_shashishalhem_kmw-9.0.js,9.2.1,sec-Latn,Sechelt (Latin) +fv,fv_skwxwumesh_snichim,Sḵwx̱wúmesh sníchim,BC Coast,fv_skwxwumesh_snichim_kmw-9.0.js,10.0,squ-Latn,Squamish (Latin) +fv,fv_smalgyax,Sm'algya̱x,BC Coast,fv_smalgyax_kmw-9.0.js,9.3,tsi-Latn,Tsimshian (Latin) +fv,fv_xaislakala,X̄a'ʼislak̓ala,BC Coast,fv_xaislakala_kmw-9.0.js,10.0,has-Latn,Haisla (Latin) +fv,fv_hlgaagilda_xaayda_kil,X̱aayda-X̱aad Kil,BC Coast,fv_hlgaagilda_xaayda_kil_kmw-9.0.js,9.4,hax,Southern Haida +fv,fv_dakelh,Dakelh,BC Interior,fv_dakelh_kmw-9.0.js,9.2.1,caf-Latn,Southern Carrier (Latin) +fv,fv_ktunaxa,Ktunaxa,BC Interior,fv_ktunaxa_kmw-9.0.js,10.0,kut-Latn,Kutenai (Latin) +fv,fv_kwadacha_tsekene,Kwadacha Tsek’ene,BC Interior,fv_kwadacha_tsekene_kmw-9.0.js,1.1,sek-Latn,Sekani +fv,fv_natwits,Nedut’en-Witsuwit'en,BC Interior,fv_natwits_kmw-9.0.js,9.2,caf-Latn,Southern Carrier (Latin) +fv,fv_nlekepmxcin,Nłeʔkepmxcin,BC Interior,fv_nlekepmxcin_kmw-9.0.js,9.5.1,thp-Latn,Thompson (Latin) +fv,fv_nlha7kapmxtsin,Nlha7kapmxtsin,BC Interior,fv_nlha7kapmxtsin_kmw-9.0.js,10.1.2,thp,Thompson +fv,fv_nlakapamuxcheen,Nlakapamuxcheen,BC Interior,fv_nlakapamuxcheen_kmw-9.0.js,1.1,thp,Thompson +fv,fv_nsilxcen,Nsilxcən,BC Interior,fv_nsilxcen_kmw-9.0.js,10.0.1,oka,Okanagan +fv,fv_secwepemctsin,Secwepemctsín,BC Interior,fv_secwepemctsin_kmw-9.0.js,9.3,shs,Shuswap +fv,fv_stlatlimxec,Sƛ̓aƛ̓imxəc,BC Interior,fv_stlatlimxec_kmw-9.0.js,9.4,lil-Latn,Lillooet (Latin) +fv,fv_statimcets,St̓át̓imcets,BC Interior,fv_statimcets_kmw-9.0.js,9.2,lil-Latn,Lillooet (Latin) +fv,fv_taltan,Tāłtān,BC Interior,fv_taltan_kmw-9.0.js,9.2,tht-Latn,Tahltan (Latin) +fv,fv_tsekehne,Tsek'ehne,BC Interior,fv_tsekehne_kmw-9.0.js,9.2,sek-Latn,Sekani (Latin) +fv,fv_tsilhqotin,Tŝilhqot'in,BC Interior,fv_tsilhqotin_kmw-9.0.js,9.3,clc-Latn,Chilcotin (Latin) +fv,fv_southern_carrier,ᑐᑊᘁᗕᑋᗸ (Southern Carrier),BC Interior,fv_southern_carrier_kmw-9.0.js,10.1,caf-Cans,Southern Carrier (Unified Canadian Aboriginal Syllabics) fv,fv_anicinapemi8in,Anicinapemi8in/Anishinàbemiwin,Eastern Subarctic,fv_anicinapemi8in_kmw-9.0.js,9.1.1,alq-Latn,Algonquin (Latin) fv,fv_atikamekw,Atikamekw,Eastern Subarctic,fv_atikamekw_kmw-9.0.js,9.1.1,atj-Latn,Atikamekw (Latin) fv,fv_ilnu_innu_aimun,Ilnu-Innu Aimun,Eastern Subarctic,fv_ilnu_innu_aimun_kmw-9.0.js,9.1.1,moe-Latn,Montagnais (Latin) fv,fv_swampy_cree,ᐃᓂᓂᒧᐎᐣ (Swampy Cree),Eastern Subarctic,fv_swampy_cree_kmw-9.0.js,9.2.1,csw-Cans,Swampy Cree (Unified Canadian Aboriginal Syllabics) fv,fv_moose_cree,ᐃᓕᓖᒧᐎᓐ (Moose Cree),Eastern Subarctic,fv_moose_cree_kmw-9.0.js,9.2.1,crm-Cans,Moose Cree (Unified Canadian Aboriginal Syllabics) fv,fv_northern_east_cree,ᐄᔨᔫ-ᐄᓅ ᐊᔨᒨᓐ (Northern East Cree),Eastern Subarctic,fv_northern_east_cree_kmw-9.0.js,9.2.1,crl-Cans,Northern East Cree (Unified Canadian Aboriginal Syllabics) -fv,fv_severn_ojibwa,ᐊᓂᔑᓂᓂᒧᐎᐣ (Severn Ojibwa),Eastern Subarctic,fv_severn_ojibwa_kmw-9.0.js,9.3.1,ojs-Cans,Severn Ojibwa (Unified Canadian Aboriginal Syllabics) -fv,fv_ojibwa,ᐊᓂᔑᓇᐯᒧᐎᓐ (Ojibwa),Eastern Subarctic,fv_ojibwa_kmw-9.0.js,9.3.1,ojb-Cans,Northwestern Ojibwa (Unified Canadian Aboriginal Syllabics) +fv,fv_severn_ojibwa,ᐊᓂᔑᓂᓂᒧᐎᐣ (Severn Ojibwa),Eastern Subarctic,fv_severn_ojibwa_kmw-9.0.js,10.1.1,ojs-Cans,Severn Ojibwa (Unified Canadian Aboriginal Syllabics) +fv,fv_severn_ojibwa_rdot,ᐊᓂᔑᓂᓂᒧᐏᐣ (Severn Ojibwa right w-dot),Eastern Subarctic,fv_severn_ojibwa_kmw-9.0.js,1.1.1,ojs,Oji-Cree +fv,fv_ojibwa,ᐊᓂᔑᓇᐯᒧᐎᓐ (a-finals),Eastern Subarctic,fv_ojibwa_kmw-9.0.js,10.1,ojb-Cans,Northwestern Ojibwa (Unified Canadian Aboriginal Syllabics) +fv,fv_ojibwa_rdot,ᐁᓂᔑᓇᐯᒧᐏᓐ (a-finals right w-dot),Eastern Subarctic,fv_ojibwa_rdot_kmw-9.0.js,1.0.3,oj,Ojibwa +fv,fv_ojibwa_ifinal,ᐊᓂᔑᓇᐯᒧᐎᣙ (i-finals),Eastern Subarctic,fv_ojibwa_ifinal_kmw-9.0.js,1.0.2,oj,Ojibwa +fv,fv_ojibwa_ifinal_rdot,ᐊᓂᔑᓇᐯᒧᐏᣙ (i-finals right w-dot),Eastern Subarctic,fv_ojibwa_ifinal_rdot_kmw-9.0.js,1.0.2,oj,Ojibwa fv,fv_naskapi,ᓇᔅᑲᐱ (Naskapi),Eastern Subarctic,fv_naskapi_kmw-9.0.js,9.3.1,nsk-Cans,Naskapi (Unified Canadian Aboriginal Syllabics) sil,sil_euro_latin,English,European,european2-1.6.js,3.0.2,en,English basic,basic_kbdcan,Français,European,canadian_french-1.0.js,1.1.1,fr-CA,French (Canada) -fv,fv_anishinaabemowin,Anishinaabemowin,Great Lakes - St. Lawrence,fv_anishinaabemowin_kmw-9.0.js,10.0.1,oj,Ojibwa +fv,fv_anishinaabemowin,Anishinaabemowin,Great Lakes - St. Lawrence,fv_anishinaabemowin_kmw-9.0.js,10.2,oj,Ojibwa fv,fv_bodewadminwen,Bodéwadminwen-Nishnabémwen,Great Lakes - St. Lawrence,fv_bodewadminwen_kmw-9.0.js,9.1.1,pot-Latn,Potawatomi (Latin) fv,fv_goyogohono,Goyogo̱hó:nǫ',Great Lakes - St. Lawrence,fv_goyogohono_kmw-9.0.js,9.2.1,cay-Latn,Cayuga (Latin) fv,fv_kanienkeha_e,Kanien'kéha-Kanyen'kéha,Great Lakes - St. Lawrence,fv_kanienkeha_e_kmw-9.0.js,9.1.3,moh-Latn,Mohawk (Latin) @@ -71,31 +76,31 @@ fv,fv_wobanakiodwawogan,Wôbanakiôdwawôgan,Great Lakes - St. Lawrence,fv_woban fv,fv_australian,Australian,Pacific,fv_australian_kmw-9.0.js,9.3.1,pjt-Latn,Pitjantjatjara (Latin) fv,fv_maori,Māori,Pacific,fv_maori_kmw-9.0.js,9.1.1,mi-Latn,Maori (Latin) fv,fv_blackfoot,Blackfoot,Prairies,fv_blackfoot_kmw-9.0.js,9.2.1,bla-Latn,Siksika (Latin) -fv,fv_cree_latin,Cree - Roman Orthography,Prairies,fv_cree_latin_kmw-9.0.js,10.0.1,cr-Latn,Cree (Latin) +fv,fv_cree_latin,Cree - Roman Orthography,Prairies,fv_cree_latin_kmw-9.0.js,10.2,cr-Latn,Cree (Latin) fv,fv_dakota_mb,Dakota,Prairies,fv_dakota_mb_kmw-9.0.js,9.1.1,dak-Latn,Dakota (Latin) fv,fv_dakota_sk,Dakot̄a,Prairies,fV_dakota_sk_kmw-9.0.js,9.1.1,dak-Latn,Dakota (Latin) fv,fv_isga_iabi,Isga Iʔabi,Prairies,fv_isga_iabi_kmw-9.0.js,9.1.1,sto-Latn,Stoney (Latin) fv,fv_lakota,Lak̇ot̄a,Prairies,fv_lakota-9.0.js,9.1.1,lkt-Latn,Lakota (Latin) fv,fv_nakoda,Nakoda,Prairies,fv_nakoda_kmw-9.0.js,9.1.1,asb-Latn,Assiniboine (Latin) fv,fv_tsuutina,Tsúùt'ínà,Prairies,fv_tsuutina_kmw-9.0.js,9.1.1,srs-Latn,Sarsi (Latin) -fv,fv_plains_cree,ᓀᐦᐃᔭᐍᐏᐣ (Plains Cree),Prairies,fv_plains_cree_kmw-9.0.js,11.0,crk-Cans,ᓀᐦᐃᔭᐍᐏᐣ (Cree syllabics) +fv,fv_plains_cree,ᓀᐦᐃᔭᐍᐏᐣ (Plains Cree),Prairies,fv_plains_cree_kmw-9.0.js,11.1,crk-Cans,ᓀᐦᐃᔭᐍᐏᐣ (Cree syllabics) fv,fv_dine_bizaad,Diné Bizaad,South West,fv_dine_bizaad_kmw-9.0.js,9.1.1,nv-Latn,Navajo (Latin) -fv,fv_dane_zaa_zaage,Dane-Z̲aa Z̲áágéʔ,Western Subarctic,fv_dane_zaa_zaage_kmw-9.0.js,9.3,bea,Beaver -fv,fv_dene_dzage,Dene Dzage,Western Subarctic,fv_dene_dzage_kmw-9.0.js,9.1.1,kkz-Latn,Kaska (Latin) -fv,fv_dene_zhatie,Dene Zhatié,Western Subarctic,fv_dene_zhatie_kmw-9.0.js,10.0.2,den,Dene Zhatıé +fv,fv_dane_zaa_zaage,Dane-Z̲aa Z̲áágéʔ,Western Subarctic,fv_dane_zaa_zaage_kmw-9.0.js,9.4.1,bea,Beaver +fv,fv_dene_dzage,Dene Dzage,Western Subarctic,fv_dene_dzage_kmw-9.0.js,11.0.1,kkz-Latn,Kaska (Latin) +fv,fv_dene_zhatie,Dene Zhatié,Western Subarctic,fv_dene_zhatie_kmw-9.0.js,10.2.1,den,Dene Zhatıé fv,fv_denesuline_epsilon,Dënesųłıné,Western Subarctic,fv_denesuline_epsilon_kmw-9.0.js,10.0.1,chp,Chipewyan fv,fv_denesuline,Dɛnɛsųłiné,Western Subarctic,fv_denesuline_kmw-9.0.js,10.0.1,chp,Chipewyan (Latin) fv,fv_gwichin,Gwich'in,Western Subarctic,fv_gwichin_kmw-9.0.js,9.2.1,gwi-Latn,Gwichʼin (Latin) fv,fv_han,Hän,Western Subarctic,fv_han_kmw-9.0.js,9.2,haa-Latn,Han (Latin) -fv,fv_kashogotine_yati,K'áshogot'ı̨nę́ Yatı̨́,Western Subarctic,fv_kashogotine_yati_kmw-9.0.js,10.0,scs,North Slavey -fv,fv_tlingit,Łingít,Western Subarctic,fv_tlingit_kmw-9.0.js,10.0.1,tli,Tlingit (Latin) +fv,fv_kashogotine_yati,K'áshogot'ı̨nę́ Yatı̨́,Western Subarctic,fv_kashogotine_yati_kmw-9.0.js,10.1,scs,North Slavey +fv,fv_tlingit,Łingít,Western Subarctic,fv_tlingit_kmw-9.0.js,10.1,tli,Tlingit (Latin) fv,fv_neeaandeg,Nee'aanděg',Western Subarctic,fv_neeaandeg_kmw-9.0.js,9.1.1,tcb-Latn,Tanacross (Latin) fv,fv_neeaaneegn,Nee'aaneegn',Western Subarctic,fv_neeaaneegn_kmw-9.0.js,9.1,tau-Latn,Upper Tanana (Latin) fv,fv_northern_tutchone,Northern Tutchone,Western Subarctic,fv_northern_tutchone_kmw-9.0.js,9.2,ttm-Latn,Northern Tutchone (Latin) fv,fv_sahugotine_yati,Sahtúgot'ı̨nę́ Yatı̨́,Western Subarctic,fv_sahugotine_yati_kmw-9.0.js,9.1.1,scs-Latn,North Slavey (Latin) fv,fv_shihgotine_yati,Shıhgot'ı̨nę́ Yatı̨́,Western Subarctic,fv_shihgotine_yati_kmw-9.0.js,9.1.1,scs-Latn,North Slavey (Latin) -fv,fv_southern_tutchone,Southern Tutchone,Western Subarctic,fv_southern_tutchone_kmw-9.0.js,9.2.2,tce-Latn,Southern Tutchone (Latin) -fv,fv_tagizi_dene,Tāgizi Dene,Western Subarctic,fv_tagizi_dene_kmw-9.0.js,9.2.1,tgx-Latn,Tagish (Latin) +fv,fv_southern_tutchone,Southern Tutchone,Western Subarctic,fv_southern_tutchone_kmw-9.0.js,9.3,tce-Latn,Southern Tutchone (Latin) +fv,fv_tagizi_dene,Tāgizi Dene,Western Subarctic,fv_tagizi_dene_kmw-9.0.js,9.4,tgx-Latn,Tagish (Latin) fv,fv_tlicho_yatii,Tłı̨chǫ Yatıı̀,Western Subarctic,fv_tlicho_yatii_kmw-9.0.js,9.1.1,dgr-Latn,Dogrib (Latin) fv,fv_dene_mb,ᑌᓀ ᔭᕠᐁ (Dene MB),Western Subarctic,fv_dene_mb_kmw-9.0.js,9.2.1,chp-Cans,Chipewyan (Unified Canadian Aboriginal Syllabics) fv,fv_dene_nt,ᑌᓀ ᔭᕱᐁ (Dene NT),Western Subarctic,fv_dene_nt_kmw-9.0.js,9.2.1,chp-Cans,Chipewyan (Unified Canadian Aboriginal Syllabics) diff --git a/oem/firstvoices/windows/src/xml/config.css b/oem/firstvoices/windows/src/xml/config.css index 28f09464765..763c7229a1c 100644 --- a/oem/firstvoices/windows/src/xml/config.css +++ b/oem/firstvoices/windows/src/xml/config.css @@ -762,10 +762,10 @@ th color: #444; position: absolute; bottom: 32px; - left: 80px; - height: 50px; - padding-top: 25px; - padding-left: 56px; + left: 73px; + height: 64px; + padding-top: 32px; + padding-left: 110px; box-sizing: border-box; } @@ -1021,4 +1021,4 @@ th width:100%; height:100%; border:none; -} \ No newline at end of file +} diff --git a/oem/firstvoices/windows/src/xml/keyman_support.xsl b/oem/firstvoices/windows/src/xml/keyman_support.xsl index dfd8c8445bd..81f72fdb66f 100644 --- a/oem/firstvoices/windows/src/xml/keyman_support.xsl +++ b/oem/firstvoices/windows/src/xml/keyman_support.xsl @@ -32,11 +32,19 @@
- + + + + +