diff --git a/.github/workflows/deb-packaging.README.md b/.github/workflows/deb-packaging.README.md new file mode 100644 index 00000000000..6bbeabd9e1e --- /dev/null +++ b/.github/workflows/deb-packaging.README.md @@ -0,0 +1,41 @@ +# Debian Packaging GitHub Action + +You can manually trigger a deb-packaging action on GitHub, e.g. to test changes: + +- push the changes to your fork of keymanapp/keyman: git push myfork HEAD:master +- make sure you have GHA enabled in the settings of your fork and created an + access token on GitHub +- trigger a build with: + + ```bash + curl --write-out '\n' --request POST \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: token $GITHUB_TOKEN" \ + --data "{ \"event_type\": \"deb-pr-packaging: master\", \ + \"client_payload\": { \ + \"buildSha\": \"$(git rev-parse refs/heads/master)\", \ + \"branch\": \"master\", \ + \"baseBranch\": \"master\", + \"baseRef\": \"$(git rev-parse refs/heads/master^)\", + \"user\": \"${USER}\", + \"isTestBuild\": \"true\"}" \ + https://api.github.com/repos//keyman/dispatches + ``` + + To trigger a build for PR #1234 in a branch `pr-1234` this would look similar + to this: + + ```bash + curl --write-out '\n' --request POST \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: token $GITHUB_TOKEN" \ + --data "{ \"event_type\": \"deb-pr-packaging: PR #1234\", \ + \"client_payload\": { \ + \"buildSha\": \"$(git rev-parse refs/heads/pr-1234)\", \ + \"branch\": \"pr-1234\", \ + \"baseBranch\": \"master\", + \"baseRef\": \"$(git rev-parse refs/heads/master)\", + \"user\": \"${USER}\", + \"isTestBuild\": \"true\"}" \ + https://api.github.com/repos//keyman/dispatches + ``` diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index 8cc431f1787..a86c60ec3de 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -1,8 +1,18 @@ name: "Ubuntu packaging" -run-name: "Ubuntu packaging - ${{ github.event.client_payload.branch }} (branch ${{ github.head_ref }}), by @${{ github.actor }}" +run-name: "Ubuntu packaging - ${{ github.event.client_payload.branch }} (branch ${{ github.event.client_payload.baseBranch }}), by @${{ github.event.client_payload.user }}" on: repository_dispatch: types: ['deb-release-packaging:*', 'deb-pr-packaging:*'] + +# Input: +# buildSha: The SHA of the commit to build, e.g. of the branch or +# refs/pull/1234/head for PR +# branch: The branch to build, for a PR in the form `PR-1234` +# baseBranch: For a PR the base branch, otherwise the same as `branch` +# baseRef: The ref of the previous commit. For a PR the same as `baseBranch`. +# user: The user that triggered the build or created the PR +# isTestBuild: false for Releases, otherwise true + env: COLOR_GREEN: "\e[32m" GH_TOKEN: ${{ github.token }} @@ -17,26 +27,21 @@ jobs: outputs: VERSION: ${{ steps.version_step.outputs.VERSION }} PRERELEASE_TAG: ${{ steps.prerelease_tag.outputs.PRERELEASE_TAG }} - GIT_SHA: ${{ steps.set_status.outputs.GIT_SHA }} - GHA_TEST_BUILD: ${{ github.event.client_payload.isTestBuild }} - GHA_BRANCH: ${{ github.event.client_payload.branch }} steps: - name: Checkout uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c #v3.3.0 with: - ref: '${{ github.event.client_payload.ref }}' + ref: '${{ github.event.client_payload.buildSha }}' - name: Set pending status on PR builds id: set_status if: github.event.client_payload.isTestBuild == 'true' shell: bash run: | - GIT_SHA="${{ github.event.client_payload.sha }}" - echo "GIT_SHA=$GIT_SHA" >> $GITHUB_OUTPUT gh api \ --method POST \ -H "Accept: application/vnd.github+json" \ - /repos/$GITHUB_REPOSITORY/statuses/$GIT_SHA \ + /repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \ -f state='pending' \ -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ -f description='Debian packaging started' \ @@ -53,10 +58,14 @@ jobs: ./scripts/deb-packaging.sh --gha dependencies - name: Build source package + id: build_source_package run: | - TIER=$(cat TIER.md) - export TIER + export TIER=$(cat TIER.md) + export GHA_TEST_BUILD="${{ github.event.client_payload.isTestBuild }}" + export GHA_BRANCH="${{ github.event.client_payload.branch }}" echo "TIER=$TIER" >> $GITHUB_ENV + echo "GHA_TEST_BUILD=${GHA_TEST_BUILD}" >> $GITHUB_ENV + echo "GHA_BRANCH=${GHA_BRANCH}" >> $GITHUB_ENV cd linux ./scripts/deb-packaging.sh --gha source @@ -84,7 +93,7 @@ jobs: if [ "${{ github.event.client_payload.isTestBuild }}" == "true" ]; then echo ":checkered_flag: **Test build of version ${{ steps.version_step.outputs.VERSION }} for ${{ github.event.client_payload.branch }}**" >> $GITHUB_STEP_SUMMARY else - echo ":ship: **Release build of ${{ github.event.client_payload.branch }} branch (${{ github.event.client_payload.ref}}), version ${{ steps.version_step.outputs.VERSION }}**" >> $GITHUB_STEP_SUMMARY + echo ":ship: **Release build of ${{ github.event.client_payload.branch }} branch (${{ github.event.client_payload.branch}}), version ${{ steps.version_step.outputs.VERSION }}**" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY echo ":gift: Generated source package:" >> $GITHUB_STEP_SUMMARY @@ -227,7 +236,8 @@ jobs: - name: Checkout uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c #v3.3.0 with: - ref: '${{ github.event.client_payload.ref }}' + ref: '${{ github.event.client_payload.buildSha }}' + fetch-depth: 0 - name: Download Artifacts uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 @@ -243,10 +253,12 @@ jobs: run: | cd linux PKG_NAME=libkeymancore - SRC_PKG="${GITHUB_WORKSPACE}/artifacts/keyman-srcpkg/keyman_${{ needs.sourcepackage.outputs.VERSION }}-1.debian.tar.xz" \ - BIN_PKG="${GITHUB_WORKSPACE}/artifacts/keyman-binarypkgs/${PKG_NAME}_${{ needs.sourcepackage.outputs.VERSION }}-1${{ needs.sourcepackage.outputs.PRERELEASE_TAG }}+jammy1_amd64.deb" \ - PKG_VERSION="${{ needs.sourcepackage.outputs.VERSION }}" \ - ./scripts/deb-packaging.sh --gha verify 2>> $GITHUB_STEP_SUMMARY + ./scripts/deb-packaging.sh \ + --gha \ + --bin-pkg "${GITHUB_WORKSPACE}/artifacts/keyman-binarypkgs/${PKG_NAME}_${{ needs.sourcepackage.outputs.VERSION }}-1${{ needs.sourcepackage.outputs.PRERELEASE_TAG }}+jammy1_amd64.deb" \ + --git-sha "${{ github.event.client_payload.buildSha }}" \ + --git-base "${{ github.event.client_payload.baseRef }}" \ + verify 2>> $GITHUB_STEP_SUMMARY - name: Archive .symbols file uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 @@ -284,7 +296,7 @@ jobs: gh api \ --method POST \ -H "Accept: application/vnd.github+json" \ - /repos/$GITHUB_REPOSITORY/statuses/${{ needs.sourcepackage.outputs.GIT_SHA }} \ + /repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \ -f state="$RESULT" \ -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ -f description="$MSG" \ diff --git a/HISTORY.md b/HISTORY.md index 8bc6b8f02aa..9d7716dcaa3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,68 @@ # Keyman Version History +## 17.0.210 alpha 2023-11-14 + +* docs(common): Update CODEOWNERS for web (#9997) +* chore(core): Add additional API checks (#9867) + +## 17.0.209 alpha 2023-11-13 + +* docs(windows): update meson and emscripten details (#9933) +* fix(web): Increase size of spacebar text (#9954) + +## 17.0.208 alpha 2023-11-08 + +* fix(web): Fix clearing of deadkeys (#9944) +* fix(web): Ignore `osk-always-visible` on non-desktop devices (#9951) +* fix(linux): Fix baseline tests (#9967) +* chore(linux): Fix GHA triggering (#9965) +* fix(linux): Fix trigger for baseline tests (#9968) + +## 17.0.207 alpha 2023-11-07 + +* refactor(web): Link to `index.html` in test pages (#9953) +* feat(developer): Add more non-printing characters (#9846) + +## 17.0.206 alpha 2023-11-06 + +* chore(developer): remove compile.pas and CompileErrorCodes.pas (#9924) +* chore(common): remove prepublish step from package.json (#9937) +* feat(developer): provide line number for some kmw compiler messages (#9938) +* fix(developer): Sentry in Server should honour reporting settings (#9940) +* fix(developer): resilience in loading Server config and cache files (#9941) + +## 17.0.205 alpha 2023-11-03 + +* fix(developer): use KeymanWeb.Codes for 17.0+ (#9913) +* fix(developer): don't use osk-always-visible on touch devices (#9917) +* chore(linux): Improve repository_dispatch (#9865) +* chore(linux): Refactor deb-packaging.sh script (#9866) +* chore(linux): Add Core API version number (#9877) +* fix(developer): kmc code generation for context(n) in context (#9932) +* chore(developer): remove obsolete 'Allow Multiple Instances' and 'Use Legacy Compiler' options (#9934) +* chore(developer): common/include dep for kmcmplib (#9935) +* feat(common): ldml update to WIP cldr data (#9919) + +## 17.0.204 alpha 2023-11-02 + +* fix(web): fixes doc-kbd display of default layer when it's not defined first (#9891) +* fix(developer): compiler crash when no project loaded (#9898) +* fix(developer): debug flag for compiling keyboards (#9901) +* chore(developer): verify kmp.json output from kmc-package (#9844) +* fix(developer): Project MRU now saves correctly (#9902) +* chore(common): fixup SchemaValidators error handling (#9903) +* chore(developer): change field label to 'Related Package ID' in Related Packages dialog (#9904) +* fix(developer): open editor links in new window (#9905) +* fix(developer): enable and update unit tests (#9907) +* fix(developer): layout builder - maintain presentation during undo (#9914) +* feat(developer): extract font family from .ttf in kmc-keyboard-info (#9859) +* fix(developer): raise error if virtual key in context string (#9908) +* feat(developer): Compile button in TIKE toolbar (#9910) +* chore(developer): rename messages.ts for clarity (#9920) +* fix(developer): warn if layer switch key is missing ID (#9921) +* fix(developer): restore selection in layout builder even with duplicate ids (#9922) +* fix(developer): enable line breaks in debugger (#9906) + ## 17.0.203 alpha 2023-10-30 * fix(linux): Fix uninstallation of shared keyboard (#9880) diff --git a/VERSION.md b/VERSION.md index d8de415ab0b..9c77e7b51c3 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.204 \ No newline at end of file +17.0.211 \ No newline at end of file diff --git a/common/test/keyboards/baseline/k_014___groups_and_virtual_keys.kmn b/common/test/keyboards/baseline/k_014___groups_and_virtual_keys.kmn index 81e26763b6d..0e32c432eb6 100644 --- a/common/test/keyboards/baseline/k_014___groups_and_virtual_keys.kmn +++ b/common/test/keyboards/baseline/k_014___groups_and_virtual_keys.kmn @@ -11,16 +11,16 @@ store(&MnemonicLayout) "0" c ---------------------------------------------- -group(UMain) using keys - + [K_A] > U+03B1 - + [CTRL K_2] > deadkey(MacronBug) - + [ALT K_2] > deadkey(MacronBug) - + [SHIFT K_2] > deadkey(BreveBug) - +group(UMain) using keys + + [K_A] > U+03B1 + + [CTRL K_2] > deadkey(MacronBug) + + [ALT K_2] > deadkey(MacronBug) + + [SHIFT K_2] > deadkey(BreveBug) + match > use(DK1) - + c ---------------------------------------------- -group(DK1) - deadkey(MacronBug) U+03B1 > U+1FB1 c won't work - deadkey(BreveBug) U+03B1 > U+1FB0 c works +group(DK1) + deadkey(MacronBug) U+03B1 > U+1FB1 + deadkey(BreveBug) U+03B1 > U+1FB0 diff --git a/common/test/keyboards/baseline/k_020___deadkeys_and_backspace.kmn b/common/test/keyboards/baseline/k_020___deadkeys_and_backspace.kmn index cf83220f9c3..af7c80f21d9 100644 --- a/common/test/keyboards/baseline/k_020___deadkeys_and_backspace.kmn +++ b/common/test/keyboards/baseline/k_020___deadkeys_and_backspace.kmn @@ -5,10 +5,15 @@ c 2. One char and one deadkey in context 'a' dk(2) + BKSP = nul c 3. One deadkey and one char in context dk(3) 'a' + BKSP = nul c 4. Two deadkeys in a row in context dk(4a) dk(4b) + BKSP = nul c 5. One char and two deadkeys in context 'a' dk(5a) dk(5b) 'b' + BKSP = 'a' -c 6. One char and two deadkeys and one char and two deadkeys in context 'a' dk(6a) dk(6b) 'b' dk(6c) dk(6d) + BKSP = 'a' -c keys: [K_1][K_BKSP][K_2][K_BKSP][K_3][K_BKSP][K_4][K_BKSP][K_5][K_BKSP][K_6][K_BKSP] -c expected: 78aa -c context: 7890 +c 6. One char and two deadkeys and one char and two deadkeys in context: +c 'a' dk(6a) dk(6b) 'b' dk(6c) dk(6d) + BKSP = 'a' +c 7. Tests behaviour when deleting two characters with deadkey prior to first character, +c verifying that deadkeys are preserved with the first backspace event: +c 'c' (dk7) 'de' + BKSP + BKSP = 'ok' + +c keys: [K_1][K_BKSP][K_2][K_BKSP][K_3][K_BKSP][K_4][K_BKSP][K_5][K_BKSP][K_6][K_BKSP][K_7][K_BKSP][K_BKSP] +c expected: wxaa ok +c context: wxyz store(&VERSION) '9.0' @@ -22,3 +27,6 @@ group(main) using keys + '4' > dk(4a) dk(4b) + '5' > 'a' dk(5a) dk(5b) 'b' + '6' > 'a' dk(6a) dk(6b) 'b' dk(6c) dk(6d) ++ '7' > 'c' dk(7) 'd' 'e' +'c' 'd' + [K_BKSP] > ' fail' +'c' dk(7) 'd' + [K_BKSP] > ' ok' diff --git a/common/test/keyboards/baseline/k_020___deadkeys_and_backspace.kmx b/common/test/keyboards/baseline/k_020___deadkeys_and_backspace.kmx index 149c2a4f97b..68271160736 100644 Binary files a/common/test/keyboards/baseline/k_020___deadkeys_and_backspace.kmx and b/common/test/keyboards/baseline/k_020___deadkeys_and_backspace.kmx differ diff --git a/common/test/resources/json/engine_tests/8568_deadkeys.json b/common/test/resources/json/engine_tests/8568_deadkeys.json new file mode 100644 index 00000000000..e3aaf8ed9e9 --- /dev/null +++ b/common/test/resources/json/engine_tests/8568_deadkeys.json @@ -0,0 +1,308 @@ +{ + "specVersion": "14.0", + "keyboard": { + "id": "test_8568_deadkeys", + "name": "Testcase for deadkeys bug (#8568)", + "filename": "resources/keyboards/test_8568_deadkeys.js", + "languages": [ + { + "id": "en", + "name": "English", + "region": "World" + } + ] + }, + "inputTestSets": [ + { + "constraint": { + "target": "hardware", + "validOSList": null, + "validBrowsers": null + }, + "testSet": [ + { + "msg": "1. One deadkey in context", + "inputs": [ + { + "type": "key", + "keyCode": 49, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "1", + "code": "Key1", + "keyCode": 49, + "location": 0, + "modifierSet": 32 + } + }, + { + "type": "key", + "keyCode": 8, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "Backspace", + "code": "Backspace", + "keyCode": 8, + "location": 0, + "modifierSet": 32 + } + } + ], + "output": "" + }, + { + "msg": "2. One char and one deadkey in context", + "inputs": [ + { + "type": "key", + "keyCode": 50, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "2", + "code": "Key2", + "keyCode": 50, + "location": 0, + "modifierSet": 32 + } + }, + { + "type": "key", + "keyCode": 8, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "Backspace", + "code": "Backspace", + "keyCode": 8, + "location": 0, + "modifierSet": 32 + } + } + ], + "output": "" + }, + { + "msg": "3. One deadkey and one char in context", + "inputs": [ + { + "type": "key", + "keyCode": 51, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "3", + "code": "Key3", + "keyCode": 51, + "location": 0, + "modifierSet": 32 + } + }, + { + "type": "key", + "keyCode": 8, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "Backspace", + "code": "Backspace", + "keyCode": 8, + "location": 0, + "modifierSet": 32 + } + } + ], + "output": "" + }, + { + "msg": "4. Two deadkeys in a row in context", + "inputs": [ + { + "type": "key", + "keyCode": 52, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "4", + "code": "Key4", + "keyCode": 52, + "location": 0, + "modifierSet": 32 + } + }, + { + "type": "key", + "keyCode": 8, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "Backspace", + "code": "Backspace", + "keyCode": 8, + "location": 0, + "modifierSet": 32 + } + } + ], + "output": "" + }, + { + "msg": "5. One char and two deadkeys in context", + "inputs": [ + { + "type": "key", + "keyCode": 53, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "5", + "code": "Key5", + "keyCode": 53, + "location": 0, + "modifierSet": 32 + } + }, + { + "type": "key", + "keyCode": 8, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "Backspace", + "code": "Backspace", + "keyCode": 8, + "location": 0, + "modifierSet": 32 + } + } + ], + "output": "a" + }, + { + "msg": "6. One char and two deadkeys and one char and two deadkeys in context", + "inputs": [ + { + "type": "key", + "keyCode": 54, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "6", + "code": "Key6", + "keyCode": 54, + "location": 0, + "modifierSet": 32 + } + }, + { + "type": "key", + "keyCode": 8, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "Backspace", + "code": "Backspace", + "keyCode": 8, + "location": 0, + "modifierSet": 32 + } + } + ], + "output": "a" + }, + { + "msg": "7. Two chars and one deadkey in between in context", + "inputs": [ + { + "type": "key", + "keyCode": 55, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "7", + "code": "Key7", + "keyCode": 55, + "location": 0, + "modifierSet": 32 + } + }, + { + "type": "key", + "keyCode": 8, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "Backspace", + "code": "Backspace", + "keyCode": 8, + "location": 0, + "modifierSet": 32 + } + }, + { + "type": "key", + "keyCode": 8, + "states": 9728, + "modifiers": 0, + "modifierChanged": false, + "isVirtualKey": true, + "eventSpec": { + "type": "key", + "key": "Backspace", + "code": "Backspace", + "keyCode": 8, + "location": 0, + "modifierSet": 32 + } + } + ], + "output": "pass" + } + ] + } + ] +} diff --git a/common/test/resources/json/keyboards/test_8568_deadkeys.json b/common/test/resources/json/keyboards/test_8568_deadkeys.json new file mode 100644 index 00000000000..bd366c1bac7 --- /dev/null +++ b/common/test/resources/json/keyboards/test_8568_deadkeys.json @@ -0,0 +1,10 @@ +{ + "id": "test_8568_deadkeys", + "name":"Testcase for deadkeys bug (#8568)", + "languages":{ + "id":"en", + "name": "English", + "region": "World" + }, + "filename":"resources/keyboards/test_8568_deadkeys.js" +} diff --git a/common/test/resources/keyboards/test_8568_deadkeys.html b/common/test/resources/keyboards/test_8568_deadkeys.html new file mode 100644 index 00000000000..1b4cf314247 --- /dev/null +++ b/common/test/resources/keyboards/test_8568_deadkeys.html @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + KeymanWeb #8568 + + + + + + + + + + + + + + + + + + +

KeymanWeb #8568

+ +
+ +
+ +

Typing x and f followed by 2 backspace should output pass:

+ +
+ + + + + diff --git a/common/test/resources/keyboards/test_8568_deadkeys.js b/common/test/resources/keyboards/test_8568_deadkeys.js new file mode 100644 index 00000000000..fad6c4fd3db --- /dev/null +++ b/common/test/resources/keyboards/test_8568_deadkeys.js @@ -0,0 +1,108 @@ +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_test_8568_deadkeys()); +} +function Keyboard_test_8568_deadkeys() +{ + 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_test_8568_deadkeys"; + this.KN="Testcases for deadkeys bug (#8568)"; + this.KMINVER="10.0"; + this.KV=null; + this.KDU=0; + this.KH=''; + this.KM=0; + this.KBVER="1.0"; + this.KMBM=0 /* 0x0000 */; + this.KVER="17.0.203.0"; + 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(3,t,['c',{t:'d',d:11},'d'])){ + r=m=1; // Line 27 + k.KDC(3,t); + k.KO(-1,t,"pass"); + } + else if(k.KFCM(2,t,['c','d'])){ + r=m=1; // Line 26 + k.KDC(2,t); + k.KO(-1,t,"fail"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_1 /* 0x31 */)) { + if(1){ + r=m=1; // Line 19 + k.KDC(0,t); + k.KDO(-1,t,0); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_2 /* 0x32 */)) { + if(1){ + r=m=1; // Line 20 + k.KDC(0,t); + k.KO(-1,t,"a"); + k.KDO(-1,t,1); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_3 /* 0x33 */)) { + if(1){ + r=m=1; // Line 21 + k.KDC(0,t); + k.KDO(-1,t,2); + k.KO(-1,t,"a"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_4 /* 0x34 */)) { + if(1){ + r=m=1; // Line 22 + k.KDC(0,t); + k.KDO(-1,t,3); + k.KDO(-1,t,4); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_5 /* 0x35 */)) { + if(1){ + r=m=1; // Line 23 + k.KDC(0,t); + k.KO(-1,t,"a"); + k.KDO(-1,t,5); + k.KDO(-1,t,6); + k.KO(-1,t,"b"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_6 /* 0x36 */)) { + if(1){ + r=m=1; // Line 24 + k.KDC(0,t); + k.KO(-1,t,"a"); + k.KDO(-1,t,7); + k.KDO(-1,t,8); + k.KO(-1,t,"b"); + k.KDO(-1,t,9); + k.KDO(-1,t,10); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_7 /* 0x37 */)) { + if(1){ + r=m=1; // Line 25 + k.KDC(0,t); + k.KO(-1,t,"c"); + k.KDO(-1,t,11); + k.KO(-1,t,"de"); + } + } + return r; + }; +} diff --git a/common/test/resources/keyboards/test_8568_deadkeys.kmn b/common/test/resources/keyboards/test_8568_deadkeys.kmn new file mode 100644 index 00000000000..4cf7cc799d1 --- /dev/null +++ b/common/test/resources/keyboards/test_8568_deadkeys.kmn @@ -0,0 +1,30 @@ +store(&NAME) 'Testcases for deadkeys bug (#8568)' +c Description: Tests deadkey backspacing +c See also common/test/keyboards/baseline/k_020___deadkeys_and_backspace.kmn +c 1. One deadkey in context dk(1) + BKSP = nul +c 2. One char and one deadkey in context 'a' dk(2) + BKSP = nul +c 3. One deadkey and one char in context dk(3) 'a' + BKSP = nul +c 4. Two deadkeys in a row in context dk(4a) dk(4b) + BKSP = nul +c 5. One char and two deadkeys in context 'a' dk(5a) dk(5b) 'b' + BKSP = 'a' +c 6. One char and two deadkeys and one char and two deadkeys in context: + 'a' dk(6a) dk(6b) 'b' dk(6c) dk(6d) + BKSP = 'a' +c 7. Tests behaviour when deleting two characters with deadkey prior to first character, +c verifying that deadkeys are preserved with the first backspace event: +c 'c' (dk7) 'de' + BKSP + BKSP = 'ok' + +store(&VERSION) "10.0" +store(&TARGETS) 'any windows' +store(©RIGHT) '© 2023 SIL International' + +begin Unicode > use(main) + +group(main) using keys + + '1' > dk(1) + + '2' > 'a' dk(2) + + '3' > dk(3) 'a' + + '4' > dk(4a) dk(4b) + + '5' > 'a' dk(5a) dk(5b) 'b' + + '6' > 'a' dk(6a) dk(6b) 'b' dk(6c) dk(6d) + + '7' > 'c' dk(7) 'de' + 'c' 'd' + [K_BKSP] > 'fail' + 'c' dk(7) 'd' + [K_BKSP] > 'pass' diff --git a/common/test/resources/keyboards/test_8568_deadkeys.kmx b/common/test/resources/keyboards/test_8568_deadkeys.kmx new file mode 100644 index 00000000000..88f00e8baf5 Binary files /dev/null and b/common/test/resources/keyboards/test_8568_deadkeys.kmx differ diff --git a/common/web/input-processor/tests/cases/inputProcessor.js b/common/web/input-processor/tests/cases/inputProcessorTests.js similarity index 78% rename from common/web/input-processor/tests/cases/inputProcessor.js rename to common/web/input-processor/tests/cases/inputProcessorTests.js index 00fc130a200..4085efee4f5 100644 --- a/common/web/input-processor/tests/cases/inputProcessor.js +++ b/common/web/input-processor/tests/cases/inputProcessorTests.js @@ -7,6 +7,7 @@ const require = createRequire(import.meta.url); import { InputProcessor } from '@keymanapp/input-processor'; import { KeyboardInterface, MinimalKeymanGlobal, Mock } from '@keymanapp/keyboard-processor'; import { NodeKeyboardLoader } from '@keymanapp/keyboard-processor/node-keyboard-loader'; +import { KeyboardTest } from '@keymanapp/recorder-core'; import { Worker } from '@keymanapp/lexical-model-layer/node'; import * as utils from '@keymanapp/web-utils'; @@ -175,4 +176,52 @@ describe('InputProcessor', function() { }); }); }); -}); \ No newline at end of file + + describe('Deadkeys bug #8568 - backspace should not reset all deadkeys', function () { + let keyboardWithHarness; + let testJSONtext = fs.readFileSync(require.resolve('@keymanapp/common-test-resources/json/engine_tests/8568_deadkeys.json')); + // For convenience we define the key sequence in a test file although we don't use the + // rest of the recorder stuff since it uses only KeyboardProcessor, not InputProcessor. + let testDefinitions = new KeyboardTest(JSON.parse(testJSONtext)); + + before(async function () { + // Load the keyboard. + let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal)); + const keyboard = await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_8568_deadkeys.js')); + keyboardWithHarness = keyboardLoader.harness; + keyboardWithHarness.activeKeyboard = keyboard; + + // This part provides extra assurance that the keyboard properly loaded. + assert.equal(keyboard.id, "Keyboard_test_8568_deadkeys"); + }); + + for (let testSet of testDefinitions.inputTestSets[0]['testSet']) { + it(testSet.msg ?? 'test', function() { + this.timeout(32); // ms + let core = new InputProcessor(device); + let context = new Mock("", 0); + + core.keyboardProcessor.keyboardInterface = keyboardWithHarness; + let keyboard = keyboardWithHarness.activeKeyboard; + + for (let keystroke of testSet.inputs) { + let keyEvent = { + Lcode: keystroke.keyCode, + Lmodifiers: keystroke.modifiers, + LmodifierChange: keystroke.modifierChanged, + vkCode: keystroke.vkCode, + Lstates: keystroke.states, + kName: '', + device: device, + isSynthetic: false, + LisVirtualKey: keyboard.definesPositionalOrMnemonic // Only false for 1.0 keyboards. + }; + + let behavior = core.processKeyEvent(keyEvent, context); + assert.isNotNull(behavior); + } + assert.equal(context.getText(), testSet.output); + }); + } + }); +}); diff --git a/common/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/web/keyboard-processor/src/text/keyboardProcessor.ts index 3726c67a170..2a2dc0bb70b 100644 --- a/common/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -530,10 +530,7 @@ export default class KeyboardProcessor extends EventEmitter { return false; } - if(Levent.Lcode == 8) { - // I3318 (always clear deadkeys after backspace) - outputTarget.deadkeys().clear(); - } else if(Levent.isModifier) { + if(Levent.isModifier) { this.activeKeyboard.notify(Levent.Lcode, outputTarget, isKeyDown ? 1 : 0); // For eventual integration - we bypass an OSK update for physical keystrokes when in touch mode. if(!Levent.device.touchable) { diff --git a/common/web/recorder/src/nodeProctor.ts b/common/web/recorder/src/nodeProctor.ts index 5548905e6e9..8693bd8c83f 100644 --- a/common/web/recorder/src/nodeProctor.ts +++ b/common/web/recorder/src/nodeProctor.ts @@ -88,9 +88,10 @@ export default class NodeProctor extends Proctor { // it's a matter of actually adding the feature. let ruleBehavior = processor.processKeystroke(new KeyEvent(keyEvent), target); - if(this.debugMode) { - console.log(JSON.stringify(target, null, ' ')); - console.log(JSON.stringify(ruleBehavior, null, ' ')); + if (this.debugMode) { + console.log("Processing %d:", keyEvent.Lcode); + console.log("target=%s", JSON.stringify(target, null, ' ')); + console.log("ruleBehavior=%s", JSON.stringify(ruleBehavior, null, ' ')); } } } else { @@ -98,4 +99,4 @@ export default class NodeProctor extends Proctor { } return target.getText(); } -} \ No newline at end of file +} diff --git a/common/web/types/package.json b/common/web/types/package.json index fd59cffa97d..e481735cc44 100644 --- a/common/web/types/package.json +++ b/common/web/types/package.json @@ -18,8 +18,7 @@ "build": "tsc -b", "build:schema": "ajv compile", "lint": "eslint .", - "test": "npm run lint && cd test && tsc -b && cd .. && c8 --skip-full --reporter=lcov --reporter=text mocha", - "prepublishOnly": "npm run build" + "test": "npm run lint && cd test && tsc -b && cd .. && c8 --skip-full --reporter=lcov --reporter=text mocha" }, "author": "Marc Durdin (https://github.com/mcdurdin)", "license": "MIT", diff --git a/common/windows/delphi/general/RegistryKeys.pas b/common/windows/delphi/general/RegistryKeys.pas index 57f865f9b29..62599851cd2 100644 --- a/common/windows/delphi/general/RegistryKeys.pas +++ b/common/windows/delphi/general/RegistryKeys.pas @@ -103,8 +103,7 @@ interface SRegValue_Engine_OEMProductPath = 'oem product path'; -// SRegValue_UnknownLayoutID = 'unknown layout id'; // LM - SRegValue_Legacy_Default_UnknownLayoutID = '000005FE'; // I4220 + SRegValue_Legacy_Default_UnknownLayoutID = '000005FE'; // I4220 SRegValue_KeymanDebug = 'debug'; // CU @@ -112,11 +111,8 @@ interface SRegValue_ShowStartup = 'show startup'; // CU SRegValue_ShowWelcome = 'show welcome'; // CU - //SRegValue_NoCheckAssociations = 'no check associations'; // CU SRegValue_UseAdvancedInstall = 'use advanced install'; // CU -//TOUCH SRegValue_UseTouchLayout = 'use touch layout'; // CU, default false - SRegValue_AltGrCtrlAlt = 'simulate altgr'; // CU SRegValue_KeyboardHotKeysAreToggle = 'hotkeys are toggles'; // CU SRegValue_ReleaseShiftKeysAfterKeyPress = 'release shift keys after key press'; // CU @@ -171,9 +167,6 @@ interface SRegValue_DeadkeyConversionMode = 'deadkey conversion mode'; // CU // I4552 SRegValue_UnderlyingLayout = 'underlying layout'; // CU -// SRegKey_AppInitDLLs = 'Software\Microsoft\Windows NT\CurrentVersion\Windows'; // LM -// SRegValue_AppInitDLLs = 'AppInit_DLLs'; // LM - SRegKey_KeyboardLayoutToggle = 'keyboard layout\toggle'; // CU // I2522 SRegValue_Toggle_Hotkey = 'Hotkey'; SRegValue_Toggle_LanguageHotkey = 'Language Hotkey'; @@ -272,10 +265,6 @@ interface SRegValue_CPIUP_CachedLanguageName = 'CachedLanguageName'; SRegValue_CPIUP_InputMethodOverride = 'InputMethodOverride'; - { User profile keys } - - //SRegKey_NTProfileList = 'Software\Microsoft\Windows NT\CurrentVersion\ProfileList'; - { Font keys } SRegKey_FontList_LM = 'Software\Microsoft\Windows\CurrentVersion\Fonts'; // LM @@ -318,13 +307,6 @@ interface SRegKey_IDETestFonts_CU = SRegKey_IDE_CU + '\TestFonts'; // CU SRegKey_IDEVisualKeyboard_CU = SRegKey_IDE_CU + '\VisualKeyboard'; // CU SRegKey_IDEToolbars_CU = SRegKey_IDE_CU + '\Toolbars'; // CU -// SRegKey_KCT = SRegKey_KeymanDeveloper_CU + '\KCT'; // LM CU -// SRegKey_KCTFiles = SRegKey_KCT_CU + '\Files'; // CU - -// SRegKey_IDEOnline = SRegKey_IDE_CU + '\Online'; // CU -// SRegKey_IDE_BrandingPackTest = SRegKey_IDE_CU + '\Branding Pack\Test'; // CU // I4873 - -// SRegKey_CRM = SRegKey_KeymanDeveloper + '\CRM'; // CU SRegValue_CheckForUpdates = 'check for updates'; // CU SRegValue_LastUpdateCheckTime = 'last update check time'; // CU @@ -335,8 +317,6 @@ interface SRegValue_KeepInTouchShown = 'keep in touch shown'; // CU. bool // I4658 - //SRegValue_OnlineUsername = 'online username'; - //SRegValue_OnlinePassword = 'online password'; SRegValue_OnlineLogin = 'online login'; { SRegKey_CRM values } @@ -349,7 +329,6 @@ interface { SRegKey_KeymanDeveloper values } - //SRegValue_ShowStartup = 'show startup'; // CU -- see Keyman option of same name SRegValue_Evaluation = 'evaluation'; // CU SRegValue_ActiveProject = 'active project'; // CU @@ -364,7 +343,6 @@ interface SRegValue_IDEMRU = 'MRU'; // CU SRegValue_CharMapSize = 'char map size'; // CU -//SRegValue_IDERegressionTestPath = 'regression test path'; // CU { SRegKey_IDEVisualKeyboard values } @@ -393,8 +371,6 @@ interface SRegValue_IDEOptAutoSaveBeforeCompiling = 'auto save before compiling'; // CU SRegValue_IDEOptOSKAutoSaveBeforeImporting = 'osk auto save before importing'; // CU - SRegValue_IDEOptUseLegacyCompiler = 'use legacy compiler'; // CU - // Note: keeping 'web host port' reg value name to ensure settings maintained // from version 14.0 and earlier of Keyman Developer. Other values are // new with Keyman Developer 15.0 @@ -409,8 +385,6 @@ interface SRegValue_IDEOptCharMapDisableDatabaseLookups = 'char map disable database lookups'; // CU SRegValue_IDEOptCharMapAutoLookup = 'char map auto lookup'; // CU - SRegValue_IDEOptMultipleInstances = 'multiple instances'; // CU - SRegValue_IDEOptOpenKeyboardFilesInSourceView = 'open keyboard files in source view'; // CU // I4751 SRegValue_IDEDisplayTheme = 'display theme'; // I4796 @@ -425,10 +399,6 @@ interface SRegValue_IDEOpt_DefaultProjectPath = 'default project path'; - { SRegKey_KCT values } - -// SRegValue_KCTTemplatePath = 'template path'; // LM - {------------------------------------------------------------------------------- - Shared keys and values - ------------------------------------------------------------------------------} diff --git a/common/windows/delphi/general/UserMessages.pas b/common/windows/delphi/general/UserMessages.pas index 9480714f0a3..0b547891490 100644 --- a/common/windows/delphi/general/UserMessages.pas +++ b/common/windows/delphi/general/UserMessages.pas @@ -42,10 +42,6 @@ interface WM_USER_CheckFonts = WM_USER + 115; const - // KeymanDeveloperUtils - WM_USER_LOADREGFILES = WM_USER+120; - //WM_USER_FORMSHOWN = WM_USER+100; - WC_COMMAND = 0; WC_HELP = 1; WC_OPENURL = 2; diff --git a/core/CORE_API_VERSION.md b/core/CORE_API_VERSION.md new file mode 100644 index 00000000000..3eefcb9dd5b --- /dev/null +++ b/core/CORE_API_VERSION.md @@ -0,0 +1 @@ +1.0.0 diff --git a/core/meson.build b/core/meson.build index 2061942d61b..716faca2dc2 100644 --- a/core/meson.build +++ b/core/meson.build @@ -12,14 +12,15 @@ project('keyman_core', 'cpp', 'c', 'cpp_std=c++14', 'b_vscrt=static_from_buildtype', 'warning_level=2'], - meson_version: '>=0.53.0') + meson_version: '>=0.57.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') -lib_version = '1.0.0' +fs = import('fs') +lib_version = fs.read('CORE_API_VERSION.md').strip() py = import('python') python = py.find_installation() diff --git a/core/tests/unit/ldml/keyboards/meson.build b/core/tests/unit/ldml/keyboards/meson.build index 89077fabaca..f4b7f23a29d 100644 --- a/core/tests/unit/ldml/keyboards/meson.build +++ b/core/tests/unit/ldml/keyboards/meson.build @@ -10,8 +10,10 @@ tests_from_cldr = [ 'ja-Latn', 'pt-t-k0-abnt2', - 'fr-t-k0-azerty', + # 'fr-t-k0-optimise', (not yet) + # 'fr-t-k0-test', 'pcm', + 'bn', ] tests_without_testdata = [ diff --git a/developer/src/common/web/utils/index.ts b/developer/src/common/web/utils/index.ts index f18f2e90de5..fe3cd7ad847 100644 --- a/developer/src/common/web/utils/index.ts +++ b/developer/src/common/web/utils/index.ts @@ -1 +1,2 @@ -export { validateMITLicense } from './src/validate-mit-license.js'; \ No newline at end of file +export { validateMITLicense } from './src/validate-mit-license.js'; +export { KeymanSentry } from './src/KeymanSentry.js'; diff --git a/developer/src/kmc/src/util/KeymanSentry.ts b/developer/src/common/web/utils/src/KeymanSentry.ts similarity index 62% rename from developer/src/kmc/src/util/KeymanSentry.ts rename to developer/src/common/web/utils/src/KeymanSentry.ts index 3314086f6a3..bc5f126803f 100644 --- a/developer/src/kmc/src/util/KeymanSentry.ts +++ b/developer/src/common/web/utils/src/KeymanSentry.ts @@ -1,5 +1,3 @@ -import { KmnCompiler } from "@keymanapp/kmc-kmn"; -import { NodeCompilerCallbacks } from "./NodeCompilerCallbacks.js"; import Sentry from "@sentry/node"; import KEYMAN_VERSION from "@keymanapp/keyman-version"; import { spawnChild } from "./spawnAwait.js"; @@ -10,52 +8,9 @@ import { spawnChild } from "./spawnAwait.js"; */ const CLOSE_TIMEOUT = 2000; -const cli = process.argv.join(' '); let isInit = false; export class KeymanSentry { - static isTestCL() { - // Note single hyphen match which matches other Developer console app - // implementations, but this will also work with - // --sentry-client-test-exception, so we get the best of both worlds. - // This parameter is undocumented by design - return cli.includes('-sentry-client-test-exception'); - } - - static async runTestIfCLRequested() { - if(KeymanSentry.isTestCL()) { - await KeymanSentry.test(); - console.error('Unexpected return from KeymanSentry.test'); - process.exit(1); - } - } - - static async test() { - KeymanSentry.init(); - if(cli.includes('kmcmplib')) { - const compiler = new KmnCompiler(); - const callbacks = new NodeCompilerCallbacks({}); - if(!await compiler.init(callbacks)) { - throw new Error('Failed to instantiate WASM compiler'); - } - try { - compiler.testSentry(); - } catch(e: any) { - await KeymanSentry.captureException(e); - } - } else if(cli.includes('event')) { - const eventId = Sentry.captureMessage('Test message from -sentry-client-test-exception event'); - await Sentry.close(CLOSE_TIMEOUT); - console.log(`Captured test message with id ${eventId}`); - process.exit(0); - } else { - try { - throw new Error('Test error from -sentry-client-test-exception event'); - } catch(e: any) { - await KeymanSentry.captureException(e); - } - } - } static async isEnabled() { if(process.argv.includes('--no-error-reporting')) { @@ -115,14 +70,22 @@ export class KeymanSentry { return null; } - static async captureException(e: any) { + static captureMessage(message: string) { + if(isInit) { + return Sentry.captureMessage(message); + } else { + return null; + } + } + + static async captureException(e: any): Promise { if(isInit) { const eventId = Sentry.captureException(e); process.stderr.write(` Fatal error: ${(e??'').toString()} `); this.writeSentryMessage(eventId); - this.close(); + await this.close(); // For local development, we don't want to bury the trace; we need the cast to avoid // TS2367 (comparison appears to be unintentional) @@ -137,7 +100,7 @@ export class KeymanSentry { static async close() { if(isInit) { - await Sentry.close(2000); + await Sentry.close(CLOSE_TIMEOUT); } } } \ No newline at end of file diff --git a/developer/src/kmc/src/util/spawnAwait.ts b/developer/src/common/web/utils/src/spawnAwait.ts similarity index 100% rename from developer/src/kmc/src/util/spawnAwait.ts rename to developer/src/common/web/utils/src/spawnAwait.ts diff --git a/developer/src/common/web/utils/tsconfig.json b/developer/src/common/web/utils/tsconfig.json index b509561362d..9d67982457f 100644 --- a/developer/src/common/web/utils/tsconfig.json +++ b/developer/src/common/web/utils/tsconfig.json @@ -10,7 +10,8 @@ }, }, "include": [ - "./*.ts", "src/validate-mit-license.ts", + "index.ts", + "src/**/*.ts", ], "references": [ { "path": "../../../../../common/web/types/" }, diff --git a/developer/src/kmc-keyboard-info/package.json b/developer/src/kmc-keyboard-info/package.json index d269e914390..e873d57d7c0 100644 --- a/developer/src/kmc-keyboard-info/package.json +++ b/developer/src/kmc-keyboard-info/package.json @@ -16,8 +16,7 @@ "scripts": { "build": "tsc -b", "lint": "eslint .", - "test": "npm run lint", - "prepublishOnly": "npm run build" + "test": "npm run lint" }, "license": "MIT", "bugs": { diff --git a/developer/src/kmc-kmn/build.sh b/developer/src/kmc-kmn/build.sh index 7766610ac7a..a6163d9eaf1 100755 --- a/developer/src/kmc-kmn/build.sh +++ b/developer/src/kmc-kmn/build.sh @@ -70,7 +70,11 @@ if builder_start_action test; then copy_deps tsc --build test/ npm run lint - c8 --reporter=lcov --reporter=text --exclude-after-remap mocha "${builder_extra_params[@]}" + readonly C8_THRESHOLD=70 + 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." + builder_finish_action success test fi diff --git a/developer/src/kmc-kmn/package.json b/developer/src/kmc-kmn/package.json index 5802c859055..b4b97cbfd45 100644 --- a/developer/src/kmc-kmn/package.json +++ b/developer/src/kmc-kmn/package.json @@ -18,8 +18,7 @@ "scripts": { "build": "tsc -b", "lint": "eslint .", - "test": "npm run lint && cd test && tsc -b && cd .. && c8 --reporter=lcov --reporter=text mocha", - "prepublishOnly": "npm run build" + "test": "npm run lint && cd test && tsc -b && cd .. && c8 --reporter=lcov --reporter=text mocha" }, "author": "Marc Durdin (https://github.com/mcdurdin)", "license": "MIT", @@ -61,7 +60,6 @@ "exclude-after-remap": true, "exclude": [ "src/import/", - "src/kmw-compiler", "test/" ] }, diff --git a/developer/src/kmc-kmn/src/compiler/compiler.ts b/developer/src/kmc-kmn/src/compiler/compiler.ts index df20b698529..317cfc757c3 100644 --- a/developer/src/kmc-kmn/src/compiler/compiler.ts +++ b/developer/src/kmc-kmn/src/compiler/compiler.ts @@ -298,6 +298,12 @@ export class KmnCompiler implements UnicodeSetParser { if(wasm_result.extra.targets & COMPILETARGETS_JS) { wasm_options.target = 1; // CKF_KEYMANWEB TODO use COMPILETARGETS_JS + + // We always want debug data in the intermediate .kmx, so that error + // messages from KMW compiler can give line numbers in .kmn. This + // 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); if(!wasm_result.result) { return null; 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 26981f5711c..92cba914c5e 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts @@ -46,3 +46,7 @@ export function IsKeyboardVersion14OrLater(): boolean { export function IsKeyboardVersion15OrLater(): boolean { return fk.fileVersion >= KMX.KMXFile.VERSION_150; } + +export function IsKeyboardVersion17OrLater(): boolean { + return fk.fileVersion >= KMX.KMXFile.VERSION_170; +} diff --git a/developer/src/kmc-kmn/src/kmw-compiler/constants.ts b/developer/src/kmc-kmn/src/kmw-compiler/constants.ts index 55bae9fdb90..1c41df145b8 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/constants.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/constants.ts @@ -1,3 +1,5 @@ +import { KMX } from "@keymanapp/common-types"; + export enum TRequiredKey { K_LOPT='K_LOPT', K_BKSP='K_BKSP', K_ENTER='K_ENTER' }; // I4447 @@ -8,9 +10,9 @@ export const // See also builder.js: specialCharacters; web/source/osk/oskKey.ts: specialCharacters export const CSpecialText10: string = '*Shift*\0*Enter*\0*Tab*\0*BkSp*\0*Menu*\0*Hide*\0*Alt*\0*Ctrl*\0*Caps*\0' + - '*ABC*\0*abc*\0*123*\0*Symbol*\0*Currency*\0*Shifted*\0*AltGr*\0*TabLeft*', + '*ABC*\0*abc*\0*123*\0*Symbol*\0*Currency*\0*Shifted*\0*AltGr*\0*TabLeft*\0', // these names were added in Keyman 14 - CSpecialText14: string = '*LTREnter*\0*LTRBkSp*\0*RTLEnter*\0*RTLBkSp*\0*ShiftLock*\0*ShiftedLock*\0*ZWNJ*\0*ZWNJiOS*\0*ZWNJAndroid*', + CSpecialText14: string = '*LTREnter*\0*LTRBkSp*\0*RTLEnter*\0*RTLBkSp*\0*ShiftLock*\0*ShiftedLock*\0*ZWNJ*\0*ZWNJiOS*\0*ZWNJAndroid*\0', CSpecialText14ZWNJ: string = '*ZWNJ*\0*ZWNJiOS*\0*ZWNJAndroid*', CSpecialText14Map: string[][] = [ ['*LTREnter*', '*Enter*'], @@ -21,9 +23,29 @@ export const ['*ShiftedLock*', '*Shifted*'], ['*ZWNJ*', '<|>'], ['*ZWNJiOS*', '<|>'], - ['*ZWNJAndroid*', '<|>'] + ['*ZWNJAndroid*', '<|>'], + ], + // these names were added in Keyman 17 + CSpecialText17: string = '*Sp*\0*NBSp*\0*NarNBSp*\0*EnQ*\0*EmQ*\0*EnSp*\0*EmSp*\0*PunctSp*\0' + + '*ThSp*\0*HSp*\0*ZWSp*\0*ZWJ*\0*WJ*\0*CGJ*\0*LTRM*\0*RTLM*\0*SH*\0*HTab*\0', + CSpecialText17ZWNJ: string = '*ZWNJGeneric*', + CSpecialText17Map: string[][] = [ + ['*ZWNJGeneric*', '<|>'] ]; + // Map for checking minimum versions and Special Text + export const CSpecialText = new Map([ + [KMX.KMXFile.VERSION_100, CSpecialText10], + // [KMX.KMXFile.VERSION_110, CSpecialText10], - this file version does not exist + // [KMX.KMXFile.VERSION_120, CSpecialText10], - this file version does not exist + // [KMX.KMXFile.VERSION_130, CSpecialText10], - this file version does not exist + [KMX.KMXFile.VERSION_140, CSpecialText14 + CSpecialText10], + [KMX.KMXFile.VERSION_150, CSpecialText14 + CSpecialText10], + [KMX.KMXFile.VERSION_160, CSpecialText14 + CSpecialText10], + [KMX.KMXFile.VERSION_170, CSpecialText17 + CSpecialText14 + CSpecialText10] + ]); + export const CSpecialTextMinVer = KMX.KMXFile.VERSION_100; + export const CSpecialTextMaxVer = KMX.KMXFile.VERSION_170; // These correspond to TSS_ values in kmx.ts export const 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 7c59f69e6f9..0b356a9a18c 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts @@ -233,7 +233,7 @@ export function JavaScript_Rules(keyboard: KMX.KEYBOARD, fMnemonic: boolean, fgp export function JavaScript_Shift(fkp: KMX.KEY, FMnemonic: boolean): number { if (FMnemonic) { if (fkp.ShiftFlags & KMX.KMXFile.VIRTUALCHARKEY) { - callbacks.reportMessage(KmwCompilerMessages.Error_VirtualCharacterKeysNotSupportedInKeymanWeb()); + callbacks.reportMessage(KmwCompilerMessages.Error_VirtualCharacterKeysNotSupportedInKeymanWeb({line:fkp.Line})); return 0; } @@ -241,7 +241,7 @@ export function JavaScript_Shift(fkp: KMX.KEY, FMnemonic: boolean): number { // We prohibit K_ keys for mnemonic layouts. We don't block T_ and U_ keys. // TODO: this doesn't resolve the issue of, e.g. SHIFT+K_SPACE // https://github.com/keymanapp/keyman/issues/265 - callbacks.reportMessage(KmwCompilerMessages.Error_VirtualKeysNotValidForMnemonicLayouts()); + callbacks.reportMessage(KmwCompilerMessages.Error_VirtualKeysNotValidForMnemonicLayouts({line:fkp.Line})); return 0; } } @@ -254,13 +254,13 @@ export function JavaScript_Shift(fkp: KMX.KEY, FMnemonic: boolean): number { // 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 - callbacks.reportMessage(KmwCompilerMessages.Warn_ExtendedShiftFlagsNotSupportedInKeymanWeb({flags: 'LALT, RALT, LCTRL, RCTRL'})); + 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({flags: 'CAPS and NCAPS'})); + 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)); @@ -378,7 +378,7 @@ export function JavaScript_Key(fkp: KMX.KEY, FMnemonic: boolean): number { if (Result == 0 || Result >= TKeymanWebTouchStandardKey.K_LOPT) { // I4141 if(!FUnreachableKeys.includes(fkp)) { - callbacks.reportMessage(KmwCompilerMessages.Hint_UnreachableKeyCode({key: FormatKeyForErrorMessage(fkp,FMnemonic)})); + callbacks.reportMessage(KmwCompilerMessages.Hint_UnreachableKeyCode({line:fkp.Line, key: FormatKeyForErrorMessage(fkp,FMnemonic)})); FUnreachableKeys.push(fkp); } } @@ -482,7 +482,7 @@ function JavaScript_CompositeContextValue(fk: KMX.KEYBOARD, fkp: KMX.KEY, pwsz: Cur--; // don't increment on ifsystemstore -- correlates with AdjustIndex function // I3910 break; case KMX.KMXFile.CODE_CONTEXTEX: // I3980 - Result += `k.KCCM(${Len-Cur},${Len-rec.ContextEx.Index+1},t)`; + Result += `k.KCCM(${Len-Cur},${Len-rec.ContextEx.Index},t)`; break; case KMX.KMXFile.CODE_NOTANY: // I3981 CheckStoreForInvalidFunctions(fk, fkp, rec.Any.Store); // I1520 @@ -490,7 +490,7 @@ function JavaScript_CompositeContextValue(fk: KMX.KEYBOARD, fkp: KMX.KEY, pwsz: `this.s${JavaScript_Name(rec.Any.StoreIndex, rec.Any.Store.dpName)})`; break; default: - callbacks.reportMessage(KmwCompilerMessages.Error_NotSupportedInKeymanWebContext({code: GetCodeName(rec.Code)})); + callbacks.reportMessage(KmwCompilerMessages.Error_NotSupportedInKeymanWebContext({line: fkp.Line, code: GetCodeName(rec.Code)})); Result += '/*.*/ 0 '; } } @@ -572,14 +572,14 @@ function JavaScript_FullContextValue(fk: KMX.KEYBOARD, fkp: KMX.KEY, pwsz: strin FullContext += `{t:'a',a:this.s${JavaScript_Name(rec.Any.StoreIndex, rec.Any.Store.dpName)},n:1}`; break; case KMX.KMXFile.CODE_CONTEXTEX: - FullContext += `{t:'c',c:${rec.ContextEx.Index}}`; // I4611 + FullContext += `{t:'c',c:${rec.ContextEx.Index+1}}`; // I4611 break; case KMX.KMXFile.CODE_INDEX: FullContext += `{t:'i',i:this.s${JavaScript_Name(rec.Index.StoreIndex, rec.Index.Store.dpName)},`+ `o:${rec.Index.Index}}`; // I4611 break; default: - callbacks.reportMessage(KmwCompilerMessages.Error_NotSupportedInKeymanWebContext({code: GetCodeName(rec.Code)})); + callbacks.reportMessage(KmwCompilerMessages.Error_NotSupportedInKeymanWebContext({line: fkp.Line, code: GetCodeName(rec.Code)})); Result += '/*.*/ 0 '; } } @@ -667,7 +667,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp // #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()); + callbacks.reportMessage(KmwCompilerMessages.Error_NotAnyRequiresVersion14({line:fkp.Line})); } Result += nlt + `k.KCXO(${len},t,${AdjustIndex(fkp.dpContext, xstrlen(fkp.dpContext))},${AdjustIndex(fkp.dpContext, ContextIndex)+1});`; break; @@ -677,7 +677,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp // These have no output for a context emit break; default: - callbacks.reportMessage(KmwCompilerMessages.Error_NotSupportedInKeymanWebContext({code: GetCodeName(recContext.Code)})); + callbacks.reportMessage(KmwCompilerMessages.Error_NotSupportedInKeymanWebContext({line: fkp.Line, code: GetCodeName(recContext.Code)})); Result += nlt + '/*.*/ '; // I4611 } } @@ -885,7 +885,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp len = -1; break; default: - callbacks.reportMessage(KmwCompilerMessages.Error_NotSupportedInKeymanWebOutput({code: GetCodeName(rec.Code)})); + callbacks.reportMessage(KmwCompilerMessages.Error_NotSupportedInKeymanWebOutput({line: fkp.Line, code: GetCodeName(rec.Code)})); Result += ''; } } 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 0c2e5bbdb64..4c4b41505ce 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,5 +1,6 @@ import { KmnCompilerMessages } from "../compiler/kmn-compiler-messages.js"; -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m } from "@keymanapp/common-types"; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec } from "@keymanapp/common-types"; +import { kmnfile } from "./compiler-globals.js"; const Namespace = CompilerErrorNamespace.KmwCompiler; // const SevInfo = CompilerErrorSeverity.Info | Namespace; @@ -8,6 +9,12 @@ const Namespace = CompilerErrorNamespace.KmwCompiler; const SevError = CompilerErrorSeverity.Error | Namespace; // const SevFatal = CompilerErrorSeverity.Fatal | Namespace; +const m = (code: number, message: string, o?: {e?: any, filename?: string, line?: number}) : CompilerEvent => ({ + ...CompilerMessageSpec(code, message, o?.e), + filename: o?.filename ?? kmnfile, + line: o?.line, +}); + export class KmwCompilerMessages extends KmnCompilerMessages { // Note: for legacy reasons, KMWCompilerMessages extends from // KMNCompilerMessages as they share the same error codes. This can be a @@ -19,51 +26,73 @@ export class KmwCompilerMessages extends KmnCompilerMessages { static Error_InvalidBegin = () => m(this.ERROR_InvalidBegin, `A "begin unicode" statement is required to compile a KeymanWeb keyboard`); + static Error_InvalidTouchLayoutFile = (o:{filename:string}) => m(this.ERROR_InvalidTouchLayoutFile, `Touch layout file ${o.filename} is not valid`); + static Warn_DontMixChiralAndNonChiralModifiers = () => m(this.WARN_DontMixChiralAndNonChiralModifiers, `This keyboard contains Ctrl,Alt and LCtrl,LAlt,RCtrl,RAlt sets of modifiers. Use only one or the other set for web target.`); + static Warn_OptionStoreNameInvalid = (o:{name:string}) => m(this.WARN_OptionStoreNameInvalid, `The option store ${o.name} should be named with characters in the range A-Z, a-z, 0-9 and _ only.`); - static Error_VirtualCharacterKeysNotSupportedInKeymanWeb = () => m(this.ERROR_VirtualCharacterKeysNotSupportedInKeymanWeb, - `Virtual character keys not currently supported in KeymanWeb`); - static Error_VirtualKeysNotValidForMnemonicLayouts = () => m(this.ERROR_VirtualKeysNotValidForMnemonicLayouts, - `Virtual keys are not valid for mnemonic layouts`); - static Warn_ExtendedShiftFlagsNotSupportedInKeymanWeb = (o:{flags:string}) => m(this.WARN_ExtendedShiftFlagsNotSupportedInKeymanWeb, - `Extended shift flags ${o.flags} are not supported in KeymanWeb`); - static Hint_UnreachableKeyCode = (o:{key:string}) => m(this.HINT_UnreachableKeyCode, - `The rule will never be matched for key ${o.key} because its key code is never fired.`); + + static Error_VirtualCharacterKeysNotSupportedInKeymanWeb = (o:{line:number}) => m(this.ERROR_VirtualCharacterKeysNotSupportedInKeymanWeb, + `Virtual character keys not currently supported in KeymanWeb`, o); + + static Error_VirtualKeysNotValidForMnemonicLayouts = (o:{line:number}) => m(this.ERROR_VirtualKeysNotValidForMnemonicLayouts, + `Virtual keys are not valid for mnemonic layouts`, o); + + static Warn_ExtendedShiftFlagsNotSupportedInKeymanWeb = (o:{line:number,flags:string}) => m(this.WARN_ExtendedShiftFlagsNotSupportedInKeymanWeb, + `Extended shift flags ${o.flags} are not supported in KeymanWeb`, o); + + static Hint_UnreachableKeyCode = (o:{line:number,key:string}) => m(this.HINT_UnreachableKeyCode, + `The rule will never be matched for key ${o.key} because its key code is never fired.`, o); + static Error_NotSupportedInKeymanWebStore = (o:{code:string,store:string}) => m(this.ERROR_NotSupportedInKeymanWebStore, - `${o.code} is not currently supported in store '${o.store}' when used by any or index`); - static Error_NotSupportedInKeymanWebContext = (o:{code:String}) => m(this.ERROR_NotSupportedInKeymanWebContext, - `Statement ${o.code} is not currently supported in context`); - static Error_NotSupportedInKeymanWebOutput = (o:{code:string}) => m(this.ERROR_NotSupportedInKeymanWebOutput, - `Statement ${o.code} is not currently supported in output`); - static Warn_HelpFileMissing = (o:{filename: string, e:any}) => m(this.WARN_HelpFileMissing, - `File ${o.filename} could not be loaded: ${(o.e??'').toString()}`); - static Warn_EmbedJsFileMissing = (o:{filename: string, e:any}) => m(this.WARN_EmbedJsFileMissing, - `File ${o.filename} could not be loaded: ${(o.e??'').toString()}`); + `'${o.code}' is not currently supported in store '${o.store}' when used by any or index for web and touch targets`); + + static Error_NotSupportedInKeymanWebContext = (o:{line:number, code:String}) => m(this.ERROR_NotSupportedInKeymanWebContext, + `Statement '${o.code}' is not currently supported in context for web and touch targets`, o); + + static Error_NotSupportedInKeymanWebOutput = (o:{line:number, code:string}) => m(this.ERROR_NotSupportedInKeymanWebOutput, + `Statement '${o.code}' is not currently supported in output for web and touch targets`, o); + + static Warn_HelpFileMissing = (o:{line:number, helpFilename:string, e:any}) => m(this.WARN_HelpFileMissing, + `File ${o.helpFilename} could not be loaded: ${(o.e??'').toString()}`,o); + + static Warn_EmbedJsFileMissing = (o:{line:number, jsFilename: string, e:any}) => m(this.WARN_EmbedJsFileMissing, + `File ${o.jsFilename} could not be loaded: ${(o.e??'').toString()}`, o); + static Warn_TouchLayoutMissingLayer = (o:{keyId:string, platformName:string, layerId:string, nextLayer:string}) => m(this.WARN_TouchLayoutMissingLayer, `Key "${o.keyId}" on platform "${o.platformName}", layer "${o.layerId}", references a missing layer "${o.nextLayer}"`); + static Warn_TouchLayoutUnidentifiedKey = (o:{layerId:string}) => m(this.WARN_TouchLayoutUnidentifiedKey, `A key on layer "${o.layerId}" has no identifier.`); + static Error_TouchLayoutInvalidIdentifier = (o:{keyId:string, platformName: string, layerId:string}) => m(this.ERROR_TouchLayoutInvalidIdentifier, `Key "${o.keyId}" on "${o.platformName}", layer "${o.layerId}" has an invalid identifier.`); + static Warn_TouchLayoutCustomKeyNotDefined = (o:{keyId:string, platformName:string, layerId:string}) => m(this.WARN_TouchLayoutCustomKeyNotDefined, `Key "${o.keyId}" on platform "${o.platformName}", layer "${o.layerId}", is a custom key but has no corresponding rule in the source.`); - static Warn_TouchLayoutSpecialLabelOnNormalKey = (o:{keyId:string, platformName:string, layerId:string, label:string}) => m(this.WARN_TouchLayoutSpecialLabelOnNormalKey, - `Key "${o.keyId}" on platform "${o.platformName}", layer "${o.layerId}" does not have the key type "Special" or "Special (active)" but has the label "${o.label}". This feature is only supported in Keyman 14 or later`); + + static Warn_TouchLayoutSpecialLabelOnNormalKey = (o:{keyId:string, platformName:string, layerId:string, label:string}) => + m(this.WARN_TouchLayoutSpecialLabelOnNormalKey, + `Key "${o.keyId}" on platform "${o.platformName}", layer "${o.layerId}" does not have `+ + `the key type "Special" or "Special (active)" but has the label "${o.label}". This feature is only supported in Keyman 14 or later`); + static Error_InvalidKeyCode = (o:{keyId: string}) => m(this.ERROR_InvalidKeyCode, `Invalid key identifier "${o.keyId}"`); + static Warn_TouchLayoutFontShouldBeSameForAllPlatforms = () => m(this.WARN_TouchLayoutFontShouldBeSameForAllPlatforms, `The touch layout font should be the same for all platforms.`); + static Warn_TouchLayoutMissingRequiredKeys = (o:{layerId:string, platformName:string, missingKeys:string}) => m(this.WARN_TouchLayoutMissingRequiredKeys, `Layer "${o.layerId}" on platform "${o.platformName}" is missing the required key(s) '${o.missingKeys}'.`); // Following messages are kmw-compiler only, so use KmwCompiler error namespace - static Error_NotAnyRequiresVersion14 = () => m(this.ERROR_NotAnyRequiresVersion14, - `Statement notany in context() match requires version 14.0+ of KeymanWeb`); + static Error_NotAnyRequiresVersion14 = (o:{line: number}) => m(this.ERROR_NotAnyRequiresVersion14, + `Statement notany in context() match requires version 14.0+ of KeymanWeb`, o); static ERROR_NotAnyRequiresVersion14 = SevError | 0x0001; static Error_TouchLayoutIdentifierRequires15 = (o:{keyId:string, platformName:string, layerId:string}) => m(this.ERROR_TouchLayoutIdentifierRequires15, 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 29922a492f2..dd891cd7cb1 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,6 @@ import { KMX, CompilerOptions, CompilerCallbacks, KvkFileReader, VisualKeyboard, KmxFileReader, KvkFile } from "@keymanapp/common-types"; import { ExpandSentinel, incxstr, xstrlen } from "./util.js"; -import { options, nl, FTabStop, setupGlobals, IsKeyboardVersion10OrLater, callbacks, FFix183_LadderLength, FCallFunctions, fk } from "./compiler-globals.js"; +import { options, nl, FTabStop, setupGlobals, IsKeyboardVersion10OrLater, callbacks, FFix183_LadderLength, FCallFunctions, fk, 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"; @@ -53,6 +53,12 @@ export function WriteCompiledKeyboard( setupGlobals(callbacks, opts, FDebug?' ':'', FDebug?'\r\n':'', kmxResult, keyboard, kmnfile); + const isStoreType = (index:number, type: number) => !!(kmxResult.extra.stores[index].storeType & type); + const isDebugStore = (index: number) => isStoreType(index, STORETYPE_DEBUG); + const isReservedStore = (index: number) => isStoreType(index, STORETYPE_RESERVED); + const isOptionStore = (index: number) => isStoreType(index, STORETYPE_OPTION); + const getStoreLine = (index: number) => kmxResult.extra.stores[index].line; + let vMnemonic: number = 0; let sRTL: string = "", sHelp: string = "''", sHelpFile: string = "", sEmbedJSFilename: string = "", sEmbedCSSFilename: string = ""; @@ -63,6 +69,7 @@ export function WriteCompiledKeyboard( let sModifierBitmask: string; let FOptionStores: string; let FKeyboardVersion = "1.0"; + let sHelpFileStoreIndex, sEmbedJSStoreIndex, sEmbedCSSStoreIndex; let result = ""; // Locate the name of the keyboard @@ -76,6 +83,7 @@ export function WriteCompiledKeyboard( } else if (fsp.dpName == 'HelpFile' || fsp.dwSystemID == KMX.KMXFile.TSS_KMW_HELPFILE) { sHelpFile = fsp.dpString; + sHelpFileStoreIndex = i; } else if (fsp.dpName == 'Help' || fsp.dwSystemID == KMX.KMXFile.TSS_KMW_HELPTEXT) { sHelp = '"'+RequotedString(fsp.dpString)+'"'; @@ -85,9 +93,11 @@ export function WriteCompiledKeyboard( } else if (fsp.dpName == 'EmbedJS' || fsp.dwSystemID == KMX.KMXFile.TSS_KMW_EMBEDJS) { sEmbedJSFilename = fsp.dpString; + sEmbedJSStoreIndex = i; } else if (fsp.dpName == 'EmbedCSS' || fsp.dwSystemID == KMX.KMXFile.TSS_KMW_EMBEDCSS) { // I4368 sEmbedCSSFilename = fsp.dpString; + sEmbedCSSStoreIndex = i; } else if (fsp.dpName == 'RTL' || fsp.dwSystemID == KMX.KMXFile.TSS_KMW_RTL) { sRTL = fsp.dpString == '1' ? FTabStop+'this.KRTL=1;'+nl : ''; // I3681 @@ -121,7 +131,7 @@ export function WriteCompiledKeyboard( sHelp = html.replace(/\r/g, '').replace(/\n/g, ' '); sHelp = requote(sHelp); } catch(e) { - callbacks.reportMessage(KmwCompilerMessages.Warn_HelpFileMissing({filename: sHelpFile, e})); + callbacks.reportMessage(KmwCompilerMessages.Warn_HelpFileMissing({line: getStoreLine(sHelpFileStoreIndex), helpFilename: sHelpFile, e})); sHelp = ''; } } @@ -133,7 +143,7 @@ export function WriteCompiledKeyboard( const data = callbacks.loadFile(sEmbedJSFilename); sEmbedJS = new TextDecoder().decode(data); } catch(e) { - callbacks.reportMessage(KmwCompilerMessages.Warn_EmbedJsFileMissing({filename: sEmbedJSFilename, e})); + callbacks.reportMessage(KmwCompilerMessages.Warn_EmbedJsFileMissing({line: getStoreLine(sEmbedJSStoreIndex), jsFilename: sEmbedJSFilename, e})); sEmbedJS = ''; } } @@ -147,7 +157,7 @@ export function WriteCompiledKeyboard( if(sEmbedCSS != '' && !sEmbedCSS.endsWith('\r\n')) sEmbedCSS += '\r\n'; // match CompileKeymanWeb.pas } catch(e) { // TODO(lowpri): rename error constant to Warn_EmbedFileMissing - callbacks.reportMessage(KmwCompilerMessages.Warn_EmbedJsFileMissing({filename: sEmbedCSSFilename, e})); + callbacks.reportMessage(KmwCompilerMessages.Warn_EmbedJsFileMissing({line: getStoreLine(sEmbedCSSStoreIndex), jsFilename: sEmbedCSSFilename, e})); sEmbedCSS = ''; } } @@ -222,12 +232,6 @@ export function WriteCompiledKeyboard( result += `${FTabStop}this.KCSS="${RequotedString(sEmbedCSS)}";${nl}`; } - const isStoreType = (index:number, type: number) => !!(kmxResult.extra.stores[index].storeType & type); - const isDebugStore = (index: number) => isStoreType(index, STORETYPE_DEBUG); - const isReservedStore = (index: number) => isStoreType(index, STORETYPE_RESERVED); - const isOptionStore = (index: number) => isStoreType(index, STORETYPE_OPTION); - const getStoreLine = (index: number) => kmxResult.extra.stores[index].line; - // Write the stores out FOptionStores = ''; for(let i = 0; i < keyboard.stores.length; i++) { @@ -438,8 +442,13 @@ function GetKeyboardModifierBitmask(keyboard: KMX.KEYBOARD, fMnemonic: boolean): function JavaScript_SetupDebug() { if(IsKeyboardVersion10OrLater()) { if(options.saveDebug) { - return 'var modCodes = keyman.osk.modifierCodes;'+nl+ - FTabStop+'var keyCodes = keyman.osk.keyCodes;'+nl; + if(IsKeyboardVersion17OrLater()) { + return 'var modCodes = KeymanWeb.Codes.modifierCodes;'+nl+ + FTabStop+'var keyCodes = KeymanWeb.Codes.keyCodes;'+nl; + } else { + return 'var modCodes = keyman.osk.modifierCodes;'+nl+ + FTabStop+'var keyCodes = keyman.osk.keyCodes;'+nl; + } } } return ''; 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 de335c088ba..dfde10772a8 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,7 @@ import { KMX, Osk, TouchLayout, TouchLayoutFileReader, TouchLayoutFileWriter } from "@keymanapp/common-types"; -import { callbacks, IsKeyboardVersion14OrLater, IsKeyboardVersion15OrLater } from "./compiler-globals.js"; +import { callbacks, fk, IsKeyboardVersion14OrLater, IsKeyboardVersion15OrLater, IsKeyboardVersion17OrLater } from "./compiler-globals.js"; import { JavaScript_Key } from "./javascript-strings.js"; -import { TRequiredKey, CRequiredKeys, CSpecialText10, CSpecialText14, CSpecialText14ZWNJ, CSpecialText14Map } 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"; @@ -144,8 +144,9 @@ 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. - if((CSpecialText10.includes(FText) || CSpecialText14.includes(FText)) && - !CSpecialText14ZWNJ.includes(FText) && + const mapVersion = Math.max(Math.min(fk.fileVersion, CSpecialTextMaxVer), CSpecialTextMinVer); + const specialText = CSpecialText.get(mapVersion); + if(specialText.includes(FText) && !IsKeyboardVersion14OrLater() && !([TouchLayout.TouchLayoutKeySp.special, TouchLayout.TouchLayoutKeySp.specialActive].includes(FKeyType))) { callbacks.reportMessage(KmwCompilerMessages.Warn_TouchLayoutSpecialLabelOnNormalKey({ @@ -199,6 +200,22 @@ function TransformSpecialKeys14(FDebug: boolean, sLayoutFile: string): string { return sLayoutFile; } +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()) { + for(let i = 0; i < CSpecialText17Map.length; i++) { + // Assumes the JSON output format will not change + if(FDebug) { + sLayoutFile = sLayoutFile.replaceAll('"text": "'+CSpecialText17Map[i][0]+'"', '"text": this._v>16 ? "'+CSpecialText17Map[i][0]+'" : "'+CSpecialText17Map[i][1]+'"'); + } else { + sLayoutFile = sLayoutFile.replaceAll('"text":"'+CSpecialText17Map[i][0]+'"', '"text":this._v>16?"'+CSpecialText17Map[i][0]+'":"'+CSpecialText17Map[i][1]+'"'); + } + } + } + return sLayoutFile; +} + export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFile: string, sVKDictionary: string, displayMap: Osk.PuaMap): VLFOutput { // I4060 // I4139 let FDictionary: string[] = sVKDictionary.split(/\s+/); @@ -286,8 +303,10 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil sLayoutFile = TransformSpecialKeys14(FDebug, sLayoutFile); + sLayoutFile = TransformSpecialKeys17(FDebug, sLayoutFile); + return { output: sLayoutFile, result } -} \ No newline at end of file +} diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_not_supported_in_keyman_web_output.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_not_supported_in_keyman_web_output.kmn new file mode 100644 index 00000000000..cd2c34bb64b --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_not_supported_in_keyman_web_output.kmn @@ -0,0 +1,11 @@ +c Tests ERROR_NotSupportedInKeymanWebOutput for `return` + +store(&NAME) 'ERROR_NotSupportedInKeymanWebOutput' +store(&VERSION) '9.0' +store(&TARGETS) 'web' + +begin unicode > use(main) + +group(main) using keys + ++ 'a' > return diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context.js b/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context.js new file mode 100644 index 00000000000..3e401d6241c --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context.js @@ -0,0 +1,54 @@ +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_test_context_in_context()); +} +function Keyboard_test_context_in_context() +{ + 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_test_context_in_context"; + this.KN="test context(n) in context, #9930"; + this.KMINVER="10.0"; + this.KV=null; + this.KDU=0; + this.KH=''; + this.KM=0; + this.KBVER="1.0"; + this.KMBM=modCodes.SHIFT /* 0x0010 */; + this.s_liveQwerty_6="qwerty"; + this.s_deadQwerty_7=[{t:'d',d:0},{t:'d',d:1},{t:'d',d:2},{t:'d',d:3},{t:'d',d:4},{t:'d',d:5}]; + 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.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_1 /* 0x31 */)) { + if(k.KFCM(1,t,[{t:'a',a:this.s_liveQwerty_6}])){ + r=m=1; // Line 13 + k.KDC(1,t); + k.KO(-1,t,"?"); + k.KIO(-1,this.s_deadQwerty_7,1,t); + k.KIO(-1,this.s_deadQwerty_7,1,t); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_PERIOD /* 0xBE */)) { + if(k.KFCM(3,t,['?',{t:'a',a:this.s_deadQwerty_7},{t:'c',c:2}])){ + r=m=1; // Line 17 + k.KDC(3,t); + k.KO(-1,t,"("); + k.KIO(-1,this.s_liveQwerty_6,2,t); + k.KIO(-1,this.s_liveQwerty_6,2,t); + k.KO(-1,t,")"); + } + } + return r; + }; +} diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context.kmn new file mode 100644 index 00000000000..4854ef491ce --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context.kmn @@ -0,0 +1,17 @@ +store(&VERSION) "10.0" +store(&TARGETS) 'web' +store(&NAME) 'test context(n) in context, #9930' + +begin Unicode > use(main) + +group(main) using keys + +store(liveQwerty) 'qwerty' +store(deadQwerty) dk(q) dk(w) dk(e) dk(r) dk(t) dk(y) + +c any/index with deadkeys in stores. +any(liveQwerty) + '!' > '?' index(deadQwerty, 1) index(deadQwerty, 1) + +c The rule below is misgenerated by kmc-17.0.205-alpha on `context(2)` statement; +c see https://github.com/keymanapp/keyman/issues/9930 +'?' any(deadQwerty) context(2) + '.' > '(' index(liveQwerty, 2) index(liveQwerty, 2) ')' diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context_9.js b/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context_9.js new file mode 100644 index 00000000000..d4a6983ffe1 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context_9.js @@ -0,0 +1,47 @@ + +KeymanWeb.KR(new Keyboard_test_context_in_context_9()); + +function Keyboard_test_context_in_context_9() +{ + + this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9; + this.KI="Keyboard_test_context_in_context_9"; + this.KN="test context(n) in context, #9930, v9.0"; + this.KMINVER="9.0"; + this.KV=null; + this.KDU=0; + this.KH=''; + this.KM=0; + this.KBVER="1.0"; + this.KMBM=0x0010; + this.s_liveQwerty_6="qwerty"; + this.s_deadQwerty_7="123456"; + 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, 0x4010, 0x31)) { + if(k.KA(0,k.KC(1,1,t),this.s_liveQwerty_6)){ + r=m=1; // Line 13 + k.KO(1,t,"?"); + k.KIO(-1,this.s_deadQwerty_7,1,t); + k.KIO(-1,this.s_deadQwerty_7,1,t); + } + } + else if(k.KKM(e, 0x4000, 0xBE)) { + if(k.KCM(3,t,"?",1)&&k.KA(1,k.KC(2,1,t),this.s_deadQwerty_7)&&k.KCCM(1,2,t)){ + r=m=1; // Line 17 + k.KO(3,t,"("); + k.KIO(-1,this.s_liveQwerty_6,2,t); + k.KIO(-1,this.s_liveQwerty_6,2,t); + k.KO(-1,t,")"); + } + } + return r; + }; +} diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context_9.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context_9.kmn new file mode 100644 index 00000000000..6b06eb6cec0 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/test_context_in_context_9.kmn @@ -0,0 +1,17 @@ +store(&VERSION) "9.0" +store(&TARGETS) 'web' +store(&NAME) 'test context(n) in context, #9930, v9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(liveQwerty) 'qwerty' +store(deadQwerty) '123456' + +c any/index with deadkeys in stores. +any(liveQwerty) + '!' > '?' index(deadQwerty, 1) index(deadQwerty, 1) + +c The rule below is misgenerated by kmc-17.0.205-alpha on `context(2)` statement; +c see https://github.com/keymanapp/keyman/issues/9930 +'?' any(deadQwerty) context(2) + '.' > '(' index(liveQwerty, 2) index(liveQwerty, 2) ')' diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output.js b/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output.js new file mode 100644 index 00000000000..da0c1b9477f --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output.js @@ -0,0 +1,44 @@ +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_test_contextn_in_output()); +} +function Keyboard_test_contextn_in_output() +{ + 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_test_contextn_in_output"; + this.KN="test context(n) in output, #9930, v10.0"; + this.KMINVER="10.0"; + this.KV=null; + this.KDU=0; + this.KH=''; + this.KM=0; + this.KBVER="1.0"; + this.KMBM=modCodes.SHIFT /* 0x0010 */; + this.s_liveQwerty_6="qwerty"; + this.s_deadQwerty_7=[{t:'d',d:0},{t:'d',d:1},{t:'d',d:2},{t:'d',d:3},{t:'d',d:4},{t:'d',d:5}]; + 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.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_1 /* 0x31 */)) { + if(k.KFCM(2,t,[{t:'a',a:this.s_liveQwerty_6},'1'])){ + r=m=1; // Line 13 + k.KDC(2,t); + k.KO(-1,t,"?"); + k.KIO(-1,this.s_deadQwerty_7,1,t); + k.KO(-1,t,"1"); + } + } + return r; + }; +} diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output.kmn new file mode 100644 index 00000000000..4a34730922b --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output.kmn @@ -0,0 +1,13 @@ +store(&VERSION) "10.0" +store(&TARGETS) 'web' +store(&NAME) 'test context(n) in output, #9930, v10.0' + +begin Unicode > use(main) + +group(main) using keys + +store(liveQwerty) 'qwerty' +store(deadQwerty) dk(q) dk(w) dk(e) dk(r) dk(t) dk(y) + +c any/index with deadkeys in stores. +any(liveQwerty) '1' + '!' > '?' index(deadQwerty, 1) context(2) diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output_9.js b/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output_9.js new file mode 100644 index 00000000000..07755dc1752 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output_9.js @@ -0,0 +1,38 @@ + +KeymanWeb.KR(new Keyboard_test_contextn_in_output_9()); + +function Keyboard_test_contextn_in_output_9() +{ + + this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9; + this.KI="Keyboard_test_contextn_in_output_9"; + this.KN="test context(n) in output, #9930, v9.0"; + this.KMINVER="9.0"; + this.KV=null; + this.KDU=0; + this.KH=''; + this.KM=0; + this.KBVER="1.0"; + this.KMBM=0x0010; + this.s_liveQwerty_6="qwerty"; + this.s_deadQwerty_7="123456"; + 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, 0x4010, 0x31)) { + if(k.KA(0,k.KC(2,1,t),this.s_liveQwerty_6)&&k.KCM(1,t,"1",1)){ + r=m=1; // Line 13 + k.KO(2,t,"?"); + k.KIO(-1,this.s_deadQwerty_7,1,t); + k.KO(-1,t,"1"); + } + } + return r; + }; +} diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output_9.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output_9.kmn new file mode 100644 index 00000000000..971d11694aa --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/test_contextn_in_output_9.kmn @@ -0,0 +1,13 @@ +store(&VERSION) "9.0" +store(&TARGETS) 'web' +store(&NAME) 'test context(n) in output, #9930, v9.0' + +begin Unicode > use(main) + +group(main) using keys + +store(liveQwerty) 'qwerty' +store(deadQwerty) '123456' + +c any/index with deadkeys in stores. +any(liveQwerty) '1' + '!' > '?' index(deadQwerty, 1) context(2) 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 42aa489a911..a7eecda019e 100644 --- a/developer/src/kmc-kmn/test/kmw/test-kmw-compiler.ts +++ b/developer/src/kmc-kmn/test/kmw/test-kmw-compiler.ts @@ -35,11 +35,11 @@ describe('KeymanWeb Compiler', function() { callbacks.clear(); }); - it('should compile a complex keyboard', async function() { + it('should compile a complex keyboard', function() { run_test_keyboard(kmnCompiler, 'khmer_angkor'); }); - it('should handle option stores', async function() { + it('should handle option stores', function() { // // This is enough to verify that the option store is set appropriately with // KLOAD because the fixture has that code present: @@ -49,7 +49,7 @@ describe('KeymanWeb Compiler', function() { run_test_keyboard(kmnCompiler, 'test_options'); }); - it('should translate every "character style" key correctly', async function() { + it('should translate every "character style" key correctly', function() { // // This is enough to verify that every character style key is encoded in the // same way as the fixture. @@ -57,9 +57,25 @@ describe('KeymanWeb Compiler', function() { run_test_keyboard(kmnCompiler, 'test_keychars'); }); - it('should handle readonly groups', async function() { + it('should handle readonly groups', function() { 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, v9.0 generation', function() { + 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, v10.0 generation', function() { + run_test_keyboard(kmnCompiler, 'test_context_in_context'); + }); }); diff --git a/developer/src/kmc-kmn/test/test-messages.ts b/developer/src/kmc-kmn/test/test-messages.ts index 0220c2d037a..6970b997417 100644 --- a/developer/src/kmc-kmn/test/test-messages.ts +++ b/developer/src/kmc-kmn/test/test-messages.ts @@ -80,4 +80,11 @@ describe('CompilerMessages', function () { assert.equal(callbacks.messages[0].message, "A key on layer \"default\" has no identifier."); }); + // ERROR_NotSupportedInKeymanWebOutput + + it('should generate ERROR_NotSupportedInKeymanWebOutput if a rule has `return` in the output', async function() { + await testForMessage(this, ['invalid-keyboards', 'error_not_supported_in_keyman_web_output.kmn'], KmnCompilerMessages.ERROR_NotSupportedInKeymanWebOutput); + assert.equal(callbacks.messages[0].message, "Statement 'return' is not currently supported in output for web and touch targets"); + }); + }); diff --git a/developer/src/kmc-ldml/package.json b/developer/src/kmc-ldml/package.json index 88173c2902e..6d84b3f2ebd 100644 --- a/developer/src/kmc-ldml/package.json +++ b/developer/src/kmc-ldml/package.json @@ -17,8 +17,7 @@ "scripts": { "build": "tsc -b", "lint": "eslint .", - "test": "npm run lint && cd test && tsc -b && cd .. && c8 --reporter=lcov --reporter=text mocha", - "prepublishOnly": "npm run build" + "test": "npm run lint && cd test && tsc -b && cd .. && c8 --reporter=lcov --reporter=text mocha" }, "author": "Marc Durdin (https://github.com/mcdurdin)", "license": "MIT", diff --git a/developer/src/kmc-model-info/package.json b/developer/src/kmc-model-info/package.json index a953062acb3..5f8819e62e0 100644 --- a/developer/src/kmc-model-info/package.json +++ b/developer/src/kmc-model-info/package.json @@ -18,8 +18,7 @@ "scripts": { "build": "tsc -b", "lint": "eslint .", - "test": "npm run lint", - "prepublishOnly": "npm run build" + "test": "npm run lint" }, "author": "Marc Durdin (https://github.com/mcdurdin)", "contributors": [ diff --git a/developer/src/kmc-model/package.json b/developer/src/kmc-model/package.json index faed0049d4a..7d9da682daf 100644 --- a/developer/src/kmc-model/package.json +++ b/developer/src/kmc-model/package.json @@ -18,8 +18,7 @@ "scripts": { "build": "tsc -b", "lint": "eslint .", - "test": "npm run lint && cd test && tsc -b && cd .. && c8 --reporter=lcov --reporter=text mocha", - "prepublishOnly": "npm run build" + "test": "npm run lint && cd test && tsc -b && cd .. && c8 --reporter=lcov --reporter=text mocha" }, "author": "Marc Durdin (https://github.com/mcdurdin)", "contributors": [ diff --git a/developer/src/kmc-package/package.json b/developer/src/kmc-package/package.json index a5f9f3e7936..597d0af078c 100644 --- a/developer/src/kmc-package/package.json +++ b/developer/src/kmc-package/package.json @@ -17,8 +17,7 @@ "build": "tsc -b", "lint": "eslint .", "test": "npm run lint && cd test && tsc -b && cd .. && c8 --reporter=lcov --reporter=text mocha", - "coverage": "npm test", - "prepublishOnly": "npm run build" + "coverage": "npm test" }, "author": "Marc Durdin (https://github.com/mcdurdin)", "contributors": [ diff --git a/developer/src/kmc/package.json b/developer/src/kmc/package.json index 7f9e0a9053a..a7b9d60df2b 100644 --- a/developer/src/kmc/package.json +++ b/developer/src/kmc/package.json @@ -15,8 +15,7 @@ "bundle-kmc": "esbuild build/src/kmc.js --bundle --platform=node --target=es2022 > build/cjs-src/kmc.cjs", "bundle-kmlmc": "esbuild build/src/kmlmc.js --bundle --platform=node --target=es2022 > build/cjs-src/kmlmc.cjs", "bundle-kmlmp": "esbuild build/src/kmlmp.js --bundle --platform=node --target=es2022 > build/cjs-src/kmlmp.cjs", - "test": "eslint . && cd test && tsc -b && cd .. && mocha", - "prepublishOnly": "npm run build" + "test": "eslint . && cd test && tsc -b && cd .. && mocha" }, "type": "module", "author": "Marc Durdin (https://github.com/mcdurdin)", @@ -36,6 +35,7 @@ }, "dependencies": { "@keymanapp/common-types": "*", + "@keymanapp/developer-utils": "*", "@keymanapp/keyman-version": "*", "@keymanapp/kmc-analyze": "*", "@keymanapp/kmc-keyboard-info": "*", @@ -50,6 +50,9 @@ "commander": "^10.0.0", "supports-color": "^9.4.0" }, + "bundleDependencies": [ + "@keymanapp/developer-utils" + ], "files": [ "build/src/" ], diff --git a/developer/src/kmc/src/kmc.ts b/developer/src/kmc/src/kmc.ts index 049dc7c6aad..9751376f524 100644 --- a/developer/src/kmc/src/kmc.ts +++ b/developer/src/kmc/src/kmc.ts @@ -6,10 +6,12 @@ import { Command, Option } from 'commander'; import { declareBuild } from './commands/build.js'; import { declareAnalyze } from './commands/analyze.js'; -import { KeymanSentry } from './util/KeymanSentry.js'; +import { KeymanSentry } from '@keymanapp/developer-utils'; import KEYMAN_VERSION from "@keymanapp/keyman-version"; +import { TestKeymanSentry } from './util/TestKeymanSentry.js'; -await KeymanSentry.runTestIfCLRequested(); +await TestKeymanSentry.runTestIfCLRequested(); +KeymanSentry.init(); try { await run(); } catch(e) { diff --git a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts index dc048317deb..f781b241929 100644 --- a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts +++ b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts @@ -10,7 +10,7 @@ import { CompilerCallbacks, CompilerEvent, import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; import chalk from 'chalk'; import supportsColor from 'supports-color'; -import { KeymanSentry } from './KeymanSentry.js'; +import { KeymanSentry } from '@keymanapp/developer-utils'; const color = chalk.default; const severityColors: {[value in CompilerErrorSeverity]: chalk.Chalk} = { diff --git a/developer/src/kmc/src/util/TestKeymanSentry.ts b/developer/src/kmc/src/util/TestKeymanSentry.ts new file mode 100644 index 00000000000..75da80e53de --- /dev/null +++ b/developer/src/kmc/src/util/TestKeymanSentry.ts @@ -0,0 +1,51 @@ +import { KmnCompiler } from "@keymanapp/kmc-kmn"; +import { NodeCompilerCallbacks } from "./NodeCompilerCallbacks.js"; +import { KeymanSentry } from '@keymanapp/developer-utils'; + +const cli = process.argv.join(' '); + +export class TestKeymanSentry { + static isTestCL() { + // Note single hyphen match which matches other Developer console app + // implementations, but this will also work with + // --sentry-client-test-exception, so we get the best of both worlds. + // This parameter is undocumented by design + return cli.includes('-sentry-client-test-exception'); + } + + static async runTestIfCLRequested() { + if(TestKeymanSentry.isTestCL()) { + await TestKeymanSentry.test(); + console.error('Unexpected return from KeymanSentry.test'); + process.exit(1); + } + } + + static async test() { + KeymanSentry.init(); + if(cli.includes('kmcmplib')) { + const compiler = new KmnCompiler(); + const callbacks = new NodeCompilerCallbacks({}); + if(!await compiler.init(callbacks)) { + throw new Error('Failed to instantiate WASM compiler'); + } + try { + compiler.testSentry(); + } catch(e: any) { + await KeymanSentry.captureException(e); + } + } else if(cli.includes('event')) { + const eventId = KeymanSentry.captureMessage('Test message from -sentry-client-test-exception event'); + await KeymanSentry.close(); + console.log(`Captured test message with id ${eventId}`); + process.exit(0); + } else { + try { + throw new Error('Test error from -sentry-client-test-exception event'); + } catch(e: any) { + await KeymanSentry.captureException(e); + } + } + } + +} \ No newline at end of file diff --git a/developer/src/kmcmplib/build.sh b/developer/src/kmcmplib/build.sh index 1ccd13575c4..ce1c5eed1af 100755 --- a/developer/src/kmcmplib/build.sh +++ b/developer/src/kmcmplib/build.sh @@ -47,6 +47,7 @@ Libraries will be built in 'build///src'. * : 'debug' or 'release' (see --debug flag) * All parameters after '--' are passed to meson or ninja " \ + "@/common/include" \ "clean" \ "configure" \ "build" \ diff --git a/developer/src/server/build.sh b/developer/src/server/build.sh index 51523c52a3e..853f94cc357 100755 --- a/developer/src/server/build.sh +++ b/developer/src/server/build.sh @@ -14,6 +14,7 @@ cd "$THIS_SCRIPT_PATH" builder_describe "Build Keyman Developer Server" \ @/common/web/keyman-version \ + @/developer/src/common/web/utils \ @/web \ clean \ configure \ @@ -105,7 +106,7 @@ function installer_server() { # Remove @keymanapp devDependencies because they won't install outside the # monorepo context cat package.json | "$JQ" \ - '. | del(.devDependencies."@keymanapp/resources-gosh") | del(.devDependencies."@keymanapp/keyman-version")' \ + '. | del(.devDependencies."@keymanapp/resources-gosh") | del(.devDependencies."@keymanapp/keyman-version") | del(.dependencies."@keymanapp/developer-utils")' \ > "$PRODBUILDTEMP/package.json" pushd "$PRODBUILDTEMP" @@ -117,6 +118,7 @@ function installer_server() { # @keymanapp/keyman-version is required in build/ now but we need to copy it 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/" # We'll build in the $KEYMAN_ROOT/developer/bin/server/ folder rm -rf "$KEYMAN_ROOT/developer/bin/server/" diff --git a/developer/src/server/package.json b/developer/src/server/package.json index fbba3454395..122ad1e24f1 100644 --- a/developer/src/server/package.json +++ b/developer/src/server/package.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@sentry/node": "^6.16.1", + "@keymanapp/developer-utils": "*", "chalk": "^4.1.2", "express": "^4.17.2", "multer": "^1.4.5-lts.1", @@ -37,6 +38,9 @@ "tsc-watch": "^4.5.0", "typescript": "^4.9.5" }, + "bundleDependencies": [ + "@keymanapp/developer-utils" + ], "mocha": { "require": "ts-node/register", "spec": "build/**/*.test.js" diff --git a/developer/src/server/src/config.ts b/developer/src/server/src/config.ts index 6197cde05aa..2f8e4bb136b 100644 --- a/developer/src/server/src/config.ts +++ b/developer/src/server/src/config.ts @@ -1,4 +1,5 @@ -import * as fs from 'fs'; +import { mkdirSync } from 'fs'; +import { loadJsonFile } from './load-json-file.js'; export class Configuration { public readonly appDataPath: string; @@ -34,13 +35,9 @@ export class Configuration { this.pidFilename = this.appDataPath + 'pid.json'; this.configFilename = this.appDataPath + 'config.json'; - fs.mkdirSync(this.cachePath, { recursive: true}); + mkdirSync(this.cachePath, { recursive: true}); - let cfg = null; - if(fs.existsSync(this.configFilename)) { - const data = fs.readFileSync(this.configFilename, 'utf-8'); - cfg = JSON.parse(data); - } + const cfg = loadJsonFile(this.configFilename); this.port = cfg?.port ?? 8008; diff --git a/developer/src/server/src/data.ts b/developer/src/server/src/data.ts index 58763f07013..6600d57b4d9 100644 --- a/developer/src/server/src/data.ts +++ b/developer/src/server/src/data.ts @@ -1,5 +1,6 @@ -import * as fs from 'fs'; +import { writeFileSync } from 'fs'; import { configuration } from './config.js'; +import { loadJsonFile } from './load-json-file.js'; export interface DebugObject { id: string; @@ -96,7 +97,7 @@ export class SiteData { } private loadState() { - const state = fs.existsSync(configuration.cacheStateFilename) ? JSON.parse(fs.readFileSync(configuration.cacheStateFilename, 'utf-8')) : null; + const state = loadJsonFile(configuration.cacheStateFilename); this.loadDebugObject(DebugKeyboard, state?.keyboards, this.keyboards); this.loadDebugObject(DebugModel, state?.models, this.models); this.loadDebugObject(DebugFont, state?.fonts, this.fonts); @@ -105,7 +106,7 @@ export class SiteData { } public saveState() { - fs.writeFileSync(configuration.cacheStateFilename, JSON.stringify(this, null, 2), 'utf-8'); + writeFileSync(configuration.cacheStateFilename, JSON.stringify(this, null, 2), 'utf-8'); } }; diff --git a/developer/src/server/src/index.ts b/developer/src/server/src/index.ts index a2975545730..bde4076fb62 100644 --- a/developer/src/server/src/index.ts +++ b/developer/src/server/src/index.ts @@ -1,12 +1,5 @@ import { environment } from './environment.js'; - -import * as Sentry from '@sentry/node'; -Sentry.init({ - dsn: 'https://39b25a09410349a58fe12aaf721565af@o1005580.ingest.sentry.io/5983519', // Keyman Developer - environment: environment.versionEnvironment, - release: environment.versionGitTag -}); - +import { KeymanSentry } from '@keymanapp/developer-utils'; import express from 'express'; import * as ws from 'ws'; import * as os from 'os'; @@ -14,8 +7,9 @@ import multer from 'multer'; import * as fs from 'fs'; import setupRoutes from './routes.js'; import { configuration } from './config.js'; -import tray from './tray.js'; +import { initTray } from './tray.js'; import chalk from 'chalk'; +import { shutdown } from './shutdown.js'; const options = { ngrokLog: false, // Set this to true if you need to see ngrok logs in the console @@ -25,102 +19,117 @@ const options = { console.log(`Starting Keyman Developer Server ${environment.versionWithTag}, listening on port ${configuration.port}.`); -// This triggers the exit event for Ctrl+C which allows us to cleanup -// consistently -process.on('SIGINT', ()=>{process.exit(0)}); +KeymanSentry.init(); +try { + await run(); +} catch(e) { + KeymanSentry.captureException(e); + throw e; +} -if(fs.existsSync(configuration.lockFilename)) { - // Attempt connection to existing port +// Ensure any messages reported to Sentry have had time to be uploaded before we +// exit. In most cases, this will be a no-op so should not affect performance. +await KeymanSentry.close(); - // If that fails, we'll assume the previous process died and delete the lockfile. -} +export async function run() { + // This triggers the exit event for Ctrl+C which allows us to cleanup + // consistently + process.on('SIGINT', shutdown); -// TODO: flock with fs-ext -let lockFileDescriptor = fs.openSync(configuration.lockFilename, 'w'); -fs.writeFileSync(configuration.pidFilename, process.pid.toString()); -fs.writeFileSync(lockFileDescriptor, process.pid.toString()); + const tray = await initTray(); -process.on('exit', () => { - if(fs.existsSync(configuration.pidFilename)) { - fs.unlinkSync(configuration.pidFilename); - } - if(lockFileDescriptor) { - fs.closeSync(lockFileDescriptor); - lockFileDescriptor = null; - fs.unlinkSync(configuration.lockFilename); + if(fs.existsSync(configuration.lockFilename)) { + // Attempt connection to existing port + + // If that fails, we'll assume the previous process died and delete the lockfile. } -}); -/* Server Setup */ + // TODO: flock with fs-ext + let lockFileDescriptor = fs.openSync(configuration.lockFilename, 'w'); + fs.writeFileSync(configuration.pidFilename, process.pid.toString()); + fs.writeFileSync(lockFileDescriptor, process.pid.toString()); + + process.on('exit', () => { + if(fs.existsSync(configuration.pidFilename)) { + fs.unlinkSync(configuration.pidFilename); + } + if(lockFileDescriptor) { + fs.closeSync(lockFileDescriptor); + lockFileDescriptor = null; + fs.unlinkSync(configuration.lockFilename); + } + }); + + /* Server Setup */ -const app = express(); -const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 1024*1024*100 /*100MB*/ } }); + const app = express(); + const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 1024*1024*100 /*100MB*/ } }); -/* Websockets */ + /* Websockets */ -const wsServer = new ws.WebSocketServer({ noServer: true }); -wsServer.on('connection', socket => { - socket.on('message', (message) => { - console.debug('wsServer.socket.onmessage: '+message.toString()); - if(message.toString() == 'ping') - socket.send('pong'); + const wsServer = new ws.WebSocketServer({ noServer: true }); + wsServer.on('connection', socket => { + socket.on('message', (message) => { + console.debug('wsServer.socket.onmessage: '+message.toString()); + if(message.toString() == 'ping') + socket.send('pong'); + }); + socket.send('refresh'); }); - socket.send('refresh'); -}); -/* Setup routes */ + /* Setup routes */ -setupRoutes(app, upload, wsServer, environment); + setupRoutes(app, upload, wsServer, environment); -/* Start the server */ + /* Start the server */ -const server = app.listen(configuration.port); + const server = app.listen(configuration.port); -server.on('upgrade', (request, socket, head) => { - wsServer.handleUpgrade(request, socket, head, socket => { - wsServer.emit('connection', socket, request); + server.on('upgrade', (request, socket, head) => { + wsServer.handleUpgrade(request, socket, head, socket => { + wsServer.emit('connection', socket, request); + }); }); -}); - -/* Launch ngrok if enabled */ - -configuration.ngrokEndpoint = ''; - -if(configuration.useNgrok && os.platform() == 'win32' && fs.existsSync(configuration.ngrokBinPath)) { - const ngrok: any = await import('ngrok'); - (async function() { - configuration.ngrokEndpoint = await ngrok.connect({ - proto: 'http', - bind_tls: true, - addr: configuration.port, - authtoken: configuration.ngrokToken, - region: configuration.ngrokRegion, - binPath: () => configuration.ngrokBinPath, - onLogEvent: (msg: string) => { - if(options.ngrokLog) { - console.log(chalk.cyan(('\n'+msg).split('\n').join('\n[ngrok] ').trim())); - } - }, - onStatusChange: (state: string) => { - if(state == 'connected') { - setTimeout(async () => { - const api = ngrok.getApi(); - const tunnels = await api.listTunnels(); - configuration.ngrokEndpoint = tunnels.tunnels[0]?.public_url ?? ''; - console.log(chalk.blueBright('ngrok tunnel established at %s'), configuration.ngrokEndpoint); - }, 1000); - } else if(state == 'closed') { - configuration.ngrokEndpoint = ''; - console.log(chalk.blueBright('ngrok tunnel closed')); + + /* Launch ngrok if enabled */ + + configuration.ngrokEndpoint = ''; + + if(configuration.useNgrok && os.platform() == 'win32' && fs.existsSync(configuration.ngrokBinPath)) { + const ngrok: any = await import('ngrok'); + (async function() { + configuration.ngrokEndpoint = await ngrok.connect({ + proto: 'http', + bind_tls: true, + addr: configuration.port, + authtoken: configuration.ngrokToken, + region: configuration.ngrokRegion, + binPath: () => configuration.ngrokBinPath, + onLogEvent: (msg: string) => { + if(options.ngrokLog) { + console.log(chalk.cyan(('\n'+msg).split('\n').join('\n[ngrok] ').trim())); + } + }, + onStatusChange: (state: string) => { + if(state == 'connected') { + setTimeout(async () => { + const api = ngrok.getApi(); + const tunnels = await api.listTunnels(); + configuration.ngrokEndpoint = tunnels.tunnels[0]?.public_url ?? ''; + console.log(chalk.blueBright('ngrok tunnel established at %s'), configuration.ngrokEndpoint); + }, 1000); + } else if(state == 'closed') { + configuration.ngrokEndpoint = ''; + console.log(chalk.blueBright('ngrok tunnel closed')); + } } - } - }); - console.log(chalk.blueBright('ngrok tunnel initially established at %s'), configuration.ngrokEndpoint); + }); + console.log(chalk.blueBright('ngrok tunnel initially established at %s'), configuration.ngrokEndpoint); + tray.start(configuration.port, configuration.ngrokEndpoint); + })(); + } + else { + /* Load the tray icon */ tray.start(configuration.port, configuration.ngrokEndpoint); - })(); -} -else { - /* Load the tray icon */ - tray.start(configuration.port, configuration.ngrokEndpoint); -} - + } +} \ No newline at end of file diff --git a/developer/src/server/src/load-json-file.ts b/developer/src/server/src/load-json-file.ts new file mode 100644 index 00000000000..28916cd83bb --- /dev/null +++ b/developer/src/server/src/load-json-file.ts @@ -0,0 +1,14 @@ +import { KeymanSentry } from '@keymanapp/developer-utils'; +import { readFileSync, existsSync } from 'fs'; + +export function loadJsonFile(filename: string): any { + try { + return existsSync(filename) ? + JSON.parse(readFileSync(filename, 'utf-8')) : + null; + } catch(e: any) { + console.error(`Error loading ${filename}: ${(e ?? '').toString()}`); + KeymanSentry.reportException(e); + return null; + } +} \ No newline at end of file diff --git a/developer/src/server/src/routes.ts b/developer/src/server/src/routes.ts index 20c09f838f7..3876bf0d7a1 100644 --- a/developer/src/server/src/routes.ts +++ b/developer/src/server/src/routes.ts @@ -14,6 +14,7 @@ import handleIncKeyboardsCss from './handlers/inc/keyboards-css.js'; import { Environment } from './version-data.js'; import { configuration } from './config.js'; import chalk from 'chalk'; +import { shutdown } from './shutdown.js'; export default function setupRoutes(app: express.Express, upload: multer.Multer, wsServer: ws.WebSocketServer, environment: Environment ) { @@ -97,7 +98,7 @@ export default function setupRoutes(app: express.Express, upload: multer.Multer, /* Localhost only routes -- todo /api/internal/ vs /api/... */ - app.post('/api/shutdown', (_req,res) => { setTimeout(() => process.exit(0), 100); res.send('ok'); }); + app.post('/api/shutdown', (_req,res) => { setTimeout(shutdown, 100); res.send('ok'); }); appGetData(app, /\/data\/keyboard\/(.+)\.js$/, data.keyboards); appGetData(app, /\/data\/model\/(.+)\.model\.js$/, data.models); diff --git a/developer/src/server/src/shutdown.ts b/developer/src/server/src/shutdown.ts new file mode 100644 index 00000000000..d33b035bfcb --- /dev/null +++ b/developer/src/server/src/shutdown.ts @@ -0,0 +1,6 @@ +import { KeymanSentry } from '@keymanapp/developer-utils'; + +export async function shutdown(code?: number): Promise { + await KeymanSentry.close(); + process.exit(code ?? 0); +} diff --git a/developer/src/server/src/site/index.html b/developer/src/server/src/site/index.html index 5a4fd139a55..8147a849a46 100644 --- a/developer/src/server/src/site/index.html +++ b/developer/src/server/src/site/index.html @@ -21,7 +21,7 @@ - +