diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 098cb70e26..d50cbeecc2 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -5,9 +5,7 @@ module.exports = { es2021: true, node: true, }, - // TODO: we need to move to the following, but this gets us started with infrastructure without breaking our build - // extends: [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended"], - extends: ["plugin:@typescript-eslint/eslint-recommended"], + extends: [ "plugin:@typescript-eslint/eslint-recommended"], parser: "@typescript-eslint/parser", parserOptions: { ecmaVersion: "latest", diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..cd9921d963 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# Always use LF for Dockerfile - otherwise HEREDOCs in Dockerfile +# will not work on Windows - we end up with a filename `foo\r` for +# the command +# ```Dockerfile +# RUN < foo +# echo "Hello world" +# EOF +# ``` +Dockerfile text eol=lf diff --git a/.github/actions/build-binary-packages/action.yml b/.github/actions/build-binary-packages/action.yml index 27f0246121..e764a26ac7 100644 --- a/.github/actions/build-binary-packages/action.yml +++ b/.github/actions/build-binary-packages/action.yml @@ -25,13 +25,13 @@ runs: using: 'composite' steps: - name: Download Artifacts - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: keyman-srcpkg path: artifacts/keyman-srcpkg - name: Build - uses: sillsdev/gha-ubuntu-packaging@7b56f50d5d5537e9e9cafd3f6139ec6da69cfcda # v1.1 + uses: sillsdev/gha-ubuntu-packaging@556b268762be9bea0f39861a7392587211ef6a58 # v2.0.1 with: dist: "${{ inputs.dist }}" platform: "${{ inputs.arch }}" @@ -49,7 +49,7 @@ runs: echo '```' >> $GITHUB_STEP_SUMMARY - name: Store binary packages - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: keyman-binarypkgs-${{ inputs.dist }}_${{ inputs.arch }} path: | diff --git a/.github/labeler.yml b/.github/labeler.yml index 19c858cc81..fc37bf64b7 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -11,51 +11,56 @@ docs: docs/** # Add labels based on changed files using actions/labeler # -android/: android/** +android/: + - android/** + - resources/teamcity/includes/** + - resources/teamcity/android/** android/app/: android/KMAPro/** android/engine/: android/KMEA/** android/samples/: android/Samples/** common/: - common/** - - resources/** - -common/models/templates/: web/src/engine/predictive-text/templates/** -common/models/wordbreakers/: web/src/engine/predictive-text/wordbreakers/** - -common/resources/: resources/** + - resources/teamcity/includes/** + - resources/teamcity/common/** common/web/: common/web/** core/: - core/** + - resources/teamcity/includes/** + - resources/teamcity/core/** developer/: - developer/** - + - resources/teamcity/includes/** + - resources/teamcity/developer/** developer/compilers/: - developer/src/kmc/** - developer/src/kmcmplib/** - developer/src/kmc-*/** - developer/ide/: - developer/src/server/** - developer/src/tike/** - -ios/: ios/** +ios/: + - ios/** + - resources/teamcity/includes/** + - resources/teamcity/ios/** ios/app/: ios/keyman/** ios/engine/: ios/engine/** ios/samples/: ios/samples/** -linux/: linux/** +linux/: + - linux/** + - resources/teamcity/includes/** + - resources/teamcity/linux/** linux/config/: linux/keyman-config/** -linux/engine/: - - linux/ibus-keyman/** - - linux/legacy/ibus-kmfl/** - - linux/legacy/kmflcomp/** - - linux/legacy/libkmfl/** +linux/engine/: linux/ibus-keyman/** -mac/: mac/** +mac/: + - mac/** + - resources/teamcity/includes/** + - resources/teamcity/mac/** # mac/config/: # mac/engine/: mac/** @@ -65,12 +70,21 @@ oem/fv/android/: oem/firstvoices/android/** oem/fv/ios/: oem/firstvoices/ios/** oem/fv/windows/: oem/firstvoices/windows/** -web/: web/** +resources/: resources/** + +web/: + - web/** + - resources/teamcity/includes/** + - resources/teamcity/web/** # web/bookmarklet/ web/engine/: web/source/** web/ui/: web/source/kmwui* web/samples/: web/samples/** +web/predictive-text/: web/src/engine/predictive-text/** -windows/: windows/** +windows/: + - windows/** + - resources/teamcity/includes/** + - resources/teamcity/windows/** windows/config/: windows/src/desktop/** windows/engine/: windows/src/engine/** diff --git a/.github/multi-labeler.yml b/.github/multi-labeler.yml index 5b494a8569..c7f2f408bc 100644 --- a/.github/multi-labeler.yml +++ b/.github/multi-labeler.yml @@ -33,6 +33,9 @@ labels: - label: 'fix' matcher: title: '^fix(\(|:)' + - label: 'maint' + matcher: + title: '^maint(\(|:)' - label: 'refactor' matcher: title: '^refactor(\(|:)' @@ -57,11 +60,6 @@ labels: matcher: title: '(🍒|:cherries:)' - # long-lived feature branches - - label: 'feature-branch' - matcher: - branch: '^feature-.+' - # # Scopes that we look for in the PR title # @@ -90,6 +88,9 @@ labels: - label: 'oem/' matcher: title: '\(.*oem.*\):' + - label: 'resources/' + matcher: + title: '\(.*resources.*\):' - label: 'web/' matcher: title: '\(.*web.*\):' diff --git a/.github/workflows/api-verification.yml b/.github/workflows/api-verification.yml index 8f37141c08..e8cc9aa95a 100644 --- a/.github/workflows/api-verification.yml +++ b/.github/workflows/api-verification.yml @@ -15,7 +15,7 @@ jobs: name: Verify API for libkeymancore*.so runs-on: ubuntu-latest outputs: - VERSION: ${{ steps.environment_step.outputs.VERSION }} + KEYMAN_VERSION: ${{ steps.environment_step.outputs.KEYMAN_VERSION }} PRERELEASE_TAG: ${{ steps.environment_step.outputs.PRERELEASE_TAG }} GIT_SHA: ${{ steps.environment_step.outputs.GIT_SHA }} GIT_BASE: ${{ steps.environment_step.outputs.GIT_BASE }} @@ -26,7 +26,7 @@ jobs: steps: - name: Restore artifacts - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | artifacts @@ -53,7 +53,7 @@ jobs: -f context="$STATUS_CONTEXT" - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 with: ref: '${{ steps.environment_step.outputs.GIT_SHA }}' fetch-depth: 0 @@ -69,7 +69,7 @@ jobs: run: | echo "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }}):" >> $GITHUB_STEP_SUMMARY - BIN_PACKAGE=$(ls "${GITHUB_WORKSPACE}/artifacts/" | grep "${PKG_NAME}[0-9]*_${{ steps.environment_step.outputs.VERSION }}-1${{ steps.environment_step.outputs.PRERELEASE_TAG }}+$(lsb_release -c -s)1_amd64.deb") + BIN_PACKAGE=$(ls "${GITHUB_WORKSPACE}/artifacts/" | grep "${PKG_NAME}[0-9]*_${{ steps.environment_step.outputs.KEYMAN_VERSION }}-1${{ steps.environment_step.outputs.PRERELEASE_TAG }}+$(lsb_release -c -s)1_amd64.deb") cd ${{ github.workspace }}/keyman/linux ./scripts/deb-packaging.sh \ --gha \ @@ -79,7 +79,7 @@ jobs: verify 2>> $GITHUB_STEP_SUMMARY - name: Archive .symbols file - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: libkeymancore.symbols path: ${{ github.workspace }}/keyman/linux/debian/tmp/DEBIAN/symbols diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index 7568a292cd..d5a25aacb6 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -13,10 +13,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: crowdin action - uses: crowdin/github-action@1.3.2 + uses: crowdin/github-action@2.7.0 with: upload_sources: true diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index 96ad55ca1a..aaeb1c6306 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -37,11 +37,11 @@ jobs: if: github.repository == 'keymanapp/keyman' runs-on: ubuntu-24.04 outputs: - VERSION: ${{ steps.version_step.outputs.VERSION }} + KEYMAN_VERSION: ${{ steps.version_step.outputs.KEYMAN_VERSION }} PRERELEASE_TAG: ${{ steps.prerelease_tag.outputs.PRERELEASE_TAG }} steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 with: ref: '${{ github.event.client_payload.buildSha }}' @@ -72,10 +72,10 @@ jobs: - name: Build source package id: build_source_package run: | - export TIER=$(cat TIER.md) + export KEYMAN_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 "KEYMAN_TIER=$KEYMAN_TIER" >> $GITHUB_ENV echo "GHA_TEST_BUILD=${GHA_TEST_BUILD}" >> $GITHUB_ENV echo "GHA_BRANCH=${GHA_BRANCH}" >> $GITHUB_ENV cd linux @@ -87,7 +87,7 @@ jobs: run: | THIS_SCRIPT="$GITHUB_WORKSPACE/.github/workflows/deb-packaging.yml" . "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh" - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "KEYMAN_VERSION=${KEYMAN_VERSION:-${VERSION}}" >> $GITHUB_OUTPUT - name: Set prerelease tag as output parameter id: prerelease_tag @@ -103,16 +103,16 @@ jobs: - name: Output which branch or PR we're building plus name of .dsc file run: | 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 + echo ":checkered_flag: **Test build of version ${{ steps.version_step.outputs.KEYMAN_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.branch}}), 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.KEYMAN_VERSION }}**" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY echo ":gift: Generated source package:" >> $GITHUB_STEP_SUMMARY echo "- $(find . -name keyman_\*.dsc)" >> $GITHUB_STEP_SUMMARY - name: Store source package - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: keyman-srcpkg path: | @@ -131,7 +131,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 with: ref: '${{ github.event.client_payload.buildSha }}' sparse-checkout: '.github/actions/' @@ -140,7 +140,7 @@ jobs: uses: ./.github/actions/build-binary-packages with: dist: ${{ matrix.dist }} - version: ${{ needs.sourcepackage.outputs.VERSION }} + version: ${{ needs.sourcepackage.outputs.KEYMAN_VERSION }} prerelease_tag: ${{ needs.sourcepackage.outputs.PRERELEASE_TAG }} deb_fullname: ${{env.DEBFULLNAME}} deb_email: ${{env.DEBEMAIL}} @@ -157,7 +157,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 with: ref: '${{ github.event.client_payload.buildSha }}' sparse-checkout: '.github/actions/' @@ -166,7 +166,7 @@ jobs: uses: ./.github/actions/build-binary-packages with: dist: ${{ matrix.dist }} - version: ${{ needs.sourcepackage.outputs.VERSION }} + version: ${{ needs.sourcepackage.outputs.KEYMAN_VERSION }} prerelease_tag: ${{ needs.sourcepackage.outputs.PRERELEASE_TAG }} deb_fullname: ${{env.DEBFULLNAME}} deb_email: ${{env.DEBEMAIL}} @@ -178,7 +178,7 @@ jobs: steps: # - name: Download Artifacts - # uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 + # uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 # with: # path: artifacts # merge-multiple: true @@ -198,7 +198,7 @@ jobs: # autopkgtest -B *.deb keyman_*.dsc -- qemu autopkgtest-jammy-amd64.img - name: Ignore run: | - echo "Ignored for now - until working solution is in place" + echo "Ignored for now - until working solution is in place (#13777)" deb_signing: name: Sign source and binary packages @@ -211,8 +211,8 @@ jobs: - name: Sign packages uses: sillsdev/gha-deb-signing@a38dbde6bc9afabede5e07609105acc83c760ad4 # v0.6 with: - src-pkg-name: "keyman_${{ needs.sourcepackage.outputs.VERSION }}-1_source.changes" - bin-pkg-name: "keyman_${{ needs.sourcepackage.outputs.VERSION }}-1${{ needs.sourcepackage.outputs.PRERELEASE_TAG }}+" + src-pkg-name: "keyman_${{ needs.sourcepackage.outputs.KEYMAN_VERSION }}-1_source.changes" + bin-pkg-name: "keyman_${{ needs.sourcepackage.outputs.KEYMAN_VERSION }}-1${{ needs.sourcepackage.outputs.PRERELEASE_TAG }}+" artifacts-prefix: "keyman-" artifacts-result-name: "keyman-signedpkgs" gpg-signing-key: "${{ secrets.GPG_SIGNING_KEY }}" @@ -247,7 +247,7 @@ jobs: echo "::endgroup::" - name: Download Artifacts - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: keyman-signedpkgs @@ -293,7 +293,7 @@ jobs: echo "::endgroup::" echo -e "::group::${COLOR_GREEN}Uploading binary packages" - pattern="keyman_${{ needs.sourcepackage.outputs.VERSION }}.+\+(.*)[0-9]_[^.]+.changes" + pattern="keyman_${{ needs.sourcepackage.outputs.KEYMAN_VERSION }}.+\+(.*)[0-9]_[^.]+.changes" for f in *_amd64.changes; do if [[ $f =~ $pattern ]]; then dist=${BASH_REMATCH[1]} @@ -303,20 +303,21 @@ jobs: echo "::endgroup::" prepare_api_verification: - name: Verify API for libkeymancore.so + name: Prepare API verification needs: [sourcepackage, binary_packages_released] runs-on: ubuntu-latest steps: - name: Download Artifacts - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: artifacts + pattern: keyman-* merge-multiple: true - name: Save environment run: | - echo "VERSION=${{ needs.sourcepackage.outputs.VERSION }}" > artifacts/env + echo "KEYMAN_VERSION=${{ needs.sourcepackage.outputs.KEYMAN_VERSION }}" > artifacts/env echo "PRERELEASE_TAG=${{ needs.sourcepackage.outputs.PRERELEASE_TAG }}" >> artifacts/env echo "GIT_SHA=${{ github.event.client_payload.buildSha }}" >> artifacts/env echo "GIT_BASE=${{ github.event.client_payload.baseRef }}" >> artifacts/env @@ -326,7 +327,7 @@ jobs: echo "GIT_USER=${{ github.event.client_payload.user }}" >> artifacts/env - name: Cache artifacts - uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | artifacts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6216a80614..64e4dd454d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -384,10 +384,11 @@ group. | `auto` | [search][search-auto] | For PRs only: automatically-opened PRs, e.g. opened by CI. | | `bug` | [search][search-bug] | For issues only: confirmed bugs or reports that are very likely to be bugs. PRs use `fix` to mark a bug fix. | | `change` | [search][search-change] | Minor change in functionality, but not new. | -| `chore` | [search][search-chore] | Cleanup work, maintenance, without change in functionality. | +| `chore` | [search][search-chore] | Cleanup work, without change in functionality. | | `docs` | [search][search-docs] | Relating to any type of documentation. | | `feat` | [search][search-feat] | Feature requests. | | `fix` | [search][search-fix] | For PRs only: a bug fix, corresponds to issue label `bug`. | +| `maint` | [search][search-maint] | Maintenance work -- continuous integration, build scripts, infrastructure. | | `question` | [search][search-question] | Questions more than bug reports or feature requests (e.g. how do I do X). [**Usually better on the Keyman Community**](https://community.software.sil.org/c/keyman) | | `refactor` | [search][search-refactor] | Code reorganization and refactoring, without change in functionality. | | `spec` | [search][search-spec] | Issues which are specifications for a large scale feature. | @@ -443,6 +444,7 @@ These labels also include sublabels, such as `windows/config/`. [search-docs]: https://github.com/keymanapp/keyman/labels/docs [search-feat]: https://github.com/keymanapp/keyman/labels/feat [search-fix]: https://github.com/keymanapp/keyman/labels/fix +[search-maint]: https://github.com/keymanapp/keyman/labels/maint [search-question]: https://github.com/keymanapp/keyman/labels/question [search-spec]: https://github.com/keymanapp/keyman/labels/spec [search-style]: https://github.com/keymanapp/keyman/labels/style diff --git a/HISTORY.md b/HISTORY.md index dc0b6a7594..daf9a20257 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,310 @@ # Keyman Version History +## 19.0.63 alpha 2025-06-12 + +* maint(web): add some more arguments for web release build script (#14167) +* maint(developer): use double quotes so that variable gets replaced (#14168) +* chore(deps): bump brace-expansion from 2.0.1 to 2.0.2 in /developer/src/server/src/win32/trayicon/addon-src (#14165) + +## 19.0.62 alpha 2025-06-11 + +* maint(developer): export environment variables (#14157) +* maint(web): use path when calling powershell script (#14156) +* maint(resources): fix regression on mac triggering builds (#14155) + +## 19.0.61 alpha 2025-06-10 + +* chore(developer): extend unit test timeout for getLastGitCommitDate (#14147) +* fix(common): Use zip.inc.sh for common test keyboards (#14145) +* maint(developer): add docker image to build developer node modules (#14058) + +## 19.0.60 alpha 2025-06-06 + +* maint(developer): allow to build locally on Linux (#14059) + +## 19.0.59 alpha 2025-06-05 + +* chore(deps): bump multer from 2.0.0 to 2.0.1 (#14127) +* maint(web): fix working directory for zipping artifacts for Web release (#14128) +* chore(linux): Update debian changelog (#14119) +* maint(web): improve TC build script for Keyman Web/Test (#14051) +* maint(developer): move TC configuration for Keyman Developer to scripts (#14052) + +## 19.0.58 alpha 2025-06-04 + +* feat(developer): update strs compiler to collect context (#14064) +* feat(developer): line numbers for UsetCompiler, others (#14090) +* feat(developer): remaining line numbers for tran/bksp compilers (#14097) +* feat(developer): ldml: update errors for unparseable reorder (#14098) +* chore(developer): ldml: messages cleanup (#14100) +* maint(android): Create "archive" builder step to prep release artifacts (#14093) +* maint(web): move TC configuration for Web to script (#14049) + +## 19.0.57 alpha 2025-06-03 + +* fix(windows): change from error to breadcrumb and log (#14069) +* maint(mac): make make-km-dmg.sh less quiet (#14105) + +## 19.0.56 alpha 2025-06-02 + +* maint(ios): Use zip.inc.sh to prep release artifacts (#13919) +* maint(linux): update to new version of gha-ubuntu-packaging (#14094) +* chore(linux): add Python TC test runner (#14062) + +## 19.0.55 alpha 2025-05-31 + +* maint(linux): `sudo` not required for removing temporary files (#14086) +* maint(linux): update to new version of gha-ubuntu-packaging (#14089) + +## 19.0.54 alpha 2025-05-30 + +* maint(linux): move TC build scripts to `resources/teamcity/linux` (#14061) + +## 19.0.53 alpha 2025-05-29 + +* fix(developer): ensure that kmc doesn't skip validate phase of kmc-ldml (#14068) + +## 19.0.52 alpha 2025-05-28 + +* feat(developer): line numbers for tran compiler (#14037) +* feat(developer): add line numbers to disp compiler (#14048) +* feat(developer): line numbers for layr compiler (#14050) +* feat(developer): line numbers for LocaCompiler (#14054) +* fix(common): properly use newly installed node version (#14057) +* maint(common): update build coordinator and labels for `resources/teamcity` changes (#14047) + +## 19.0.51 alpha 2025-05-27 + +* maint: add stable-19.0 triggers (#14043) +* maint: add empty 'scripts' tag to package.json to silence warning (#14044) +* fix(linux): fix version variable references in kmconfig (#14032) + +## 19.0.50 alpha 2025-05-26 + +* chore(web): mark async methods in UI components (#14034) + +## 19.0.49 alpha 2025-05-25 + +* feat(developer): revamp line numbers per design doc (#14016) +* docs(developer): add compiler message internal documentation (#14025) +* feat(developer): add line numbers to vars compiler (#14036) + +## 19.0.48 alpha 2025-05-23 + +* maint(web): upgrade Sentry to 8.55.0 (#14030) + +## 19.0.47 alpha 2025-05-22 + +* fix(windows): only breadcrumb unexpected state on first run (#13984) +* maint: change builder_completion script to a .inc.sh (#14007) +* fix(windows): skip sending handle kmshell events to upgrade state machine for keyboard install modes (#13989) +* fix(windows): fix sentry error message reporting (#14027) + +## 19.0.46 alpha 2025-05-21 + +* docs(common): update README with number of languages and keyboards (#14023) + +## 19.0.45 alpha 2025-05-21 + +* maint(linux): use correct syntax to define local variables in script (#14021) +* maint(developer): update multer to v2.0.0 (#14012) + +## 19.0.44 alpha 2025-05-21 + +* feat(android): log active keyboard, model IDs with reported errors (#13983) +* fix(windows): rename Enter and Exit methods TState (#13920) +* maint: update macOS environment setup, related documentation (#13979) +* chore(deps): bump multer from 1.4.5-lts.1 to 2.0.0 (#13982) +* feat(ios): log active keyboard, model IDs with reported errors (#13987) +* maint(linux): add quilt to release build configuration (#13986) +* chore(ios): update FirstVoices AppIcon (#13976) +* docs(developer): add security note to Upload documentation (#14015) +* fix(android): Check network access before trying to download keyboard (#13978) + +## 19.0.43 alpha 2025-05-20 + +* chore(ios): FirstVoices certificate update (#13991) +* fix(linux): configurable path in `.service` file when using basu (#13980) + +## 19.0.42 alpha 2025-05-20 + +* maint(linux): add `--no-werror` option to not report warnings as errors (#13988) + +## 19.0.41 alpha 2025-05-19 + +* fix(web): inspectable test-script mode should not be auto-enabled in local-env tests (#13895) +* maint(common): evaluate and apply npm audit fix (without forced changes) (#13897) +* maint: update mocha (#13899) +* maint: update @actions/github + @octokit references (#13900) +* maint(common): update fast-json-patch to ^3.1.1 (#13901) +* change(web): cancel flicks when both returning to and releasing at original tap location (#13683) +* fix(web): empty layout fontSize property should default to 1em (#13959) +* maint(linux): fix release build to run all tests except integration tests (#13968) + +## 19.0.40 alpha 2025-05-17 + +* maint(common): Add zip.inc.sh for cross-platform zipping (#13812) + +## 19.0.39 alpha 2025-05-16 + +* chore(mac,windows): Update Crowdin strings for Italian (#13871) +* refactor(web): rename generic type variables (#13930) +* maint(linux): properly fallback to `VERSION` in deb-packaging (#13961) +* maint(linux): update gha-ubuntu-packaging dependency (#13964) +* fix(linux): use correct header guard define in OrderedOutputDevice.h (#13962) +* maint(linux): don't include `dockerbuild` log files (#13965) +* maint(linux): update GHA dependencies for Linux package builds (#13966) + +## 19.0.38 alpha 2025-05-15 + +* chore(android): Update FirstVoices launcher icon and red theme (#13907) +* feat(developer): add more line number issues for KeysCompiler (#13915) +* feat(developer): add line number for MetaCompiler (#13916) +* maint(linux): address comments from Debian package review (#13904) +* maint(linux): add Questing to launchpad package builds (#13910) +* maint(linux): fix logging in `deb-packaging.sh` (#13912) +* maint(linux): remove temporary files created during dependency installation (#13913) +* maint(common): move TC build configs for Linux to scripts (#13754) +* fix(developer): remove ngrok binary at build time (#13847) +* chore(linux): Update debian changelog (#13889) +* refactor(web): rename `HardKeyboard` to `HardKeyboardBase` (#13929) +* refactor(web): rename base `KeymanEngine` to `KeymanEngineBase` (#13942) + +## 19.0.37 alpha 2025-05-14 + +* maint(linux): remove generated file that got accidentally committed (#13902) +* maint(linux): remove check for patched ibus from `meson.build` (#13909) +* maint(common): report builder errors to stderr (#13882) + +## 19.0.36 alpha 2025-05-13 + +* feat(developer): line numbers for XML parsing and kmc-ldml (#13375) +* maint(android): Update to Java 21 and Gradle dependencies (#13858) +* maint(linux): improve getting PR# in `upload-to-debian.sh` (#13890) +* fix(linux): fallback to `VERSION` if `KEYMAN_VERSION` isn't set during package build (#13891) +* maint(common): move and document `builder_is_ci_*` functions (#13892) +* maint(android): use new variable name `KEYMAN_TIER` (#13898) +* fix(mac): handle PackageInfo section in kmp.inf file (#13876) + +## 19.0.35 alpha 2025-05-12 + +* maint(common): add two more functions to detect if running on CI (#13869) +* maint(common): use unique names for Keyman version variables (#13854) +* maint(common): use new `builder_is_running_on_*` functions (#13875) +* maint(common): remove obsolete code from docker-images `run.sh` (#13879) +* chore(mac): remove obsolete key processing code (#13848) + +## 19.0.34 alpha 2025-05-08 + +* docs(common): update `builder_echo` documentation for TC blocks (#13855) +* fix(windows): add breadcrumb to download failures (#13826) +* fix(web): sustain active batched-layout mode when keyboard is switched (#13860) +* maint(common): remove obsolete code from docker-images `build.sh` (#13861) +* maint(common): output start/end messages if not running on TC (#13868) + +## 19.0.33 alpha 2025-05-07 + +* maint(common): automatically set --debug option for local-environment builder-script uses (#13827) +* docs(web): add a Web engine keystroke lifecycle internal doc (#13759) +* maint(common): use function to detect if running on TC (#13844) +* docs(common): document new `builder_is_running_on_teamcity` function (#13852) + +## 19.0.32 alpha 2025-05-06 + +* change(web): replace USVString with string in predictive-text engine (#13785) +* chore(web): replace --ci flag with builder_is_ci_build (#13819) +* maint(web): simplify worker-main test.sh (#13820) +* change(web): lint kmwstring web-utility (#13800) +* fix(web): fix extraction of gesture-engine fixture used in DOM-based unit tests (#13805) +* fix(web): add robustness for bad touch-layout fontsize specs (#13774) +* fix(web): fix var declaration affected by linter strictness + length-style fix (#13839) +* chore(ios): Update inuktitut_pirurvik to 1.4.1 (#13840) +* chore(common): override check when running on TC (#13779) +* change(linux): add ICU dependency needed to build on Arch Linux (#13842) +* maint(common): add support for TC blocks (#13810) + +## 19.0.31 alpha 2025-05-05 + +* fix(mac): add version metadata after building tests (#13818) +* maint(common): remove `eslint:recommended` configuration for .ts (#13825) +* maint(common): Update write_download_info() to be cross-platform (#13813) +* change(ios): limit reports about devices without predefined scaling entries (#13806) +* change(web): lint engine/common/web-utils (#13797) +* chore(web): replace kmwFromCharCode with its ES6 equivalent (#13716) +* chore(web): add unit tests for SMP-aware string-handling helper methods (#13719) +* refactor(web): refactor string-helper library to eliminate its module side-effect (#13726) + +## 19.0.30 alpha 2025-05-02 + +* chore(android): Use `builder_is_ci_build` rather than `--ci` flag (#13772) +* docs: add `--deps` flags and move builder.md into /docs (#13815) +* chore(web): improve clean actions (#13804) +* fix(ios): restore keyboard-scale settings for older, still-supported iOS devices (#13773) +* maint(web): merge LMLayer CI build config with standard Web CI build config (#13807) +* maint(core): batch build kmx in tests for performance (#13795) +* maint(common): cleanup meson versioning (#13796) +* maint(common): test imported resource versions (#13803) + +## 19.0.29 alpha 2025-04-30 + +* change(web): enable linting for Keyman Engine for Web modules (#13787) +* fix(web): reconnect unit tests for low-level web utility classes/methods (#13714) +* fix(android): Change Alert Dialog instead of Toast if package missing languages (#13786) +* maint(common): remove platform advocates from CODEOWNERS (#13801) +* chore(common): allow to run tests in virtual environment (#13793) +* chore: increase linter strictness (#13788) + +## 19.0.28 alpha 2025-04-29 + +* chore(common): Update dependencies in crowdin GitHub action (#13775) +* chore: additional tweaks to scopes/labels (#13765) +* chore(common): rename `chore` GH label to `maint` (#13780) +* change(linux): add link to issue (#13778) + +## 19.0.27 alpha 2025-04-28 + +* chore: update labels for v19 (#13744) + +## 19.0.26 alpha 2025-04-24 + +* chore: merge beta version 18.0.235 history to master (#13735) +* chore(linux): Update debian changelog (#13738) +* chore: comment out CODEOWNERS (#13739) +* chore(linux): add a stub unit test to allow successful coverage reporting (#13740) + +## 19.0.25 alpha 2025-04-22 + +* chore: merge beta to master (pre-release prep) (#13730) + +## 19.0.24 alpha 2025-04-21 + +* build(deps): bump koa from 2.15.3 to 2.16.1 (#13694) +* chore: merge beta into master B18S5 (#13712) + +## 19.0.23 alpha 2025-04-07 + +* chore(web): remove dead methods regarding keyboard state (#13671) +* chore(linux): Update debian changelog (#13675) + +## 19.0.22 alpha 2025-04-04 + +* build(deps): bump tar-fs and puppeteer-core (#13661) + +## 19.0.21 alpha 2025-04-03 + +* chore: use explicit `node:` prefix for node imports (#13650) +* refactor(developer): rename developer/web/utils/common-messages.ts to developer-utils-messages.ts (#13653) +* refactor(developer): move sentry and developer options into console apps (#13654) +* refactor(developer): consolidate project loader (#13656) + +## 19.0.20 alpha 2025-04-01 + +* fix(android): move responsibility for setting keyboard height preference into `applyKeyboardHeight()` (#13578) + +## 19.0.19 alpha 2025-03-31 + +* chore: merge beta to master B18S4 (#13606) + ## 19.0.18 alpha 2025-03-25 * chore(developer): improve ldml code coverage (#13516) @@ -80,6 +385,134 @@ * refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168) * chore: increment to alpha 19.0 (#13187) +## 18.0.238 stable 2025-06-04 + +* fix(developer): ldml: make sure .run() calls compile() for validation (#14071) +* maint(linux): fix typo of temporary dependency package (#14085) +* maint(linux): update to new version of gha-ubuntu-packaging (#14096) +* maint(mac): make make-km-dmg.sh less quiet (#14106) + +## 18.0.237 stable 2025-05-30 + +* maint(android): Update FirstVoices launcher icon and red theme (#13917) +* fix(web): empty layout fontSize property should default to 1em (#13960) +* fix(linux): use correct header guard define in OrderedOutputDevice.h (#13963) +* chore(linux): Update debian changelog (#13888) +* fix(mac): handle PackageInfo section in kmp.inf file (#13957) +* feat(android): log active keyboard, model IDs with reported errors (#14003) +* chore(ios): FirstVoices certificate update (#14005) +* feat(ios): log active keyboard, model IDs with reported errors (#14006) +* fix(linux): configurable path in `.service` file when using basu (#13999) +* maint(linux): add `--no-werror` option to not report warnings as errors (#14000) +* fix(windows): use json file name against cache folder and ename Enter and Exit methods for TState Object (#14010) +* chore(ios): update FirstVoices AppIcon (#14011) +* maint: change builder_completion script to a .inc.sh (#14008) +* fix(windows): only breadcrumb unexpected state on first run (#14026) +* fix(windows): skip sending handle kmshell events to upgrade state machine for keyboard install modes (#14028) +* fix(windows): fix sentry error message reporting (#14029) + +## 18.0.236 stable 2025-05-12 + +* chore(linux): Update debian changelog (#13737) +* chore(linux): add a stub unit test to allow successful coverage reporting (#13741) +* fix(android): Use Alert Dialog if keyboard pkg missing languages (#13802) +* maint(web): merge LMLayer CI build config with standard Web CI build config (#13808) +* fix(ios): restore keyboard-scale settings for older, still-supported iOS devices (#13817) +* change(ios): limit reports about devices without predefined scaling entries (#13828) +* chore(ios): Update inuktitut_pirurvik to 1.4.1 (#13841) +* fix(web): add robustness for bad touch-layout fontsize specs (#13838) +* change(linux): add ICU dependency needed to build on Arch Linux (#13843) +* fix(mac): add version metadata after building tests (#13853) +* fix(web): sustain active batched-layout mode when keyboard is switched (#13863) +* fix(developer): remove ngrok binary at build time (#13873) +* maint(common): apply new codeowners from master (#13874) +* chore(mac,windows): Update Crowdin strings for Italian (#13872) + +## 18.0.234 beta 2025-04-22 + +* fix(android): Require RELEASE_OEM env vars to build FirstVoices Android (#13725) + +## 18.0.233 beta 2025-04-21 + +* chore: cleanup kps.xsd version typos (#13670) +* fix(android): refresh system keyboard banner on reload (#13702) +* chore(ios): update keyboard-size data for current iOS devices (#13697) +* fix(ios): ensure update notification does not go translucent/transparent (#13698) +* fix(ios): resummon keyboard when keyboard picker is dismissed (#13701) +* fix(web): match banner font scaling to osk font scaling (#13646) +* docs(common): Document Keyman website server-side dependencies (#13642) +* docs(windows): update FAQ (#13708) + +## 18.0.232 beta 2025-04-10 + +* fix(android): Downgrade UnknownHostException error to info (#13692) +* change(web): add unique classname to mobile-device page-trailer element (#13632) +* chore(android,mac,windows): Update crowdin strings for Spanish Latin America (#13691) + +## 18.0.231 beta 2025-04-09 + +* docs(android): Document default keyboard height for xxxhdpi devices (#13673) +* fix(ios): allow swipe-dismissal of Settings to be cancelled (#13686) +* fix(ios): do not show notification for downloaded keyboard when showing its installer (#13687) +* fix(ios): restore banner-hiding mechanism for keyboard calibration tool (#13689) + +## 18.0.230 beta 2025-04-08 + +* docs(web): documentation for touch-keyboard gesture models (#13659) + +## 18.0.229 beta 2025-04-07 + +* fix(ios): ensure keyboard height is set when engine settings view is used in a NavigationController (#13643) +* chore(android,mac,windows): update Crowdin strings for Czech (#13680) +* chore(android,mac,windows): update Crowdin strings for Khmer (#13681) +* chore(linux): Update debian changelog (#13674) + +## 18.0.228 beta 2025-04-04 + +* fix(ios): prevent re-display of keyboard when re-entering app to its menus (#13630) +* fix(ios): slide dismissal of keyboard-height adjuster will properly resize the keyboard (#13631) +* docs(web): improve section on motivation for gesture input serialization (#13652) +* fix(linux): fix crash if kmp doesn't have name field (#13648) +* chore(web): improve error logging in predictive-text worker message validation (#13660) +* fix(android): Adjust keyboard and banner heights for mdpi and hdpi devices (#13651) +* fix(android): adjust keyboard and banner height for xhdpi portrait (#13655) +* fix(android): Adjust keyboard and banner height for 7" tablet landscape (#13667) +* fix(android): Adjust keyboard height for 10" tablet landscape (#13668) + +## 18.0.227 beta 2025-04-03 + +* fix(developer): remove redundant keyboard fields from .kps (#13600) +* fix(developer): remove redundant Name and RTL fields from .kps `LexicalModel` (#13640) +* fix(mac): reevaluate API compliance for each change in context (#13550) +* fix(android): Move responsibility for setting keyboard height preference (#13635) +* fix(android): Add back button to "Adjust Keyboard Height "menu (#13645) +* fix(linux): allow unhandled keys to pass through to compliant apps (#13625) + +## 18.0.226 beta 2025-04-02 + +* docs(web): document internals for gesture modeling and processing (#13587) +* docs(web): document input-serialization aspects of gesture processing (#13597) +* fix(android): Add dimensions for 320+dpi phones in landscape (#13592) +* chore(common): Update crowdin strings for Mon (#13595) +* docs(linux): add function headers for some of the engine methods (#13637) + +## 18.0.225 beta 2025-04-01 + +* fix(web): remove extra px offset from last OSK row (#13621) +* fix(developer): add special handling for 'und' in kmc-keyboard-info (#13622) +* chore(mac,windows): Update Crowdin strings for Italian (#13594) +* change(ios): prevent file erasure during copyWithOverwrite error state (#13548) +* fix(ios): uninstalls keyboards upon failure to load (#13549) +* refactor(linux): remove warning if no custom keyboards exist (#13624) + +## 18.0.224 beta 2025-03-31 + +* fix(windows): reload right modifier key option (#13585) + +## 18.0.223 beta 2025-03-29 + +* chore(developer): improve kmc-ldml 'invalid version' message (#13602) + ## 18.0.222 beta 2025-03-28 * fix(android): Handle banner overrides independently when device is locked and unlocked (#13546) diff --git a/LICENSE.md b/LICENSE.md index 5b20376235..332e5a63ea 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,16 +1,14 @@ # License -Copyright (c) 2017-2024 SIL Global. All rights reserved. +Copyright (c) 2017-2025 SIL Global. All rights reserved. Licensed under the MIT License. -[Keyman for Linux](./linux) is licensed under the MIT License apart from [ibus-kmfl](./linux/ibus-kmfl) which is licensed under the GNU General Public License as published by the Free Software Foundation; either [version 2](./linux/ibus-kmfl/COPYING) of the License, or (at your option) any later version. - --- The MIT License -Copyright (c) 2017-2024 SIL Global +Copyright (c) 2017-2025 SIL Global Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28,4 +26,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index caab39c2f9..601fdcc696 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ ![Keyman](https://keyman.com/cdn/dev/img/logo2.png) -[Keyman](https://keyman.com) makes it possible for you to type in any language on Windows, macOS, Linux, iPhone, iPad, Android tablets and phones, and even instantly in your web browser. Create keyboard layouts with Keyman Developer and share them with the community in the [keyboards repository](https://github.com/keymanapp/keyboards). The [Keyman Community](https://community.software.sil.org/c/keyman) have already contributed keyboard layouts for over 2,000 languages! +[Keyman](https://keyman.com) makes it possible for you to type in any language +on Windows, macOS, Linux, iPhone, iPad, Android tablets and phones, and even +instantly in your web browser. Create keyboard layouts with Keyman Developer and +share them with the community in the [keyboards +repository](https://github.com/keymanapp/keyboards). The [Keyman +Community](https://community.software.sil.org/c/keyman) have so far contributed +well over 1,000 keyboard layouts for over 2,500 languages! # License diff --git a/VERSION.md b/VERSION.md index 9dcade37d9..efcfb4b252 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.19 +19.0.64 \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore index a5720bccc7..e822a25c2b 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -57,6 +57,9 @@ KMAPro/**/assets/*.kmp # Play Store release notes (copied from whatsnew.md) KMAPro/kMAPro/src/main/play/release-notes/**/*.txt +# Release artifacts +upload/ + # Eclipse files .project .settings/ diff --git a/android/KMAPro/build-play-store-notes.inc.sh b/android/KMAPro/build-play-store-notes.inc.sh index 56b9a92326..667a9eb90f 100755 --- a/android/KMAPro/build-play-store-notes.inc.sh +++ b/android/KMAPro/build-play-store-notes.inc.sh @@ -11,8 +11,8 @@ function generateReleaseNotes() { # Copy release notes for Gradle Play Publisher to upload # Reference: https://github.com/Triple-T/gradle-play-publisher#uploading-release-notes # - local PLAY_RELEASE_NOTES="$KEYMAN_ROOT/android/KMAPro/kMAPro/src/main/play/release-notes/en-US/$TIER.txt" - if [ $TIER = "stable" ]; then + local PLAY_RELEASE_NOTES="$KEYMAN_ROOT/android/KMAPro/kMAPro/src/main/play/release-notes/en-US/${KEYMAN_TIER}.txt" + if [ $KEYMAN_TIER = "stable" ]; then PLAY_RELEASE_NOTES="$KEYMAN_ROOT/android/KMAPro/kMAPro/src/main/play/release-notes/en-US/default.txt" fi builder_heading "Generating Play Store release notes to $PLAY_RELEASE_NOTES" diff --git a/android/KMAPro/build.gradle b/android/KMAPro/build.gradle index 5614fcae6a..c644d44fca 100644 --- a/android/KMAPro/build.gradle +++ b/android/KMAPro/build.gradle @@ -8,10 +8,10 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.9.2' // sentry-android-gradle-plugin 2.1.5+ requires AGP 7.0 classpath 'io.sentry:sentry-android-gradle-plugin:4.6.0' - classpath 'name.remal:gradle-plugins:1.5.0' + classpath 'name.remal:gradle-plugins:1.9.2' // From jcenter() which was deprecated August 2024 // https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/ diff --git a/android/KMAPro/build.sh b/android/KMAPro/build.sh index 1eafe1e667..fd54215d1c 100755 --- a/android/KMAPro/build.sh +++ b/android/KMAPro/build.sh @@ -7,6 +7,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" ## END STANDARD BUILD SCRIPT INCLUDE . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" . "$KEYMAN_ROOT/resources/build/build-help.inc.sh" . "$KEYMAN_ROOT/resources/build/build-download-resources.sh" @@ -28,7 +29,6 @@ builder_describe "Builds Keyman for Android app." \ "build" \ "test Runs lint and unit tests." \ "publish Publishes symbols to Sentry and the APK to the Play Store." \ - "--ci Don't start the Gradle daemon. For CI" \ "--upload-sentry Upload to sentry" # parse before describe_outputs to check debug flags @@ -37,32 +37,32 @@ builder_parse "$@" if builder_is_debug_build; then builder_heading "### Debug config ####" CONFIG="debug" - BUILD_FLAGS="assembleDebug -x lint -x test" + BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug" TEST_FLAGS="-x assembleDebug lintDebug testDebug" fi builder_describe_outputs \ configure /android/KMAPro/kMAPro/libs/keyman-engine.aar \ - build /android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${VERSION}.apk + build /android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION}.apk #### Build # Parse args -if builder_has_option --ci; then +if builder_is_ci_build; then DAEMON_FLAG=--no-daemon fi #### Build action definitions #### function makeLocalSentryRelease() { - echo "Making a Sentry release for tag $VERSION_GIT_TAG" + echo "Making a Sentry release for tag $KEYMAN_VERSION_GIT_TAG" sentry-cli upload-dif -p keyman-android --include-sources - sentry-cli releases -p keyman-android files $VERSION_GIT_TAG upload-sourcemaps ./ + sentry-cli releases -p keyman-android files $KEYMAN_VERSION_GIT_TAG upload-sourcemaps ./ - echo "Finalizing release tag $VERSION_GIT_TAG" - sentry-cli releases finalize "$VERSION_GIT_TAG" + echo "Finalizing release tag $KEYMAN_VERSION_GIT_TAG" + sentry-cli releases finalize "$KEYMAN_VERSION_GIT_TAG" } diff --git a/android/KMAPro/gradle.properties b/android/KMAPro/gradle.properties index 93aaf1410c..f56c02ac58 100644 --- a/android/KMAPro/gradle.properties +++ b/android/KMAPro/gradle.properties @@ -1,4 +1,7 @@ android.enableJetifier=true android.useAndroidX=true +# AGP 8.0 generates R classes for resources defined in the current module only. +android.nonTransitiveRClass=false + org.gradle.jvmargs=-Xmx1024m diff --git a/android/KMAPro/gradle/wrapper/gradle-wrapper.properties b/android/KMAPro/gradle/wrapper/gradle-wrapper.properties index 3994438e22..d6e308a637 100644 --- a/android/KMAPro/gradle/wrapper/gradle-wrapper.properties +++ b/android/KMAPro/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index 768d98d933..864fb36ee9 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -20,6 +20,15 @@ android { noCompress "kmp" } + buildFeatures { + buildConfig = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + defaultConfig { applicationId "com.tavultesoft.kmapro" minSdkVersion 21 @@ -29,11 +38,11 @@ android { //dumpProperties(project) // Use this to dump all external properties for debugging TeamCity integration //println "===END DUMP===" - // VERSION strings from version.gradle - versionCode VERSION_CODE as Integer - versionName VERSION_NAME - buildConfigField 'String', 'VERSION_ENVIRONMENT', '"'+VERSION_ENVIRONMENT+'"' - buildConfigField 'String', 'VERSION_GIT_TAG', '"'+VERSION_GIT_TAG+'"' + // KEYMAN_VERSION strings from version.gradle + versionCode KEYMAN_VERSION_CODE as Integer + versionName KEYMAN_VERSION_NAME + buildConfigField 'String', 'KEYMAN_VERSION_ENVIRONMENT', '"'+KEYMAN_VERSION_ENVIRONMENT+'"' + buildConfigField 'String', 'KEYMAN_VERSION_GIT_TAG', '"'+KEYMAN_VERSION_GIT_TAG+'"' } String env_release_store_file = System.getenv("release_store_file") @@ -77,7 +86,7 @@ android { // - PR builds also have "-test-1234" suffix (where 1234 is PR#) // Gradle sync will nullify these values, so have a fallback String appSuffix = '' - String tag = VERSION_TAG ? VERSION_TAG : '-' + tier; + String tag = KEYMAN_VERSION_TAG ? KEYMAN_VERSION_TAG : '-' + tier; if (tag.startsWithAny("-beta", "beta")) { appSuffix = ' Beta'; } else if (tag.startsWithAny("-alpha", "alpha")) { @@ -88,16 +97,16 @@ android { // -test-PR_NUMBER appSuffix = appSuffix + match.group(1) } else { - String environment = VERSION_ENVIRONMENT ? VERSION_ENVIRONMENT : 'local'; + String environment = KEYMAN_VERSION_ENVIRONMENT ? KEYMAN_VERSION_ENVIRONMENT : 'local'; if (environment.matches("local")) { appSuffix = appSuffix + '-' + environment; } } variant.resValue "string", "app_name", 'Keyman' + appSuffix; - // Adjust output name to "keyman-${VERSION_MD}.apk" + // Adjust output name to "keyman-${KEYMAN_VERSION_MD}.apk" variant.outputs.all { - outputFileName = "keyman-" + VERSION_MD + ".apk" + outputFileName = "keyman-" + KEYMAN_VERSION_MD + ".apk" } } lintOptions { @@ -156,7 +165,7 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' implementation 'com.stepstone.stepper:material-stepper:4.3.1' api(name: 'keyman-engine', ext: 'aar') diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index 023195d1c4..2d03311d50 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -62,8 +62,8 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven Log.d(TAG, "Initializing Sentry"); SentryAndroid.init(getApplicationContext(), options -> { options.setEnableAutoSessionTracking(false); - options.setRelease(com.tavultesoft.kmapro.BuildConfig.VERSION_GIT_TAG); - options.setEnvironment(com.tavultesoft.kmapro.BuildConfig.VERSION_ENVIRONMENT); + options.setRelease(com.tavultesoft.kmapro.BuildConfig.KEYMAN_VERSION_GIT_TAG); + options.setEnvironment(com.tavultesoft.kmapro.BuildConfig.KEYMAN_VERSION_ENVIRONMENT); }); } if (BuildConfig.DEBUG) { @@ -110,9 +110,6 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven @Override public void onInitializeInterface() { super.onInitializeInterface(); - - // KeymanWeb reloaded, so we have to pass the banner again - BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); } /** @@ -272,7 +269,8 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven @Override public void onKeyboardShown() { - // Do nothing + // Refresh banner theme + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); } @Override diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java index de4530fe3a..cef9ec33fc 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java @@ -4,7 +4,6 @@ package com.tavultesoft.kmapro; import android.content.Context; -import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; @@ -34,7 +33,6 @@ public class AdjustKeyboardHeightActivity extends BaseActivity { private static ImageView sampleKeyboard = null; // Keeps track of the adjusted keyboard height for saving - private static SharedPreferences.Editor editor = null; private static int currentHeight = 0; private static ViewGroup.LayoutParams layoutParams; @@ -52,22 +50,19 @@ public class AdjustKeyboardHeightActivity extends BaseActivity { if (actionBar != null) { actionBar.setTitle(null); actionBar.setDisplayUseLogoEnabled(false); - actionBar.setDisplayShowHomeEnabled(false); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); actionBar.setDisplayShowTitleEnabled(false); actionBar.setDisplayShowCustomEnabled(true); actionBar.setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.keyman_blue))); } TextView adjustKeyboardHeightActivityTitle = (TextView) findViewById(R.id.bar_title); - adjustKeyboardHeightActivityTitle.setWidth((int) getResources().getDimension(R.dimen.package_label_width)); String titleStr = getString(R.string.adjust_keyboard_height); adjustKeyboardHeightActivityTitle.setTextColor(ContextCompat.getColor(this, R.color.ms_white)); adjustKeyboardHeightActivityTitle.setText(titleStr); - SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - editor = prefs.edit(); - sampleKeyboard = (ImageView) findViewById(R.id.sample_keyboard); layoutParams = sampleKeyboard.getLayoutParams(); currentHeight = KMManager.getKeyboardHeight(context); @@ -77,9 +72,7 @@ public class AdjustKeyboardHeightActivity extends BaseActivity { resetButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // Clear the keyboard height preferences to reset - editor.remove(KMManager.KMKey_KeyboardHeightPortrait); - editor.remove(KMManager.KMKey_KeyboardHeightLandscape); - editor.commit(); + KMManager.applyKeyboardHeight(context, KMManager.KeyboardHeight_Reset); // Restore default height currentHeight = KMManager.getKeyboardHeight(context); @@ -107,11 +100,7 @@ public class AdjustKeyboardHeightActivity extends BaseActivity { break; case MotionEvent.ACTION_UP: // Save the currentHeight when the user releases - int orientation = KMManager.getOrientation(context); - String keyboardHeightKey = (orientation == Configuration.ORIENTATION_LANDSCAPE) ? - KMManager.KMKey_KeyboardHeightLandscape : KMManager.KMKey_KeyboardHeightPortrait; - editor.putInt(keyboardHeightKey, currentHeight); - editor.commit(); + KMManager.applyKeyboardHeight(context, currentHeight); break; } return true; @@ -140,11 +129,8 @@ public class AdjustKeyboardHeightActivity extends BaseActivity { } @Override - public void onBackPressed() { - // Apply the adjusted height on exit - KMManager.applyKeyboardHeight(this, currentHeight); - - super.onBackPressed(); + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; } - -} \ No newline at end of file +} diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java index e66d5e9ebc..09a8921e7b 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java @@ -114,7 +114,14 @@ public class KMPBrowserActivity extends BaseActivity { Intent intent = new Intent(context, MainActivity.class); intent.setData(downloadURI); KMLog.LogBreadcrumb(TAG, "Successful keyboard search now triggering package install", true); - startActivity(intent); + + // Verify network access before trying to download + if (KMManager.hasConnection(context)) { + startActivity(intent); + } else { + Toast.makeText(context, getString(R.string.no_internet_connection), Toast.LENGTH_LONG).show(); + return true; + } // Finish activity finish(); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index 07ce5c4cbb..3eec444fbd 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -144,8 +144,8 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene if (KMManager.getMaySendCrashReport()) { SentryAndroid.init(context, options -> { options.setEnableAutoSessionTracking(false); - options.setRelease(com.tavultesoft.kmapro.BuildConfig.VERSION_GIT_TAG); - options.setEnvironment(com.tavultesoft.kmapro.BuildConfig.VERSION_ENVIRONMENT); + options.setRelease(com.tavultesoft.kmapro.BuildConfig.KEYMAN_VERSION_GIT_TAG); + options.setEnvironment(com.tavultesoft.kmapro.BuildConfig.KEYMAN_VERSION_ENVIRONMENT); }); } @@ -440,41 +440,41 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_info: - showInfo(); - return true; - case R.id.action_share: - showShareDialog(); - return true; + // Android Gradle 8.0 no longer declares resources final, so can't use switch statement here + if (item.getItemId() == R.id.action_info) { + showInfo(); + return true; + } else if (item.getItemId() == R.id.action_share) { + showShareDialog(); + return true; /* Disable Web Browser to investigate Google sign-in - case R.id.action_web: + } else if ((item.getItemId() == R.id.action_web) { showWebBrowser(); return true;*/ - case R.id.action_text_size: - showTextSizeDialog(); - return true; - case R.id.action_clear_text: - showClearTextDialog(); - return true; - case R.id.action_get_started: - showGetStarted(); - return true; - case R.id.action_settings: - showSettings(); - return true; - case R.id.action_update_keyboards: - KMManager.getUpdateTool().executeOpenUpdates(); - // Dismiss icon - updateUpdateCountIndicator(0); - final MenuItem _keyboardupdate = menu.findItem(R.id.action_update_keyboards); - if (_keyboardupdate != null && _keyboardupdate.isVisible()) { - _keyboardupdate.setVisible(false); - } + } else if (item.getItemId() == R.id.action_text_size) { + showTextSizeDialog(); + return true; + } else if (item.getItemId() == R.id.action_clear_text){ + showClearTextDialog(); + return true; + } else if (item.getItemId() == R.id.action_get_started) { + showGetStarted(); + return true; + } else if (item.getItemId() == R.id.action_settings) { + showSettings(); + return true; + } else if (item.getItemId() == R.id.action_update_keyboards) { + KMManager.getUpdateTool().executeOpenUpdates(); + // Dismiss icon + updateUpdateCountIndicator(0); + final MenuItem _keyboardupdate = menu.findItem(R.id.action_update_keyboards); + if (_keyboardupdate != null && _keyboardupdate.isVisible()) { + _keyboardupdate.setVisible(false); + } - return true; - default: - return super.onOptionsItemSelected(item); + return true; + } else { + return super.onOptionsItemSelected(item); } } @@ -632,7 +632,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene Intent i = new Intent(this, InfoActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); startActivity(i); - overridePendingTransition(android.R.anim.fade_in, R.anim.hold); + overridePendingTransition(android.R.anim.fade_in, com.keyman.engine.R.anim.hold); } private void showWebBrowser() { @@ -643,7 +643,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene Intent i = new Intent(this, WebBrowserActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); startActivity(i); - overridePendingTransition(android.R.anim.fade_in, R.anim.hold); + overridePendingTransition(android.R.anim.fade_in, com.keyman.engine.R.anim.hold); } private void showShareDialog() { diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java index 161d966308..ee76265a97 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java @@ -116,12 +116,14 @@ public class PackageActivity extends AppCompatActivity implements kmpProcessor.getLanguageCount(pkgInfo, PackageProcessor.PP_KEYBOARDS_KEY, 0) : 0; // Sanity check for keyboard packages + // Not using showErrorToast to avoid cluttering Sentry with noise if (pkgTarget.equals(PackageProcessor.PP_TARGET_KEYBOARDS)) { if (keyboardCount == 0) { showErrorDialog(context, pkgId, getString(R.string.no_new_touch_keyboards_to_install)); return; } else if (languageCount == 0) { - showErrorToast(getString(R.string.no_associated_languages)); + showErrorDialog(context, pkgId, getString(R.string.no_associated_languages)); + return; } } diff --git a/android/KMAPro/kMAPro/src/main/res/values-b+es+419/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-b+es+419/strings.xml index 0d1f0efde5..0e0172d75c 100644 --- a/android/KMAPro/kMAPro/src/main/res/values-b+es+419/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values-b+es+419/strings.xml @@ -68,7 +68,7 @@ Ajustar la altura del teclado - Adjust longpress delay + Ajustar demora de pulsación larga Leyenda de barra espaciadora @@ -132,7 +132,7 @@ Tiempo de retraso más corto - Longpress delay time slider + Ajustador del tiempo de retraso de pulsación larga Buscar o escribir una dirección web diff --git a/android/KMEA/app/build.gradle b/android/KMEA/app/build.gradle index c80a4fd66e..dd4251025b 100644 --- a/android/KMEA/app/build.gradle +++ b/android/KMEA/app/build.gradle @@ -1,4 +1,5 @@ plugins { + id 'base' id 'com.android.library' id 'name.remal.default-plugins' } @@ -6,20 +7,32 @@ plugins { ext.rootPath = '../../' apply from: "$rootPath/version.gradle" +base { + // Specify library filename keyman-engine-$CONFIG.aar + archivesName = "keyman-engine" +} + android { compileSdk 34 namespace "com.keyman.engine" + buildFeatures { + // needed for custom BuildConfigField values in defaultConfig + buildConfig = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + defaultConfig { minSdkVersion 21 targetSdkVersion 34 - // VERSION_CODE and VERSION_NAME from version.gradle but Gradle removes them for libraries - buildConfigField "String", "KEYMAN_ENGINE_VERSION_NAME", "\""+VERSION_NAME+"\"" - buildConfigField "String", "VERSION_ENVIRONMENT", "\""+VERSION_ENVIRONMENT+"\"" - - // Sepcify library filename keyman-engine-$CONFIG.aar - setProperty("archivesBaseName", "keyman-engine") + // KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle but Gradle removes them for libraries + buildConfigField "String", "KEYMAN_ENGINE_VERSION_NAME", "\""+KEYMAN_VERSION_NAME+"\"" + buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\"" } buildTypes { @@ -36,10 +49,6 @@ android { } testOptions { - unitTests { - // Allows use of a simulated Android API for tests. (Thanks, roboelectric!) - includeAndroidResources = true - } unitTests.all { testLogging { // May also add "passed", "skipped". @@ -55,17 +64,17 @@ android { dependencies { // 1.6.0-rc01 needed to resolve https://issuetracker.google.com/issues/238425626 - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' // material:1.7.0 will need Gradle plugin 7.1.0+ implementation 'com.google.android.material:material:1.12.0' - implementation 'commons-io:commons-io:2.13.0' + implementation 'commons-io:commons-io:2.16.1' implementation 'io.sentry:sentry-android:7.8.0' implementation 'androidx.preference:preference:1.2.1' // Robolectric - testImplementation 'androidx.test:core:1.5.0' - testImplementation 'androidx.test.ext:junit:1.1.5' - testImplementation 'org.robolectric:robolectric:4.10.3' + testImplementation 'androidx.test.ext:junit:1.2.1' + testImplementation 'org.robolectric:robolectric:4.14.1' + testImplementation 'androidx.test:core:1.6.1' // Generate QR Codes implementation ('com.github.kenglxn.QRGen:android:3.0.1') { diff --git a/android/KMEA/app/src/main/assets/keyboard.html b/android/KMEA/app/src/main/assets/keyboard.html index 8d9d2d67dc..38f5ad617e 100644 --- a/android/KMEA/app/src/main/assets/keyboard.html +++ b/android/KMEA/app/src/main/assets/keyboard.html @@ -9,7 +9,6 @@ Keyman - diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index 7080f0a1fb..75b0443537 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -25,6 +25,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Point; import android.graphics.Typeface; import android.inputmethodservice.InputMethodService; @@ -273,6 +274,8 @@ public final class KMManager { // will be blocked. private static boolean maySendCrashReport = true; + private final static String KMEngine_PrefsKey = "KMAPreferences"; + // Keyman public keys public static final String KMKey_ID = "id"; public static final String KMKey_Name = "name"; @@ -295,8 +298,8 @@ public final class KMManager { public static final String KMKey_KMPInstall_Mode = "kmpInstallMode"; public static final String KMKey_KeyboardModified = "lastModified"; public static final String KMKey_KeyboardRTL = "rtl"; - public static final String KMKey_KeyboardHeightPortrait = "keyboardHeightPortrait"; - public static final String KMKey_KeyboardHeightLandscape = "keyboardHeightLandscape"; + private static final String KMKey_KeyboardHeightPortrait = "keyboardHeightPortrait"; + private static final String KMKey_KeyboardHeightLandscape = "keyboardHeightLandscape"; public static final String KMKey_LongpressDelay = "longpressDelay"; @@ -345,14 +348,21 @@ public final class KMManager { public static final int KMMinimum_LongpressDelay = 300; public static final int KMMaximum_LongpressDelay = 1500; + // Default keyboard heights + public static final int KeyboardHeight_Reset = 0; // To reset to default, applyKeyboardHeight + public static final int KeyboardHeight_Invalid = -1; // If invalid orientation passed to functions + public static int KeyboardHeight_Context_Portrait_Default = 0; // Default portrait height + public static int KeyboardHeight_Context_Landscape_Default = 0; // Default landscape height + public static int KeyboardHeight_Context_Portrait_Current = 0; // Current portrait height + public static int KeyboardHeight_Context_Landscape_Current = 0; // Current landscape height + // Default prediction/correction setting public static final int KMDefault_Suggestion = SuggestionType.PREDICTIONS_WITH_CORRECTIONS.toInt(); // Keyman files protected static final String KMFilename_KeyboardHtml = "keyboard.html"; protected static final String KMFilename_JSEngine = "keymanweb-webview.js"; - protected static final String KMFilename_JSSentry = "sentry.min.js"; - protected static final String KMFilename_JSSentryInit = "keyman-sentry.js"; + protected static final String KMFilename_JSSentry = "keyman-sentry.js"; protected static final String KMFilename_AndroidHost = "android-host.js"; protected static final String KMFilename_KmwCss = "kmwosk.css"; protected static final String KMFilename_KmwGlobeHintCss = "globe-hint.css"; @@ -498,6 +508,12 @@ public final class KMManager { migrateCloudKeyboards(appContext); CloudDownloadMgr.getInstance().initialize(appContext); + + calculateDefaultKeyboardHeights(context); + SharedPreferences prefs = context.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE); + KeyboardHeight_Context_Portrait_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightPortrait, KMManager.KeyboardHeight_Context_Portrait_Default); + KeyboardHeight_Context_Landscape_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightLandscape, KMManager.KeyboardHeight_Context_Landscape_Default); + } public static void executeResourceUpdate(Context aContext) @@ -965,7 +981,6 @@ public final class KMManager { copyAsset(context, KMFilename_JSEngine, "", true); copyAsset(context, KMFilename_JSSentry, "", true); - copyAsset(context, KMFilename_JSSentryInit, "", true); copyAsset(context, KMFilename_AndroidHost, "", true); copyAsset(context, KMFilename_KmwCss, "", true); copyAsset(context, KMFilename_KmwGlobeHintCss, "", true); @@ -1463,7 +1478,7 @@ public final class KMManager { * @param suggestType SuggestionType */ public static void setMaySuggest(String languageID, SuggestionType suggestType) { - SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + SharedPreferences prefs = appContext.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putInt(KMManager.getLanguageAutoCorrectionPreferenceKey(languageID), suggestType.toInt()); editor.commit(); @@ -1582,7 +1597,7 @@ public final class KMManager { model = model.replaceAll("\"", "'"); // When entering password field, maySuggest should override to disabled - SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + SharedPreferences prefs = appContext.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE); int suggestionPreference = prefs.getInt(getLanguageAutoCorrectionPreferenceKey(languageID), KMDefault_Suggestion); RelativeLayout.LayoutParams params; @@ -2213,7 +2228,7 @@ public final class KMManager { */ public static int getLongpressDelay() { SharedPreferences prefs = appContext.getSharedPreferences( - appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE); return prefs.getInt(KMKey_LongpressDelay, KMDefault_LongpressDelay); } @@ -2230,7 +2245,7 @@ public final class KMManager { } SharedPreferences prefs = appContext.getSharedPreferences( - appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putInt(KMKey_LongpressDelay, longpressDelay); editor.commit(); @@ -2266,21 +2281,87 @@ public final class KMManager { return bannerHeight; } + /** + * Returns the current keyboard height in dp for the current device orientation + */ public static int getKeyboardHeight(Context context) { - int defaultHeight = (int) context.getResources().getDimension(R.dimen.keyboard_height); - SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - int orientation = getOrientation(context); - if (orientation == Configuration.ORIENTATION_PORTRAIT) { - return prefs.getInt(KMManager.KMKey_KeyboardHeightPortrait, defaultHeight); - } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - return prefs.getInt(KMManager.KMKey_KeyboardHeightLandscape, defaultHeight); - } - - return defaultHeight; + return getKeyboardHeight(context, orientation); } + /** + * Returns the current keyboard height in dp for the specified device + * orientation + */ + public static int getKeyboardHeight(Context context, int orientation) { + SharedPreferences prefs = context.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE); + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + return KMManager.KeyboardHeight_Context_Portrait_Current; + } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + return KMManager.KeyboardHeight_Context_Landscape_Current; + } else { + return KMManager.KeyboardHeight_Invalid; // Invalid orientation + } + } + + /** + * Apply the keyboard height for the current orientation and store it in SharedPreferences. + * If height is KMManager.KeyboardHeight_Reset, remove the stored height for that + * orientation, resetting to the default height for the device/orientation. + * @param context Context + * @param height int - keyboard height in dp + */ public static void applyKeyboardHeight(Context context, int height) { + int orientation = getOrientation(context); + applyKeyboardHeight(context, height, orientation); + } + + /** + * Apply the keyboard height for the specified orientation and store it in SharedPreferences. + * If height is KMManager.KeyboardHeight_Reset, remove the stored height for that orientation. + * @param context Context + * @param height int - keyboard height in dp + * @param orientation int - Configuration.ORIENTATION_PORTRAIT or Configuration.ORIENTATION_LANDSCAPE + */ + public static void applyKeyboardHeight(Context context, int height, int orientation) { + SharedPreferences prefs = context.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + int defaultHeightForContext = getDefaultKeyboardHeight(orientation); + if (orientation != Configuration.ORIENTATION_PORTRAIT && orientation != Configuration.ORIENTATION_LANDSCAPE) { + return; // Invalid orientation, do nothing + } + if (height == KMManager.KeyboardHeight_Reset) { + // Passing 0 will reset the stored height for this orientation. + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + editor.remove(KMManager.KMKey_KeyboardHeightLandscape); + } else /* (orientation == Configuration.ORIENTATION_PORTRAIT) */{ + editor.remove(KMManager.KMKey_KeyboardHeightPortrait); + } + height = defaultHeightForContext; + } else { + // Applying gating to 50%-200% of default height (following Keyman) + if (height < (defaultHeightForContext / 2)) { + height = (int) (defaultHeightForContext / 2); + } else if (height > (defaultHeightForContext * 2)) { + height = (int) (defaultHeightForContext * 2); + } + + // Store the new height based on the current orientation + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + editor.putInt(KMManager.KMKey_KeyboardHeightLandscape, height); + } else /* (orientation == Configuration.ORIENTATION_PORTRAIT) */ { + editor.putInt(KMManager.KMKey_KeyboardHeightPortrait, height); + } + } + + // Update the height based on the current orientation + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + KeyboardHeight_Context_Landscape_Current = height; + } else /* (orientation == Configuration.ORIENTATION_PORTRAIT) */ { + KeyboardHeight_Context_Portrait_Current = height; + } + editor.commit(); + // Confirm new LayoutParams for in-app or system keyboards if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP)) { InAppKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height)); RelativeLayout.LayoutParams params = getKeyboardLayoutParams(); @@ -2293,6 +2374,53 @@ public final class KMManager { } } + /** + * Returns the default keyboard height in dp for the current device + * and orientation + */ + public static int getDefaultKeyboardHeight(Context context) { + int orientation = getOrientation(context); + return getDefaultKeyboardHeight(orientation); + } + + /** + * Returns the current keyboard height in dp for the specified orientation + * for the current device + */ + public static int getDefaultKeyboardHeight(int orientation) { + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + return KMManager.KeyboardHeight_Context_Portrait_Default; + } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + return KMManager.KeyboardHeight_Context_Landscape_Default; + } else { + return KMManager.KeyboardHeight_Invalid; // Invalid orientation + } + } + + private static void calculateDefaultKeyboardHeights(Context context) { + if (isTestMode()) { + // Keyboard height not needed for unit tests #13578 + KeyboardHeight_Context_Portrait_Default = 0; + KeyboardHeight_Context_Landscape_Default = 0; + return; + } + + Resources resources = context.getResources(); + Configuration originalConfig = resources.getConfiguration(); + + Configuration newConfig = new Configuration(originalConfig); + newConfig.orientation = Configuration.ORIENTATION_PORTRAIT; + Context newContext = context.createConfigurationContext(newConfig); + Resources newResources = newContext.getResources(); + KeyboardHeight_Context_Portrait_Default = (int) newResources.getDimension(R.dimen.keyboard_height); + + newConfig = new Configuration(originalConfig); + newConfig.orientation = Configuration.ORIENTATION_LANDSCAPE; + newContext = context.createConfigurationContext(newConfig); + newResources = newContext.getResources(); + KeyboardHeight_Context_Landscape_Default = (int) newResources.getDimension(R.dimen.keyboard_height); + } + /** * Get the size of the area the window would occupy. * API 30+ @@ -2322,7 +2450,7 @@ public final class KMManager { } protected static void setPersistentShouldShowHelpBubble(boolean flag) { - SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + SharedPreferences prefs = appContext.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(KMManager.KMKey_ShouldShowHelpBubble, flag); editor.commit(); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java index bb8ee190b6..ce32f2beeb 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java @@ -1,5 +1,6 @@ package com.keyman.engine.cloud; +import android.annotation.SuppressLint; import android.app.DownloadManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -60,6 +61,7 @@ public class CloudDownloadMgr{ * Append downloadreceiver to the main context. * @param aContext the context */ + @SuppressLint("UnspecifiedRegisterReceiverFlag") public synchronized void initialize(Context aContext) { if(isInitialized) @@ -73,6 +75,7 @@ public class CloudDownloadMgr{ aContext.registerReceiver(completeListener, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_EXPORTED); } else { + // Needs @SuppressLint UnspecifiedRegisterReceiverFlag aContext.registerReceiver(completeListener, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); } } catch (IllegalArgumentException e) { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/Connection.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/Connection.java index 591a03e4dd..626eed444b 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/Connection.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/Connection.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.net.URL; +import java.net.UnknownHostException; public final class Connection { private static final String TAG = "Connection"; @@ -103,6 +104,9 @@ public final class Connection { } catch (SocketTimeoutException e) { KMLog.LogBreadcrumb(TAG, "Connection.initialize timeout for: " + originalUrl, true); KMLog.LogInfo(TAG, "SocketTimeoutException"); + } catch (UnknownHostException e) { + KMLog.LogBreadcrumb(TAG, "Connection.initialize could not resolve host: " + originalUrl, true); + KMLog.LogInfo(TAG, "UnknownHostException"); } catch (Exception e) { KMLog.LogException(TAG, "Initialization failed:", e); } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java index cb13cc967e..1ce55bf0ce 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java @@ -4,15 +4,19 @@ package com.keyman.engine.util; +import static com.keyman.engine.KMManager.KMKey_LexicalModelID; + import android.util.Log; import android.widget.Toast; import com.keyman.engine.BaseActivity; import com.keyman.engine.BuildConfig; import com.keyman.engine.KMManager; -import com.keyman.engine.util.DependencyUtil; +import com.keyman.engine.data.Keyboard; import com.keyman.engine.util.DependencyUtil.LibraryType; +import java.util.Map; + import io.sentry.Breadcrumb; import io.sentry.Sentry; import io.sentry.SentryLevel; @@ -20,19 +24,67 @@ import io.sentry.SentryLevel; public final class KMLog { private static final String TAG = "KMLog"; + private static final String KEYBOARD_TAG = "keyboardId"; + private static final String KEYBOARD_COUNT_TAG = "installedKeyboardCount"; + private static final String MODEL_TAG = "modelId"; + private static final String LANGCODE_TAG = "languageCode"; + + // Some of the methods used to generate debug logging information can, themselves, + // trigger errors that can also trigger the same logging. We must not get + // caught in an infinite loop / stack-overflow; this field helps us avoid states + // that would otherwise cause error-looping, etc. + private static boolean isLogging = false; + + private static void tagDebugInfo() { + String kbdId = ""; + String lngCode = ""; + String modelId = ""; + int kbdCount = 0; + // Do not risk raising a new error while tagging info for another error. + try { + // Both take a context parameter... but don't actually need or use it! + Keyboard kbd = KMManager.getCurrentKeyboardInfo(null); + kbdCount = KMManager.getKeyboardsList(null).size(); + if (kbd != null) { + kbdId = kbd.getKeyboardID(); + lngCode = kbd.getLanguageCode(); + Map modelMap = KMManager.getAssociatedLexicalModel(kbd.getLanguageID()); + if (modelMap != null) { + modelId = modelMap.get(KMKey_LexicalModelID); + if (modelId == null) { + modelId = ""; + } + } + } + } catch (Exception ex) { + String msg = ex.getMessage() == null ? "" : ex.getMessage(); + Sentry.setExtra("debugLoggingError", msg); + } + Sentry.setExtra(KEYBOARD_TAG, kbdId); + Sentry.setExtra(KEYBOARD_COUNT_TAG, "" + kbdCount); + Sentry.setExtra(LANGCODE_TAG, lngCode); + Sentry.setExtra(MODEL_TAG, modelId); + } + /** * Utility to log info and send to Sentry * @param tag String of the caller * @param msg String of the info message */ public static void LogInfo(String tag, String msg) { + if(isLogging) { + return; + } + isLogging = true; if (msg != null && !msg.isEmpty()) { Log.i(tag, msg); if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { + tagDebugInfo(); Sentry.captureMessage(msg, SentryLevel.INFO); } } + isLogging = false; } /** @@ -46,9 +98,15 @@ public final class KMLog { return; } + if(isLogging) { + return; + } + isLogging = true; + Log.i(tag, msg); if (!DependencyUtil.libraryExists(LibraryType.SENTRY) || !Sentry.isEnabled()) { + isLogging = false; return; } @@ -74,7 +132,9 @@ public final class KMLog { crumb.setData("stacktrace", trace); } } + tagDebugInfo(); Sentry.addBreadcrumb(crumb); + isLogging = false; } /** @@ -83,6 +143,10 @@ public final class KMLog { * @param msg String of the error message */ public static void LogError(String tag, String msg) { + if(isLogging) { + return; + } + isLogging = true; if (msg != null && !msg.isEmpty()) { Log.e(tag, msg); @@ -91,9 +155,11 @@ public final class KMLog { } if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { + tagDebugInfo(); Sentry.captureMessage(msg, SentryLevel.ERROR); } } + isLogging = false; } /** @@ -103,6 +169,10 @@ public final class KMLog { * @param e Throwable exception */ public static void LogException(String tag, String msg, Throwable e) { + if(isLogging) { + return; + } + isLogging = true; String errorMsg = ""; if (msg != null && !msg.isEmpty()) { errorMsg = msg + "\n" + e; @@ -116,9 +186,11 @@ public final class KMLog { } if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { + tagDebugInfo(); Sentry.addBreadcrumb(errorMsg); Sentry.captureException(e); } + isLogging = false; } /** @@ -131,7 +203,12 @@ public final class KMLog { */ public static void LogExceptionWithData(String tag, String msg, String objName, Object obj, Throwable e) { + if(isLogging) { + return; + } + isLogging = true; if (obj != null && DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { + tagDebugInfo(); String objStr = null; try { objStr = obj.toString(); @@ -141,6 +218,10 @@ public final class KMLog { } // Report the original exception LogException(tag, msg, e); + // And remove the exception-specific tagged data, lest it also be + // tracked on subsequent errors not associated with the current call. + Sentry.removeExtra(objName); } + isLogging = false; } } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/VersionUtils.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/VersionUtils.java index 58e80399f5..7188280615 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/VersionUtils.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/VersionUtils.java @@ -12,11 +12,11 @@ public final class VersionUtils { public static final String TAG = "VersionUtils"; public static boolean isLocalBuild() { - return BuildConfig.VERSION_ENVIRONMENT.equalsIgnoreCase("local"); + return BuildConfig.KEYMAN_VERSION_ENVIRONMENT.equalsIgnoreCase("local"); } public static boolean isTestBuild() { - return BuildConfig.VERSION_ENVIRONMENT.equalsIgnoreCase("test"); + return BuildConfig.KEYMAN_VERSION_ENVIRONMENT.equalsIgnoreCase("test"); } // Utility for local and PR test builds - e.g. force check of keyboard updates diff --git a/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml b/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml index 5fda42853c..1270bc7f21 100644 --- a/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml +++ b/android/KMEA/app/src/main/res/values-cs-rCZ/strings.xml @@ -86,6 +86,14 @@ Fatální chyba klávesnice s %1$s:%2$s pro jazyk %3$s . Načítání výchozí klávesnice. Chyba v klávesnici %1$s:%2$s pro jazyk %3$s. + + Keyman vyžaduje, aby byl nainstalován WebView.“ + + Keyman vyžaduje, aby byl povolen WebView.“ + + Nainstalovat WebView + + Povolit WebView Kontroluji související slovník ke stažení Nelze se připojit ke Keyman serveru a zkontrolovat, zda je možné stáhnout přidružený slovník @@ -119,6 +127,8 @@ Nepodařilo se načíst stažený soubor Přístup k serveru se nezdařil! + + DownloadManager zakázán - nelze zkontrolovat aktualizace "Všechny zdroje jsou aktuální!" diff --git a/android/KMEA/app/src/main/res/values-hdpi/dimens.xml b/android/KMEA/app/src/main/res/values-hdpi/dimens.xml new file mode 100644 index 0000000000..0ac77f2ef2 --- /dev/null +++ b/android/KMEA/app/src/main/res/values-hdpi/dimens.xml @@ -0,0 +1,6 @@ + + + + 40dp + 170dp + diff --git a/android/KMEA/app/src/main/res/values-km-rKH/strings.xml b/android/KMEA/app/src/main/res/values-km-rKH/strings.xml index a687e6d377..55340f80cb 100644 --- a/android/KMEA/app/src/main/res/values-km-rKH/strings.xml +++ b/android/KMEA/app/src/main/res/values-km-rKH/strings.xml @@ -80,6 +80,14 @@ មាន​បញ្ហាធ្ងន់ធ្ងរចំពោះក្ដារចុច %1$s:%2$s សម្រាប់​ភាសា %3$s។ កំពុង​ផ្ទុក​ក្ដារចុច​លំនាំដើម។ បញ្ហាក្នុងក្ដារចុច %1$s:%2$s សម្រាប់ភាសា %3$s។ + + Keyman តម្រូវឱ្យដំឡើង WebView ។\" + + Keyman តម្រូវឱ្យបើក WebView ។\" + + ដំឡើង WebView + + បើក WebView កំពុង​ពិនិត្យ​រក​បញ្ជី​ពាក្យ​ពាក់ព័ន្ធ​ដើម្បី​ទាញយក មិនអាចភ្ជាប់ទៅម៉ាស៊ីនមេ Keyman ដើម្បីរកបញ្ជីពាក្យសម្រាប់ទាញយកបានទេ @@ -113,6 +121,8 @@ រក​មិន​ឃើញ​ឯកសារដែល​បាន​ទាញ​យក មិន​អាច​ចូលប្រើ​ម៉ាស៊ីនមេ​បាន​ទេ! + + DownloadManager បានបិទដំណើរការ - មិនអាចពិនិត្យរកអាប់ដេតផ្សេងៗ "ធនធានទាំង​អស់​គឺ​ថ្មី​បំផុត​ហើយ!" diff --git a/android/KMEA/app/src/main/res/values-land-hdpi/dimens.xml b/android/KMEA/app/src/main/res/values-land-hdpi/dimens.xml new file mode 100644 index 0000000000..a5cbd993f9 --- /dev/null +++ b/android/KMEA/app/src/main/res/values-land-hdpi/dimens.xml @@ -0,0 +1,6 @@ + + + + 25dp + 100dp + diff --git a/android/KMEA/app/src/main/res/values-land-xhdpi/dimens.xml b/android/KMEA/app/src/main/res/values-land-xhdpi/dimens.xml new file mode 100644 index 0000000000..292cfcc6b3 --- /dev/null +++ b/android/KMEA/app/src/main/res/values-land-xhdpi/dimens.xml @@ -0,0 +1,18 @@ + + + + 30dp + 140dp + 83.5dp + 78.5dp + 38dp + 14dp + 6dp + 4dp + 8dp + 125dp + 85dp + 0.5dp + 8dp + 6dp + diff --git a/android/KMEA/app/src/main/res/values-land/dimens.xml b/android/KMEA/app/src/main/res/values-land/dimens.xml index 00cb4ae358..a0e9cb5918 100644 --- a/android/KMEA/app/src/main/res/values-land/dimens.xml +++ b/android/KMEA/app/src/main/res/values-land/dimens.xml @@ -1,8 +1,8 @@ - 40dp - 100dp + 28dp + 120dp 49.25dp 49.25dp 21dp diff --git a/android/KMEA/app/src/main/res/values-mnw-rMM/strings.xml b/android/KMEA/app/src/main/res/values-mnw-rMM/strings.xml index 1ba2e20cbe..9cde602180 100644 --- a/android/KMEA/app/src/main/res/values-mnw-rMM/strings.xml +++ b/android/KMEA/app/src/main/res/values-mnw-rMM/strings.xml @@ -80,6 +80,14 @@ ကဳဗုဒ်ယောၚ်ယာဇၞော်ဇၞော်နကဵု %1$s:%2$s သွက် ဘာသာ %3$s ။ ဂြဲပ္တိုန်မံၚ် ကဳဗုဒ်ပကတိ။ တၚ်ယောၚ်ယာ ပ္ဍဲကဳဗုဒ် %1$s:%2$s သွက် ဘာသာ %3$s + + သ္ဂောံဂြဲဖျေံ Keyman ဂှ် ဒးသုၚ်စောဲ WebView ရောၚ်။ + + သ္ဂောံပံက် Keyman ဂှ် ဒးသုၚ်စောဲ WebView ရောၚ်။ + + ပ္တိုန်စုတ် WebView ရောၚ် + + ပံက် WebView စၟဳစၟတ်ဒၟံၚ် အဘိဓာန် မဆက်စပ် သ္ဂောံဂြဲဖျေံ သ္ဂောံဂြဲဖျေံ အဘိဓာန်မဆက်စပ်တံဂှ် ဆက်စၠောံကု သာပါ ကဳမာန်ဟွံဂွံ @@ -113,6 +121,8 @@ ဝှာၚ်မဂၠောၚ်ဂြဲ​ဖျေံလဝ်ဂှ် ကလေၚ်​ကေတ်ဟွံဂွံ ဆက်စၠောံကုသာပါဟွံအံၚ်ဇၞး! + + စက်ဂြဲကၟာတ်လဝ်ရောၚ် - စၟဳစၟတ်တၚ်သမၠုၚ်ဟွံဂွံ "တံရိုဟ်ဖအိုတ်ဂှ် ဒှ်တၟိရ!" diff --git a/android/KMEA/app/src/main/res/values-sw600dp-land/dimens.xml b/android/KMEA/app/src/main/res/values-sw600dp-land/dimens.xml index 613a8b8526..51afef68a4 100644 --- a/android/KMEA/app/src/main/res/values-sw600dp-land/dimens.xml +++ b/android/KMEA/app/src/main/res/values-sw600dp-land/dimens.xml @@ -1,8 +1,8 @@ - 50dp - 140dp + 45dp + 200dp 73.5dp 58.5dp 32dp diff --git a/android/KMEA/app/src/main/res/values-sw720dp-land/dimens.xml b/android/KMEA/app/src/main/res/values-sw720dp-land/dimens.xml index 3944826815..330e70de71 100644 --- a/android/KMEA/app/src/main/res/values-sw720dp-land/dimens.xml +++ b/android/KMEA/app/src/main/res/values-sw720dp-land/dimens.xml @@ -2,7 +2,7 @@ 60dp - 200dp + 280dp 98.5dp 98.5dp 42dp diff --git a/android/KMEA/app/src/main/res/values-xhdpi/dimens.xml b/android/KMEA/app/src/main/res/values-xhdpi/dimens.xml new file mode 100644 index 0000000000..1b9d88a536 --- /dev/null +++ b/android/KMEA/app/src/main/res/values-xhdpi/dimens.xml @@ -0,0 +1,6 @@ + + + + 50dp + 270dp + diff --git a/android/KMEA/build.gradle b/android/KMEA/build.gradle index 9aa2d0dcc7..4ca999ea60 100644 --- a/android/KMEA/build.gradle +++ b/android/KMEA/build.gradle @@ -5,10 +5,10 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.9.2' // io.sentry:sentry-android-gradle-plugin not available for library project classpath 'io.sentry:sentry-android:7.8.0' - classpath 'name.remal:gradle-plugins:1.5.0' + classpath 'name.remal:gradle-plugins:1.9.2' } } diff --git a/android/KMEA/build.sh b/android/KMEA/build.sh index 29cb519cf5..4d4ea7f8e3 100755 --- a/android/KMEA/build.sh +++ b/android/KMEA/build.sh @@ -8,6 +8,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" ## END STANDARD BUILD SCRIPT INCLUDE . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" # ################################ Main script ################################ @@ -28,8 +29,7 @@ builder_describe "Builds Keyman Engine for Android." \ "configure" \ "build" \ "test Runs lint and unit tests." \ - ":engine Builds Engine" \ - "--ci Don't start the Gradle daemon. For CI" + ":engine Builds Engine" # parse before describe_outputs to check debug flags builder_parse "$@" @@ -37,7 +37,7 @@ builder_parse "$@" if builder_is_debug_build; then builder_heading "### Debug config ####" CONFIG="debug" - BUILD_FLAGS="assembleDebug -x lint -x test" + BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug" TEST_FLAGS="-x assembleDebug lintDebug testDebug" JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\build\test-results\testDebugUnitTest\']" fi @@ -51,7 +51,7 @@ builder_describe_outputs \ # Parse args DAEMON_FLAG= -if builder_has_option --ci; then +if builder_is_ci_build; then DAEMON_FLAG=--no-daemon fi @@ -82,8 +82,6 @@ if builder_start_action build:engine; then cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/kmwosk.css" cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/globe-hint.css" "$ENGINE_ASSETS/globe-hint.css" cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/keymanweb-osk.ttf" "$ENGINE_ASSETS/keymanweb-osk.ttf" - - cp "$KEYMAN_ROOT/node_modules/@sentry/browser/build/bundle.min.js" "$ENGINE_ASSETS/sentry.min.js" cp "$KEYMAN_ROOT/web/src/engine/sentry-manager/build/lib/index.js" "$ENGINE_ASSETS/keyman-sentry.js" echo "Copying es6-shim polyfill" @@ -98,7 +96,7 @@ fi if builder_start_action test:engine; then - if builder_has_option --ci; then + if builder_is_ci_build; then # Report JUnit test results to CI echo "$JUNIT_RESULTS" fi diff --git a/android/KMEA/gradle/wrapper/gradle-wrapper.properties b/android/KMEA/gradle/wrapper/gradle-wrapper.properties index 3994438e22..8330c6a158 100644 --- a/android/KMEA/gradle/wrapper/gradle-wrapper.properties +++ b/android/KMEA/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Wed Apr 30 11:17:37 ICT 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/README.md b/android/README.md index 6abb94161d..db1eb592b0 100644 --- a/android/README.md +++ b/android/README.md @@ -141,6 +141,10 @@ android { aaptOptions { noCompress "kmp" } + + buildFeatures { + buildConfig = true + } ``` 5. After the `android {}` object, include the following: ````gradle @@ -154,7 +158,7 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' api (name:'keyman-engine', ext:'aar') implementation 'androidx.preference:preference:1.2.1' diff --git a/android/Samples/KMSample1/app/build.gradle b/android/Samples/KMSample1/app/build.gradle index 51490c5ab2..cc92dee298 100644 --- a/android/Samples/KMSample1/app/build.gradle +++ b/android/Samples/KMSample1/app/build.gradle @@ -36,7 +36,7 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' api(name: 'keyman-engine', ext: 'aar') implementation 'androidx.preference:preference:1.2.1' diff --git a/android/Samples/KMSample1/build.gradle b/android/Samples/KMSample1/build.gradle index 15824022e2..18d4c46cd5 100644 --- a/android/Samples/KMSample1/build.gradle +++ b/android/Samples/KMSample1/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.9.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/android/Samples/KMSample1/build.sh b/android/Samples/KMSample1/build.sh index 66a04b8d7c..172ef6c5e7 100755 --- a/android/Samples/KMSample1/build.sh +++ b/android/Samples/KMSample1/build.sh @@ -8,6 +8,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" ## END STANDARD BUILD SCRIPT INCLUDE . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" ################################ Main script ################################ @@ -22,8 +23,7 @@ builder_describe "Build KMSample1 app for Android." \ "configure" \ "build" \ "test" \ - ":app KMSample1" \ - "--ci Don't start the Gradle daemon. Use for CI" + ":app KMSample1" # parse before describe_outputs to check debug flags builder_parse "$@" @@ -46,7 +46,7 @@ builder_describe_outputs \ # Parse args -if builder_has_option --ci; then +if builder_is_ci_build; then SAMPLE_FLAGS="$SAMPLE_FLAGS -no-daemon" fi diff --git a/android/Samples/KMSample1/gradle.properties b/android/Samples/KMSample1/gradle.properties index 80f4cae5a8..f486cc756a 100644 --- a/android/Samples/KMSample1/gradle.properties +++ b/android/Samples/KMSample1/gradle.properties @@ -18,4 +18,8 @@ # org.gradle.parallel=true android.enableJetifier=true android.useAndroidX=true + +# AGP 8.0 generates R classes for resources defined in the current module only. +android.nonTransitiveRClass=false + org.gradle.jvmargs=-Xmx1024m diff --git a/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties b/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties index fb79de47c0..4fba274552 100644 --- a/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties +++ b/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/android/Samples/KMSample2/app/build.gradle b/android/Samples/KMSample2/app/build.gradle index 71ab1dab03..cd2f4d6225 100644 --- a/android/Samples/KMSample2/app/build.gradle +++ b/android/Samples/KMSample2/app/build.gradle @@ -35,7 +35,7 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' api (name:'keyman-engine', ext:'aar') implementation 'androidx.preference:preference:1.2.1' diff --git a/android/Samples/KMSample2/build.gradle b/android/Samples/KMSample2/build.gradle index 15824022e2..18d4c46cd5 100644 --- a/android/Samples/KMSample2/build.gradle +++ b/android/Samples/KMSample2/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.9.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/android/Samples/KMSample2/build.sh b/android/Samples/KMSample2/build.sh index 75f985aa1f..2f7a99c764 100755 --- a/android/Samples/KMSample2/build.sh +++ b/android/Samples/KMSample2/build.sh @@ -8,6 +8,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" ## END STANDARD BUILD SCRIPT INCLUDE . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" ################################ Main script ################################ @@ -22,8 +23,7 @@ builder_describe "Build KMSample2 app for Android." \ "configure" \ "build" \ "test" \ - ":app KMSample2" \ - "--ci Don't start the Gradle daemon. Use for CI" + ":app KMSample2" # parse before describe_outputs to check debug flags builder_parse "$@" @@ -47,7 +47,7 @@ builder_describe_outputs \ # Parse args -if builder_has_option --ci; then +if builder_is_ci_build; then SAMPLE_FLAGS="$SAMPLE_FLAGS -no-daemon" fi diff --git a/android/Samples/KMSample2/gradle.properties b/android/Samples/KMSample2/gradle.properties index 80f4cae5a8..f486cc756a 100644 --- a/android/Samples/KMSample2/gradle.properties +++ b/android/Samples/KMSample2/gradle.properties @@ -18,4 +18,8 @@ # org.gradle.parallel=true android.enableJetifier=true android.useAndroidX=true + +# AGP 8.0 generates R classes for resources defined in the current module only. +android.nonTransitiveRClass=false + org.gradle.jvmargs=-Xmx1024m diff --git a/android/Samples/KMSample2/gradle/wrapper/gradle-wrapper.properties b/android/Samples/KMSample2/gradle/wrapper/gradle-wrapper.properties index 4b0560a7a2..0c0a7aec7d 100644 --- a/android/Samples/KMSample2/gradle/wrapper/gradle-wrapper.properties +++ b/android/Samples/KMSample2/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/android/Tests/KeyboardHarness/app/build.gradle b/android/Tests/KeyboardHarness/app/build.gradle index 5462622cfa..ab8066393b 100644 --- a/android/Tests/KeyboardHarness/app/build.gradle +++ b/android/Tests/KeyboardHarness/app/build.gradle @@ -14,15 +14,19 @@ android { noCompress "kmp" } + buildFeatures { + buildConfig = true + } + defaultConfig { applicationId "com.keyman.android.tests.keyboardHarness" minSdkVersion 21 targetSdkVersion 34 - // VERSION_CODE and VERSION_NAME from version.gradle - versionCode VERSION_CODE as Integer - versionName VERSION_NAME - buildConfigField "String", "VERSION_ENVIRONMENT", "\""+VERSION_ENVIRONMENT+"\"" + // KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle + versionCode KEYMAN_VERSION_CODE as Integer + versionName KEYMAN_VERSION_NAME + buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\"" } buildTypes { release { @@ -42,7 +46,7 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' api (name:'keyman-engine', ext:'aar') implementation 'androidx.preference:preference:1.2.1' diff --git a/android/Tests/KeyboardHarness/build.gradle b/android/Tests/KeyboardHarness/build.gradle index ce0b936509..8fdf2aa422 100644 --- a/android/Tests/KeyboardHarness/build.gradle +++ b/android/Tests/KeyboardHarness/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.0' + classpath 'com.android.tools.build:gradle:8.9.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/android/Tests/KeyboardHarness/build.sh b/android/Tests/KeyboardHarness/build.sh index 71b2deddf2..c23f8ca0f5 100755 --- a/android/Tests/KeyboardHarness/build.sh +++ b/android/Tests/KeyboardHarness/build.sh @@ -8,6 +8,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" ## END STANDARD BUILD SCRIPT INCLUDE . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" ################################ Main script ################################ @@ -22,8 +23,7 @@ builder_describe "Build KeyboardHarness test app for Android." \ "configure" \ "build" \ "test" \ - ":app KeyboardHarness" \ - "--ci Don't start the Gradle daemon. Use for CI" + ":app KeyboardHarness" # parse before describe outputs to check debug flags builder_parse "$@" @@ -33,7 +33,7 @@ ARTIFACT="app-release-unsigned.apk" if builder_is_debug_build; then builder_heading "### Debug config ####" CONFIG="debug" - BUILD_FLAGS="assembleDebug -x lint -x test" + BUILD_FLAGS="assembleDebug -x lintDebug -x testDebug" TEST_FLAGS="-x assembleDebug lintDebug testDebug" ARTIFACT="app-$CONFIG.apk" fi @@ -55,7 +55,7 @@ SHLVL=0 # Parse args # Build flags that apply to all targets -if builder_has_option --ci; then +if builder_is_ci_build; then BUILD_FLAGS="$BUILD_FLAGS -no-daemon" TEST_FLAGS="$TEST_FLAGS -no-daemon" fi diff --git a/android/Tests/KeyboardHarness/gradle.properties b/android/Tests/KeyboardHarness/gradle.properties index 80f4cae5a8..8bb139036f 100644 --- a/android/Tests/KeyboardHarness/gradle.properties +++ b/android/Tests/KeyboardHarness/gradle.properties @@ -19,3 +19,6 @@ android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx1024m + +# AGP 8.0 generates R classes for resources defined in the current module only. +android.nonTransitiveRClass=false diff --git a/android/Tests/KeyboardHarness/gradle/wrapper/gradle-wrapper.properties b/android/Tests/KeyboardHarness/gradle/wrapper/gradle-wrapper.properties index da62d22563..3b14c8260c 100644 --- a/android/Tests/KeyboardHarness/gradle/wrapper/gradle-wrapper.properties +++ b/android/Tests/KeyboardHarness/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/android/Tests/keycode/app/build.gradle b/android/Tests/keycode/app/build.gradle index ffba4c0cc4..a0b24abb44 100644 --- a/android/Tests/keycode/app/build.gradle +++ b/android/Tests/keycode/app/build.gradle @@ -12,10 +12,10 @@ android { minSdkVersion 21 targetSdkVersion 34 - // VERSION_CODE and VERSION_NAME from version.gradle - versionCode VERSION_CODE as Integer - versionName VERSION_NAME - buildConfigField "String", "VERSION_ENVIRONMENT", "\""+VERSION_ENVIRONMENT+"\"" + // KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle + versionCode KEYMAN_VERSION_CODE as Integer + versionName KEYMAN_VERSION_NAME + buildConfigField "String", "KEYMAN_VERSION_ENVIRONMENT", "\""+KEYMAN_VERSION_ENVIRONMENT+"\"" } buildTypes { release { diff --git a/android/Tests/keycode/gradle/wrapper/gradle-wrapper.properties b/android/Tests/keycode/gradle/wrapper/gradle-wrapper.properties index 763217c50f..8357d84852 100644 --- a/android/Tests/keycode/gradle/wrapper/gradle-wrapper.properties +++ b/android/Tests/keycode/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/build.sh b/android/build.sh index e6d9927898..12017e2c6b 100755 --- a/android/build.sh +++ b/android/build.sh @@ -2,6 +2,11 @@ # # Build Keyman Engine for Android, Keyman for Android, OEM FirstVoices Android app, # Samples: KMsample1 and KMSample2, Test - KeyboardHarness +# +# OEM FirstVoices Android app requires the following env vars set: +# RELEASE_OEM should be set +# RELEASE_OEM_FIRSTVOICES should be true + ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" @@ -9,9 +14,22 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" ## END STANDARD BUILD SCRIPT INCLUDE . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" +. "$KEYMAN_ROOT/resources/build/zip.inc.sh" ################################ Main script ################################ +# +# Restrict available OEM publish targets to those that can be built on the current system +# + +oemtargets=() +if [ ! -z "${RELEASE_OEM+x}" ]; then + if [ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]; then + oemtargets+=(":fv=../oem/firstvoices/android OEM FirstVoices for Android app") + fi +fi + builder_describe \ "Build Keyman Engine for Android, Keyman for Android, and FirstVoices Android app." \ "@/resources/tools/check-markdown test:help" \ @@ -20,7 +38,8 @@ builder_describe \ build \ test \ "publish Publishes symbols to Sentry and the APKs to the Play Store." \ - --ci+ \ + "archive Copy release artifacts to upload/ and rsync to downloads.keyman" \ + "--ci+ Deprecated build option. Remove in 20.0" \ --upload-sentry+ \ ":engine=KMEA Keyman Engine for Android" \ ":app=KMAPro Keyman for Android" \ @@ -28,7 +47,7 @@ builder_describe \ ":sample1=Samples/KMSample1 Sample app: KMSample1" \ ":sample2=Samples/KMSample2 Sample app: KMSample2" \ ":keyboardharness=Tests/KeyboardHarness Test app: KeyboardHarness" \ - ":fv=../oem/firstvoices/android OEM FirstVoices for Android app" + "${oemtargets[@]}" \ builder_parse "$@" @@ -53,4 +72,51 @@ function do_test_help() { builder_run_action test:help do_test_help -builder_run_child_actions publish \ No newline at end of file +builder_run_child_actions publish + +# Copy release artifacts to upload/ and rsync to downloads.keyman.com +if builder_start_action archive; then + UPLOAD_PATH="$KEYMAN_ROOT/android/upload/${KEYMAN_VERSION}" + KEYMAN_ENGINE_ANDROID_ZIP="keyman-engine-android-${KEYMAN_VERSION}.zip" + KEYMAN_APK="keyman-${KEYMAN_VERSION}.apk" + FIRSTVOICES_APK="firstvoices-${KEYMAN_VERSION}.apk" + ZIP_FILE="${UPLOAD_PATH}/${KEYMAN_ENGINE_ANDROID_ZIP}" + ZIP_FLAGS=("-q" "-r") # quiet, recursive + + mkdir -p "${UPLOAD_PATH}" + + # Create Keyman Engine for Android archive + builder_echo "Copying Keyman Engine for Android into ${UPLOAD_PATH}..." + cd "${UPLOAD_PATH}" + cp "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs/keyman-engine.aar" ./ + add_zip_files "${ZIP_FILE}" "${ZIP_FLAGS[@]}" "keyman-engine.aar" + rm -f "keyman-engine.aar" + + builder_echo "Copying Keyman Engine for Android Sample projects into ${UPLOAD_PATH}..." + cp -rf "${KEYMAN_ROOT}/android/Samples" ./ + add_zip_files "${ZIP_FILE}" "-x@../../zip-excludes" "${ZIP_FLAGS[@]}" "Samples" + rm -rf "Samples" + + # Copy release APK + cp "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/release/${KEYMAN_APK}" ./ + + # FirstVoices app + + if [ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]; then + cp "${KEYMAN_ROOT}/oem/firstvoices/android/app/build/outputs/apk/release/${FIRSTVOICES_APK}" ./ + fi + + # + # Write download info files + # + + cd "${UPLOAD_PATH}" + write_download_info "Keyman Engine for Android" "${KEYMAN_ENGINE_ANDROID_ZIP}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android" + write_download_info "Keyman for Android" "${KEYMAN_APK}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android" + + if [ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]; then + write_download_info "FirstVoices Keyboards" "${FIRSTVOICES_APK}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android" + fi + + builder_finish_action success archive +fi diff --git a/android/docs/engine/KMManager/applyKeyboardHeight.md b/android/docs/engine/KMManager/applyKeyboardHeight.md index 2830519c62..70af3e7652 100644 --- a/android/docs/engine/KMManager/applyKeyboardHeight.md +++ b/android/docs/engine/KMManager/applyKeyboardHeight.md @@ -3,14 +3,19 @@ title: KMManager.applyKeyboardHeight() --- ## Summary -The **applyKeyboardHeight()** method sets the height of the keyboard frame for -the device's current [screen orientation](https://developer.android.com/training/multiscreen/screensizes#TaskUseOriQuali) -(portrait vs landscape). +The **applyKeyboardHeight()** method sets the height of the keyboard frame for +the device's current [screen orientation](https://developer.android.com/training/multiscreen/screensizes#TaskUseOriQuali) +(portrait vs landscape) or a specified orientation. ## Syntax ```java KMManager.applyKeyboardHeight(Context context, int height) ``` +or + +```java +KMManager.applyKeyboardHeight(Context context, int height, int orientation) +``` ### Parameters @@ -18,25 +23,28 @@ KMManager.applyKeyboardHeight(Context context, int height) : The context `height` -: The height of the keyboard frame in *density-independent pixels (dp)* +: The height of the keyboard frame in *density-independent pixels (dp)*. Pass `KMManager.KeyboardHeight_Reset` to reset the keyboard height (for current orientation) to device-specific defaults. + +`orientation` _(Optional)_ +: Accepts a [screen orientation](https://developer.android.com/training/multiscreen/screensizes#TaskUseOriQuali) value. This is most useful if you want to change the size of the keyboard in the other (non-current) orientation. If `orientation` is not defined, keyboard height is set for the current device orientation. ## Description -Use this method when you want to increase or decrease the keyboard height for -the device in the current screen orientation. This height is independent from +Use this method when you want to increase or decrease the keyboard height for +the device in the current screen orientation. This height is independent from the height of the suggestion banner frame. For reference, here's a table of the default Keyman keyboard heights for various devices and screen orientation. - - Device Type and Screen Orientation | Default height (dp) | -|-----------------------------------|---------------------| -| Default handset in portrait | 205 | -| Default handset in landscape | 150 | -| 7" tablet in portrait | 305 | -| 7" tablet in landscape | 200 | -| 10" tablet in portrait | 405 | -| 10" tablet in landscape | 300 | -**Note:** This new keyboard height would be applied for all platforms, so an + Device Type | Default height (dp)
Portrait | Default height (dp)
Landscape | +|-------------------------------|---------------------------------|----------------------------------| +| Default handset (160dpi) | 205 | 120 | +| High Density handset (240dpi) | 170 | 100 | +| Extra High Density handset (320dpi) | 270 | 140 | +| Extra Extra Extra
High Density handset (640 dpi) | 270 | 140 | +| 7" tablet | 305 | 200 | +| 10" tablet | 405 | 280 | + +**Note:** This new keyboard height would be applied for all platforms, so an adjusted keyboard height for a phone would appear too small for a tablet. ## Examples @@ -50,5 +58,23 @@ The following script illustrates the use of `applyKeyboardHeight()`: KMManager.applyKeyboardHeight(this, newKeyboardHeight); ``` +or + +```java + import android.content.res.Configuration; + ... + // Increase the Keyman keyboard height for landscape mode to 250dp (default Keyman value for most phones is 100dp) + int newKeyboardHeight = 250; + KMManager.applyKeyboardHeight(this, newKeyboardHeight, Configuration.ORIENTATION_LANDSCAPE); +``` + + +The following script illustrates the use of `applyKeyboardHeight()` to reset keyboard height to default: + +```java + // Reset keyboard to default height + KMManager.applyKeyboardHeight(this, KMManager.KeyboardHeight_Reset); +``` + ## See also * [getKeyboardHeight()](getKeyboardHeight) diff --git a/android/docs/engine/KMManager/getDefaultKeyboardHeight.md b/android/docs/engine/KMManager/getDefaultKeyboardHeight.md new file mode 100644 index 0000000000..56705cae7b --- /dev/null +++ b/android/docs/engine/KMManager/getDefaultKeyboardHeight.md @@ -0,0 +1,55 @@ +--- +title: KMManager.getDefaultKeyboardHeight() +--- + +## Summary + +The **`getDefaultKeyboardHeight()`** method returns the default height of the keyboard frame for the current or specefied orientation. + +## Syntax + +``` javascript +KMManager.getDefaultKeyboardHeight(Context context) +``` +or +``` javascript +KMManager.getDefaultKeyboardHeight(int orientation) +``` + +### Parameters (Choose One) + +`context` _(Alternate)_ +: The context. + +`orientation` _(Alternate)_ +: Accepts a [screen orientation](https://developer.android.com/training/multiscreen/screensizes#TaskUseOriQuali) value. This is most useful if you want to get the size of the keyboard in the other (non-current) orientation. If `orientation` parameter is not passed in, keyboard height is returned for the current device orientation. + +### Returns + +Returns the default height of the keyboard frame in *density-independent pixels (dp)*. + +## Description + +Use this method to get the default height of the keyboard frame. + +## Examples + +### Example: Using `getDefaultKeyboardHeight()` + +The following script illustrate the use of `getDefaultKeyboardHeight()`: + +``` javascript + int keyboardHeight = KMManager.getDefaultKeyboardHeight(this); +``` +or +```java + import android.content.res.Configuration; + ... + // Get the current Keyman keyboard height for landscape mode. + + int keyboardHeightLandscape = KMManager.getDefaultKeyboardHeight( Configuration.ORIENTATION_LANDSCAPE); +``` + +## See also + +- [applyKeyboardHeight()](applyKeyboardHeight) diff --git a/android/docs/engine/KMManager/getKeyboardHeight.md b/android/docs/engine/KMManager/getKeyboardHeight.md index c459e45ddb..fb8efb2b96 100644 --- a/android/docs/engine/KMManager/getKeyboardHeight.md +++ b/android/docs/engine/KMManager/getKeyboardHeight.md @@ -12,12 +12,19 @@ frame. ``` javascript KMManager.getKeyboardHeight(Context context) ``` +or +``` javascript +KMManager.getKeyboardHeight(Context context, int orientation) +``` ### Parameters `context` : The context. +`orientation` _(Optional)_ +: Accepts a [screen orientation](https://developer.android.com/training/multiscreen/screensizes#TaskUseOriQuali) value. This is most useful if you want to get the size of the keyboard in the other (non-current) orientation. If `orientation` parameter is not passed in, keyboard height is returned for the current device orientation. + ### Returns Returns the height of the keyboard frame in *density-independent pixels @@ -36,7 +43,15 @@ The following script illustrate the use of `getKeyboardHeight()`: ``` javascript int keyboardHeight = KMManager.getKeyboardHeight(this); ``` +or +```java + import android.content.res.Configuration; + ... + // Get the current Keyman keyboard height for landscape mode. + + int keyboardHeightLandscape = KMManager.getKeyboardHeight(this, Configuration.ORIENTATION_LANDSCAPE); +``` ## See also -- [applyKeyboardHeight](applyKeyboardHeight) +- [applyKeyboardHeight()](applyKeyboardHeight) diff --git a/android/docs/engine/KMManager/setHTMLBanner.md b/android/docs/engine/KMManager/setHTMLBanner.md index 5fcc2c7003..a13fe9da35 100644 --- a/android/docs/engine/KMManager/setHTMLBanner.md +++ b/android/docs/engine/KMManager/setHTMLBanner.md @@ -28,7 +28,7 @@ Use this method to specify the HTML content to display in the banner to theme yo If the banner theme references assets (like .svg or .css files), ensure you've also called [copyHTMLBannerAssets()](copyHTMLBannerAssets). -Note: The HTML banner needs to be updated whenever the keyboard is reloaded, so call this in `SystemKeyboard.onInitializeInterface(()`. +Note: The HTML banner needs to be updated whenever the keyboard is reloaded, so call this in `SystemKeyboard.onKeyboardShown()`. ## Examples diff --git a/android/docs/help/basic/config/adjust-keyboard-height.md b/android/docs/help/basic/config/adjust-keyboard-height.md index fcc459a5e5..6b18c5dc2a 100644 --- a/android/docs/help/basic/config/adjust-keyboard-height.md +++ b/android/docs/help/basic/config/adjust-keyboard-height.md @@ -6,11 +6,11 @@ title: Adjust Keyboard Height - Keyman for Android Help This menu is used to adjust the height of the on-screen keyboard (OSK), which does not include the suggestion banner. -Drag the blank OSK at the bottom of the screen to increase or decrease the keyboard height for the device's current orientation (portrait vs landscape). The adjusted keyboard heights are automatically saved when the blank OSK is released. +Drag the blank OSK at the bottom of the screen to increase or decrease the keyboard height for the device's current orientation (portrait vs landscape). Turn the device 90 degrees to the alternate orientation and repeat the process. The adjusted keyboard heights are automatically saved when the blank OSK is released. ![](../../android_images/blank-osk.png) ### Reset to Default Height -Press the button to reset the OSK height to the default values. +Press the button to reset the OSK height (for the current orientation) to the default device-specific height. ![](../../android_images/reset-to-default.png) diff --git a/android/version.gradle b/android/version.gradle index ebf68a94e0..ea7342166c 100644 --- a/android/version.gradle +++ b/android/version.gradle @@ -7,9 +7,9 @@ // def getVersionCode = { -> - String env_version_major = System.getenv("VERSION_MAJOR") - String env_version_minor = System.getenv("VERSION_MINOR") - String env_version_patch = System.getenv("VERSION_PATCH") + String env_version_major = System.getenv("KEYMAN_VERSION_MAJOR") + String env_version_minor = System.getenv("KEYMAN_VERSION_MINOR") + String env_version_patch = System.getenv("KEYMAN_VERSION_PATCH") if(env_version_patch != null && env_version_minor != null && env_version_patch != null) { // Version code is, for 17.1.33: 1810033. This supports up to 9999 // builds for a given major.minor version. We only support up to 9 minor @@ -36,10 +36,10 @@ def getVersionCode = { -> } def getVersionMD = { -> - String env_version = System.getenv("VERSION") + String env_version = System.getenv("KEYMAN_VERSION") if (env_version != null) { - // If building from script, we have build number in VERSION - println "Using build $env_version from VERSION" + // If building from script, we have build number in KEYMAN_VERSION + println "Using build $env_version from KEYMAN_VERSION" return "$env_version" } else { String version_md = file("$rootPath/../VERSION.md").text.trim() @@ -48,15 +48,15 @@ def getVersionMD = { -> } def getVersionName = { -> - String env_version = System.getenv("VERSION_WITH_TAG") + String env_version = System.getenv("KEYMAN_VERSION_WITH_TAG") if (env_version != null) { - // If building from script, we have build number in VERSION_WITH_TAG - println "Using build $env_version from VERSION_WITH_TAG" + // If building from script, we have build number in KEYMAN_VERSION_WITH_TAG + println "Using build $env_version from KEYMAN_VERSION_WITH_TAG" return "$env_version" } else { // Building probably from IDE, so let's use VERSION.md and TIER.md directly - // This is a close match with VERSION_WITH_TAG, except that 'stable' tier - // is not normally included with VERSION_WITH_TAG + // This is a close match with KEYMAN_VERSION_WITH_TAG, except that 'stable' tier + // is not normally included with KEYMAN_VERSION_WITH_TAG String version_md = file("$rootPath/../VERSION.md").text.trim() String tier_md = file("$rootPath/../TIER.md").text.trim() println "Using build $version_md-$tier_md-local from project VERSION.md/TIER.md" @@ -65,10 +65,10 @@ def getVersionName = { -> } def getVersionEnvironment = { -> - String env_environment = System.getenv("VERSION_ENVIRONMENT") + String env_environment = System.getenv("KEYMAN_VERSION_ENVIRONMENT") if (env_environment != null) { - // If building from script, we have build number in VERSION_ENVIRONMENT - println "Using $env_environment from VERSION_ENVIRONMENT" + // If building from script, we have build number in KEYMAN_VERSION_ENVIRONMENT + println "Using $env_environment from KEYMAN_VERSION_ENVIRONMENT" return "$env_environment" } else { // Building probably from IDE, so use "local" @@ -78,23 +78,23 @@ def getVersionEnvironment = { -> } def getVersionGitTag = { -> - String env_git_tag = System.getenv("VERSION_GIT_TAG") + String env_git_tag = System.getenv("KEYMAN_VERSION_GIT_TAG") if (env_git_tag != null) { - println "Using $env_git_tag from VERSION_GIT_TAG" + println "Using $env_git_tag from KEYMAN_VERSION_GIT_TAG" return "$env_git_tag" } else { // Building probably from IDE - println "Using release@$VERSION_NAME" - return "release@$VERSION_NAME" + println "Using release@$KEYMAN_VERSION_NAME" + return "release@$KEYMAN_VERSION_NAME" } } ext { - VERSION_CODE=getVersionCode() - VERSION_MD=getVersionMD() - VERSION_NAME=getVersionName() - VERSION_ENVIRONMENT=getVersionEnvironment() - VERSION_TAG=System.getenv("VERSION_TAG") - VERSION_GIT_TAG=getVersionGitTag() + KEYMAN_VERSION_CODE=getVersionCode() + KEYMAN_VERSION_MD=getVersionMD() + KEYMAN_VERSION_NAME=getVersionName() + KEYMAN_VERSION_ENVIRONMENT=getVersionEnvironment() + KEYMAN_VERSION_TAG=System.getenv("KEYMAN_VERSION_TAG") + KEYMAN_VERSION_GIT_TAG=getVersionGitTag() } -//println "version.gradle: VERSION_TAG: " + VERSION_TAG \ No newline at end of file +//println "version.gradle: KEYMAN_VERSION_TAG: " + KEYMAN_VERSION_TAG \ No newline at end of file diff --git a/android/zip-excludes b/android/zip-excludes new file mode 100644 index 0000000000..1c2b62205d --- /dev/null +++ b/android/zip-excludes @@ -0,0 +1 @@ +Samples/*/build.sh diff --git a/common/schemas/kps/README.md b/common/schemas/kps/README.md index aea03f2a5b..7cf3c71373 100644 --- a/common/schemas/kps/README.md +++ b/common/schemas/kps/README.md @@ -2,6 +2,14 @@ Master version: https://github.com/keymanapp/api.keyman.com/blob/master/schemas/kps/17.0/kps.xsd +## 2025-04-02 18.0 +* Version 18.0 deprecates the following fields, and kmc-package does not use them (#13600): + - Keyboards/Keyboard/Version + - Keyboards/Keyboard/Name + - Keyboards/Keyboard/RTL + - LexicalModels/LexicalModel/Name + - LexicalModels/LexicalModel/RTL (Note: was never read or written by any Keyman tooling) + ## 2023-10-19 17.0 * Version 17.0 adds: - LicenseFile - a .md file, usually named LICENSE.md @@ -9,13 +17,13 @@ Master version: https://github.com/keymanapp/api.keyman.com/blob/master/schemas/ - Info/Description - a short Markdown description of the content of the package, e.g. shown in search results on keyman.com - RelatedPackages - a list of other packages which relate to this one, or are deprecated by it - Keyboards/Keyboard/Examples - a list of typing examples for the keyboard - - Keyboarsd/Keyboard/WebOSKFonts - a list of font filenames (not necessarily in package) suitable for rendering the on screen keyboard - - Keyboarsd/Keyboard/WebDisplayFonts - a list of font filenames (not necessarily in package) suitable for use with the keyboard + - Keyboards/Keyboard/WebOSKFonts - a list of font filenames (not necessarily in package) suitable for rendering the on screen keyboard + - Keyboards/Keyboard/WebDisplayFonts - a list of font filenames (not necessarily in package) suitable for use with the keyboard * Version 17.0 removes: - LexicalModels/LexicalModel/Version - version information is not stored in the models, but only in the package metadata (was unused) ## 2023-04-21 7.0.1 -* Removes LexicalModel.Version, as it was never read or written +* Removes LexicalModels/LexicalModel/Version, as it was never read or written ## 2021-07-19 7.0 * Initial version 7.0 diff --git a/common/schemas/kps/kps.xsd b/common/schemas/kps/kps.xsd index 292f0c8fc6..ee467eef66 100644 --- a/common/schemas/kps/kps.xsd +++ b/common/schemas/kps/kps.xsd @@ -1,12 +1,12 @@ @@ -119,10 +119,10 @@ - + - - + + @@ -143,9 +143,9 @@ - + - + diff --git a/common/test/keyboards/build.sh b/common/test/keyboards/build.sh index 2c8cc19ff4..2d6a986b87 100755 --- a/common/test/keyboards/build.sh +++ b/common/test/keyboards/build.sh @@ -5,6 +5,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" ## END STANDARD BUILD SCRIPT INCLUDE +. "$KEYMAN_ROOT/resources/build/zip.inc.sh" + targets=() # Build list of available targets from subfolders, if none specified for d in */; do @@ -29,7 +31,7 @@ builder_parse "$@" function zipsource() { local target="$1" pushd "$1" > /dev/null - 7z a -r -x!build -x"!$target.kpj.user" "${target}_source.zip" . + add_zip_files "${target}_source.zip" -x@../zip-excludes -q -r . # -q quiet, -r recursive popd > /dev/null } @@ -40,10 +42,10 @@ function build_index() { - Test Keyboards - $VERSION_WITH_TAG + Test Keyboards - $KEYMAN_VERSION_WITH_TAG -

Test Keyboards - $VERSION_WITH_TAG

+

Test Keyboards - $KEYMAN_VERSION_WITH_TAG

    EOF @@ -90,4 +92,4 @@ function build() { fi } -builder_run_action build build \ No newline at end of file +builder_run_action build build diff --git a/common/test/keyboards/zip-excludes b/common/test/keyboards/zip-excludes new file mode 100644 index 0000000000..1a27385042 --- /dev/null +++ b/common/test/keyboards/zip-excludes @@ -0,0 +1,3 @@ +build/ +*.kpj.user +*_source.zip diff --git a/common/test/resources/model-helpers.mjs b/common/test/resources/model-helpers.mjs index b2ad083f96..a3f141cd54 100644 --- a/common/test/resources/model-helpers.mjs +++ b/common/test/resources/model-helpers.mjs @@ -13,17 +13,9 @@ import vm from 'vm'; // Ensure that we can successfully load the module & apply kmwLength, as it's // needed for some of the unit tests. -// // Verify that the KMW string extensions are loaded via side-effect. - -import { extendString } from '@keymanapp/web-utils'; - import { createRequire } from "module"; import { fileURLToPath } from 'url'; -// Ensure that our KMW string-extensions activate. -extendString(); -assert.ok('💩'.kmwLength); - /** * Creates a MessageEvent (for inter-worker communication), with the given data payload. * diff --git a/common/test/resources/package.json b/common/test/resources/package.json index 9295190f2d..69a4a7a1b6 100644 --- a/common/test/resources/package.json +++ b/common/test/resources/package.json @@ -3,6 +3,7 @@ "description": "Common test resources for Keyman's JS & TS-based components & products", "type": "module", "license": "MIT", + "scripts": {}, "devDependencies": { "@keymanapp/resources-gosh": "*", "typescript": "^5.4.5" diff --git a/common/tools/hextobin/index.ts b/common/tools/hextobin/index.ts index b53b2d734a..168ccb0539 100644 --- a/common/tools/hextobin/index.ts +++ b/common/tools/hextobin/index.ts @@ -1,5 +1,5 @@ -import * as fs from 'fs'; -import * as rd from 'readline'; +import * as fs from 'node:fs'; +import * as rd from 'node:readline'; export default async function hextobin(inputFilename: string, outputFilename?: string, options?: {silent?: boolean}): Promise { diff --git a/common/tools/sourcemap-path-remapper/package.json b/common/tools/sourcemap-path-remapper/package.json index 312b3ed964..88aaae8296 100644 --- a/common/tools/sourcemap-path-remapper/package.json +++ b/common/tools/sourcemap-path-remapper/package.json @@ -18,7 +18,6 @@ "homepage": "https://github.com/keymanapp/keyman#readme", "devDependencies": { "@keymanapp/resources-gosh": "*", - "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", "typescript": "^5.4.5" }, diff --git a/common/tools/sourcemap-path-remapper/src/index.ts b/common/tools/sourcemap-path-remapper/src/index.ts index fca96c3b14..af1f184698 100644 --- a/common/tools/sourcemap-path-remapper/src/index.ts +++ b/common/tools/sourcemap-path-remapper/src/index.ts @@ -1,5 +1,5 @@ -import * as fs from 'fs'; -import path from 'path'; +import * as fs from 'node:fs'; +import path from 'node:path'; import convertSourceMap from 'convert-source-map'; // Transforms sourcemaps among various common formats. // Base64, stringified-JSON, end-of-file comment... diff --git a/common/web/keyman-version/build.sh b/common/web/keyman-version/build.sh index 8fb4884812..9de3da3f8f 100755 --- a/common/web/keyman-version/build.sh +++ b/common/web/keyman-version/build.sh @@ -33,16 +33,16 @@ function do_build() { // Note: does not use the 'default' keyword so that the export name is // correct when converted to a CommonJS module with \`esbuild\`. export class KEYMAN_VERSION { - static readonly VERSION = \"$VERSION\"; - static readonly VERSION_RELEASE =\"$VERSION_RELEASE\"; - static readonly VERSION_MAJOR = \"$VERSION_MAJOR\"; - static readonly VERSION_MINOR = \"$VERSION_MINOR\"; - static readonly VERSION_PATCH = \"$VERSION_PATCH\"; - static readonly TIER =\"$TIER\"; - static readonly VERSION_TAG = \"$VERSION_TAG\"; - static readonly VERSION_WITH_TAG = \"$VERSION_WITH_TAG\"; - static readonly VERSION_ENVIRONMENT = \"$VERSION_ENVIRONMENT\"; - static readonly VERSION_GIT_TAG = \"$VERSION_GIT_TAG\"; + static readonly VERSION = \"$KEYMAN_VERSION\"; + static readonly VERSION_RELEASE =\"$KEYMAN_VERSION_RELEASE\"; + static readonly VERSION_MAJOR = \"$KEYMAN_VERSION_MAJOR\"; + static readonly VERSION_MINOR = \"$KEYMAN_VERSION_MINOR\"; + static readonly VERSION_PATCH = \"$KEYMAN_VERSION_PATCH\"; + static readonly TIER =\"$KEYMAN_TIER\"; + static readonly VERSION_TAG = \"$KEYMAN_VERSION_TAG\"; + static readonly VERSION_WITH_TAG = \"$KEYMAN_VERSION_WITH_TAG\"; + static readonly VERSION_ENVIRONMENT = \"$KEYMAN_VERSION_ENVIRONMENT\"; + static readonly VERSION_GIT_TAG = \"$KEYMAN_VERSION_GIT_TAG\"; } // Also provides it as a 'default' export. diff --git a/common/web/keyman-version/package.json b/common/web/keyman-version/package.json index bdd2e9bc84..74cce896bd 100644 --- a/common/web/keyman-version/package.json +++ b/common/web/keyman-version/package.json @@ -11,6 +11,7 @@ "/build/" ], "license": "MIT", + "scripts": {}, "type": "module", "devDependencies": { "typescript": "^5.4.5" diff --git a/common/web/types/.eslintrc.cjs b/common/web/types/.eslintrc.cjs index 4fd5954e49..92206e4ee4 100644 --- a/common/web/types/.eslintrc.cjs +++ b/common/web/types/.eslintrc.cjs @@ -16,8 +16,5 @@ module.exports = { files: "src/**/*.ts", extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], }, - ], - rules: { - "prefer-const": "off", // TODO: enable this once infrastructure is in place and cleanup the problem cases - }, + ] }; diff --git a/common/web/types/build.sh b/common/web/types/build.sh index a0211194f2..a053beb120 100755 --- a/common/web/types/build.sh +++ b/common/web/types/build.sh @@ -83,7 +83,7 @@ function do_configure() { function do_test() { local MOCHA_FLAGS= - if [[ "${TEAMCITY_GIT_PATH:-}" != "" ]]; then + if builder_is_running_on_teamcity; then # we're running in TeamCity MOCHA_FLAGS="-reporter mocha-teamcity-reporter" fi diff --git a/common/web/types/package.json b/common/web/types/package.json index 429dfbcddb..0bfda5f4ed 100644 --- a/common/web/types/package.json +++ b/common/web/types/package.json @@ -44,7 +44,6 @@ "c8": "^7.12.0", "chalk": "^2.4.2", "hexy": "^0.3.4", - "mocha": "^8.4.0", "typescript": "^5.4.5" }, "mocha": { diff --git a/common/web/types/src/kmx/keyman-targets.ts b/common/web/types/src/kmx/keyman-targets.ts index fc666584da..ca2174ca85 100644 --- a/common/web/types/src/kmx/keyman-targets.ts +++ b/common/web/types/src/kmx/keyman-targets.ts @@ -54,7 +54,7 @@ export const export function keymanTargetsFromString(targets: string, options?: {expandTargets?: boolean}): KeymanTarget[] { - let result = new Set(targets.split(/ +/)); + const result = new Set(targets.split(/ +/)); if(options?.expandTargets) { if(result.has(KeymanTarget.any)) { return AllKeymanTargets; diff --git a/common/web/types/src/kmx/kmx-file-reader.ts b/common/web/types/src/kmx/kmx-file-reader.ts index 73106bf058..6cd45199b1 100644 --- a/common/web/types/src/kmx/kmx-file-reader.ts +++ b/common/web/types/src/kmx/kmx-file-reader.ts @@ -60,14 +60,13 @@ export class KmxFileReader { } public read(source: Uint8Array): KEYBOARD { - let binaryKeyboard: BUILDER_COMP_KEYBOARD; - let kmx = new KMXFile(); - binaryKeyboard = kmx.COMP_KEYBOARD.fromBuffer(source); + const kmx = new KMXFile(); + const binaryKeyboard = kmx.COMP_KEYBOARD.fromBuffer(source); if(binaryKeyboard.dwIdentifier != KMXFile.FILEID_COMPILED) { throw new KmxFileReaderError(`Not a .kmx file: header does not contain FILEID_COMPILED`); } - let result = new KEYBOARD(); + const result = new KEYBOARD(); result.fileVersion = binaryKeyboard.dwFileVersion; result.flags = binaryKeyboard.dwFlags; result.hotkey = binaryKeyboard.dwHotKey; @@ -88,7 +87,7 @@ export class KmxFileReader { this.readGroupsAndRules(binaryKeyboard, kmx, source, result); // Process system stores once we have all stores and groups loaded - for(let store of result.stores) { + for(const store of result.stores) { this.processSystemStore(store, result); } @@ -109,8 +108,8 @@ export class KmxFileReader { let offset = binaryKeyboard.dpGroupArray; for (let i = 0; i < binaryKeyboard.cxGroupArray; i++) { const data = new Uint8Array(source.buffer, source.byteOffset + offset); - let binaryGroup = kmx.COMP_GROUP.fromBuffer(data); - let group = new GROUP(); + const binaryGroup = kmx.COMP_GROUP.fromBuffer(data); + const group = new GROUP(); group.dpMatch = this.readString(source, binaryGroup.dpMatch); group.dpName = this.readString(source, binaryGroup.dpName); group.dpNoMatch = this.readString(source, binaryGroup.dpNoMatch); @@ -120,8 +119,8 @@ export class KmxFileReader { let keyOffset = binaryGroup.dpKeyArray; for (let j = 0; j < binaryGroup.cxKeyArray; j++) { const keyData = new Uint8Array(source.buffer, source.byteOffset + keyOffset); - let binaryKey = kmx.COMP_KEY.fromBuffer(keyData); - let key = new KEY(); + const binaryKey = kmx.COMP_KEY.fromBuffer(keyData); + const key = new KEY(); key.Key = binaryKey.Key; key.Line = binaryKey.Line; key.ShiftFlags = binaryKey.ShiftFlags; @@ -140,8 +139,8 @@ export class KmxFileReader { let offset = binaryKeyboard.dpStoreArray; for (let i = 0; i < binaryKeyboard.cxStoreArray; i++) { const data = new Uint8Array(source.buffer, source.byteOffset + offset); - let binaryStore = kmx.COMP_STORE.fromBuffer(data); - let store = new STORE(); + const binaryStore = kmx.COMP_STORE.fromBuffer(data); + const store = new STORE(); store.dwSystemID = binaryStore.dwSystemID; store.dpName = this.readString(source, binaryStore.dpName); store.dpString = this.readString(source, binaryStore.dpString); diff --git a/common/web/types/src/kmx/kmx-plus/element-string.ts b/common/web/types/src/kmx/kmx-plus/element-string.ts index d14c4a9011..e65d996f34 100644 --- a/common/web/types/src/kmx/kmx-plus/element-string.ts +++ b/common/web/types/src/kmx/kmx-plus/element-string.ts @@ -1,5 +1,5 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; -import { DependencySections, StrsItem, UsetItem } from './kmx-plus.js'; +import { DependencySections, StrsItem, StrsOptions, UsetItem } from './kmx-plus.js'; import { ElementParser, ElementSegment, ElementType } from '../../ldml-keyboard/pattern-parser.js'; import * as util from '../../util/util.js'; import MATCH_HEX_ESCAPE = util.MATCH_HEX_ESCAPE; @@ -33,7 +33,7 @@ export class ElementString extends Array { * @param source if a string array, does not get reinterpreted as UnicodeSet. This is used with vars, etc. Or pass `["str"]` for an explicit 1-element elem. * If it is a string, will be interpreted per reorder element rules. */ - static fromStrings(sections: DependencySections, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string) : ElementString { + static fromStrings(sections: DependencySections, options: StrsOptions, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string) : ElementString { // the returned array const array = new ElementString(); if(!source) { @@ -69,7 +69,7 @@ export class ElementString extends Array { } for(let i = 0; i < items.length; i++) { - let elem = new ElemElement(); + const elem = new ElemElement(); const item = items[i]; let typeFlag = 0; if (item.type === ElementType.uset) { @@ -81,21 +81,21 @@ export class ElementString extends Array { // error. So we can just exit here. return null; // UnicodeSet error } - const uset = sections.usetparser.parseUnicodeSet(item.segment, needRanges); + const uset = sections.usetparser.parseUnicodeSet(item.segment, needRanges, options?.x); if (!uset) { - return null; // UnicodeSet error already thrown + return null; // UnicodeSet error already added to callback } - elem.uset = sections.uset.allocUset(uset, sections); - elem.value = sections.strs.allocString('', {singleOk: true}); // no string + elem.uset = sections.uset.allocUset(uset, sections, options?.x); + elem.value = sections.strs.allocString('', {...options, singleOk: true}); // no string } else if (item.type === ElementType.codepoint || item.type === ElementType.escaped || item.type === ElementType.string) { // some kind of a string let str = item.segment; if (item.type === ElementType.escaped && !MATCH_HEX_ESCAPE.test(str)) { str = unescapeOneQuadString(str); // TODO-LDML: any other escape forms here? - elem.value = sections.strs.allocString(str, { singleOk: true }); + elem.value = sections.strs.allocString(str, { ...options, singleOk: true }); } else { - elem.value = sections.strs.allocString(str, { unescape: true, singleOk: true }); + elem.value = sections.strs.allocString(str, { ...options, unescape: true, singleOk: true }); } // Now did we end up with one char or no? if (elem.value.isOneChar) { diff --git a/common/web/types/src/kmx/kmx-plus/kmx-plus.ts b/common/web/types/src/kmx/kmx-plus/kmx-plus.ts index 24538c0108..373cc57b27 100644 --- a/common/web/types/src/kmx/kmx-plus/kmx-plus.ts +++ b/common/web/types/src/kmx/kmx-plus/kmx-plus.ts @@ -41,14 +41,14 @@ export class Elem extends Section { strings: ElementString[] = []; constructor(sections: DependencySections) { super(); - this.strings.push(ElementString.fromStrings(sections, '')); // C7043: null element string + this.strings.push(ElementString.fromStrings(sections, {}, '')); // C7043: null element string } /** * @param source if a string array, does not get reinterpreted as UnicodeSet. This is used with vars, etc. Or pass `["str"]` for an explicit 1-element elem. * If it is a string, will be interpreted per reorder element ruls. */ - allocElementString(sections: DependencySections, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string): ElementString { - let s = ElementString.fromStrings(sections, source, order, tertiary, tertiary_base, prebase); + allocElementString(sections: DependencySections, options: StrsOptions, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string): ElementString { + const s = ElementString.fromStrings(sections, options, source, order, tertiary, tertiary_base, prebase); if (!s) return s; let result = this.strings.find(item => item.isEqual(s)); if(result === undefined) { @@ -135,6 +135,18 @@ export class StrsItem { isEqual(a: StrsItem): boolean { return a.value === this.value && a.char === this.char; } + + private _context: any = undefined; + + /** add any context from the options to this strsitem */ + setContext(opts?: StrsOptions) { + // At present, there's only a single piece of context available + this._context = this._context || opts?.x; + } + + get context() : any { + return this._context; + } }; /** @@ -161,6 +173,8 @@ export interface StrsOptions { nfd?: boolean; /** string can be stored as a single CharStrsItem, not in strs table. */ singleOk?: boolean; + /** optional context */ + x?: any; }; export class Strs extends Section { @@ -192,6 +206,8 @@ export class Strs extends Section { result = new StrsItem(s); this.strings.push(result); } + // give an option to set the context + result.setContext(opts); return result; } @@ -200,7 +216,7 @@ export class Strs extends Section { s = s ?? ''; // type check everything else if (typeof s !== 'string') { - throw new Error('alloc_string: s must be a string, undefined, or null.'); + throw new Error(`Internal Error: processString: s must be a string, undefined, or null, not ${typeof s} ${s}`); } // substitute variables if (opts?.stringVariables) { @@ -358,11 +374,13 @@ export class Vars extends Section { export class VarsItem extends Section { id: StrsItem; value: StrsItem; + x?: any; - constructor(id: string, value: string, sections: DependencySections) { + constructor(id: string, value: string, sections: DependencySections, x?: any) { super(); this.id = sections.strs.allocString(id); this.value = sections.strs.allocString(value, {unescape: true}); + this.x = x; } valid() : boolean { @@ -371,8 +389,8 @@ export class VarsItem extends Section { }; export class UnicodeSetItem extends VarsItem { - constructor(id: string, value: string, sections: DependencySections, usetparser: UnicodeSetParser) { - super(id, value, sections); + constructor(id: string, value: string, sections: DependencySections, usetparser: UnicodeSetParser, x?: any) { + super(id, value, sections, x); const needRanges = sections.usetparser.sizeUnicodeSet(value); if (needRanges >= 0) { this.unicodeSet = sections.usetparser.parseUnicodeSet(value, needRanges); @@ -385,9 +403,9 @@ export class UnicodeSetItem extends VarsItem { }; export class SetVarItem extends VarsItem { - constructor(id: string, value: string[], sections: DependencySections) { - super(id, value.join(' '), sections); - this.items = sections.elem.allocElementString(sections, value); + constructor(id: string, value: string[], sections: DependencySections, x?: any) { + super(id, value.join(' '), sections, x); + this.items = sections.elem.allocElementString(sections, {x}, value); } items: ElementString; // element string array valid() : boolean { @@ -396,8 +414,8 @@ export class SetVarItem extends VarsItem { }; export class StringVarItem extends VarsItem { - constructor(id: string, value: string, sections: DependencySections) { - super(id, value, sections); + constructor(id: string, value: string, sections: DependencySections, x?: any) { + super(id, value, sections, x); } // no added fields }; @@ -435,7 +453,7 @@ export class Tran extends Section { }; export class UsetItem { - constructor(public uset: UnicodeSet, public str: StrsItem) { + constructor(public uset: UnicodeSet, public str: StrsItem, public x?: any) { } compareTo(other: UsetItem) : number { return this.str.compareTo(other.str); @@ -444,11 +462,11 @@ export class UsetItem { export class Uset extends Section { usets: UsetItem[] = []; - allocUset(set: UnicodeSet, sections: DependencySections) : UsetItem { + allocUset(set: UnicodeSet, sections: DependencySections, x?: any) : UsetItem { // match the same pattern let result = this.usets.find(s => set.pattern == s.uset.pattern); if (result === undefined) { - result = new UsetItem(set, sections.strs.allocString(set.pattern)); + result = new UsetItem(set, sections.strs.allocString(set.pattern), x); this.usets.push(result); } return result; @@ -546,7 +564,7 @@ export class Keys extends Section { kmap: KeysKmap[] = []; constructor(strs: Strs) { super(); - let nullFlicks = new KeysFlicks(strs.allocString('')); + const nullFlicks = new KeysFlicks(strs.allocString('')); this.flicks.push(nullFlicks); // C7043: null element string } }; diff --git a/common/web/types/src/kvk/kvk-file-reader.ts b/common/web/types/src/kvk/kvk-file-reader.ts index 6e3cff8070..64b29f48be 100644 --- a/common/web/types/src/kvk/kvk-file-reader.ts +++ b/common/web/types/src/kvk/kvk-file-reader.ts @@ -1,16 +1,15 @@ -import KVKFile, { BUILDER_KVK_FILE, BUILDER_KVK_HEADER_IDENTIFIER, BUILDER_KVK_HEADER_VERSION } from "./kvk-file.js"; +import KVKFile, { BUILDER_KVK_HEADER_IDENTIFIER, BUILDER_KVK_HEADER_VERSION } from "./kvk-file.js"; import { VisualKeyboard, VisualKeyboardKey } from "./visual-keyboard.js"; export default class KvkFileReader { public read(source: Uint8Array): VisualKeyboard { - let binary: BUILDER_KVK_FILE; - let kvk = new KVKFile(); - binary = kvk.KVK_FILE.fromBuffer(source); + const kvk = new KVKFile(); + const binary = kvk.KVK_FILE.fromBuffer(source); if(binary.header.identifier != BUILDER_KVK_HEADER_IDENTIFIER || binary.header.version != BUILDER_KVK_HEADER_VERSION) { return null; } - let result = new VisualKeyboard(); + const result = new VisualKeyboard(); result.header.version = binary.header.version; result.header.flags = binary.header.flags; result.header.associatedKeyboard = binary.header.associatedKeyboard.str; @@ -18,8 +17,8 @@ export default class KvkFileReader { result.header.ansiFont.size = binary.header.ansiFont.size; result.header.unicodeFont.name = binary.header.unicodeFont.name.str; result.header.unicodeFont.size = binary.header.unicodeFont.size; - for(let binaryKey of binary.keys) { - let key: VisualKeyboardKey = { + for(const binaryKey of binary.keys) { + const key: VisualKeyboardKey = { flags: binaryKey.flags, vkey: binaryKey.vkey, shift: binaryKey.shift, diff --git a/common/web/types/src/kvk/kvk-file-writer.ts b/common/web/types/src/kvk/kvk-file-writer.ts index a9845c0d85..75e28995be 100644 --- a/common/web/types/src/kvk/kvk-file-writer.ts +++ b/common/web/types/src/kvk/kvk-file-writer.ts @@ -50,7 +50,7 @@ export default class KvkFileWriter { this.setString(binary.header.ansiFont.name, source.header.ansiFont.name); this.setString(binary.header.unicodeFont.name, source.header.unicodeFont.name); - for(let sourceKey of source.keys) { + for(const sourceKey of source.keys) { const binaryKey: BUILDER_KVK_KEY = { flags: sourceKey.flags, vkey: sourceKey.vkey, diff --git a/common/web/types/src/ldml-keyboard/pattern-parser.ts b/common/web/types/src/ldml-keyboard/pattern-parser.ts index 1f1be6b3d9..97fef9ebc8 100644 --- a/common/web/types/src/ldml-keyboard/pattern-parser.ts +++ b/common/web/types/src/ldml-keyboard/pattern-parser.ts @@ -233,7 +233,7 @@ export class MarkerParser { } } // Then, take each codepoint (from back to front) - for (let p of [...s].reverse()) { + for (const p of [...s].reverse()) { // reverse order code units, prepend to out out = p + out; @@ -525,7 +525,7 @@ export class ElementParser { throw Error(`Unsupported: nested square brackets in element segment: ${str}`); } const list: ElementSegment[] = []; - for(let m of str.match(ElementParser.MATCH_ELEMENT_SEGMENTS)) { + for(const m of str.match(ElementParser.MATCH_ELEMENT_SEGMENTS)) { const e = new ElementSegment(m); if (e.type === ElementType.escaped) { // unescape @@ -534,7 +534,7 @@ export class ElementParser { list.push(e); } else { // need to split the escaped segment, \u{41 42} -> \u{41}, \u{42} - for (let s of unescaped) { + for (const s of unescaped) { list.push(new ElementSegment(`\\u{${s.codePointAt(0).toString(16)}}`)); } } diff --git a/common/web/types/src/ldml-keyboard/string-list.ts b/common/web/types/src/ldml-keyboard/string-list.ts index 5d11a5ca7c..2d7c49c960 100644 --- a/common/web/types/src/ldml-keyboard/string-list.ts +++ b/common/web/types/src/ldml-keyboard/string-list.ts @@ -39,7 +39,7 @@ export class ListItem extends Array implements OrderedStringList { return a; } for (const str of source) { - let index = new ListIndex(sections.strs.allocString(str, opts, sections)); + const index = new ListIndex(sections.strs.allocString(str, opts, sections)); a.push(index); } return a; diff --git a/common/web/types/src/ldml-keyboard/unicodeset-parser-api.ts b/common/web/types/src/ldml-keyboard/unicodeset-parser-api.ts index 0c890aa95a..797a5415b9 100644 --- a/common/web/types/src/ldml-keyboard/unicodeset-parser-api.ts +++ b/common/web/types/src/ldml-keyboard/unicodeset-parser-api.ts @@ -8,13 +8,13 @@ export interface UnicodeSetParser { * @param pattern string to parse such as `[a-z]` * @param rangeCount number of ranges to allow for */ - parseUnicodeSet(pattern: string, rangeCount: number) : UnicodeSet | null; + parseUnicodeSet(pattern: string, rangeCount: number, x?: any) : UnicodeSet | null; /** * Calculate the number of ranges in a UnicodeSet * @param pattern string to parse such as `[a-z]` * @returns number of ranges, or -1 (with callback-reported err) on err */ - sizeUnicodeSet(pattern: string) : number; + sizeUnicodeSet(pattern: string, x?: any) : number; } /** diff --git a/common/web/types/src/lexical-model-types.ts b/common/web/types/src/lexical-model-types.ts index 7c5cfdf643..e2670d2e21 100644 --- a/common/web/types/src/lexical-model-types.ts +++ b/common/web/types/src/lexical-model-types.ts @@ -5,18 +5,6 @@ /****************************** Lexical Models ******************************/ -/** - * A JavaScript string with the restriction that it must only - * contain Unicode scalar values. - * - * This means that any lone high surrogate must be paired with - * a low surrogate, if it exists. Lone surrogate code units are - * forbidden. - * - * See also: https://developer.mozilla.org/en-US/docs/Web/API/USVString - */ -export type USVString = string; - export type CasingForm = 'lower' | 'initial' | 'upper'; /** @@ -67,7 +55,7 @@ export interface LexiconTraversal { * - If `char` = 'e', the child represents a prefix of 'the'. * - Then `traversal` allows traversing the part of the lexicon prefixed by 'the'. */ - children(): Generator<{char: USVString, traversal: () => LexiconTraversal}>; + children(): Generator<{char: string, traversal: () => LexiconTraversal}>; /** * Allows direct access to the traversal state that results when appending one @@ -81,7 +69,7 @@ export interface LexiconTraversal { * That is, if a model "keys" `è` to `e`, there will be no `è` child. * @param char */ - child(char: USVString): LexiconTraversal | undefined; + child(char: string): LexiconTraversal | undefined; /** * Any entries directly keyed by the currently-represented lookup prefix. Entries and @@ -178,7 +166,7 @@ export interface LexicalModel { * @param text The original input text. * @returns The 'keyed' form of that text. */ - toKey?(text: USVString): USVString; + toKey?(text: string): string; /** * Generates predictive suggestions corresponding to the state of context after the proposed @@ -248,7 +236,7 @@ export interface LexicalModel { * @param context * @deprecated */ - wordbreak?(context: Context): USVString; + wordbreak?(context: Context): string; /** * Lexical models _may_ provide a LexiconTraversal object usable to enhance @@ -279,7 +267,7 @@ export interface Transform { * * Corresponds to `s` in com.keyman.KeyboardInterface.output. */ - insert: USVString; + insert: string; /** * The number of code units to delete to the left of the cursor. @@ -380,7 +368,7 @@ export interface Context { * buffer. If there is nothing to the left of the buffer, this is * an empty string. */ - readonly left: USVString; + readonly left: string; /** * Up to maxRightContextCodeUnits code units of Unicode scalar value @@ -390,7 +378,7 @@ export interface Context { * * This property may be missing entirely. */ - readonly right?: USVString; + readonly right?: string; /** * Whether the insertion point is at the start of the buffer. diff --git a/common/web/types/src/util/util.ts b/common/web/types/src/util/util.ts index 753718df8c..3001641431 100644 --- a/common/web/types/src/util/util.ts +++ b/common/web/types/src/util/util.ts @@ -9,8 +9,8 @@ export { MATCH_HEX_ESCAPE, CONTAINS_QUAD_ESCAPE, MATCH_QUAD_ESCAPE }; * @param x Name of element to box */ export function boxXmlArray(o: any, x: string): void { - if(typeof o == 'object' && !Array.isArray(o[x])) { - if(o[x] === null || o[x] === undefined) { + if (typeof o == 'object' && !Array.isArray(o[x])) { + if (o[x] === null || o[x] === undefined) { o[x] = []; } else { @@ -64,7 +64,7 @@ export function unescapeQuadString(s: string): string { * @returns */ export function unescapeString(s: string): string { - if(!s) { + if (!s) { return s; } try { @@ -74,13 +74,13 @@ export function unescapeString(s: string): string { * @param matched the entire match such as '0127' or '22 22' * @returns the unescaped match */ - function processMatch(str: string, matched: string) : string { + function processMatch(str: string, matched: string): string { const codepoints = matched.split(' '); const unescaped = codepoints.map(unescapeOne); return unescaped.join(''); } s = s.replaceAll(MATCH_HEX_ESCAPE, processMatch); - } catch(e) { + } catch (e) { if (e instanceof RangeError) { throw new UnescapeError(`Out of range while unescaping '${s}': ${e.message}`, { cause: e }); /* c8 ignore next 3 */ @@ -142,7 +142,7 @@ function regexOne(hex: string): string { /** * Escape a string (\uxxxx form) if there are any problematic codepoints */ -export function escapeStringForRegex(s: string) : string { +export function escapeStringForRegex(s: string): string { return s.split('').map(ch => escapeRegexCharIfSyntax(ch)).join(''); } @@ -152,7 +152,7 @@ export function escapeStringForRegex(s: string) : string { * @returns */ export function unescapeStringToRegex(s: string): string { - if(!s) { + if (!s) { return s; } try { @@ -162,13 +162,13 @@ export function unescapeStringToRegex(s: string): string { * @param matched the entire match such as '0127' or '22 22' * @returns the unescaped match */ - function processMatch(str: string, matched: string) : string { + function processMatch(str: string, matched: string): string { const codepoints = matched.split(' '); const unescaped = codepoints.map(regexOne); return unescaped.join(''); } s = s.replaceAll(MATCH_HEX_ESCAPE, processMatch); - } catch(e) { + } catch (e) { if (e instanceof RangeError) { throw new UnescapeError(`Out of range while unescaping '${s}': ${e.message}`, { cause: e }); /* c8 ignore next 3 */ @@ -181,19 +181,19 @@ export function unescapeStringToRegex(s: string): string { /** True if this string *could* be a UTF-32 single char */ export function -isOneChar(value: string) : boolean { + isOneChar(value: string): boolean { return [...value].length === 1; } export function -toOneChar(value: string) : number { + toOneChar(value: string): number { if (!isOneChar(value)) { throw Error(`Not a single char: ${value}`); } return value.codePointAt(0); } -export function describeCodepoint(ch : number) : string { +export function describeCodepoint(ch: number): string { let s; const p = BadStringAnalyzer.getProblem(ch); if (p != null) { @@ -207,13 +207,85 @@ export function describeCodepoint(ch : number) : string { } /** - * @brief function to convert a numeric character reference to a unicode codepoint e.g. c -> U+0063; 􏘁 -> U+10F601 - * @param instr the value that will converted - * @return a unicode codepoint if instr is a numeric character reference - * instr if instr is not a numeric character reference + * @brief function to convert a Html-Entity Hex value to a unicode character e.g. c -> c; 😎 -> 😎 + * @param hex_Entity the value that will converted + * @return a unicode character or undefined if in_str is not recognized */ +export function decodeHexEntity(hex_Entity: string) { + const hex = hex_Entity.replace('&#x', '').replace(';', ''); + if (hex_Entity.length > 9) + return undefined; + else + return String.fromCodePoint(parseInt(hex, 16)); +} + +/** + * @brief function to convert a Html-Entity decimal value value to a unicode character e.g. c -> c; 😎 -> 😎 + * @param dec_Entity the value that will converted + * @return a unicode character or undefined if in_str is not recognized + */ +export function decodeDecEntity(dec_Entity: string) { + const dec = dec_Entity.replace('&#', '').replace(';', ''); + if (dec_Entity.length > 9) + return undefined; + else + return String.fromCodePoint(parseInt(dec, 10)); +} + +/** + * @brief function to convert a unicode hex value to a unicode character e.g. U+0063 -> c; U+1F60E; -> 😎 + * @param Uni_Val the value that will converted + * @return a unicode character or undefined if in_str is not recognized + */ +export function decodeUniHexValue(Uni_Val: string): any { + const hexval = Uni_Val.replace('U+', '0x'); + if (Uni_Val.length > 8) + return undefined; + else + return String.fromCodePoint(parseInt(hexval, 16)); +} + +/** + * @brief function to convert a numeric character reference or a unicode value to a unicode character e.g. c -> c; U+1F60E -> 😎 + * @param in_str the value that will converted + * @return a unicode character or undefined if in_str is not recognized + */ +export function convertToUnicodeCharacter(in_str: string): string { + + if ((in_str === null) || (in_str === undefined)) { + return undefined; + } + + else if (in_str.substring(0, 3) === "&#x") { + return decodeHexEntity(in_str); + } + + else if (in_str.substring(0, 2) === "&#") { + return decodeDecEntity(in_str); + } + + else if (in_str.substring(0, 1) === "&") { + return undefined; + } + + else if (in_str.substring(0, 2) === "U+") { + return decodeUniHexValue(in_str); + } + + else if ( [...new Intl.Segmenter().segment(in_str)].length <= 1) { + return in_str; + } + else { + return undefined; + } +} + export function convertToUnicodeCodePoint(instr: string): string { + if ((instr === null) || (instr === undefined)) { + return undefined; + } + if (instr.substring(0, 3) === "&#x") { const num_length = instr.length - instr.indexOf("x") - 1; const num_str = instr.substring(instr.indexOf("x") + 1, instr.length - 1); @@ -251,26 +323,26 @@ const Uni_FFFE_NONCHARACTER = 0xFFFE; const Uni_PLANE_MASK = 0x1F0000; const Uni_MAX_CODEPOINT = 0x10FFFF; // plane 0, 15, and 16 PUA -const Uni_PUA_00_START = 0xE000; -const Uni_PUA_00_END = 0xF8FF; +const Uni_PUA_00_START = 0xE000; +const Uni_PUA_00_END = 0xF8FF; const Uni_PUA_15_START = 0x0F0000; -const Uni_PUA_15_END = 0x0FFFFD; +const Uni_PUA_15_END = 0x0FFFFD; const Uni_PUA_16_START = 0x100000; -const Uni_PUA_16_END = 0x10FFFD; +const Uni_PUA_16_END = 0x10FFFD; /** * @brief True if a lead surrogate * \def Uni_IsSurrogate1 */ -export function Uni_IsSurrogate1(ch : number) { +export function Uni_IsSurrogate1(ch: number) { return ((ch) >= Uni_LEAD_SURROGATE_START && (ch) <= Uni_LEAD_SURROGATE_END); } /** * @brief True if a trail surrogate * \def Uni_IsSurrogate2 */ -export function Uni_IsSurrogate2(ch : number) { +export function Uni_IsSurrogate2(ch: number) { return ((ch) >= Uni_TRAIL_SURROGATE_START && (ch) <= Uni_TRAIL_SURROGATE_END); } @@ -278,19 +350,19 @@ export function Uni_IsSurrogate2(ch : number) { * @brief True if any surrogate * \def UniIsSurrogate */ -export function Uni_IsSurrogate(ch : number) { +export function Uni_IsSurrogate(ch: number) { return (Uni_IsSurrogate1(ch) || Uni_IsSurrogate2(ch)); } -function Uni_IsEndOfPlaneNonCharacter(ch : number) { +function Uni_IsEndOfPlaneNonCharacter(ch: number) { return (((ch) & Uni_FFFE_NONCHARACTER) == Uni_FFFE_NONCHARACTER); // matches FFFF or FFFE } -function Uni_IsNoncharacter(ch : number) { +function Uni_IsNoncharacter(ch: number) { return (((ch) >= Uni_FD_NONCHARACTER_START && (ch) <= Uni_FD_NONCHARACTER_END) || Uni_IsEndOfPlaneNonCharacter(ch)); } -function Uni_InCodespace(ch : number) { +function Uni_InCodespace(ch: number) { return (ch >= 0 && ch <= Uni_MAX_CODEPOINT); }; @@ -329,8 +401,8 @@ export function isPUA(ch: number) { } /** @returns false if s is NEITHER NFC nor NFD. (Returns true for falsy) */ -export function isNormalized(s: string) : boolean { - if(!s) return true; // empty or null +export function isNormalized(s: string): boolean { + if (!s) return true; // empty or null const nfc = s.normalize("NFC"); const nfd = s.normalize("NFD"); if (s !== nfc && s !== nfd) return false; @@ -338,7 +410,7 @@ export function isNormalized(s: string) : boolean { } class BadStringMap extends Map> { - public toString() : string { + public toString(): string { if (!this.size) { return "{}"; } @@ -349,7 +421,7 @@ class BadStringMap extends Map> { /** abstract class for analyzing and categorizing strings */ export abstract class StringAnalyzer { /** add a string for analysis */ - public add(s : string) { + public add(s: string) { for (const c of [...s]) { const ch = c.codePointAt(0); const problem = this.analyzeCodePoint(c, ch); @@ -364,10 +436,10 @@ export abstract class StringAnalyzer { * @param c single codepoint to analyze (string) * @param ch single codepoint to analyze (scalar) */ - protected abstract analyzeCodePoint(c: string, ch: number) : BadStringType; + protected abstract analyzeCodePoint(c: string, ch: number): BadStringType; /** internal interface for the result of an analysis */ - protected addProblem(ch : number, type : BadStringType) { + protected addProblem(ch: number, type: BadStringType) { if (!this.m.has(type)) { this.m.set(type, new Set()); } @@ -375,7 +447,7 @@ export abstract class StringAnalyzer { } /** get the results of the analysis */ - public analyze() : BadStringMap { + public analyze(): BadStringMap { if (this.m.size == 0) { return null; } else { @@ -397,7 +469,7 @@ export class BadStringAnalyzer extends StringAnalyzer { public static getProblem(ch: number) { if (!isValidUnicode(ch)) { return BadStringType.illegal; - } else if(isPUA(ch)) { + } else if (isPUA(ch)) { return BadStringType.pua; } else { // TODO-LDML: unassigned return null; diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 49a4c2c954..3818f7022b 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -91,12 +91,12 @@ describe('Test of ElementString file', () => { }); describe('Test of fromStrings()', () => { it('returns an empty ElementString if source is null', () => { - const es = ElementString.fromStrings({}, null); + const es = ElementString.fromStrings({}, {}, null); assert.deepEqual(es, new ElementString()); }); it('can create an ElementString from a string array', () => { sections.strs.allocString = stubStrsAllocString_Char; - const actual = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); + const actual = ElementString.fromStrings(sections, {}, ["𐌰", "𐌱", "𐌲"]); const expected = [ initElemElement(GOTHIC_A), initElemElement(GOTHIC_B), @@ -106,7 +106,7 @@ describe('Test of ElementString file', () => { }); it('can create an ElementString from a string', () => { sections.strs.allocString = stubStrsAllocString_Char; - const actual = ElementString.fromStrings(sections, "𐌰𐌱𐌲"); + const actual = ElementString.fromStrings(sections, {}, "𐌰𐌱𐌲"); const expected = [ initElemElement(GOTHIC_A), initElemElement(GOTHIC_B), @@ -118,6 +118,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", "1 2 3", ); @@ -132,6 +133,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", "1", ); @@ -146,6 +148,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", "1 2", ); @@ -160,6 +163,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", "1 A 3", ); @@ -174,6 +178,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", null, "1 2 3", @@ -189,6 +194,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", null, "1", @@ -204,6 +210,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", null, "1 2", @@ -219,6 +226,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", null, "1 A 3", @@ -234,6 +242,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", null, null, @@ -250,6 +259,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", null, null, @@ -266,6 +276,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", null, null, @@ -282,6 +293,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", null, null, @@ -299,6 +311,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", null, null, @@ -316,6 +329,7 @@ describe('Test of ElementString file', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, + {}, "𐌰𐌱𐌲", null, null, @@ -333,7 +347,7 @@ describe('Test of ElementString file', () => { ElementParser.segment = stubElementParserSegment_Uset; sections.strs.allocString = stubStrsAllocString_Str; sections.uset.allocUset = stubUsetAllocUset; - const actual = ElementString.fromStrings(sections, "[𐌰-𐍊]"); + const actual = ElementString.fromStrings(sections, {}, "[𐌰-𐍊]"); const expected = [ initElemElement( new StrsItem(''), @@ -348,17 +362,17 @@ describe('Test of ElementString file', () => { it('returns null for an invalid unicode set size', () => { ElementParser.segment = stubElementParserSegment_Uset; sections.usetparser.sizeUnicodeSet = (pattern: string) : number => -1; - assert.isNull(ElementString.fromStrings(sections, "[𐌰-𐍊]")); + assert.isNull(ElementString.fromStrings(sections, {}, "[𐌰-𐍊]")); }); it('returns null if it cannot parse the unicode set', () => { ElementParser.segment = stubElementParserSegment_Uset; sections.usetparser.parseUnicodeSet = (pattern: string, rangeCount: number) : UnicodeSet | null => null; - assert.isNull(ElementString.fromStrings(sections, "[𐌰-𐍊]")); + assert.isNull(ElementString.fromStrings(sections, {}, "[𐌰-𐍊]")); }); it('can handle quad strings', () => { sections.strs.allocString = stubStrsAllocString_Char; ElementParser.segment = stubElementParserSegment_Escaped; - const actual = ElementString.fromStrings(sections, "\\ud800\\udf30"); + const actual = ElementString.fromStrings(sections, {}, "\\ud800\\udf30"); const expected = [ initElemElement(HI_GOTHIC_A), initElemElement(LO_GOTHIC_A), @@ -367,7 +381,7 @@ describe('Test of ElementString file', () => { }); it('can handle ElemElement of string type', () => { sections.strs.allocString = stubStrsAllocString_Str; - const actual = ElementString.fromStrings(sections, ["𐌰𐌱𐌲",]); + const actual = ElementString.fromStrings(sections, {}, ["𐌰𐌱𐌲",]); const expected = [ initElemElement( new StrsItem("𐌰𐌱𐌲"), diff --git a/common/web/types/tests/lexical-model-types.tests.ts b/common/web/types/tests/lexical-model-types.tests.ts index ef8bed28a5..3075582a39 100644 --- a/common/web/types/tests/lexical-model-types.tests.ts +++ b/common/web/types/tests/lexical-model-types.tests.ts @@ -7,7 +7,7 @@ import { KMXPlus, LdmlKeyboardTypes, LexicalModelTypes } from "@keymanapp/common-types"; -export let u: LexicalModelTypes.USVString; +export let u: string; export let l: LexicalModelTypes.Transform; export let s: LexicalModelTypes.Suggestion; export let st: LexicalModelTypes.SuggestionTag; diff --git a/common/windows/delphi/general/DUnitX.Loggers.TeamCity.pas b/common/windows/delphi/general/DUnitX.Loggers.TeamCity.pas index 1277d8436c..a8cec8047d 100644 --- a/common/windows/delphi/general/DUnitX.Loggers.TeamCity.pas +++ b/common/windows/delphi/general/DUnitX.Loggers.TeamCity.pas @@ -14,7 +14,7 @@ var KeymanRoot: string; ReportPath: string; begin - if GetEnvironmentVariable('TEAMCITY_VERSION') <> '' then + if GetEnvironmentVariable('TEAMCITY_GIT_PATH') <> '' then begin KeymanRoot := ExcludeTrailingPathDelimiter(GetEnvironmentVariable('KEYMAN_ROOT')); ReportPath := ExtractRelativePath(KeymanRoot, ExtractFilePath(ParamStr(0)) + 'dunitx-results.xml'); diff --git a/common/windows/delphi/packages/PackageInfo.pas b/common/windows/delphi/packages/PackageInfo.pas index 3f56db33f3..8370416e5e 100644 --- a/common/windows/delphi/packages/PackageInfo.pas +++ b/common/windows/delphi/packages/PackageInfo.pas @@ -315,14 +315,11 @@ type TPackageKeyboard = class(TPackageBaseObject) private - FName: string; FOSKFont: TPackageContentFile; FID: string; - FRTL: Boolean; FDisplayFont: TPackageContentFile; FLanguages: TPackageKeyboardLanguageList; FExamples: TPackageKeyboardExampleList; - FVersion: string; FMinKeymanVersion: string; FWebOSKFonts: TPackageContentFileReferenceList; FWebDisplayFonts: TPackageContentFileReferenceList; @@ -336,10 +333,7 @@ type constructor Create(APackage: TPackage); override; destructor Destroy; override; procedure Assign(Source: TPackageKeyboard); virtual; - property Name: string read FName write FName; property ID: string read FID write FID; - property RTL: Boolean read FRTL write FRTL; - property Version: string read FVersion write FVersion; property Languages: TPackageKeyboardLanguageList read FLanguages; property Examples: TPackageKeyboardExampleList read FExamples; property OSKFont: TPackageContentFile read FOSKFont write SetOSKFont; @@ -407,17 +401,13 @@ type TPackageLexicalModel = class(TPackageBaseObject) private - FName: string; FID: string; FLanguages: TPackageKeyboardLanguageList; - FRTL: Boolean; public constructor Create(APackage: TPackage); override; destructor Destroy; override; procedure Assign(Source: TPackageLexicalModel); virtual; - property Name: string read FName write FName; property ID: string read FID write FID; - property RTL: Boolean read FRTL write FRTL; property Languages: TPackageKeyboardLanguageList read FLanguages; end; @@ -528,10 +518,7 @@ const const SXML_PackageKeyboards = 'Keyboards'; SXML_PackageKeyboard = 'Keyboard'; - SXML_PackageKeyboard_Name = 'Name'; SXML_PackageKeyboard_ID = 'ID'; - SXML_PackageKeyboard_Version = 'Version'; - SXML_PackageKeyboard_RTL = 'RTL'; SXML_PackageKeyboard_OSKFont = 'OSKFont'; SXML_PackageKeyboard_DisplayFont = 'DisplayFont'; SXML_PackageKeyboard_Languages = 'Languages'; @@ -554,9 +541,7 @@ const SXML_PackageLexicalModels = 'LexicalModels'; SXML_PackageLexicalModel = 'LexicalModel'; - SXML_PackageLexicalModel_Name = 'Name'; SXML_PackageLexicalModel_ID = 'ID'; - SXML_PackageLexicalModel_RTL = 'RTL'; SXML_PackageLexicalModel_Languages = 'Languages'; SXML_PackageRelatedPackages = 'RelatedPackages'; @@ -620,10 +605,7 @@ const SJSON_Files_CopyLocation = 'copyLocation'; SJSON_Keyboards = 'keyboards'; - SJSON_Keyboard_Name = 'name'; SJSON_Keyboard_ID = 'id'; - SJSON_Keyboard_RTL = 'rtl'; - SJSON_Keyboard_Version = 'version'; SJSON_Keyboard_OSKFont = 'oskFont'; SJSON_Keyboard_DisplayFont = 'displayFont'; @@ -641,9 +623,7 @@ const SJSON_Keyboard_WebDisplayFonts = 'webDisplayFonts'; SJSON_LexicalModels = 'lexicalModels'; - SJSON_LexicalModel_Name = 'name'; SJSON_LexicalModel_ID = 'id'; - SJSON_LexicalModel_RTL = 'rtl'; SJSON_LexicalModel_Languages = 'languages'; SJSON_RelatedPackages = 'relatedPackages'; @@ -2023,11 +2003,8 @@ var FLanguage: TPackageKeyboardLanguage; FExample: TPackageKeyboardExample; begin - FName := Source.Name; FID := Source.ID; - FVersion := Source.Version; FMinKeymanVersion := Source.FMinKeymanVersion; - FRTL := Source.RTL; if Assigned(Source.OSKFont) then FOSKFont := Package.Files.FromFileNameEx(Source.OSKFont.FileName) else FOSKFont := nil; @@ -2186,10 +2163,7 @@ begin Break; keyboard := TPackageKeyboard.Create(Package); - keyboard.Name := AIni.ReadString(FSectionName, SXML_PackageKeyboard_Name, ''); keyboard.ID := AIni.ReadString(FSectionName, SXML_PackageKeyboard_ID, ''); - keyboard.Version := AIni.ReadString(FSectionName, SXML_PackageKeyboard_Version, '1.0'); - keyboard.RTL := AIni.ReadBool(FSectionName, SXML_PackageKeyboard_RTL, False); keyboard.OSKFont := Package.Files.FromFileNameEx(AIni.ReadString(FSectionName, SXML_PackageKeyboard_OSKFont, '')); keyboard.DisplayFont := Package.Files.FromFileNameEx(AIni.ReadString(FSectionName, SXML_PackageKeyboard_DisplayFont, '')); @@ -2231,10 +2205,7 @@ begin AKeyboard := ANode.Items[i] as TJSONObject; keyboard := TPackageKeyboard.Create(Package); - keyboard.Name := GetJsonValueString(AKeyboard, SJSON_Keyboard_Name); keyboard.ID := GetJsonValueString(AKeyboard,SJSON_Keyboard_ID); - keyboard.Version := GetJsonValueString(AKeyboard, SJSON_Keyboard_Version); - keyboard.RTL := GetJsonValueBool(AKeyboard, SJSON_Keyboard_RTL); keyboard.OSKFont := Package.Files.FromFileNameEx(GetJsonValueString(AKeyboard, SJSON_Keyboard_OSKFont)); keyboard.DisplayFont := Package.Files.FromFileNameEx(GetJsonValueString(AKeyboard, SJSON_Keyboard_DisplayFont)); keyboard.Languages.LoadJSON(AKeyboard); @@ -2266,10 +2237,7 @@ begin AKeyboard := ANode.ChildNodes[i]; keyboard := TPackageKeyboard.Create(Package); - keyboard.Name := XmlVarToStr(AKeyboard.ChildValues[SXML_PackageKeyboard_Name]); keyboard.ID := XmlVarToStr(AKeyboard.ChildValues[SXML_PackageKeyboard_ID]); - keyboard.Version := XmlVarToStr(AKeyboard.ChildValues[SXML_PackageKeyboard_Version]); - keyboard.RTL := ANode.ChildNodes.IndexOf(SXML_PackageKeyboard_RTL) >= 0; keyboard.OSKFont := Package.Files.FromFileNameEx(XmlVarToStr(AKeyboard.ChildValues[SXML_PackageKeyboard_OSKFont])); keyboard.DisplayFont := Package.Files.FromFileNameEx(XmlVarToStr(AKeyboard.ChildValues[SXML_PackageKeyboard_DisplayFont])); @@ -2300,10 +2268,7 @@ begin for i := 0 to Count-1 do begin FSectionName := 'Keyboard'+IntToStr(i); - AIni.WriteString(FSectionName, SXML_PackageKeyboard_Name, Items[i].Name); AIni.WriteString(FSectionName, SXML_PackageKeyboard_ID, Items[i].ID); - AIni.WriteString(FSectionName, SXML_PackageKeyboard_Version, Items[i].Version); - if Items[i].RTL then AIni.WriteBool(FSectionName, SXML_PackageKeyboard_RTL, True); if Assigned(Items[i].OSKFont) then AIni.WriteString(FSectionName, SXML_PackageKeyboard_OSKFont, Items[i].OSKFont.RelativeFileName); if Assigned(Items[i].DisplayFont) then @@ -2335,10 +2300,7 @@ begin AKeyboard := TJSONObject.Create; AKeyboards.Add(AKeyboard); - AKeyboard.AddPair(SJSON_Keyboard_Name, Items[i].Name); AKeyboard.AddPair(SJSON_Keyboard_ID, Items[i].ID); - AKeyboard.AddPair(SJSON_Keyboard_Version, Items[i].Version); - if Items[i].RTL then AKeyboard.AddPair(SJSON_Keyboard_RTL, TJSONTrue.Create); if Assigned(Items[i].OSKFont) then AKeyboard.AddPair(SJSON_Keyboard_OSKFont, Items[i].OSKFont.RelativeFileName); if Assigned(Items[i].DisplayFont) then @@ -2372,11 +2334,7 @@ begin begin AKeyboard := ANode.AddChild(SXML_PackageKeyboard); - AKeyboard.ChildNodes[SXML_PackageKeyboard_Name].NodeValue := Items[i].Name; AKeyboard.ChildNodes[SXML_PackageKeyboard_ID].NodeValue := Items[i].ID; - AKeyboard.ChildNodes[SXML_PackageKeyboard_Version].NodeValue := Items[i].Version; - if Items[i].RTL then - AKeyboard.ChildNodes[SXML_PackageKeyboard_RTL].NodeValue := True; if Assigned(Items[i].OSKFont) then AKeyboard.ChildNodes[SXML_PackageKeyboard_OSKFont].NodeValue := Items[i].OSKFont.RelativeFileName; if Assigned(Items[i].DisplayFont) then @@ -2420,9 +2378,7 @@ var i: Integer; FLanguage: TPackageKeyboardLanguage; begin - FName := Source.Name; FID := Source.ID; - FRTL := Source.RTL; FLanguages.Clear; for i := 0 to Source.Languages.Count - 1 do begin @@ -2487,9 +2443,7 @@ begin ALexicalModel := ANode.Items[i] as TJSONObject; lexicalModel := TPackageLexicalModel.Create(Package); - lexicalModel.Name := GetJsonValueString(ALexicalModel, SJSON_LexicalModel_Name); lexicalModel.ID := GetJsonValueString(ALexicalModel,SJSON_LexicalModel_ID); - lexicalModel.RTL := GetJsonValueBool(ALexicalModel, SJSON_LexicalModel_RTL); lexicalModel.Languages.LoadJSON(ALexicalModel); Add(lexicalModel); @@ -2510,9 +2464,7 @@ begin ALexicalModel := ANode.ChildNodes[i]; lexicalModel := TPackageLexicalModel.Create(Package); - lexicalModel.Name := XmlVarToStr(ALexicalModel.ChildValues[SXML_PackageLexicalModel_Name]); lexicalModel.ID := XmlVarToStr(ALexicalModel.ChildValues[SXML_PackageLexicalModel_ID]); - lexicalModel.RTL := ALexicalModel.ChildNodes.IndexOf(SXML_PackageLexicalModel_RTL) >= 0; lexicalModel.Languages.LoadXML(ALexicalModel); Add(lexicalModel); end; @@ -2535,9 +2487,7 @@ begin ALexicalModel := TJSONObject.Create; ALexicalModels.Add(ALexicalModel); - ALexicalModel.AddPair(SJSON_LexicalModel_Name, Items[i].Name); ALexicalModel.AddPair(SJSON_LexicalModel_ID, Items[i].ID); - if Items[i].RTL then ALexicalModel.AddPair(SJSON_LexicalModel_RTL, TJSONTrue.Create); Items[i].Languages.SaveJSON(ALexicalModel); end; end; @@ -2552,10 +2502,7 @@ begin begin ALexicalModel := ANode.AddChild(SXML_PackageLexicalModel); - ALexicalModel.ChildNodes[SXML_PackageLexicalModel_Name].NodeValue := Items[i].Name; ALexicalModel.ChildNodes[SXML_PackageLexicalModel_ID].NodeValue := Items[i].ID; - if Items[i].RTL then - ALexicalModel.ChildNodes[SXML_PackageLexicalModel_RTL].NodeValue := True; Items[i].Languages.SaveXML(ALexicalModel); end; diff --git a/core/build.sh b/core/build.sh index b97ce73515..94e8ae4e38 100755 --- a/core/build.sh +++ b/core/build.sh @@ -69,7 +69,8 @@ Libraries will be built in 'build///src'. "uninstall uninstall libraries from current system" \ "${archtargets[@]}" \ "--no-tests do not configure tests (used by other projects)" \ - "--test,-t=opt_tests test[s] to run (space separated)" + "--test,-t=opt_tests test[s] to run (space separated)" \ + "--no-werror don't report warnings as errors" builder_parse "$@" @@ -115,12 +116,15 @@ builder_describe_outputs \ build:arch /core/build/arch/$BUILDER_CONFIGURATION/src/libkeymancore.a \ build:wasm /core/build/wasm/$BUILDER_CONFIGURATION/src/libkeymancore.a -# Import our standard compiler defines; this is copied from -# /resources/build/meson/standard.meson.build by build.sh, because meson doesn't -# allow us to reference a file outside its root +MESON_ARGS=--werror +if builder_has_option --no-werror; then + MESON_ARGS= +fi + if builder_has_action configure; then - mkdir -p "$THIS_SCRIPT_PATH/resources" - cp "$KEYMAN_ROOT/resources/build/meson/standard.meson.build" "$THIS_SCRIPT_PATH/resources/meson.build" + # Import our standard compiler defines + source "$KEYMAN_ROOT/resources/build/meson/standard_meson_build.inc.sh" + standard_meson_build fi # Iterate through all possible targets; note that targets that cannot be built diff --git a/core/commands.inc.sh b/core/commands.inc.sh index f2c49d88ca..761be3db4d 100644 --- a/core/commands.inc.sh +++ b/core/commands.inc.sh @@ -55,7 +55,7 @@ do_configure() { else pushd "$THIS_SCRIPT_PATH" > /dev/null # Additional arguments are used by Linux build, e.g. -Dprefix=${INSTALLDIR} - meson setup "$MESON_PATH" $MESON_CROSS_FILE --werror --buildtype $BUILDER_CONFIGURATION $STANDARD_MESON_ARGS "${builder_extra_params[@]}" + meson setup "$MESON_PATH" $MESON_CROSS_FILE ${MESON_ARGS} --buildtype $BUILDER_CONFIGURATION $STANDARD_MESON_ARGS "${builder_extra_params[@]}" popd > /dev/null fi diff --git a/core/getversion.bat b/core/getversion.bat deleted file mode 100644 index 69cf57c27b..0000000000 --- a/core/getversion.bat +++ /dev/null @@ -1,3 +0,0 @@ -@echo off -REM Get the version number from VERSION.md -more ..\VERSION.md diff --git a/core/getversion.sh b/core/getversion.sh deleted file mode 100755 index 4d436285a3..0000000000 --- a/core/getversion.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -# Get the version number from VERSION.md -cat ../VERSION.md diff --git a/core/include/ldml/keyman_core_ldml.h b/core/include/ldml/keyman_core_ldml.h index 189b37fc96..f521cb356f 100644 --- a/core/include/ldml/keyman_core_ldml.h +++ b/core/include/ldml/keyman_core_ldml.h @@ -46,6 +46,8 @@ #define LDML_KEYS_MOD_OTHER 0x10000 #define LDML_KEYS_MOD_SHIFT 0x10 #define LDML_LAYR_LIST_HARDWARE_TOUCH "touch" +#define LDML_LAYR_MAX_MINDEVICEWIDTH 0x3E7 +#define LDML_LAYR_MIN_MINDEVICEWIDTH 0x1 #define LDML_LENGTH_BKSP 0xC #define LDML_LENGTH_BKSP_ITEM 0x10 #define LDML_LENGTH_DISP 0x10 diff --git a/core/include/ldml/keyman_core_ldml.ts b/core/include/ldml/keyman_core_ldml.ts index 093ad839c0..c975c94b71 100644 --- a/core/include/ldml/keyman_core_ldml.ts +++ b/core/include/ldml/keyman_core_ldml.ts @@ -362,6 +362,16 @@ class Constants { */ readonly length_layr_key = 4; + /** + * Minimum allowed minDeviceWidth for a layer list + */ + readonly layr_min_minDeviceWidth = 1; + + /** + * Maximum allowed minDeviceWidth for a layer list + */ + readonly layr_max_minDeviceWidth = 999; + /* ------------------------------------------------------------------ * list section ------------------------------------------------------------------ */ diff --git a/core/include/ldml/package.json b/core/include/ldml/package.json index 1e9a26c1c0..ba9acf063e 100644 --- a/core/include/ldml/package.json +++ b/core/include/ldml/package.json @@ -13,6 +13,7 @@ "license": "MIT", "type": "module", "main": "build/keyman_core_ldml.js", + "scripts": {}, "repository": { "type": "git", "url": "git+https://github.com/keymanapp/keyman.git" diff --git a/core/meson.build b/core/meson.build index 8c6cbfdc61..01e016ad27 100644 --- a/core/meson.build +++ b/core/meson.build @@ -6,7 +6,7 @@ # project('keyman_core', 'cpp', 'c', - version: run_command(find_program('getversion.bat', 'getversion.sh'), check:true).stdout().strip(), + version: files('../VERSION.md'), license: 'MIT', default_options : ['buildtype=release', 'cpp_std=c++17', diff --git a/core/tests/unit/kmx/keyboards.kpj.in b/core/tests/unit/kmx/keyboards.kpj.in new file mode 100644 index 0000000000..9d704396c4 --- /dev/null +++ b/core/tests/unit/kmx/keyboards.kpj.in @@ -0,0 +1,13 @@ + + + + $PROJECTPATH + False + False + False + keyboard + + + @keyboards_kpj@ + + diff --git a/core/tests/unit/kmx/meson.build b/core/tests/unit/kmx/meson.build index 0b10d2d4cc..22158b6a83 100644 --- a/core/tests/unit/kmx/meson.build +++ b/core/tests/unit/kmx/meson.build @@ -99,13 +99,14 @@ else cat_cmd = [find_program('cat', required: true)] endif -test_keyboards = '' +test_keyboards_kpj = '' +test_keyboards_kmp_json = '' foreach kbd : tests - if test_keyboards.startswith('{') - test_keyboards += ''', + if test_keyboards_kmp_json.startswith('{') + test_keyboards_kmp_json += ''', ''' endif - test_keyboards += '''{ + test_keyboards_kmp_json += '''{ "name": "''' + kbd + '''", "id": "''' + kbd + '''", "version": "0.0", @@ -116,16 +117,46 @@ foreach kbd : tests } ] }''' + + test_keyboards_kpj += ''' + + keyboard_'''+kbd+''' + '''+kbd+'''.kmn + '''+kbd+'''.kmn + + + package_'''+kbd+''' + '''+kbd+'''.kps + '''+kbd+'''.kps + + ''' endforeach cfg = configuration_data() -cfg.set('keyboards', test_keyboards) +cfg.set('keyboards', test_keyboards_kmp_json) +cfg.set('keyboards_kpj', test_keyboards_kpj) + +# Build a single kmp.json which lists all compiled keyboards configure_file( configuration: cfg, input: 'kmp.json.in', output: 'kmp.json' ) +# Build a single keyboards.kpj which lists all keyboard + package sources +keyboards_kpj = configure_file( + configuration: cfg, + input: 'keyboards.kpj.in', + output: 'keyboards.kpj' +) + +all_keyboards = custom_target('all_keyboards', + input: keyboards_kpj, + output: [tests[0]+'.kmp'], + depfile: 'keyboards.dep', + command: kmc_cmd + ['build', '--debug', '--no-compiler-version', '@INPUT@'] +) + foreach kbd : tests kbd_src_path = common_test_keyboards_baseline / kbd + '.kmn' @@ -151,24 +182,15 @@ foreach kbd : tests input: kbd_src_path, output: kbd + '.kmn' ) - kbd_kpj = configure_file( - configuration: cfg, - input: join_paths('template', 'keyboard.kpj.in'), - output: kbd + '.kpj' - ) kbd_kps = configure_file( configuration: cfg, input: join_paths('template', 'keyboard.kps.in'), output: kbd + '.kps' ) - kbd_kmp = custom_target(kbd + '.kmp', - input: kbd_kpj, - output: [kbd + '.kmp', kbd + '.kmx'], - depend_files: [kbd_kps, kbd_kmn], - depfile: kbd + '.dep', - command: kmc_cmd + ['build', '--debug', '--no-compiler-version', '@INPUT@']) +endforeach - test(kbd, kmx, depends: [kbd_kmp], args: [test_path / kbd + '.kmn', test_path / kbd + '.kmx']) +foreach kbd : tests + test(kbd, kmx, depends: [all_keyboards], args: [test_path / kbd + '.kmn', test_path / kbd + '.kmx']) endforeach # binary file unit tests diff --git a/core/tests/unit/ldml/keyboards/keyboards.kpj.in b/core/tests/unit/ldml/keyboards/keyboards.kpj.in new file mode 100644 index 0000000000..9d704396c4 --- /dev/null +++ b/core/tests/unit/ldml/keyboards/keyboards.kpj.in @@ -0,0 +1,13 @@ + + + + $PROJECTPATH + False + False + False + keyboard + + + @keyboards_kpj@ + + diff --git a/core/tests/unit/ldml/keyboards/meson.build b/core/tests/unit/ldml/keyboards/meson.build index 04a288710c..8e9539a3dd 100644 --- a/core/tests/unit/ldml/keyboards/meson.build +++ b/core/tests/unit/ldml/keyboards/meson.build @@ -65,13 +65,7 @@ kmc_cmd = [node, '--enable-source-maps', kmc_root] # Build all keyboards in output folder foreach kbd : tests_needing_copy - fs.copyfile(kbd + '.xml', kbd + '.xml') - - configure_file( - command: kmc_cmd + ['build', '@INPUT@', '--out-file', '@OUTPUT@'], - input: kbd + '.xml', - output: kbd + '.kmx', - ) + configure_file(input: kbd + '.xml', output: kbd + '.xml', copy: true) endforeach foreach kbd : tests_with_testdata @@ -83,15 +77,40 @@ foreach kbd : tests_with_testdata endforeach foreach kbd : tests_from_cldr - fs.copyfile(ldml_data / kbd + '.xml', kbd + '.xml') - configure_file( - command: kmc_cmd + ['build', '@INPUT@', '--out-file', '@OUTPUT@'], - input: join_paths(ldml_data, kbd + '.xml'), - output: kbd + '.kmx', - ) + configure_file(input: ldml_data / kbd + '.xml', output: kbd + '.xml', copy: true) configure_file( command: kmc_cmd + ['build', 'ldml-test-data', '@INPUT@', '--out-file', '@OUTPUT@'], input: join_paths(ldml_testdata, kbd + '-test.xml'), output: kbd + '-test.json', ) endforeach + +# Build a single keyboards.kpj which lists all keyboard sources + +test_ldml_kmx_targets = tests_needing_copy + tests_from_cldr + +test_ldml_keyboards_kpj = '' +foreach kbd : test_ldml_kmx_targets + test_ldml_keyboards_kpj += ''' + + keyboard_'''+kbd+''' + '''+kbd+'''.xml + '''+kbd+'''.xml + + ''' +endforeach + +cfg = configuration_data() +cfg.set('keyboards_kpj', test_ldml_keyboards_kpj) + +ldml_keyboards_kpj = configure_file( + configuration: cfg, + input: 'keyboards.kpj.in', + output: 'keyboards.kpj' +) + +configure_file( + input: ldml_keyboards_kpj, + output: tests_from_cldr[0]+'.kmx', + command: kmc_cmd + ['build', '@INPUT@'] +) diff --git a/core/tools/api-header-extractor/package.json b/core/tools/api-header-extractor/package.json index 73b7a77bf3..f8fb512a3a 100644 --- a/core/tools/api-header-extractor/package.json +++ b/core/tools/api-header-extractor/package.json @@ -2,5 +2,6 @@ "name": "@keymanapp/api-header-extractor", "private": true, "type": "module", + "scripts": {}, "main": "build/src/index.js" } diff --git a/core/tools/api-header-extractor/src/index.ts b/core/tools/api-header-extractor/src/index.ts index e0c6cab918..76498044df 100644 --- a/core/tools/api-header-extractor/src/index.ts +++ b/core/tools/api-header-extractor/src/index.ts @@ -1,5 +1,5 @@ -import * as fs from 'fs'; -import * as path from 'path'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; // TODO: support multiple infiles to generate from multiple headers diff --git a/developer/docs/help/context/server.md b/developer/docs/help/context/server.md index 0a04d425d3..bccf637af1 100644 --- a/developer/docs/help/context/server.md +++ b/developer/docs/help/context/server.md @@ -240,6 +240,10 @@ controlled from the **Device** menu. This will be most useful when running Server on macOS or Linux, where integration with the Keyman Developer IDE is not available. + Note that for security reasons, upload is only available when accessing + Keyman Developer Server on a localhost URL (127.0.0.1, ::1, or + ::ffff:127.0.0.1). + You can also drag and drop these same files onto the page. **Help...** diff --git a/developer/docs/internal/developer-messaging.md b/developer/docs/internal/developer-messaging.md new file mode 100644 index 0000000000..d7fe3bb9d4 --- /dev/null +++ b/developer/docs/internal/developer-messaging.md @@ -0,0 +1,140 @@ +--- +author: "Steven R. Loomis " +date: "2025-05-19" +issue: "https://github.com/keymanapp/keyman/issues/14020" +--- + +# Keyman Developer Messaging + +## Summary + +This document discusses the mechanism for kmc Compiler \+ Toolchain Messages in Keyman Developer. + +## Objectives + +1. Provide developers (end users) with feedback on the failure (or success) of their use of Keyman Developer tools. (Compilers and others: All compilers are tools, but not all tools are compilers.) +2. Enable that feedback to be actionable: + 1. Clear: so the developer knows **what** happened. + 2. Concise: so the developer can identify the relevant parts of the message without extraneous information. Enable deduplication: allow the consumer of the messages to deduplicate messages, but don't deduplicate them prematurely. + 3. Context: so the developer knows **where** the issue happened. Exactly what part of the process, which file, which line/column number. + Enable presentation of failing LOC by message consumer +3. Empower the \#developer-team of Keyman to effectively author these messages, that is, encourage deployment of messages meeting above goals + 1. Contain ceremony (repetitive content) around message call sites + 1. declutter the calling code + 2. encourage developers to make use of distinct messaging vs. 'catch-alls': "Something Bad Happened \[Here\]" + 2. Contain burden on tool/platform implementation code + 3. Single mechanism for reporting (and it's not `console.log`). + 1. we don't expect exceptions to bubble up to users, that's an exceptional \[sic\] case. + 4. [publish messages to the website](https://help.keyman.com/developer/current-version/reference/messages/) + 5. validation/ linting + 6. formal representation of parameterized messages rather than string concatenation or in-compiler message massaging code + 1. in short, string content should live in the messages, not the compiler +4. Responsibility of consumer (i.e. CLI tool, IDE) to: + 1. format + 2. display + 3. future: localize ( i18n )? + +## Details of the Current Mechanism + +### Types + +#### `CompilerEvent` + +* `filename` — string +* `offset` — number, in some cases, resolves to line:column +* `line` — number, may be set directoly +* `column` — number, may be set directly +* `code` — hex, [see below](#code) +* `message` — resolved formatted text +* `detail` — Markdown with more detail and suggestions +* `exceptionVar` — used for internal tracking of exceptions + +### Code + +1. [https://help.keyman.com/developer/current-version/reference/messages/](https://help.keyman.com/developer/current-version/reference/messages/) +2. Example: `KM03009: ERROR_UnknownWordBreaker` + +* `00503009` - hex value + * `RRSNNBBB` \- (see below) + * `RR______` \- Reserved + * `__S_____` \- Severity, example, `5` = error + * `___NN___` \- Namespace, example `03` = model compiler + * `_____BBB` \- Base error, ex `009`= "unknown word breaker" + +### Callback + +The `callbacks` object includes the function: + +* `reportMessage(e: CompilerEvent)` provided by the consumer + + We have chained/wrapper callbacks in some cases. + +### Metadata + +* The fast-xml-parser package has an option to generate metadata (via a special symbol property) on each object in the parse tree. +* This metadata includes line numbering. + +```js +interface XMLMetaData { startIndex?: number }; +``` + +* We extend this metadata concept to include symbols for import status and filename, in the case of imported files. + +## Flow + +### Call Site + +Example: keys.ts \- compiler \- call site for error + +```js + this.callbacks.reportMessage( + LdmlCompilerMessages.Error_InvalidScanCode({ + invalidCodeList: codes.join(' '), id:x?.id }, form) + ); +``` + + * `{ id, codes }` \- message parameters (optional) + * `form` \- object that caused the error (contains line number metadata) + +### Message Expansion + + Message expansion `m()` or `mx()` (in LDML) function is used. + +```js +static ERROR_InvalidScanCode = SevError | 0x0009; +static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, x: ObjectWithMetadata) => mx( + this.ERROR_InvalidScanCode, context, +Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}', + `…additional markdown detail…` +); +``` + +Parameters: + +* 'o' (Object) used for parameters- here, the object id and the codes. +* 'x' (conteXt) used for extracting the offset, which will be resolved to line numbers. + +_Q: But wait, doesn't `x.id` already have the 'id'? DRY!?_ + +_A: Yes, however, we want a clean separation between parameters and context. This way, even though repetitive, makes it clear exactly what's being passed and consumed as a parameter. `x` has other irrelevent stuff on it._ + +That is, no additional code in the \*_*() function (the caller of m(), mx() \- put helper functions outside ,move into the call site) + +### Context extraction + +The `mx()` function does the following (note that `m()` is called first). + +```js +let evt = m(code, message, detail); // raw message +evt = LdmlCompilerMessages.offset(evt, x); // with offset +``` + +Note: We could have: `ml(e:CompilerEvent, lineNumber?: number)` so that compilers with a line number could pass that in directly. + +### Callbacks + + +`reportMessage()` + +* pre-process if needed \- expand `offset` (ex: byte offset 276\) to `line:column` (line 13 column 4\) +* "end user" of compiler receives result with `messages[]` array diff --git a/developer/src/common/delphi/packages/Keyman.System.PackageInfoRefreshKeyboards.pas b/developer/src/common/delphi/packages/Keyman.System.PackageInfoRefreshKeyboards.pas index 23b7cb4664..1b32711ff1 100644 --- a/developer/src/common/delphi/packages/Keyman.System.PackageInfoRefreshKeyboards.pas +++ b/developer/src/common/delphi/packages/Keyman.System.PackageInfoRefreshKeyboards.pas @@ -120,15 +120,8 @@ begin if FillKeyboardDetails(pack.Files[i], pki) then begin - k.Name := pki.Name; k.ID := pki.ID; - k.Version := pki.Version; k.MinKeymanVersion := pki.MinKeymanVersion; - if IsKeyboardFileByName(pack.Files[i]) = ftJavascript then - // RTL flag is currently only relevant for KMW so although the - // information can be read from .kmx we only copy it over for - // js keyboards. - k.RTL := pki.RTL; end; end; end; diff --git a/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts b/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts index fad1dd646f..04e5734f18 100644 --- a/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts +++ b/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts @@ -1,5 +1,5 @@ -import * as fs from 'fs'; -import * as path from 'path'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; import { CompilerEvent, CompilerCallbacks, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerError, CompilerNetAsyncCallbacks, DefaultCompilerFileSystemAsyncCallbacks, CompilerFileSystemAsyncCallbacks, diff --git a/developer/src/common/web/utils/package.json b/developer/src/common/web/utils/package.json index 05cba553bb..089800c92f 100644 --- a/developer/src/common/web/utils/package.json +++ b/developer/src/common/web/utils/package.json @@ -10,9 +10,8 @@ ], "dependencies": { "@keymanapp/common-types": "*", - "@sentry/node": "^7.57.0", "eventemitter3": "^5.0.0", - "fast-xml-parser": "^5.0.9", + "fast-xml-parser": "^5.2.2", "path-browserify": "^1.0.1", "restructure": "^3.0.1", "sax": ">=0.6.0", @@ -26,7 +25,6 @@ "@types/semver": "^7.3.12", "c8": "^7.12.0", "git-diff": "^2.0.6", - "mocha": "^8.4.0", "typescript": "^5.4.5" }, "scripts": { diff --git a/developer/src/common/web/utils/src/compiler-callbacks.ts b/developer/src/common/web/utils/src/compiler-callbacks.ts index a7163175e8..dda2c1485b 100644 --- a/developer/src/common/web/utils/src/compiler-callbacks.ts +++ b/developer/src/common/web/utils/src/compiler-callbacks.ts @@ -7,6 +7,21 @@ import { CompilerEvent, CompilerCallbackOptions, CompilerErrorSeverity, CompilerError, CompilerMessageOverrideMap, CompilerErrorMask } from "./compiler-interfaces.js"; +/** + * The EventResolver implementation is able to expand error messages, + * particularly to change a byte offset into a line number. + */ +export interface EventResolver { + resolve(event: CompilerEvent): void; +} + +/** A do-nothing EventResolver. Implementations can use this as their default EventResolver. */ +export class NullEventResolver implements EventResolver { + resolve(event: CompilerEvent): void { + // do nothing, this is the null event resolver. + } +} + /** * Abstract interface for callbacks, to abstract out file i/o */ @@ -40,6 +55,10 @@ export interface CompilerCallbacks { */ resolveFilename(baseFilename: string, filename: string): string; + /** + * Report a message from the compiler back to the caller. + * @param event + */ reportMessage(event: CompilerEvent): void; debug(msg: string): void; @@ -106,14 +125,64 @@ export interface CompilerFileSystemAsyncCallbacks { resolveFilename(baseFilename: string, filename: string): string; } +/** a CompilerCallbacks implementation that delegates to a parent */ +export class DelegatingCompilerCallbacks implements CompilerCallbacks { + constructor(protected options: CompilerCallbackOptions, protected parent: CompilerCallbacks) { + } + + loadFile(filename: string): Uint8Array { + return this.parent.loadFile(filename); + } + + fileSize(filename: string): number { + return this.parent.fileSize(filename); + } + + isDirectory(filename: string): boolean { + return this.parent.isDirectory(filename); + } + + get path(): CompilerPathCallbacks { + return this.parent.path; + } + + get fs(): CompilerFileSystemCallbacks { + return this.parent.fs; + } + + get net(): CompilerNetAsyncCallbacks { + return this.parent.net; + } + + get fsAsync(): CompilerFileSystemAsyncCallbacks { + return this.parent.fsAsync; + } + + resolveFilename(baseFilename: string, filename: string): string { + return this.parent.resolveFilename(baseFilename, filename); + } + + debug(msg: string): void { + return this.parent.debug(msg); + } + + fileURLToPath(url: string | URL): string { + return this.parent.fileURLToPath(url); + } + + reportMessage(event: CompilerEvent) { + this.parent.reportMessage(event); + } +} /** * Wrapper class for CompilerCallbacks for a given input file */ -export class CompilerFileCallbacks implements CompilerCallbacks { +export class CompilerFileCallbacks extends DelegatingCompilerCallbacks { messages: CompilerEvent[] = []; - constructor(private filename: string, private options: CompilerCallbackOptions, private parent: CompilerCallbacks) { + constructor(private filename: string, options: CompilerCallbackOptions, parent: CompilerCallbacks) { + super(options, parent); } /** @@ -170,38 +239,6 @@ export class CompilerFileCallbacks implements CompilerCallbacks { this.messages = []; } - loadFile(filename: string): Uint8Array { - return this.parent.loadFile(filename); - } - - fileSize(filename: string): number { - return this.parent.fileSize(filename); - } - - isDirectory(filename: string): boolean { - return this.parent.isDirectory(filename); - } - - get path(): CompilerPathCallbacks { - return this.parent.path; - } - - get fs(): CompilerFileSystemCallbacks { - return this.parent.fs; - } - - get net(): CompilerNetAsyncCallbacks { - return this.parent.net; - } - - get fsAsync(): CompilerFileSystemAsyncCallbacks { - return this.parent.fsAsync; - } - - resolveFilename(baseFilename: string, filename: string): string { - return this.parent.resolveFilename(baseFilename, filename); - } - reportMessage(event: CompilerEvent): void { const disable = CompilerFileCallbacks.applyMessageOverridesToEvent(event, this.options.messageOverrides); this.messages.push(event); @@ -209,14 +246,6 @@ export class CompilerFileCallbacks implements CompilerCallbacks { this.parent.reportMessage({ filename: this.filename, ...event }); } } - - debug(msg: string): void { - return this.parent.debug(msg); - } - - fileURLToPath(url: string | URL): string { - return this.parent.fileURLToPath(url); - } } export class DefaultCompilerFileSystemAsyncCallbacks implements CompilerFileSystemAsyncCallbacks { @@ -242,3 +271,15 @@ export class DefaultCompilerFileSystemAsyncCallbacks implements CompilerFileSyst return this.owner.resolveFilename(baseFilename, filename); } } + +/** a CompilerCallbacks that applies the EventResolver to any reported message */ +export class ResolvingCompilerCallbacks extends DelegatingCompilerCallbacks { + constructor(private eventResolver: EventResolver, options: CompilerCallbackOptions, parent: CompilerCallbacks) { + super(options, parent); + } + + reportMessage(event: CompilerEvent) { + this.eventResolver.resolve(event); + this.parent.reportMessage(event); + } +} diff --git a/developer/src/common/web/utils/src/compiler-interfaces.ts b/developer/src/common/web/utils/src/compiler-interfaces.ts index e9abb40b61..443121955b 100644 --- a/developer/src/common/web/utils/src/compiler-interfaces.ts +++ b/developer/src/common/web/utils/src/compiler-interfaces.ts @@ -1,11 +1,23 @@ import { CompilerCallbacks } from "./compiler-callbacks.js"; +import { ObjectWithMetadata } from "./symbol-utils.js"; +import { KeymanXMLReader, XML_FILENAME_SYMBOL } from "./xml-utils.js"; /** * Abstract interface for compiler error and warning messages */ export interface CompilerEvent { filename?: string; + /** line where a message applies */ line?: number; + /** + * column where a message applies. + */ + column?: number; + /** + * offset where a message applies. + * If set, encompasses line and column. + */ + offset?: number; code: number; message: string; /** @@ -196,6 +208,27 @@ export class CompilerError { } return null; } + + /** + * Get an offset from o and set event's offset field + * @param event a compiler event, such as from functions in this class + * @param x any object parsed from XML or with the XML_META_DATA_SYMBOL symbol copied over + * @returns modified event object + */ + public static setFromMetadata(event: CompilerEvent, x?: ObjectWithMetadata): CompilerEvent { + if(x) { + const metadata = KeymanXMLReader.getMetaData(x) || {}; + const offset = metadata?.startIndex; + if (offset) { + event.offset = offset; + } + const filename = event.filename || metadata[XML_FILENAME_SYMBOL]; + if (filename) { + event.filename = filename; + } + } + return event; + } }; /** @deprecated use `CompilerError.severity` instead */ @@ -233,9 +266,10 @@ export enum CompilerErrorNamespace { */ LdmlKeyboardCompiler = 0x0000, /** - * common/web/types errors between 0x1000…0x1FFF + * common/web/utils errors between 0x1000…0x1FFF + * was previously called CommonTypes */ - CommonTypes = 0x1000, + DeveloperUtils = 0x1000, /** * kmc-kmn errors between 0x2000…0x2FFF; these map to * the base codes found in kmn_compiler_errors.h, exclusive severity flags @@ -416,6 +450,40 @@ export function dedentCompilerMessageDetail(event: CompilerEvent) { return (event.detail ?? '').replace(/^[ ]+/gm, ''); } +/** + * Convenience function for constructing CompilerEvents with line numbers. + * Use it as below: (abbreviated as mx()) + * + * ```js + * // Note: Indentation makes "InvalidScanCode" line up thrice + * static ERROR_InvalidScanCode = SevError | 0x0009; + * // Note: + * // 1. All parameters are passed in 'o', the context object is only used for context even if + * // it contains redundant info. + * // 2. No code execution within the arrow function other than the 'mx' call, string interpolation, + * // with `${def(o.property)}` as the max complexity of interpolation. + * static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, x: ObjectWithMetadata) => mx( + * this.ERROR_InvalidScanCode, x, + * `Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}'`, + * // Note: If detail is omitted, leave the trailing comma on the prior line to leave room for it + * `…additional markdown detail…` + * ); + * ``` + * + * @param code Unique numeric value of the event + * @param message A short description of the error presented to the user + * @param context Object to be used as a source for line number information + * @param detail Detailed Markdown-formatted description of the error + * including references to documentation, remediation options. + * @see CompilerMessageSpec + * @returns the event + */ +export function CompilerMessageObjectSpec(code: number, context: ObjectWithMetadata, message: string, detail?: string): CompilerEvent { + let evt = CompilerMessageSpec(code, message, detail); // constructs raw message + evt = CompilerError.setFromMetadata(evt, context); // updates with offset from context + return evt; +}; + export const CompilerMessageDef = (param: any) => String(param ?? ``); export const CompilerMessageSpecWithException = (code: number, message: string, exceptionVar: any, detail?: string) : CompilerEvent => ({ diff --git a/developer/src/common/web/utils/src/common-messages.ts b/developer/src/common/web/utils/src/developer-utils-messages.ts similarity index 71% rename from developer/src/common/web/utils/src/common-messages.ts rename to developer/src/common/web/utils/src/developer-utils-messages.ts index 6eca08d37c..6fd328802b 100644 --- a/developer/src/common/web/utils/src/common-messages.ts +++ b/developer/src/common/web/utils/src/developer-utils-messages.ts @@ -4,14 +4,14 @@ import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m } from './compiler-interfaces.js'; import { constants } from '@keymanapp/ldml-keyboard-constants'; -const CommonTypesErrMask = CompilerErrorNamespace.CommonTypes; -// const SevInfo = CompilerErrorSeverity.Info | CommonTypesErrMask; -// const SevHint = CompilerErrorSeverity.Hint | CommonTypesErrMask; -// const SevWarn = CompilerErrorSeverity.Warn | CommonTypesErrMask; -const SevError = CompilerErrorSeverity.Error | CommonTypesErrMask; -// const SevFatal = CompilerErrorSeverity.Fatal | CommonTypesErrMask; +const DeveloperUtilsErrMask = CompilerErrorNamespace.DeveloperUtils; +// const SevInfo = CompilerErrorSeverity.Info | DeveloperUtilsErrMask; +// const SevHint = CompilerErrorSeverity.Hint | DeveloperUtilsErrMask; +// const SevWarn = CompilerErrorSeverity.Warn | DeveloperUtilsErrMask; +const SevError = CompilerErrorSeverity.Error | DeveloperUtilsErrMask; +// const SevFatal = CompilerErrorSeverity.Fatal | DeveloperUtilsErrMask; -export class CommonTypesMessages { +export class DeveloperUtilsMessages { // structured Ajv validation error static ERROR_SchemaValidationError = SevError | 0x0001; static Error_SchemaValidationError = (o:{instancePath:string, keyword:string, message: string, params: string}) => m(this.ERROR_SchemaValidationError, @@ -58,4 +58,22 @@ export class CommonTypesMessages { this.ERROR_InvalidPackageFile, `Package source file is invalid: ${(o.e ?? 'unknown error').toString()}` ); + + static ERROR_InvalidProjectFile = SevError | 0x000A; + static Error_InvalidProjectFile = (o:{message:string}) => m( + this.ERROR_InvalidProjectFile, + `Project file is not valid: ${def(o.message)}`, + ); + + static ERROR_UnsupportedProjectVersion = SevError | 0x000B; + static Error_UnsupportedProjectVersion = (o:{version:string}) => m( + this.ERROR_UnsupportedProjectVersion, + `Project version ${def(o.version)} is not supported by this version of Keyman Developer.`, + ); + + static ERROR_ProjectFileCouldNotBeRead = SevError | 0x000C; + static Error_ProjectFileCouldNotBeRead = () => m( + this.ERROR_ProjectFileCouldNotBeRead, + `Project file could not be read` + ); }; diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index f3e23e79f2..c39afa8bff 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -3,8 +3,6 @@ */ export { validateMITLicense } from './utils/validate-mit-license.js'; -export { KeymanSentry, SentryNodeOptions } from './utils/KeymanSentry.js'; -export { getOption, loadOptions, clearOptions } from './utils/options.js'; export { escapeMarkdownChar } from './utils/markdown.js'; export { KeymanUrls } from './utils/keyman-urls.js'; @@ -42,11 +40,15 @@ export { CompilerPathCallbacks, CompilerFileSystemCallbacksFolderEntry as FileSystemFolderEntry, DefaultCompilerFileSystemAsyncCallbacks, + EventResolver, + NullEventResolver, + DelegatingCompilerCallbacks, + ResolvingCompilerCallbacks, } from './compiler-callbacks.js'; export { defaultCompilerOptions, CompilerBaseOptions, CompilerOptions, CompilerEvent, CompilerErrorNamespace, CompilerErrorSeverity, CompilerCallbackOptions, - CompilerError, CompilerMessageSpec, CompilerMessageSpecWithException, compilerErrorSeverity, CompilerErrorMask, compilerErrorSeverityName, + CompilerError, CompilerMessageSpec, CompilerMessageObjectSpec, CompilerMessageSpecWithException, compilerErrorSeverity, CompilerErrorMask, compilerErrorSeverityName, compilerErrorFormatCode, CompilerMessageDef, compilerLogLevelToSeverity, CompilerLogLevel, compilerEventFormat, ALL_COMPILER_LOG_LEVELS, ALL_COMPILER_LOG_FORMATS, CompilerLogFormat, @@ -63,13 +65,16 @@ export { defaultCompilerOptions, CompilerBaseOptions, CompilerOptions, CompilerE export { UrlSubpathCompilerCallback } from './utils/UrlSubpathCompilerCallback.js'; -export { CommonTypesMessages } from './common-messages.js'; +export { DeveloperUtilsMessages } from './developer-utils-messages.js'; export * as SourceFilenamePatterns from './source-filename-patterns.js'; -export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js'; - +export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader, KeymanXMLMetadata, XML_FILENAME_SYMBOL } from './xml-utils.js'; +export { SymbolUtils, ObjectWithMetadata } from './symbol-utils.js'; +export * as LineUtils from './line-utils.js'; export * as GitHubUrls from './github-urls.js'; export * as CloudUrls from './cloud-urls.js'; export { getFontFamily, getFontFamilySync } from './font-family.js'; export * as ValidIds from './valid-ids.js'; + +export * as ProjectLoader from './project-loader.js'; diff --git a/developer/src/common/web/utils/src/line-utils.ts b/developer/src/common/web/utils/src/line-utils.ts new file mode 100644 index 0000000000..2e1cced292 --- /dev/null +++ b/developer/src/common/web/utils/src/line-utils.ts @@ -0,0 +1,124 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by srl on 2025-04-03 + * + * Abstraction for line number processing + */ + +import { CompilerEvent, EventResolver } from "./index.js"; + +/** line number with an optional column number */ +export interface LineColumn { + line: number; + column?: number; +} + +export type LineList = number[]; + +/** a LineFinder is able to convert from an offset to a line/column */ +export class LineFinder { + private list: LineList; + constructor(public text: string) { + } + + public getLineList(): LineList { + if (!this.list) { + this.list = LineFinder.textToLines(this.text); + } + return this.list; + } + + /** + * Convert an offset into line:column + * @param offset input offset into the text + * @returns line:column information + */ + public findOffset(offset: number): LineColumn { + return LineFinder.offsetToLineColumn(offset, this.getLineList()); + } + + /** + * Preprocess text to turn it into arrays of line lengths. + * This is effectively a 1-based line length, since line 0 has length of + * 0. + * Note that the fast-xml-parser converts \r\n to \n before processing, + * and this function does as well. So, any offsets passed in will need to + * have that taken into consideration. + */ + public static textToLines(text: string): LineList { + return [ + 0, // "line 0" is empty + ...text.replaceAll("\r\n", "\n").split("\n") + .map(l => l.length + 1) // line length (counting the trailing newline) + ]; + } + + /** + * convert a line number array to a line/column. + * Note that the fast-xml-parser converts \r\n to \n before processing, + * and this function does as well. So, any offsets passed in will need to + * have that taken into consideration. + */ + public static offsetToLineColumn(offset: number, list: LineList): LineColumn { + for (let line = 1; line < list.length; line++) { // 1-based (assume the first row is 0) + if (list[line] > offset) { + return { line, column: offset }; + } + offset = offset - (list[line]); // count newline at end + } + // default: line 0, error + return { line: 0 } + } +} + +/** + * Interface for a class which can receive file contents, + * keyed by the filename + */ +export interface FileConsumer { + /** + * @param filename name of the file + * @param contents string contents of the file + */ + addFile(filename: string, contents: string): void; +} + +/** Cache of LineFinder elements, organized by filename */ +export class LineFinderCache implements FileConsumer { + contentsCache: Map = new Map(); + + /** add or update a source file */ + addFile(filename: string, contents: string): void { + this.contentsCache.set(filename, new LineFinder(contents)); + } + + getByFilename(filename: string): LineFinder | undefined { + const lf = this.contentsCache.get(filename); + return lf; + } +} + +/** + * This EventResolver expands events that have a filename and offset set, + * by looking up the event.offset in the LineFinder list. + * The expanded event has the event.line and event.column fields set. + */ +export class LineFinderEventResolver implements EventResolver, FileConsumer { + private lfcache = new LineFinderCache(); + + addFile(filename: string, contents: string): void { + this.lfcache.addFile(filename, contents); + } + + /** resolve a CompilerEvent by expanding the line numbers */ + resolve(event: CompilerEvent) { + if (event.offset && !event.line && event.filename) { + const lf = this.lfcache.getByFilename(event.filename); + const loc = lf.findOffset(event.offset); + event.line = loc.line; + event.column = loc.column; + } + return event; + } +} diff --git a/developer/src/common/web/utils/src/project-loader.ts b/developer/src/common/web/utils/src/project-loader.ts new file mode 100644 index 0000000000..a62545e877 --- /dev/null +++ b/developer/src/common/web/utils/src/project-loader.ts @@ -0,0 +1,34 @@ +import { KeymanDeveloperProject } from "./types/kpj/keyman-developer-project.js"; +import { KPJFileReader } from "./types/kpj/kpj-file-reader.js"; +import { CompilerAsyncCallbacks, CompilerCallbacks } from "./compiler-callbacks.js"; +import { DeveloperUtilsMessages } from "./developer-utils-messages.js"; + +// TODO-KMC-TEST: refactor asyncCallbacks, make it a first-class part of CompilerCallbacks +export async function loadProjectFromFile(infile: string, callbacks: CompilerCallbacks, asyncCallbacks: CompilerAsyncCallbacks = null): Promise { + const kpjData = await (asyncCallbacks ?? callbacks).fsAsync.readFile(infile); + if(!kpjData) { + callbacks.reportMessage(DeveloperUtilsMessages.Error_ProjectFileCouldNotBeRead()); + return null; + } + const reader = new KPJFileReader(callbacks); + let kpj = null; + try { + kpj = reader.read(kpjData); + if(!kpj) { + callbacks.reportMessage(DeveloperUtilsMessages.Error_ProjectFileCouldNotBeRead()); + return null; + } + + if(kpj.KeymanDeveloperProject?.Options?.Version && kpj.KeymanDeveloperProject.Options.Version != "1.0" && kpj.KeymanDeveloperProject.Options.Version != "2.0") { + callbacks.reportMessage(DeveloperUtilsMessages.Error_UnsupportedProjectVersion({version: kpj.KeymanDeveloperProject.Options.Version})); + return null; + } + reader.validate(kpj); + } catch(e) { + callbacks.reportMessage(DeveloperUtilsMessages.Error_InvalidProjectFile({message: (e??'').toString()})); + return null; + } + const project = await reader.transform(infile, kpj); + return project; +} + diff --git a/developer/src/common/web/utils/src/symbol-utils.ts b/developer/src/common/web/utils/src/symbol-utils.ts new file mode 100644 index 0000000000..9404b404d8 --- /dev/null +++ b/developer/src/common/web/utils/src/symbol-utils.ts @@ -0,0 +1,41 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by srl on 2025-03-27 + * + * Utilities for manipulating Symbol properties + */ + + +/** + * Any object with metadata, for example for line number errs. + * Defined as 'any' here to reduce noise on the client side. + * @see {@link KeymanXMLReader.getMetaData()} + */ +export type ObjectWithMetadata = any; + +export class SymbolUtils { + /** + * Copy symbols shallowly from 'from' onto 'onto' + * @param onto object to copy onto + * @param from source for symbols + * @returns the onto object + */ + public static copySymbols(onto: T, from: ObjectWithMetadata): T { + const o = onto as ObjectWithMetadata; + for (const sym of Object.getOwnPropertySymbols(from)) { + o[sym] = from[sym]; + } + return onto; + } + + /** use Object.entries to remove all symbols, recursively. */ + public static removeSymbols(from: T): T { + if (Array.isArray(from)) { + return from.map(o => SymbolUtils.removeSymbols(o)) as T; + } + if (typeof from !== "object") return from; + return Object.fromEntries(Object.entries(from).map(([k, v]) => ([k, SymbolUtils.removeSymbols(v)]))) as T; + } + +} diff --git a/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts b/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts index 73f2280ebc..c41a30ebb4 100644 --- a/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts +++ b/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts @@ -9,7 +9,7 @@ import { util } from '@keymanapp/common-types'; import boxXmlArray = util.boxXmlArray; -import { CommonTypesMessages } from "../../common-messages.js"; +import { DeveloperUtilsMessages } from "../../developer-utils-messages.js"; import { CompilerCallbacks } from "../../compiler-callbacks.js"; import { KeymanXMLReader } from "../../xml-utils.js"; import { KpsPackage } from "./kps-file.js"; @@ -29,7 +29,7 @@ export class KpsFileReader { a = new KeymanXMLReader('kps') .parse(data) as KpsPackage; } catch(e) { - this.callbacks.reportMessage(CommonTypesMessages.Error_InvalidPackageFile({e})); + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_InvalidPackageFile({e})); } return a; })(); diff --git a/developer/src/common/web/utils/src/types/kps/kps-file.ts b/developer/src/common/web/utils/src/types/kps/kps-file.ts index aac583e11f..f5fadcf299 100644 --- a/developer/src/common/web/utils/src/types/kps/kps-file.ts +++ b/developer/src/common/web/utils/src/types/kps/kps-file.ts @@ -80,8 +80,9 @@ export interface KpsFileContentFile { } export interface KpsFileLexicalModel { - Name: string; + // Defined in .kps schema but not used: Name: string; #13600(comment) ID: string; + // Defined in .kps schema but not used: RTL: string; #13600(comment) Languages: KpsFileLanguages; } @@ -113,12 +114,12 @@ export interface KpsFileRelatedPackage { } export interface KpsFileKeyboard { - Name: string; /// the descriptive name of the keyboard + // Defined in .kps schema but not used: Name: string; #13593 ID: string; /// the keyboard identifier, equal to the basename of the keyboard file sans extension - Version: string; + // Defined in .kps schema but not used: Version: string; #13593 OSKFont?: string; DisplayFont?: string; - RTL?: string; + // Defined in .kps schema but not used: RTL?: string; #13593 Languages?: KpsFileLanguages; Examples?: KpsFileLanguageExamples; /** diff --git a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts index 0a5abf6355..0a31bc051b 100644 --- a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -4,13 +4,15 @@ * Reads a LDML XML keyboard file into JS object tree and resolves imports */ import { SchemaValidators, util } from '@keymanapp/common-types'; -import { CommonTypesMessages } from '../../common-messages.js'; -import { CompilerCallbacks } from "../../compiler-callbacks.js"; +import { DeveloperUtilsMessages } from '../../developer-utils-messages.js'; +import { CompilerCallbacks, EventResolver } from "../../compiler-callbacks.js"; +import { CompilerEvent } from "../../compiler-interfaces.js"; import { LDMLKeyboardXMLSourceFile, LKImport, ImportStatus } from './ldml-keyboard-xml.js'; import { constants } from '@keymanapp/ldml-keyboard-constants'; import { LDMLKeyboardTestDataXMLSourceFile, LKTTest, LKTTests } from './ldml-keyboard-testdata-xml.js'; -import { KeymanXMLReader } from '@keymanapp/developer-utils'; import boxXmlArray = util.boxXmlArray; +import { LineFinderEventResolver } from '../../line-utils.js'; +import { XML_FILENAME_SYMBOL, KeymanXMLReader } from '../../xml-utils.js'; interface NameAndProps { '$'?: any; // content @@ -25,28 +27,44 @@ export class LDMLKeyboardXMLSourceFileReaderOptions { localImportsPaths: string[]; }; -export class LDMLKeyboardXMLSourceFileReader { +export class LDMLKeyboardXMLSourceFileReader implements EventResolver { + /** for resolving messages involving line numbers */ + private eventResolver: LineFinderEventResolver = new LineFinderEventResolver(); + constructor(private options: LDMLKeyboardXMLSourceFileReaderOptions, private callbacks : CompilerCallbacks) { } + resolve(event: CompilerEvent): void { + this.eventResolver.resolve(event); + } static get defaultImportsURL(): [string,string] { return ['../import/', import.meta.url]; } - readImportFile(version: string, subpath: string): Uint8Array { - const importPath = this.callbacks.resolveFilename(this.options.cldrImportsPath, `${version}/${subpath}`); - return this.callbacks.loadFile(importPath); + /** bottleneck for reading keyboard XML files */ + readFile(path: string): Uint8Array { + const data = this.callbacks.loadFile(path); + if (data) { + this.eventResolver.addFile(path, new TextDecoder().decode(data)); + } + return data; } - readLocalImportFile(path: string): Uint8Array { + /** @returns [data, filename] */ + readImportFile(version: string, subpath: string): [Uint8Array, string] { + const importPath = this.callbacks.resolveFilename(this.options.cldrImportsPath, `${version}/${subpath}`); + return [this.readFile(importPath), importPath]; + } + + readLocalImportFile(path: string): [Uint8Array, string] { // try each of the local imports paths for (const localPath of this.options.localImportsPaths) { const importPath = this.callbacks.path.join(localPath, path); if(this.callbacks.fs.existsSync(importPath)) { - return this.callbacks.loadFile(importPath); + return [this.readFile(importPath), importPath]; } } - return null; // was not able to load from any of the paths + return [null, null]; // was not able to load from any of the paths } /** @@ -219,37 +237,39 @@ export class LDMLKeyboardXMLSourceFileReader { const { base, path } = asImport; // If base is not an empty string (or null/undefined), then it must be 'cldr' if (base && base !== constants.cldr_import_base) { - this.callbacks.reportMessage(CommonTypesMessages.Error_ImportInvalidBase({base, path, subtag})); + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_ImportInvalidBase({base, path, subtag})); return false; } let importData: Uint8Array; + let importPath: string; if (base === constants.cldr_import_base) { // CLDR import const paths = path.split('/'); if (paths[0] == '' || paths[1] == '' || paths.length !== 2) { - this.callbacks.reportMessage(CommonTypesMessages.Error_ImportInvalidPath({base, path, subtag})); + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_ImportInvalidPath({base, path, subtag})); return false; } if (constants.treatAsLatest(paths[0])) { /** There's no data or DTD change in 45, 46, 46.1, 47 so map them all to 46 at present. */ paths[0] = constants.cldr_version_latest; } - importData = this.readImportFile(paths[0], paths[1]); + [importData, importPath] = this.readImportFile(paths[0], paths[1]); } else { // local import - importData = this.readLocalImportFile(path); + [importData, importPath] = this.readLocalImportFile(path); } if (!importData || !importData.length) { - this.callbacks.reportMessage(CommonTypesMessages.Error_ImportReadFail({base, path, subtag})); + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_ImportReadFail({base, path, subtag})); return false; } const importXml: any = this.loadUnboxed(importData); // TODO-LDML: have to load as any because it is an arbitrary part const importRootNode = importXml[subtag]; // e.g. + this.eventResolver.addFile(importPath, new TextDecoder().decode(importData)); // TODO: double decode // importXml will have one property: the root element. if (!importRootNode) { - this.callbacks.reportMessage(CommonTypesMessages.Error_ImportWrongRoot({base, path, subtag})); + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_ImportWrongRoot({base, path, subtag})); return false; } // pull all children of importXml[subtag] into obj @@ -259,11 +279,15 @@ export class LDMLKeyboardXMLSourceFileReader { if (!Array.isArray(subsubval)) { // This is somewhat of an internal error, indicating that a non-mergeable XML file was imported // Not exercisable with the standard LDML imports. - this.callbacks.reportMessage(CommonTypesMessages.Error_ImportMergeFail({base, path, subtag, subsubtag})); + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_ImportMergeFail({base, path, subtag, subsubtag})); return false; } // Mark all children as an import - subsubval.forEach(o => o[ImportStatus.import] = basePath); + subsubval.forEach(o => { + o[ImportStatus.import] = basePath; + KeymanXMLReader.setMetaData(o, {[XML_FILENAME_SYMBOL as any]: importPath}); // mark overriding import path + }); + if (implied) { // mark all children as an implied import subsubval.forEach(o => o[ImportStatus.impliedImport] = basePath); @@ -286,7 +310,7 @@ export class LDMLKeyboardXMLSourceFileReader { public validate(source: LDMLKeyboardXMLSourceFile | LDMLKeyboardTestDataXMLSourceFile): boolean { if(!SchemaValidators.default.ldmlKeyboard3(source)) { for (const err of (SchemaValidators.default.ldmlKeyboard3).errors) { - this.callbacks.reportMessage(CommonTypesMessages.Error_SchemaValidationError({ + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_SchemaValidationError({ instancePath: err.instancePath, keyword: err.keyword, message: err.message || 'Unknown AJV Error', // docs say 'message' is optional if 'messages:false' in options @@ -318,7 +342,7 @@ export class LDMLKeyboardXMLSourceFileReader { try { source = this.loadUnboxed(file); } catch(e) { - this.callbacks.reportMessage(CommonTypesMessages.Error_InvalidXml({e})); + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_InvalidXml({e})); return null; } @@ -359,7 +383,7 @@ export class LDMLKeyboardXMLSourceFileReader { } else if (r.length === 1) { return r[0]; } else { - this.callbacks.reportMessage(CommonTypesMessages.Error_TestDataUnexpectedArray({subtag})); + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_TestDataUnexpectedArray({subtag})); return null; // ERROR } } diff --git a/developer/src/common/web/utils/src/xml-utils.ts b/developer/src/common/web/utils/src/xml-utils.ts index f44e29bb30..77d4f43d22 100644 --- a/developer/src/common/web/utils/src/xml-utils.ts +++ b/developer/src/common/web/utils/src/xml-utils.ts @@ -6,10 +6,16 @@ * Abstraction for XML reading and writing */ -import { XMLParser, XMLBuilder, XmlBuilderOptions, X2jOptions } from 'fast-xml-parser'; +import { XMLParser, XMLBuilder, XMLMetaData, X2jOptions, XmlBuilderOptions } from 'fast-xml-parser'; +import { SymbolUtils } from "./symbol-utils.js"; + +/** Symbol giving the start offset, in chars, of the node */ +const XML_META_DATA_SYMBOL = XMLParser.getMetaDataSymbol(); +/** Symbol giving an override which file a node came from */ +export const XML_FILENAME_SYMBOL = Symbol("XML Filename"); export type KeymanXMLType = - 'keyboard3' // LDML + 'keyboard3' // LDML | 'keyboardTest3' // LDML | 'keylayout' // keylayout | 'kps' // @@ -49,6 +55,7 @@ const PARSER_OPTIONS: KeymanXMLParserOptionsBag = { // if we do need elements in the future, we'd check the preserve-space attribute here. return tagValue?.trim(); }, + captureMetaData: true, trimValues: false, // preserve spaces, but see tagValueProcessor }, 'keyboardTest3': { @@ -122,11 +129,54 @@ const GENERATOR_OPTIONS: KeymanXMLGeneratorOptionsBag = { }, }; +export interface KeymanXMLMetadata extends XMLMetaData { + /** override of name of XML file */ + [XML_FILENAME_SYMBOL]?: string; +} + /** wrapper for XML parsing support */ export class KeymanXMLReader { public constructor(public type: KeymanXMLType) { } + /** Get metadata on a node if not already set */ + static getMetaData(o: any) : KeymanXMLMetadata { + if(!o) return o; + const metadata : KeymanXMLMetadata = o[XML_META_DATA_SYMBOL as any]; + return metadata; + } + + /** Set metadata if not already set */ + public static setMetaData(o: any, metadata: KeymanXMLMetadata) : KeymanXMLMetadata { + let m : KeymanXMLMetadata = KeymanXMLReader.getMetaData(o); + if (!m) { + m = {}; + } + // copy non-symbols + m = {...metadata, ...m}; + // copy symbols + SymbolUtils.copySymbols(m, metadata); + o[XML_META_DATA_SYMBOL as any] = m; + return m; + } + + /** set metadata on this and children with the default filename - if not already set */ + public static setDefaultFilename(data: any, filename: string) { + if (!data || !filename) return; + if (typeof data === 'object') { + const m = KeymanXMLReader.getMetaData(data) || {}; + if (!m[XML_FILENAME_SYMBOL]) { + (m as any)[XML_FILENAME_SYMBOL] = filename; + KeymanXMLReader.setMetaData(data, m); + } + if (Array.isArray(data)) { + data.forEach(e => KeymanXMLReader.setDefaultFilename(e, filename)); + } else for(const k of Object.keys(data)) { + KeymanXMLReader.setDefaultFilename(data[k], filename); + } + } + } + /** move `{ $abc: 4 }` into `{ $: { abc: 4 } }` */ private static fixupDollarAttributes(data: any) : any { if (typeof data === 'object') { @@ -145,9 +195,9 @@ export class KeymanXMLReader { } }); if (attrs.length) { - e.push(['$', Object.fromEntries(attrs)]); + e.push(['$', SymbolUtils.copySymbols(Object.fromEntries(attrs), data)]); } - return Object.fromEntries(e); + return SymbolUtils.copySymbols(Object.fromEntries(e), data); } else { return data; } @@ -183,7 +233,7 @@ export class KeymanXMLReader { } } }); - return Object.fromEntries(e); + return SymbolUtils.copySymbols(Object.fromEntries(e), data); } else { return data; } @@ -306,12 +356,11 @@ export class KeymanXMLReader { } public parser() { - let options = PARSER_OPTIONS[this.type]; + const options = PARSER_OPTIONS[this.type]; if (!options) { /* c8 ignore next 1 */ throw Error(`Internal error: unhandled XML type ${this.type}`); } - options = Object.assign({}, options); // TODO: xml2js likes to mutate the options here. Shallow clone the object. return new XMLParser(options); } } diff --git a/developer/src/common/web/utils/test/helpers/index.ts b/developer/src/common/web/utils/test/helpers/index.ts index e36d07429b..25fd6856c7 100644 --- a/developer/src/common/web/utils/test/helpers/index.ts +++ b/developer/src/common/web/utils/test/helpers/index.ts @@ -1,9 +1,9 @@ /** * Helpers and utilities for the Mocha tests. */ -import * as path from 'path'; -import * as fs from "fs"; -import { fileURLToPath } from 'url'; +import * as path from 'node:path'; +import * as fs from "node:fs"; +import { fileURLToPath } from 'node:url'; /** * Builds a path to the fixture with the given path components. diff --git a/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts b/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts index 700d223406..d1a7cdddb1 100644 --- a/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts +++ b/developer/src/common/web/utils/test/kpj/kpj-file-writer.tests.ts @@ -6,6 +6,7 @@ import { KPJFileReader } from "../../src/types/kpj/kpj-file-reader.js"; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { KPJFileWriter } from '../../src/types/kpj/kpj-file-writer.js'; import { KeymanDeveloperProjectOptions } from '../../src/types/kpj/keyman-developer-project.js'; +import { SymbolUtils } from '../../src/symbol-utils.js'; const callbacks = new TestCompilerCallbacks(); @@ -21,12 +22,12 @@ describe('kpj-file-writer', function () { const writer = new KPJFileWriter(); const output = writer.write(project); - const outputKpj = reader.read(new TextEncoder().encode(output)); + // Remove XML metadata symbols to reduce clutter for testing purposes + const outputKpj = SymbolUtils.removeSymbols(reader.read(new TextEncoder().encode(output))); // The outputKpj may not contain all the fields from the inputKpj, only the // essential fields. Many of the fields in .kpj are deprecated, when they // relate to file content (e.g. parented files, file details) - assert.deepEqual(outputKpj.KeymanDeveloperProject.Options, { "BuildPath": "$PROJECTPATH\\build", "CompilerWarningsAsErrors": "True", @@ -61,7 +62,8 @@ describe('kpj-file-writer', function () { const writer = new KPJFileWriter(); const output = writer.write(project); - const outputKpj = reader.read(new TextEncoder().encode(output)); + // Remove XML metadata symbols to reduce clutter for testing purposes + const outputKpj = SymbolUtils.removeSymbols(reader.read(new TextEncoder().encode(output))); // The outputKpj may not contain all the fields from the inputKpj, only the // essential fields. Many of the fields in .kpj are deprecated, when they diff --git a/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts b/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts index 1b809f39d0..117a48a281 100644 --- a/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts +++ b/developer/src/common/web/utils/test/kps/kps-file-reader.tests.ts @@ -14,7 +14,8 @@ import { makePathToFixture } from '../helpers/index.js'; import { KpsFileReader } from "../../src/types/kps/kps-file-reader.js"; import { KpsFileWriter } from '../../src/types/kps/kps-file-writer.js'; -import { CommonTypesMessages } from '../../src/common-messages.js'; +import { SymbolUtils } from '../../src/symbol-utils.js'; +import { DeveloperUtilsMessages } from '../../src/developer-utils-messages.js'; const callbacks = new TestCompilerCallbacks(); @@ -47,8 +48,6 @@ describe('kps-file-reader', function () { assert.lengthOf(kps.Package.Keyboards.Keyboard, 1); assert.equal(kps.Package.Keyboards.Keyboard[0].ID, 'khmer_angkor'); - assert.equal(kps.Package.Keyboards.Keyboard[0].Name, 'Khmer Angkor'); - assert.equal(kps.Package.Keyboards.Keyboard[0].Version, '1.3'); assert.lengthOf(kps.Package.Keyboards.Keyboard[0].Languages.Language, 1); assert.equal(kps.Package.Keyboards.Keyboard[0].Languages.Language[0]._, 'Central Khmer (Khmer, Cambodia)'); @@ -61,13 +60,15 @@ describe('kps-file-reader', function () { it('kps-file-reader should round-trip with kps-file-writer', function() { const input = fs.readFileSync(makePathToFixture('kps', 'khmer_angkor.kps')); const reader = new KpsFileReader(callbacks); - const kps = reader.read(input); + // Remove XML metadata symbols to reduce clutter for testing purposes + const kps = SymbolUtils.removeSymbols(reader.read(input)); const writer = new KpsFileWriter(); const output = writer.write(kps); // Round Trip - const kps2 = reader.read(new TextEncoder().encode(output)); + // Remove XML metadata symbols to reduce clutter for testing purposes + const kps2 = SymbolUtils.removeSymbols(reader.read(new TextEncoder().encode(output))); assert.deepEqual(kps2, kps); }); @@ -80,6 +81,6 @@ describe('kps-file-reader', function () { assert.isNull(kps); assert.lengthOf(callbacks.messages, 1); - assert.isTrue(callbacks.hasMessage(CommonTypesMessages.ERROR_InvalidPackageFile)); + assert.isTrue(callbacks.hasMessage(DeveloperUtilsMessages.ERROR_InvalidPackageFile)); }); }); diff --git a/developer/src/common/web/utils/test/kvks/kvks-file.tests.ts b/developer/src/common/web/utils/test/kvks/kvks-file.tests.ts index 422a2e4a40..7e60119d28 100644 --- a/developer/src/common/web/utils/test/kvks/kvks-file.tests.ts +++ b/developer/src/common/web/utils/test/kvks/kvks-file.tests.ts @@ -5,6 +5,7 @@ import KvksFileReader from "../../src/types/kvks/kvks-file-reader.js"; import KvksFileWriter from "../../src/types/kvks/kvks-file-writer.js"; import { verify_khmer_angkor, verify_balochi_inpage } from './kvk-utils.tests.js'; import { assert } from 'chai'; +import { SymbolUtils } from '../../src/symbol-utils.js'; describe('kvks-file-reader', function() { it('should read a valid file', function() { @@ -63,7 +64,8 @@ describe('kvks-file-writer', function() { const input = fs.readFileSync(path); const reader = new KvksFileReader(); - const kvksExpected = reader.read(input); + // Remove XML metadata symbols to reduce clutter for testing purposes + const kvksExpected = SymbolUtils.removeSymbols(reader.read(input)); const invalidVkeys: string[] = []; const vk = reader.transform(kvksExpected, invalidVkeys); assert.isEmpty(invalidVkeys); @@ -74,6 +76,7 @@ describe('kvks-file-writer', function() { // We compare the (re)loaded data, because there may be // minor, irrelevant formatting differences in the emitted xml const kvks = reader.read(Buffer.from(output, 'utf8')); - assert.deepEqual(kvks, kvksExpected); + // Remove XML metadata symbols to reduce clutter for testing purposes + assert.deepEqual(SymbolUtils.removeSymbols(kvks), kvksExpected); }); }); diff --git a/developer/src/common/web/utils/test/ldml/ldml-keyboard-xml-reader.tests.ts b/developer/src/common/web/utils/test/ldml/ldml-keyboard-xml-reader.tests.ts index dc6f3021ae..d09c1dfc41 100644 --- a/developer/src/common/web/utils/test/ldml/ldml-keyboard-xml-reader.tests.ts +++ b/developer/src/common/web/utils/test/ldml/ldml-keyboard-xml-reader.tests.ts @@ -1,13 +1,14 @@ import 'mocha'; import {assert} from 'chai'; +import { DeveloperUtilsMessages } from '../../src/developer-utils-messages.js'; import { Constants } from '@keymanapp/common-types'; -import { CommonTypesMessages } from '../../src/common-messages.js'; import { LKKey, ImportStatus } from '../../src/types/ldml-keyboard/ldml-keyboard-xml.js'; import { testReaderCases } from '../helpers/reader-callback-test.js'; import CLDRScanToVkey = Constants.CLDRScanToVkey; import CLDRScanToKeyMap = Constants.CLDRScanToKeyMap; import USVirtualKeyCodes = Constants.USVirtualKeyCodes; +import { KeymanXMLReader, XML_FILENAME_SYMBOL } from '../../src/xml-utils.js'; function pluckKeysFromKeybag(keys: LKKey[], ids: string[]) { return keys.filter(({id}) => ids.indexOf(id) !== -1); @@ -19,7 +20,7 @@ describe('ldml keyboard xml reader tests', function () { testReaderCases([ { subpath: 'invalid-structure-per-dtd.xml', - errors: [CommonTypesMessages.Error_SchemaValidationError({ + errors: [DeveloperUtilsMessages.Error_SchemaValidationError({ instancePath: '/keyboard3', keyword: 'required', message: `must have required property 'info'`, @@ -28,7 +29,7 @@ describe('ldml keyboard xml reader tests', function () { }, { subpath: 'invalid-conforms-to.xml', - errors: [CommonTypesMessages.Error_SchemaValidationError({ + errors: [DeveloperUtilsMessages.Error_SchemaValidationError({ instancePath: '/keyboard3/conformsTo', keyword: 'enum', message: `must be equal to one of the allowed values`, @@ -140,17 +141,25 @@ describe('ldml keyboard xml reader tests', function () { { id: 'interrobang', output: '‽' }, { id: 'snail', output: '@' }, ]); + const snailKey = source?.keyboard3?.keys.key.find(({ id }) => id === 'snail'); // all of the keys are implied imports here - assert.isFalse(ImportStatus.isImpliedImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail'))); - assert.isTrue(ImportStatus.isImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail'))); - assert.isTrue(ImportStatus.isLocalImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail'))); + assert.isFalse(ImportStatus.isImpliedImport(snailKey)); + assert.isTrue(ImportStatus.isImport(snailKey)); + assert.isTrue(ImportStatus.isLocalImport(snailKey)); + // get the actual filename of where the import was located + const metadata = KeymanXMLReader.getMetaData(snailKey); + assert.ok(metadata); + const snailFilename = (metadata)[XML_FILENAME_SYMBOL]; + assert.ok(snailFilename); + assert.ok(/keys-Zyyy-morepunctuation.xml$/.test(snailFilename) + , `snail key filename is ${snailFilename}`); }, }, { subpath: 'invalid-import-base.xml', loadfail: true, errors: [ - CommonTypesMessages.Error_ImportInvalidBase({ + DeveloperUtilsMessages.Error_ImportInvalidBase({ base: 'SOME_INVALID_BASE', path: 'B', subtag: 'C' @@ -161,7 +170,7 @@ describe('ldml keyboard xml reader tests', function () { subpath: 'invalid-import-local.xml', loadfail: true, errors: [ - CommonTypesMessages.Error_ImportReadFail({ + DeveloperUtilsMessages.Error_ImportReadFail({ base: undefined, path: 'keys-Zyyy-DOESNOTEXIST.xml', subtag: 'keys' @@ -172,7 +181,7 @@ describe('ldml keyboard xml reader tests', function () { subpath: 'invalid-import-path.xml', loadfail: true, errors: [ - CommonTypesMessages.Error_ImportInvalidPath({ + DeveloperUtilsMessages.Error_ImportInvalidPath({ base: 'cldr', path: '45/too/many/slashes/leading/to/nothing-Zxxx-does-not-exist.xml', subtag: null, @@ -183,7 +192,7 @@ describe('ldml keyboard xml reader tests', function () { subpath: 'invalid-import-readfail.xml', loadfail: true, errors: [ - CommonTypesMessages.Error_ImportReadFail({ + DeveloperUtilsMessages.Error_ImportReadFail({ base: 'cldr', path: '45/none-Zxxx-does-not-exist.xml', subtag: null, @@ -194,7 +203,7 @@ describe('ldml keyboard xml reader tests', function () { subpath: 'invalid-import-wrongroot.xml', loadfail: true, errors: [ - CommonTypesMessages.Error_ImportWrongRoot({ + DeveloperUtilsMessages.Error_ImportWrongRoot({ base: null, path: '45/keys-Zyyy-punctuation.xml', subtag: 'flicks', diff --git a/developer/src/common/web/utils/test/line-utils.tests.ts b/developer/src/common/web/utils/test/line-utils.tests.ts new file mode 100644 index 0000000000..ffbdba5b0e --- /dev/null +++ b/developer/src/common/web/utils/test/line-utils.tests.ts @@ -0,0 +1,28 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by srl on 2025-04-03 + * + * Test for LineFinder + */ + +import { assert } from 'chai'; +import { readFileSync } from 'node:fs'; +import 'mocha'; +import { LineFinder } from '../src/line-utils.js'; +import { makePathToFixture } from './helpers/index.js'; + +describe(`LineFinder test`, () => { + const path = 'tran_fail-empty.xml'; + + it(`Should be able to process ${path}`, () => { + const xmlPath = makePathToFixture('xml', `${path}`); + const data = readFileSync(xmlPath, 'utf-8'); + assert.ok(data); + const lf = new LineFinder(data); + assert.ok(lf); + assert.deepEqual(lf.findOffset(40), { line: 3, column: 0 }); + assert.deepEqual(lf.findOffset(136), { line: 4, column: 2 }); + assert.deepEqual(lf.findOffset(186), { line: 8, column: 2 }); + }); +}); diff --git a/developer/src/common/web/utils/test/xml-utils.tests.ts b/developer/src/common/web/utils/test/xml-utils.tests.ts index 1cc9724b27..4b3df736b6 100644 --- a/developer/src/common/web/utils/test/xml-utils.tests.ts +++ b/developer/src/common/web/utils/test/xml-utils.tests.ts @@ -13,7 +13,9 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { KeymanXMLType, KeymanXMLReader, KeymanXMLWriter } from '../src/xml-utils.js'; +import { LineFinder } from '../src/line-utils.js'; import { makePathToFixture } from './helpers/index.js'; +import { SymbolUtils } from '../src/symbol-utils.js'; // if true, attempt to WRITE the fixtures const { GEN_XML_FIXTURES } = env; @@ -119,7 +121,7 @@ describe(`XML Reader Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => writeJson(jsonPath, actual); } else { assert.ok(expect, `Could not read ${jsonPath} - run with env GEN_XML_FIXTURES=1 to update.`); - assert.deepEqual(actual, expect, `Mismatch of ${xmlPath} vs ${jsonPath}`); + assert.deepEqual(SymbolUtils.removeSymbols(actual), expect, `Mismatch of ${xmlPath} vs ${jsonPath}`); } }); } @@ -160,3 +162,41 @@ describe(`XML Writer Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => }); } }); + +describe(`XML Reader line number test`, () => { + const path = 'tran_fail-empty.xml'; + const xmlPath = makePathToFixture('xml', `${path}`); + const type: KeymanXMLType = 'keyboard3'; + it(`Should report line numbers on parse of ${type} ${path}`, () => { + let xml = readData(xmlPath); + assert.ok(xml, `Could not read ${xmlPath}`); + xml = xml.replace(/\r\n/g, '\n'); + const reader = new KeymanXMLReader(type); + assert.ok(reader); + + // now, parse. subsitute endings for Win + const actual = reader.parse(xml); + const lines = LineFinder.textToLines(xml); + assert.ok(actual, `Parser failed on ${xmlPath}`); + + // now, assert char offset + const getMetaData = KeymanXMLReader.getMetaData; + assert.ok(getMetaData(actual.keyboard3)); + assert.equal( + getMetaData(actual.keyboard3)?.startIndex, 40); // index of element + assert.equal( + getMetaData(actual.keyboard3.info)?.startIndex, 136); // index of etc + assert.equal( + getMetaData(actual.keyboard3.transforms)?.startIndex, 186); + assert.deepEqual( + LineFinder.offsetToLineColumn( + getMetaData(actual.keyboard3).startIndex, lines), { line: 3, column: 0 }); + assert.deepEqual( + LineFinder.offsetToLineColumn( + getMetaData(actual.keyboard3.info).startIndex, lines), { line: 4, column: 2 }); + assert.deepEqual( + LineFinder.offsetToLineColumn( + getMetaData(actual.keyboard3.transforms).startIndex, lines), { line: 8, column: 2 }); + }); +}); + diff --git a/developer/src/inst/build.sh b/developer/src/inst/build.sh index 95551a45a7..8f5b722506 100755 --- a/developer/src/inst/build.sh +++ b/developer/src/inst/build.sh @@ -16,7 +16,7 @@ builder_describe "Installation files for Keyman Developer" \ # after all other builds complete builder_describe_outputs \ - publish /developer/src/inst/keymandeveloper-${VERSION}.exe + publish /developer/src/inst/keymandeveloper-${KEYMAN_VERSION}.exe builder_parse "$@" @@ -25,8 +25,8 @@ builder_parse "$@" . "$KEYMAN_ROOT/resources/build/win/zip.inc.sh" # In dev environments, we'll hack the tier to alpha; CI sets this for us in real builds. -if [[ -z ${TIER+x} ]]; then - TIER=alpha +if [[ -z ${KEYMAN_TIER+x} ]]; then + KEYMAN_TIER=alpha fi # We use different directories so that heat generates @@ -90,13 +90,13 @@ function do_test() { function copy-kmdev() { builder_heading copy-kmdev - mkdir -p "$DEVELOPER_ROOT/release/${VERSION}" + mkdir -p "$DEVELOPER_ROOT/release/${KEYMAN_VERSION}" make-installer make-kmc-install-zip - cp -f "$DEVELOPER_ROOT/src/inst/keymandeveloper.msi" "$DEVELOPER_ROOT/release/${VERSION}/keymandeveloper.msi" - cp -f "$DEVELOPER_ROOT/src/inst/keymandeveloper-${VERSION}.exe" "$DEVELOPER_ROOT/release/${VERSION}/keymandeveloper-${VERSION}.exe" + cp -f "$DEVELOPER_ROOT/src/inst/keymandeveloper.msi" "$DEVELOPER_ROOT/release/${KEYMAN_VERSION}/keymandeveloper.msi" + cp -f "$DEVELOPER_ROOT/src/inst/keymandeveloper-${KEYMAN_VERSION}.exe" "$DEVELOPER_ROOT/release/${KEYMAN_VERSION}/keymandeveloper-${KEYMAN_VERSION}.exe" } function verify-program-signatures() { @@ -108,7 +108,7 @@ function verify-program-signatures() { function verify-installer-signatures() { builder_heading verify-installer-signatures - verify-all-executable-signatures-in-folder "$DEVELOPER_ROOT/release/${VERSION}" + verify-all-executable-signatures-in-folder "$DEVELOPER_ROOT/release/${KEYMAN_VERSION}" } function verify-node-installer-version() { @@ -126,8 +126,8 @@ function verify-node-installer-version() { } function test-releaseexists() { - if [[ -d "$DEVELOPER_ROOT/release/${VERSION}" ]]; then - builder_die "Release ${VERSION} already exists. Delete it or update VERSION.md and try again" + if [[ -d "$DEVELOPER_ROOT/release/${KEYMAN_VERSION}" ]]; then + builder_die "Release ${KEYMAN_VERSION} already exists. Delete it or update VERSION.md and try again" fi } @@ -141,12 +141,12 @@ function candle() { heat-kmc builder_heading "candle: calling WIX:candle" - "$WIXCANDLE" -wx -nologo "-dVERSION=${VERSION_WIN}" "-dRELEASE=${VERSION_RELEASE}" kmdev.wxs - "$WIXCANDLE" -wx -nologo "-dVERSION=${VERSION_WIN}" "-dRELEASE=${VERSION_RELEASE}" "-dXmlSourceDir=${DEVELOPER_ROOT}/src/tike/xml" xml.wxs - "$WIXCANDLE" -wx -nologo "-dVERSION=${VERSION_WIN}" "-dRELEASE=${VERSION_RELEASE}" "-dCefSourceDir=${KEYMAN_CEF4DELPHI_ROOT}" cef.wxs - "$WIXCANDLE" -wx -nologo "-dVERSION=${VERSION_WIN}" "-dRELEASE=${VERSION_RELEASE}" "-dTemplatesSourceDir=${KEYMAN_DEVELOPER_TEMPLATES_ROOT}" templates.wxs - "$WIXCANDLE" -wx -nologo "-dVERSION=${VERSION_WIN}" "-dRELEASE=${VERSION_RELEASE}" "-dkmcSourceDir=${KEYMAN_WIX_TEMP_KMC}" kmc.wxs - "$WIXCANDLE" -wx -nologo "-dVERSION=${VERSION_WIN}" "-dRELEASE=${VERSION_RELEASE}" "-dServerSourceDir=${KEYMAN_WIX_KMDEV_SERVER}" server.wxs + "$WIXCANDLE" -wx -nologo "-dKEYMAN_VERSION=${KEYMAN_VERSION_WIN}" "-dRELEASE=${KEYMAN_VERSION_RELEASE}" kmdev.wxs + "$WIXCANDLE" -wx -nologo "-dKEYMAN_VERSION=${KEYMAN_VERSION_WIN}" "-dRELEASE=${KEYMAN_VERSION_RELEASE}" "-dXmlSourceDir=${DEVELOPER_ROOT}/src/tike/xml" xml.wxs + "$WIXCANDLE" -wx -nologo "-dKEYMAN_VERSION=${KEYMAN_VERSION_WIN}" "-dRELEASE=${KEYMAN_VERSION_RELEASE}" "-dCefSourceDir=${KEYMAN_CEF4DELPHI_ROOT}" cef.wxs + "$WIXCANDLE" -wx -nologo "-dKEYMAN_VERSION=${KEYMAN_VERSION_WIN}" "-dRELEASE=${KEYMAN_VERSION_RELEASE}" "-dTemplatesSourceDir=${KEYMAN_DEVELOPER_TEMPLATES_ROOT}" templates.wxs + "$WIXCANDLE" -wx -nologo "-dKEYMAN_VERSION=${KEYMAN_VERSION_WIN}" "-dRELEASE=${KEYMAN_VERSION_RELEASE}" "-dkmcSourceDir=${KEYMAN_WIX_TEMP_KMC}" kmc.wxs + "$WIXCANDLE" -wx -nologo "-dKEYMAN_VERSION=${KEYMAN_VERSION_WIN}" "-dRELEASE=${KEYMAN_VERSION_RELEASE}" "-dServerSourceDir=${KEYMAN_WIX_KMDEV_SERVER}" server.wxs } function heat-xml() { @@ -230,12 +230,12 @@ function make-installer() { builder_heading make-installer echo [Setup] > setup.inf - echo "Version=$VERSION" >> setup.inf + echo "Version=$KEYMAN_VERSION" >> setup.inf echo "MSIFileName=keymandeveloper.msi" >> setup.inf - echo "Title=Keyman Developer ${VERSION_WITH_TAG}" >>setup.inf + echo "Title=Keyman Developer ${KEYMAN_VERSION_WITH_TAG}" >>setup.inf wzzip setup.zip keymandeveloper.msi setup.inf - cat "$DEVELOPER_PROGRAM/setup.exe" setup.zip > "keymandeveloper-$VERSION.exe" - wrap-signcode //d "Keyman Developer" "keymandeveloper-$VERSION.exe" + cat "$DEVELOPER_PROGRAM/setup.exe" setup.zip > "keymandeveloper-$KEYMAN_VERSION.exe" + wrap-signcode //d "Keyman Developer" "keymandeveloper-$KEYMAN_VERSION.exe" # # Zip the files we distribute as part of the standalone kmc distro into release\$Version\kmcomp-$Version.zip @@ -243,7 +243,7 @@ function make-installer() { } # TODO: rename this to keyman-developer-cli-$Version.zip -KMC_ZIP="$DEVELOPER_ROOT/release/$VERSION/kmcomp-$VERSION.zip" +KMC_ZIP="$DEVELOPER_ROOT/release/$KEYMAN_VERSION/kmcomp-$KEYMAN_VERSION.zip" function make-kmc-install-zip() { builder_heading make-kmc-install-zip diff --git a/developer/src/inst/kmdev.wxs b/developer/src/inst/kmdev.wxs index 1602f624ff..c57de43d82 100644 --- a/developer/src/inst/kmdev.wxs +++ b/developer/src/inst/kmdev.wxs @@ -2,7 +2,7 @@ + Name="Keyman Developer $(var.RELEASE)" Version="$(var.KEYMAN_VERSION)" Language="1033"> @@ -40,10 +40,10 @@ - - + Maximum="$(var.KEYMAN_VERSION)" IncludeMaximum="no" MigrateFeatures="yes" Property="VERSION11FOUND" /> + + @@ -309,7 +309,7 @@ - + diff --git a/developer/src/kmc-analyze/.eslintrc.cjs b/developer/src/kmc-analyze/.eslintrc.cjs index 148ad7579f..baaa3f5bc7 100644 --- a/developer/src/kmc-analyze/.eslintrc.cjs +++ b/developer/src/kmc-analyze/.eslintrc.cjs @@ -7,8 +7,5 @@ module.exports = { files:"src/**/*.ts", extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } - ], - rules: { - "prefer-const": "off", // TODO: enable this once infrastructure is in place and cleanup the problem cases - }, + ] }; diff --git a/developer/src/kmc-analyze/package.json b/developer/src/kmc-analyze/package.json index 3d31dc04dd..496e04be05 100644 --- a/developer/src/kmc-analyze/package.json +++ b/developer/src/kmc-analyze/package.json @@ -33,7 +33,6 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "typescript": "^5.4.5" }, "mocha": { diff --git a/developer/src/kmc-analyze/src/osk-character-use/index.ts b/developer/src/kmc-analyze/src/osk-character-use/index.ts index 248944ccfc..de725b9b69 100644 --- a/developer/src/kmc-analyze/src/osk-character-use/index.ts +++ b/developer/src/kmc-analyze/src/osk-character-use/index.ts @@ -82,7 +82,7 @@ export class AnalyzeOskCharacterUse { public async analyze(file: string): Promise { switch(KeymanFileTypes.sourceTypeFromFilename(file)) { case KeymanFileTypes.Source.VisualKeyboard: { - let strings = this.scanVisualKeyboard(file); + const strings = this.scanVisualKeyboard(file); if(!strings) { return false; } @@ -90,7 +90,7 @@ export class AnalyzeOskCharacterUse { break; } case KeymanFileTypes.Source.TouchLayout: { - let strings = this.scanTouchLayout(file); + const strings = this.scanTouchLayout(file); if(!strings) { return false; } @@ -122,17 +122,17 @@ export class AnalyzeOskCharacterUse { private async analyzeKmnKeyboard(filename: string): Promise { this.callbacks.reportMessage(AnalyzerMessages.Info_ScanningFile({type:'keyboard source', name:filename})); - let osk = await getOskFromKmnFile(this.callbacks, filename); + const osk = await getOskFromKmnFile(this.callbacks, filename); if(osk.kvksFilename) { - let strings = this.scanVisualKeyboard(osk.kvksFilename); + const strings = this.scanVisualKeyboard(osk.kvksFilename); if(!strings) { return false; } this.addStrings(strings, osk.kvksFilename); } if(osk.touchLayoutFilename) { - let strings = this.scanTouchLayout(osk.touchLayoutFilename); + const strings = this.scanTouchLayout(osk.touchLayoutFilename); if(!strings) { return false; } @@ -144,13 +144,13 @@ export class AnalyzeOskCharacterUse { private addStrings(strings: string[], filename: string) { // Reduce all references to get usage count per file - let reducedStrings: Osk.StringRef[] = [...new Set(strings)].map(e =>({ + const reducedStrings: Osk.StringRef[] = [...new Set(strings)].map(e =>({ str:e, usages: [{filename: this.callbacks.path.basename(filename), count: strings.filter(n => n===e).length }] })); // Merge result with existing found strings - for(let e of reducedStrings) { + for(const e of reducedStrings) { this._strings[e.str] = [].concat(this._strings[e.str] ?? [], ...e.usages); } } @@ -168,7 +168,7 @@ export class AnalyzeOskCharacterUse { private scanVisualKeyboard(filename: string): string[] { this.callbacks.reportMessage(AnalyzerMessages.Info_ScanningFile({type:'visual keyboard', name:filename})); - let strings: string[] = []; + const strings: string[] = []; const reader = new KvksFileReader(); let source: KvksFile.default; try { @@ -177,13 +177,13 @@ export class AnalyzeOskCharacterUse { this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e})); return null; } - let invalidKeys: string[] = []; + const invalidKeys: string[] = []; const vk = reader.transform(source, invalidKeys); if(!vk) { this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e:null})); return null; } - for(let key of vk.keys) { + for(const key of vk.keys) { if(key.text) { strings.push(this.cleanString(key.text)); } @@ -193,7 +193,7 @@ export class AnalyzeOskCharacterUse { private scanTouchLayout(filename: string): string[] { this.callbacks.reportMessage(AnalyzerMessages.Info_ScanningFile({type:'touch layout', name:filename})); - let strings: string[] = []; + const strings: string[] = []; const reader = new TouchLayoutFileReader(); const source = reader.read(this.callbacks.loadFile(filename)); @@ -212,9 +212,9 @@ export class AnalyzeOskCharacterUse { if(!platform) { return; } - for(let layer of platform.layer) { - for(let row of layer.row) { - for(let key of row.key) { + for(const layer of platform.layer) { + for(const row of layer.row) { + for(const key of row.key) { scanKey(key); if(key.hint && !key.hint.match(/^\*.+\*$/)) { strings.push(this.cleanString(key.hint)); @@ -223,10 +223,10 @@ export class AnalyzeOskCharacterUse { for(f in key.flick ?? {}) { scanKey(key.flick[f]); } - for(let sk of key.sk ?? []) { + for(const sk of key.sk ?? []) { scanKey(sk); } - for(let mt of key.multitap ?? []) { + for(const mt of key.multitap ?? []) { scanKey(mt); } } @@ -258,12 +258,12 @@ export class AnalyzeOskCharacterUse { previousMap = []; } - let result: Osk.StringResult[] = [...previousMap]; + const result: Osk.StringResult[] = [...previousMap]; // Note: we are assuming same base as previous runs let pua = Math.max(this.options.puaBase, ...previousMap.map(item => parseInt(item.pua,16) + 1)); - for(let str of Object.keys(strings)) { + for(const str of Object.keys(strings)) { const r = result.find(item => item.str == str); if(!r) { result.push({ @@ -370,8 +370,8 @@ export class AnalyzeOskCharacterUse { private static getStringsAsText(strings: Osk.StringResult[]) { // Text result only returns PUA, unicode sequence, and plain string - let lines: string[] = []; - for(let s of strings) { + const lines: string[] = []; + for(const s of strings) { const ux = this.stringToUnicodeSequence(s.str); lines.push('U+'+s.pua + '\t' + ux + '\t' + s.str); } @@ -380,10 +380,10 @@ export class AnalyzeOskCharacterUse { private static getStringsAsMarkdown(strings: Osk.StringResult[]) { // Markdown result only returns PUA, unicode sequence, and plain string - let lines: string[] = []; + const lines: string[] = []; lines.push('PUA | Code Points | Key Caps'); lines.push('-------|-------------|---------'); - for(let s of strings) { + for(const s of strings) { const ux = this.stringToUnicodeSequence(s.str); lines.push('U+'+s.pua + ' | ' + ux + ' | ' + escapeMarkdownChar(s.str, true)); } @@ -392,13 +392,13 @@ export class AnalyzeOskCharacterUse { private static getStringsAsJson(strings: Osk.StringResult[]) { // For future expansion, we wrap the array in a 'map' property - let map = { "map": strings }; + const map = { "map": strings }; return JSON.stringify(map, null, 2).split('\n'); } private static stringToUnicodeSequence(s: string, addUPlusPrefix: boolean = true): string { - let result = []; - for(let ch of s) { + const result = []; + for(const ch of s) { let c = ch.codePointAt(0).toString(16).toUpperCase(); if(c.length < 4) c = '0'.repeat(4 - c.length) + c; result.push((addUPlusPrefix ? 'U+' : '') + c); diff --git a/developer/src/kmc-analyze/src/osk-rewrite-pua/index.ts b/developer/src/kmc-analyze/src/osk-rewrite-pua/index.ts index dc56ebfb3a..6ff449c9a9 100644 --- a/developer/src/kmc-analyze/src/osk-rewrite-pua/index.ts +++ b/developer/src/kmc-analyze/src/osk-rewrite-pua/index.ts @@ -40,7 +40,7 @@ export class AnalyzeOskRewritePua { * @returns true if the file is successfully loaded and rewritten */ public async analyze(file: string, mapping: Osk.StringResult[]): Promise { - let map: Osk.PuaMap = Osk.parseMapping(mapping); + const map: Osk.PuaMap = Osk.parseMapping(mapping); switch(KeymanFileTypes.sourceTypeFromFilename(file)) { case KeymanFileTypes.Source.VisualKeyboard: @@ -82,7 +82,7 @@ export class AnalyzeOskRewritePua { private async analyzeKmnKeyboard(filename: string, map: Osk.PuaMap): Promise { this.callbacks.reportMessage(AnalyzerMessages.Info_ScanningFile({type:'keyboard source', name:filename})); - let osk = await getOskFromKmnFile(this.callbacks, filename); + const osk = await getOskFromKmnFile(this.callbacks, filename); if(osk.kvksFilename) { this._data[osk.kvksFilename] = this.analyzeVisualKeyboard(osk.kvksFilename, map); @@ -110,7 +110,7 @@ export class AnalyzeOskRewritePua { this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e})); return null; } - let invalidKeys: string[] = []; + const invalidKeys: string[] = []; const vk = reader.transform(source, invalidKeys); if(!vk) { this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e:null})); @@ -134,7 +134,7 @@ export class AnalyzeOskRewritePua { const reader = new TouchLayoutFileReader(); const source = reader.read(this.callbacks.loadFile(filename)); - let dirty = Osk.remapTouchLayout(source, map); + const dirty = Osk.remapTouchLayout(source, map); if(!dirty) { return null; diff --git a/developer/src/kmc-analyze/src/util/get-osk-from-kmn-file.ts b/developer/src/kmc-analyze/src/util/get-osk-from-kmn-file.ts index b9fd9cb602..a56a777294 100644 --- a/developer/src/kmc-analyze/src/util/get-osk-from-kmn-file.ts +++ b/developer/src/kmc-analyze/src/util/get-osk-from-kmn-file.ts @@ -18,7 +18,7 @@ export async function getOskFromKmnFile(callbacks: CompilerCallbacks, filename: return null; } - let result = await kmnCompiler.run(filename, null); + const result = await kmnCompiler.run(filename, null); if(!result) { // kmnCompiler will report any errors diff --git a/developer/src/kmc-convert/package.json b/developer/src/kmc-convert/package.json index 93ea8999f4..4798331e00 100644 --- a/developer/src/kmc-convert/package.json +++ b/developer/src/kmc-convert/package.json @@ -39,7 +39,6 @@ "@types/semver": "^7.3.12", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "typescript": "^5.4.5" }, "mocha": { diff --git a/developer/src/kmc-convert/src/converter-messages.ts b/developer/src/kmc-convert/src/converter-messages.ts index ec24a813f9..1ce694739a 100644 --- a/developer/src/kmc-convert/src/converter-messages.ts +++ b/developer/src/kmc-convert/src/converter-messages.ts @@ -17,12 +17,14 @@ const SevError = CompilerErrorSeverity.Error | Namespace; */ export class ConverterMessages { static ERROR_OutputFilenameIsRequired = SevError | 0x0001; - static Error_OutputFilenameIsRequired = () => - m(this.ERROR_OutputFilenameIsRequired, `An output filename is required for keyboard conversion.`); + static Error_OutputFilenameIsRequired = () => m( + this.ERROR_OutputFilenameIsRequired, + `An output filename is required for keyboard conversion.`); static ERROR_NoConverterFound = SevError | 0x0002; - static Error_NoConverterFound = (o: { inputFilename: string, outputFilename: string; }) => - m(this.ERROR_NoConverterFound, `No converter is available that can convert from '${def(o.inputFilename)}' to '${def(o.outputFilename)}'.`); + static Error_NoConverterFound = (o: { inputFilename: string, outputFilename: string; }) => m( + this.ERROR_NoConverterFound, + `No converter is available that can convert from '${def(o.inputFilename)}' to '${def(o.outputFilename)}'.`); static ERROR_FileNotFound = SevError | 0x0003; static Error_FileNotFound = (o: { inputFilename: string; }) => @@ -38,5 +40,23 @@ export class ConverterMessages { static Error_SchemaValidationError = (o:{instancePath:string, keyword:string, message: string, params: string}) => m(this.ERROR_SchemaValidationError, `!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Error validating Keylayout XML file: ${def(o.instancePath)}: ${def(o.keyword)}: ${def(o.message)} ${def(o.params)}`); + static ERROR_UnableToRead = SevError | 0x0004; + static Error_UnableToRead = (o: { inputFilename: string; }) => m( + this.ERROR_UnableToRead, `Input file '${def(o.inputFilename)} + ' could not be read.`); -} + static ERROR_UnableToConvert = SevError | 0x0005; + static Error_UnableToConvert = (o: { inputFilename: string; }) => m( + this.ERROR_UnableToConvert, `Input file '${def(o.inputFilename)} + ' could not be converted.`); + + static ERROR_UnableToWrite = SevError | 0x0006; + static Error_UnableToWrite = (o: { outputFilename: string; }) => m( + this.ERROR_UnableToWrite, `Output file for '${def(o.outputFilename)} + ' could not be written.`); + + static ERROR_UnsupportedCharactersDetected = SevError | 0x0007; + static Error_UnsupportedCharactersDetected = (o: { inputFilename: string, keymap_index: string, key: string, output: string; }) => m( + this.ERROR_UnsupportedCharactersDetected, `Input file ${def(o.inputFilename)} contains unsupported character ('${def(o.output)}') on key (${def(o.key)}) at keyMap index ${def(o.keymap_index)}`); + + } diff --git a/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-file-reader.ts b/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-file-reader.ts index 96cb1fc063..4958b50fe2 100644 --- a/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-file-reader.ts +++ b/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-file-reader.ts @@ -10,7 +10,6 @@ import { CompilerCallbacks } from "@keymanapp/developer-utils"; import { XMLParser } from 'fast-xml-parser'; import { util } from '@keymanapp/common-types'; -import { KeylayoutToKmnConverter } from './keylayout-to-kmn-converter.js'; import { ConverterMessages } from '../converter-messages.js'; import { SchemaValidators } from '@keymanapp/common-types'; @@ -84,7 +83,6 @@ export class KeylayoutFileReader { */ public read(inputFilename: string): KeylayoutXMLSourceFile { - const options = { /*ignoreAttributes: false, attributeNamePrefix: '@_', // to access the attribute @@ -104,7 +102,7 @@ export class KeylayoutFileReader { }; try { - const xmlFile = this.callbacks.fs.readFileSync(this.callbacks.path.join(process.cwd(), KeylayoutToKmnConverter.TEST_SUBFOLDER, KeylayoutToKmnConverter.DATA_SUBFOLDER, this.callbacks.path.basename(inputFilename)), 'utf8'); + const xmlFile = this.callbacks.fs.readFileSync(inputFilename, 'utf8'); const parser = new XMLParser(options); const jsonObj = parser.parse(xmlFile); // get plain Object this.boxArray(jsonObj.keyboard); // jsonObj now contains arrays; no single fields diff --git a/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-to-kmn-converter.ts b/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-to-kmn-converter.ts index eaaf36b37c..385a7b9640 100644 --- a/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-to-kmn-converter.ts +++ b/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-to-kmn-converter.ts @@ -1,6 +1,8 @@ /* * Keyman is 2025 copyright (C) SIL International. MIT License. * + * Created by S. Schmitt on 2025-05-12 + * * Convert macOS/Ukelele .keylayout files to Keyman .kmn * */ @@ -26,13 +28,9 @@ export class KeylayoutToKmnConverter { static readonly INPUT_FILE_EXTENSION = '.keylayout'; static readonly OUTPUT_FILE_EXTENSION = '.kmn'; - static readonly USED_KEYS_COUNT = 51; // we use key Nr 0 (A) -> key Nr 49 (Space) + static readonly USED_KEYS_COUNT = 49; // we use key Nr 0 (A) -> key Nr 49 (Space) static readonly MAX_CTRL_CHARACTER = 32; static readonly SKIP_COMMENTED_LINES = false; - static readonly KMC_CONVERT_VERSION = "0.1"; - - static readonly DATA_SUBFOLDER = 'data'; - static readonly TEST_SUBFOLDER = 'test'; private options: CompilerOptions; @@ -50,10 +48,12 @@ export class KeylayoutToKmnConverter { * @param outputFilename the resulting keyman .kmn-file * @return null on success */ - async run(inputFilename: string, outputFilename: string = ""): Promise { + async run(inputFilename: string, outputFilename?: string): Promise { + if (!inputFilename) { - throw new Error('Invalid parameters'); + this.callbacks.reportMessage(ConverterMessages.Error_FileNotFound({ inputFilename })); + return null; } const KeylayoutReader = new KeylayoutFileReader(this.callbacks/*, this.options*/); @@ -75,30 +75,48 @@ export class KeylayoutToKmnConverter { if (!jsonO) { - throw new Error('Error while processing read()'); + this.callbacks.reportMessage(ConverterMessages.Error_UnableToRead({ inputFilename })); + return null; } + outputFilename = outputFilename ?? inputFilename.replace(/\.keylayout$/, '.kmn'); + const outArray: convert_object = await this.convert(jsonO, outputFilename); if (!outArray) { - throw new Error('Error while processing convert()'); + this.callbacks.reportMessage(ConverterMessages.Error_UnableToConvert({ inputFilename })); + return null; } const kmnFileWriter = new KmnFileWriter(this.callbacks, this.options); - const out_text_ok: boolean = kmnFileWriter.write(outArray); + const out_text_ok: boolean = kmnFileWriter.write(outArray); if (!out_text_ok) { - throw new Error('Error while processing write()'); + this.callbacks.reportMessage(ConverterMessages.Error_UnableToWrite({ outputFilename })); + return null; } + + /*const out_Uint8: Uint8Array = kmnFileWriter.writeToUint8Array(outArray); + if (!out_Uint8) { + this.callbacks.reportMessage(ConverterMessages.Error_UnableToWrite({ inputFilename })); + return null; + }*/ + + /*const out_str: string = kmnFileWriter.writeToString(outArray); + if (!out_str) { + this.callbacks.reportMessage(ConverterMessages.Error_UnableToWrite({ inputFilename })); + return null; + }*/ + return null; } + /** * @brief member function to read filename and behaviour of a json object into a convert_object * @param jsonObj containing filename, behaviour and rules of a json object * @return an convert_object containing all data ready to print out */ - public convert(jsonObj: any, outputfilename: string = ""): convert_object { - + private convert(jsonObj: any, outputfilename: string): convert_object { const modifierBehavior: string[][] = []; // modifier for each behaviour const rules: Rule[] = []; // an array of data for a kmn rule @@ -112,25 +130,14 @@ export class KeylayoutToKmnConverter { // ToDo in a new PR: check tags ( issue # 13599) if ((jsonObj !== null) && (jsonObj.hasOwnProperty("keyboard"))) { - // get filename from data that had been read - const fileNameNoExtention = jsonObj.keyboard['@_name']; - const filePath = this.callbacks.path.join(process.cwd(), KeylayoutToKmnConverter.TEST_SUBFOLDER, KeylayoutToKmnConverter.DATA_SUBFOLDER); - - data_object.keylayout_filename = this.callbacks.path.join(filePath, (fileNameNoExtention + ".keylayout")); - - // if no outputfile name is specified: create one from input file name - if ((outputfilename === "") || (outputfilename === null)) { - data_object.kmn_filename = this.callbacks.path.join(filePath, fileNameNoExtention + ".kmn"); - } - // if outputfile name is specified: use it - else - data_object.kmn_filename = outputfilename; - - console.log("RUN kmc convert - input file: ", data_object.keylayout_filename, " --> output file: ", data_object.kmn_filename); - + data_object.keylayout_filename = outputfilename.replace(/\.kmn$/, '.keylayout'); + data_object.kmn_filename = outputfilename; data_object.arrayOf_Modifiers = modifierBehavior; // ukelele uses behaviours e.g. 18 modifiersCombinations in 8 KeyMapSelect(behaviors) data_object.arrayOf_Rules = rules; + // _S2 ToDo remove this console.log + console.log("RUN kmc convert - input file: ",data_object.keylayout_filename, " --> output file: ", data_object.kmn_filename); + // create an array of modifier combinations and store in data_object for (let j = 0; j < jsonObj.keyboard.modifierMap.keyMapSelect.length; j++) { const singleModifierSet: string[] = []; @@ -162,7 +169,7 @@ export class KeylayoutToKmnConverter { const isCapsused: boolean = this.checkIfCapsIsUsed(data_ukelele.arrayOf_Modifiers); // loop keys 0-50 (= all keys we use) - for (let j = 0; j < KeylayoutToKmnConverter.USED_KEYS_COUNT; j++) { + for (let j = 0; j <= KeylayoutToKmnConverter.USED_KEYS_COUNT; j++) { // loop behaviors (in ukelele it is possible to define multiple modifier combinations that behave in the same way) for (let i = 0; i < jsonObj.keyboard.keyMapSet[0].keyMap.length; i++) { @@ -420,6 +427,17 @@ export class KeylayoutToKmnConverter { } } } else { + + /* QUESTION + // _S2 why does this not work here? Correct data in this.callback.messages is available !?!?! + this.callbacks.reportMessage(ConverterMessages.Error_UnsupportedCharactersDetected({ + inputFilename: jsonObj.keyboard['@_name'] + ".keylayout", + keymap_index: jsonObj.keyboard.keyMapSet[0].keyMap[i]['@_index'], + output: jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['@_output'], + key: jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['@_code'] + })); + return null;*/ + console.log("ERROR : some output characters can not be used in Keyman \"", (jsonObj.keyboard['@_name'] + ".keylayout\""), "\"\" :", @@ -466,13 +484,12 @@ export class KeylayoutToKmnConverter { } if (isFirstUsedHere_dk) { - const ruleArray: string[] = []; object_array[i].unique_deadkey = unique_dkB_count; - ruleArray.push(object_array[i].modifier_deadkey); - ruleArray.push(object_array[i].deadkey); - ruleArray.push(String(unique_dkB_count)); + list_of_unique_Text2_rules.push([ + object_array[i].modifier_deadkey, + object_array[i].deadkey, + String(unique_dkB_count)]); unique_dkB_count++; - list_of_unique_Text2_rules.push(ruleArray); } } } @@ -508,13 +525,13 @@ export class KeylayoutToKmnConverter { } if (isFirstUsedHere_prev_dk) { - const ruleArray: string[] = []; object_array[i].unique_deadkey = unique_dkB_count; - ruleArray.push(object_array[i].modifier_prev_deadkey); - ruleArray.push(object_array[i].prev_deadkey); - ruleArray.push(String(unique_dkB_count)); + list_of_unique_Text2_rules.push([ + object_array[i].modifier_prev_deadkey, + object_array[i].prev_deadkey, + String(unique_dkB_count) + ]); unique_dkB_count++; - list_of_unique_Text2_rules.push(ruleArray); } } } @@ -672,72 +689,71 @@ export class KeylayoutToKmnConverter { * @return keycode on a Windows Keyboard */ public map_UkeleleKC_To_VK(pos: number): string { + const vk = [ + "K_A" /* A */, + "K_S" /* S */, + "K_D" /* D */, + "K_F" /* F */, + "K_H" /* H */, + "K_G" /* G */, + "K_Z" /* Z */, + "K_X" /* X */, + "K_C" /* C */, + "K_V" /* V */, + "K_BKQUOTE" /* ^ */, + "K_B" /* B */, + "K_Q" /* Q */, + "K_W" /* W */, + "K_E" /* E */, + "K_R" /* R */, + "K_Y" /* Y */, + "K_T" /* T */, + "K_1" /* 1 */, + "K_2" /* 2 */, + "K_3" /* 3 */, + "K_4" /* 4 */, + "K_6" /* 6 */, + "K_5" /* 5 */, + "K_EQUAL" /* ´ */, + "K_9" /* 9 */, + "K_7" /* 7 */, + "K_HYPHEN" /* ß */, + "K_8" /* 8 */, + "K_0" /* 0 */, + "K_RBRKT" /* ] */, + "K_O" /* O */, + "K_U" /* U */, + "K_LBRKT" /* [ */, + "K_I" /* I */, + "K_P" /* P */, + "K_ENTER", + "K_L" /* L */, + "K_J" /* J */, + "K_QUOTE" /* " */, + "K_K" /* K */, + "K_COLON" /* : */, + "K_BKSLASH" /* \ */, // 42 for ISO correct?? + "K_COMMA" /* , */, + "K_SLASH" /* / */, + "K_N" /* N */, + "K_M" /* M */, + "K_PERIOD" /* . */, + "K_?C1" /* \ */, // 48 for ANSI correct?? + "K_SPACE" /* \ */ + ]; - // ukelele KC --> VK_US - if (pos === 0x0A) return "K_BKQUOTE"; /* ^ */ - else if (pos === 0x12) return "K_1"; /* 1 */ - else if (pos === 0x13) return "K_2"; /* 2 */ - else if (pos === 0x14) return "K_3"; /* 3 */ - else if (pos === 0x15) return "K_4"; /* 4 */ - else if (pos === 0x17) return "K_5"; /* 5 */ - else if (pos === 0x16) return "K_6"; /* 6 */ - else if (pos === 0x1A) return "K_7"; /* 7 */ - else if (pos === 0x1C) return "K_8"; /* 8 */ - else if (pos === 0x19) return "K_9"; /* 9 */ - else if (pos === 0x1D) return "K_0"; /* 0 */ - else if (pos === 0x1B) return "K_HYPHEN"; /* ß */ - else if (pos === 0x18) return "K_EQUAL"; /* ´ */ - - else if (pos === 0x0C) return "K_Q"; /* Q */ - else if (pos === 0x0D) return "K_W"; /* W */ - else if (pos === 0x0E) return "K_E"; /* E */ - else if (pos === 0x0F) return "K_R"; /* R */ - else if (pos === 0x11) return "K_T"; /* T */ - else if (pos === 0x10) return "K_Y"; /* Y */ - else if (pos === 0x20) return "K_U"; /* U */ - else if (pos === 0x22) return "K_I"; /* I */ - else if (pos === 0x1F) return "K_O"; /* O */ - else if (pos === 0x23) return "K_P"; /* P */ - else if (pos === 0x21) return "K_LBRKT"; /* [ */ - else if (pos === 0x1E) return "K_RBRKT"; /* ] */ - else if (pos === 0x31) return "K_SPACE"; /* \ */ - else if (pos === 0x2A) return "K_BKSLASH"; /* \ */ // 42 for ISO correct?? - // else if (pos === 0x30) return "K_?C1" /* \ */ // 48 for ANSI correct?? - - else if (pos === 0x00) return "K_A"; /* A */ - else if (pos === 0x01) return "K_S"; /* S */ - else if (pos === 0x02) return "K_D"; /* D */ - else if (pos === 0x03) return "K_F"; /* F */ - else if (pos === 0x05) return "K_G"; /* G */ - else if (pos === 0x04) return "K_H"; /* H */ - else if (pos === 0x26) return "K_J"; /* J */ - else if (pos === 0x28) return "K_K"; /* K */ - else if (pos === 0x25) return "K_L"; /* L */ - else if (pos === 0x29) return "K_COLON"; /* : */ - else if (pos === 0x27) return "K_QUOTE"; /* " */ - - else if (pos === 0x23) return "K_oE2"; /* | */ - else if (pos === 0x06) return "K_Z"; /* Z */ - else if (pos === 0x07) return "K_X"; /* X */ - else if (pos === 0x08) return "K_C"; /* C */ - else if (pos === 0x09) return "K_V"; /* V */ - else if (pos === 0x0B) return "K_B"; /* B */ - else if (pos === 0x2D) return "K_N"; /* N */ - else if (pos === 0x2E) return "K_M"; /* M */ - else if (pos === 0x2B) return "K_COMMA"; /* , */ - else if (pos === 0x2F) return "K_PERIOD"; /* . */ - else if (pos === 0x2C) return "K_SLASH"; /* / */ - - else if (pos === 0x24) return "K_ENTER"; - else return ""; + if (!(pos >= 0 && pos <= 0x31) || (pos === null) || (pos === undefined)) { + return ""; + } else { + return vk[pos]; + } } - /** - * @brief member function to return an index for a given actionID - * @param data :any - an object containing all data read from a .keylayout file - * @param search :string - value 'id' to be found - * @return a number specifying the index of an actionId - */ + /* @brief member function to return an index for a given actionID + * @param data :any - an object containing all data read from a .keylayout file + * @param search :string - value 'id' to be found + * @return a number specifying the index of an actionId + */ public get_ActionIndex__From__ActionId(data: any, search: string): number { for (let i = 0; i < data.keyboard.actions.action.length; i++) { if (data.keyboard.actions.action[i]['@_id'] === search) { @@ -816,17 +832,15 @@ export class KeylayoutToKmnConverter { for (let k = 0; k < search.length; k++) { for (let i = 0; i < data.keyboard.keyMapSet[0].keyMap.length; i++) { for (let j = 0; j < data.keyboard.keyMapSet[0].keyMap[i].key.length; j++) { - const returnarray: string[] = []; if (data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_action'] === search[k][0] && - data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_code'] < KeylayoutToKmnConverter.USED_KEYS_COUNT) { - returnarray.push(data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_code']); - returnarray.push(this.map_UkeleleKC_To_VK(Number(data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_code']))); - returnarray.push(data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_action']); - returnarray.push(data.keyboard.keyMapSet[0].keyMap[i]['@_index']); - returnarray.push(search[k][2]); - } - if (returnarray.length > 0) { - returnarray2D.push(returnarray); + data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_code'] <= KeylayoutToKmnConverter.USED_KEYS_COUNT) { + returnarray2D.push([ + data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_code'], + this.map_UkeleleKC_To_VK(Number(data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_code'])), + data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_action'], + data.keyboard.keyMapSet[0].keyMap[i]['@_index'], + search[k][2] + ]); } } } @@ -845,14 +859,12 @@ export class KeylayoutToKmnConverter { for (let i = 0; i < data.keyboard.actions.action.length; i++) { for (let j = 0; j < data.keyboard.actions.action[i].when.length; j++) { - const returnarray: string[] = []; if ((data.keyboard.actions.action[i].when[j]['@_state'] === search)) { - returnarray.push(data.keyboard.actions.action[i]['@_id']); - returnarray.push(data.keyboard.actions.action[i].when[j]['@_state']); - returnarray.push(data.keyboard.actions.action[i].when[j]['@_output']); - } - if (returnarray.length > 0) { - returnarray2D.push(returnarray); + returnarray2D.push([ + data.keyboard.actions.action[i]['@_id'], + data.keyboard.actions.action[i].when[j]['@_state'], + data.keyboard.actions.action[i].when[j]['@_output'] + ]); } } } @@ -872,23 +884,19 @@ export class KeylayoutToKmnConverter { if (!((search === undefined) || (search === null) || (search.length === 0))) { for (let i = 0; i < search.length; i++) { const behaviour: number = Number(search[i][3]); - for (let j = 0; j < data.keyboard.modifierMap.keyMapSelect[behaviour].modifier.length; j++) { - const returnarray1D: string[] = []; - // KeyName - returnarray1D.push(String(search[i][1])); - // actionId - returnarray1D.push(String(search[i][2])); - // behaviour - returnarray1D.push(String(search[i][3])); - // modifier - returnarray1D.push(String(this.create_kmn_modifier(data.keyboard.modifierMap.keyMapSelect[behaviour].modifier[j]['@_keys'], isCAPSused))); - // output - returnarray1D.push(String(search[i][4])); - - if (returnarray1D.length > 0) { - returnarray.push(returnarray1D); - } + returnarray.push([ + // KeyName + String(search[i][1]), + // actionId + String(search[i][2]), + // behaviour + String(search[i][3]), + // modifier + String(this.create_kmn_modifier(data.keyboard.modifierMap.keyMapSelect[behaviour].modifier[j]['@_keys'], isCAPSused)), + // output + String(search[i][4]) + ]); } } } @@ -924,24 +932,19 @@ export class KeylayoutToKmnConverter { // loop behaviors (in ukelele it is possible to define multiple modifier combinations that behave in the same) for (let i = 0; i < data.keyboard.keyMapSet[0].keyMap.length; i++) { - for (let j = 0; j < KeylayoutToKmnConverter.USED_KEYS_COUNT; j++) { + for (let j = 0; j <= KeylayoutToKmnConverter.USED_KEYS_COUNT; j++) { if (data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_action'] === search) { for (let k = 0; k < modi[data.keyboard.keyMapSet[0].keyMap[i]['@_index']].length; k++) { - const returnarray: string[] = []; const behaviour: string = data.keyboard.keyMapSet[0].keyMap[i]['@_index']; const modifierkmn: string = this.create_kmn_modifier(modi[behaviour][k], isCapsused); const keyName: string = this.map_UkeleleKC_To_VK(Number(data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_code'])); - - returnarray.push(search); - returnarray.push(outchar); - returnarray.push(data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_action']); - returnarray.push(behaviour); - returnarray.push(keyName); - returnarray.push(modifierkmn); - - if (returnarray.length > 0) { - returnarray2D.push(returnarray); - } + returnarray2D.push([ + search, + outchar, + data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_action'], + behaviour, + keyName, + modifierkmn]); } } } @@ -970,19 +973,19 @@ export class KeylayoutToKmnConverter { public get_KeyModifier_array__From__ActionID(data: any, search: string): number[][] { const mapIndexArray_2D: number[][] = []; for (let i = 0; i < data.keyboard.keyMapSet[0].keyMap.length; i++) { - for (let j = 0; j < KeylayoutToKmnConverter.USED_KEYS_COUNT; j++) { - const mapIndexArrayperKey: number[] = []; + for (let j = 0; j <= KeylayoutToKmnConverter.USED_KEYS_COUNT; j++) { if (data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_action'] === search) { - mapIndexArrayperKey.push(data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_code']); - mapIndexArrayperKey.push(i); - } - if (mapIndexArrayperKey.length > 0) { - mapIndexArray_2D.push(mapIndexArrayperKey); + mapIndexArray_2D.push([data.keyboard.keyMapSet[0].keyMap[i].key[j]['@_code'], i]); } } } return mapIndexArray_2D; } + + /** @internal */ + public convert_bound = { + convert: this.convert.bind(this), + }; } /** diff --git a/developer/src/kmc-convert/src/keylayout-to-kmn/kmn-file-writer.ts b/developer/src/kmc-convert/src/keylayout-to-kmn/kmn-file-writer.ts index ed887292cc..35a76d5977 100644 --- a/developer/src/kmc-convert/src/keylayout-to-kmn/kmn-file-writer.ts +++ b/developer/src/kmc-convert/src/keylayout-to-kmn/kmn-file-writer.ts @@ -1,6 +1,8 @@ /* * Keyman is 2025 copyright (C) SIL International. MIT License. * + * Created by S. Schmitt on 2025-05-12 + * * Write Keyman .kmn files * */ @@ -9,6 +11,7 @@ import { CompilerCallbacks, CompilerOptions } from "@keymanapp/developer-utils"; import { KeylayoutToKmnConverter, convert_object, Rule } from './keylayout-to-kmn-converter.js'; import { util } from '@keymanapp/common-types'; import { ConverterMessages } from '../converter-messages.js'; +import KEYMAN_VERSION from "@keymanapp/keyman-version"; export class KmnFileWriter { @@ -34,12 +37,46 @@ export class KmnFileWriter { this.callbacks.fs.writeFileSync(data_ukelele.kmn_filename, new TextEncoder().encode(data)); return true; } catch (err) { - //console.log('ERROR writing kmn file:' + err.message); this.callbacks.reportMessage(ConverterMessages.Error_OutputFilenameIsRequired()); return false; } } + public writeToString(data_ukelele: convert_object): string { + let data: string = "\n"; + + // add top part of kmn file: STORES + data += this.writeData_Stores(data_ukelele); + + // add bottom part of kmn file: RULES + data += this.writeData_Rules(data_ukelele); + + try { + return data; + } catch (err) { + this.callbacks.reportMessage(ConverterMessages.Error_OutputFilenameIsRequired()); + return null; + } + } + + public writeToUint8Array(data_ukelele: convert_object): Uint8Array { + let data: string = "\n"; + + // add top part of kmn file: STORES + data += this.writeData_Stores(data_ukelele); + + // add bottom part of kmn file: RULES + data += this.writeData_Rules(data_ukelele); + + try { + return new TextEncoder().encode(data); + } catch (err) { + this.callbacks.reportMessage(ConverterMessages.Error_OutputFilenameIsRequired()); + return null; + } + } + + /** * @brief member function to create data for stores that will be printed to the resulting kmn file * @param data_ukelele an object containing all data read from a .keylayout file @@ -51,16 +88,13 @@ export class KmnFileWriter { data += "c ..................................................................................................................\n"; data += "c ..................................................................................................................\n"; - data += "c Keyman keyboard generated by kmn-convert version: " + KeylayoutToKmnConverter.KMC_CONVERT_VERSION + "\n"; + data += "c Keyman keyboard generated by kmn-convert version: " + KEYMAN_VERSION.VERSION + "\n"; data += "c from Ukelele file: " + data_ukelele.keylayout_filename + "\n"; data += "c ..................................................................................................................\n"; data += "c ..................................................................................................................\n"; data += "\n"; - data += "store(&VERSION) \'10.0\'\n"; - data += "store(&TARGETS) \'any\'\n"; - data += "store(&KEYBOARDVERSION) \'1.0\'\n"; - data += "store(©RIGHT) '© 2024 SIL International\'\n"; + data += "store(&TARGETS) \'desktop\'\n"; data += "\n"; data += "begin Unicode > use(main)\n\n"; @@ -116,7 +150,7 @@ export class KmnFileWriter { // lookup key nr of the key which is being processed let keyNr: number = 0; - for (let j = 0; j < KeylayoutToKmnConverter.USED_KEYS_COUNT; j++) { + for (let j = 0; j <= KeylayoutToKmnConverter.USED_KEYS_COUNT; j++) { if (keylayoutKmnConverter.map_UkeleleKC_To_VK(j) === unique_data_Rules[k].key) { keyNr = j; break; @@ -135,20 +169,34 @@ export class KmnFileWriter { data += '\n'; } + // use of Unicode Character vs Unicode Codepoint; + // If it`s a ctrl character we print out the Unicode Codepoint else we print out the Unicode Character + let version_output_character; const warn_text = this.reviewRules(unique_data_Rules, k); const output_character = new TextDecoder().decode(unique_data_Rules[k].output); - const output_character_unicode = util.convertToUnicodeCodePoint(output_character); + const output_Unicode_Character = util.convertToUnicodeCharacter(output_character); + const output_Unicode_CodePoint = util.convertToUnicodeCodePoint(output_character); - // if we are about to print a unicode codepoint instead of a single character we need to check if it is a control character and add a warning - if ((output_character_unicode.length > 1) - && (Number("0x" + output_character_unicode.substring(2, output_character_unicode.length)) < KeylayoutToKmnConverter.MAX_CTRL_CHARACTER)) { - if (warn_text[2] == "") - warn_text[2] = warn_text[2] + "c WARNING: use of a control character "; - else - warn_text[2] = warn_text[2] + " Use of a control character "; + if ((output_Unicode_Character !== undefined) && (output_Unicode_CodePoint !== undefined)) { + + // if we are about to print a unicode codepoint instead of a single character we need to check if it is a control character + if ((Number("0x" + output_Unicode_CodePoint.substring(2, output_Unicode_CodePoint.length)) < KeylayoutToKmnConverter.MAX_CTRL_CHARACTER)) { + + version_output_character = output_Unicode_CodePoint; + + if (output_Unicode_CodePoint.length > 1) { + if (warn_text[2] == "") { + warn_text[2] = warn_text[2] + "c WARNING: use of a control character " /*+ output_Unicode_CodePoint*/; + } + else { + warn_text[2] = warn_text[2] + " Use of a control character "/* + output_Unicode_CodePoint*/; + } + } + } else { + version_output_character = output_Unicode_Character; + } } - // add a warning in front of rules in case unavailable modifiers or ambiguous rules are used // if warning contains duplicate rules we do not write out the entire rule // (even if there are other warnings for the same rule) since that rule had been written before @@ -164,7 +212,7 @@ export class KmnFileWriter { + "+ [" + (unique_data_Rules[k].modifier_key + ' ' + unique_data_Rules[k].key).trim() + `] > \'` - + output_character_unicode + + version_output_character + '\'\n'; } } @@ -176,18 +224,32 @@ export class KmnFileWriter { if (unique_data_Rules[k].rule_type === "C2") { + // use of Unicode Character vs Unicode Codepoint; + // If it`s a ctrl character we print out the Unicode Codepoint else we print out the Unicode Character + let version_output_character; const warn_text = this.reviewRules(unique_data_Rules, k); const output_character = new TextDecoder().decode(unique_data_Rules[k].output); - const output_character_unicode = util.convertToUnicodeCodePoint(output_character); + const output_Unicode_Character = util.convertToUnicodeCharacter(output_character); + const output_Unicode_CodePoint = util.convertToUnicodeCodePoint(output_character); - // if we are about to print a unicode codepoint instead of a single character we need to check if it is a control character and add a warning - if ((output_character_unicode.length > 1) - && (Number("0x" + output_character_unicode.substring(2, output_character_unicode.length)) < KeylayoutToKmnConverter.MAX_CTRL_CHARACTER)) { - if (warn_text[2] == "") - warn_text[2] = warn_text[2] + "c WARNING: use of a control character "; - else - warn_text[2] = warn_text[2] + "; Use of a control character "; + if ((output_Unicode_Character !== undefined) && (output_Unicode_CodePoint !== undefined)) { + // if we are about to print a unicode codepoint instead of a single character we need to check if it is a control character + if (Number("0x" + output_Unicode_Character.substring(2, output_Unicode_Character.length)) < KeylayoutToKmnConverter.MAX_CTRL_CHARACTER) { + + version_output_character = output_Unicode_CodePoint; + if (output_Unicode_Character.length > 1) { + if (warn_text[2] == "") { + warn_text[2] = warn_text[2] + "c WARNING: use of a control character "; + } + else { + warn_text[2] = warn_text[2] + "; Use of a control character "; + } + } + + } else { + version_output_character = output_Unicode_Character; + } } // add a warning in front of rules in case unavailable modifiers or ambiguous rules are used @@ -223,7 +285,7 @@ export class KmnFileWriter { + unique_data_Rules[k].modifier_key).trim() + " " + unique_data_Rules[k].key + "] > \'" - + output_character_unicode + + version_output_character + "\'\n"; } data += "\n"; @@ -236,27 +298,40 @@ export class KmnFileWriter { for (let k = 0; k < unique_data_Rules.length; k++) { if (unique_data_Rules[k].rule_type === "C3") { + // use of Unicode Character vs Unicode Codepoint; + // If it`s a ctrl character we print out the Unicode Codepoint else we print out the Unicode Character + let version_output_character; + const warn_text = this.reviewRules(unique_data_Rules, k); - const output_character = new TextDecoder().decode(unique_data_Rules[k].output); - const output_character_unicode = util.convertToUnicodeCodePoint(output_character); + const output_Unicode_Character = util.convertToUnicodeCharacter(output_character); + const output_Unicode_CodePoint = util.convertToUnicodeCodePoint(output_character); - // if we are about to print a unicode codepoint instead of a single character we need to check if it is a control character and add a warning - if ((output_character_unicode.length > 1) - && (Number("0x" + output_character_unicode.substring(2, output_character_unicode.length)) < KeylayoutToKmnConverter.MAX_CTRL_CHARACTER)) { - if (warn_text[2] == "") - warn_text[2] = warn_text[2] + "c WARNING: use of a control character "; - else - warn_text[2] = warn_text[2] + "; Use of a control character "; + if ((output_Unicode_Character !== undefined) && (output_Unicode_CodePoint !== undefined)) { + // if we are about to print a unicode codepoint instead of a single character we need to check if a control character is to be used + if (Number("0x" + output_Unicode_Character.substring(2, output_Unicode_Character.length)) < KeylayoutToKmnConverter.MAX_CTRL_CHARACTER) { + + version_output_character = output_Unicode_CodePoint; + + if (output_Unicode_Character.length > 1) { + if (warn_text[2] == "") { + warn_text[2] = warn_text[2] + "c WARNING: use of a control character "; + } + else { + warn_text[2] = warn_text[2] + "; Use of a control character "; + } + } + } else { + version_output_character = output_Unicode_Character; + } } - // add a warning in front of rules in case unavailable modifiers or ambiguous rules are used // if warning contains duplicate rules we do not write out the entire rule // (even if there are other warnings for the same rule) since that rule had been written before if ((warn_text[0].indexOf("duplicate") < 0)) { - let warningTextToWrite = ""; + if (!KeylayoutToKmnConverter.SKIP_COMMENTED_LINES && (warn_text[0].length > 0)) { warningTextToWrite = warn_text[0]; } @@ -305,7 +380,7 @@ export class KmnFileWriter { + " " + unique_data_Rules[k].key + "] > \'" - + output_character_unicode + + version_output_character + "\'\n"; } } diff --git a/developer/src/kmc-convert/test/keylayout-to-kmn-converter.tests.ts b/developer/src/kmc-convert/test/keylayout-to-kmn-converter.tests.ts index 12053b83b2..d48007029f 100644 --- a/developer/src/kmc-convert/test/keylayout-to-kmn-converter.tests.ts +++ b/developer/src/kmc-convert/test/keylayout-to-kmn-converter.tests.ts @@ -1,6 +1,8 @@ /* * Keyman is 2025 copyright (C) SIL International. MIT License. * + * Created by S. Schmitt on 2025-05-12 + * * Tests for KeylayoutToKmnConverter, KeylayoutFileReader, KmnFileWriter * */ @@ -10,6 +12,7 @@ import { assert } from 'chai'; import { compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './helpers/index.js'; import { KeylayoutToKmnConverter } from '../src/keylayout-to-kmn/keylayout-to-kmn-converter.js'; import { KeylayoutFileReader } from '../src/keylayout-to-kmn/keylayout-file-reader.js'; +import { ConverterMessages } from '../src/converter-messages.js'; import * as NodeAssert from 'node:assert'; describe('KeylayoutToKmnConverter', function () { @@ -58,51 +61,74 @@ describe('KeylayoutToKmnConverter', function () { describe('run() ', function () { + + // _S2 can I do this??? - some tests of the run() function might take longer than 2000 ms + // see https://pramod-mallick.medium.com/mocha-with-selenium-webdriver-exception-timeout-of-2000ms-exceeded-89df7b6230c6 + this.timeout(10000); + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); it('run() should throw on null input file name and null output file name', async function () { // note, could use 'chai as promised' library to make this more fluent: - await NodeAssert.rejects(async () => sut.run(null, null)); + const result = sut.run(null, null); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 1); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_FileNotFound({ inputFilename: null })); }); it('run() should throw on null input file name and empty output file name', async function () { - await NodeAssert.rejects(async () => sut.run(null, '')); + const result = sut.run(null, ''); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 1); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_FileNotFound({ inputFilename: null })); }); + it('run() should throw on null input file name and unknown output file name', async function () { - await NodeAssert.rejects(async () => sut.run(null, 'X')); + const result = sut.run(null, 'X'); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 1); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_FileNotFound({ inputFilename: null })); }); it('run() should throw on unavailable input file name and null output file name', async function () { - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Unavailable.keylayout'); - await NodeAssert.rejects(async () => sut.run(inputFilename, null)); + const inputFilename = makePathToFixture('../data/Unavailable.keylayout'); + const result = sut.run(inputFilename, null); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 2); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_FileNotFound({ inputFilename: inputFilename })); + assert.deepEqual(compilerTestCallbacks.messages[1], ConverterMessages.Error_UnableToRead({ inputFilename: inputFilename })); }); it('run() should return on correct input file name and empty output file name ', async function () { - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); await NodeAssert.doesNotReject(async () => sut.run(inputFilename, '')); }); it('run() should return on correct input file name and null output file name', async function () { - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); await NodeAssert.doesNotReject(async () => sut.run(inputFilename, null)); }); it('run() should return on correct input file name and given output file name ', async function () { - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); - const outputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/OutputName.kmn'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); + const outputFilename = makePathToFixture('../data/OutputName.kmn'); await NodeAssert.doesNotReject(async () => sut.run(inputFilename, outputFilename)); }); it('run() should throw on incorrect input file extention and output file extention', async function () { - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test_command.A'); - const outputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/data/OutputXName.B'); - await NodeAssert.rejects(async () => sut.run(inputFilename, outputFilename)); + const inputFilename = makePathToFixture('../data/Test_command.A'); + const outputFilename = makePathToFixture('../data/data/OutputXName.B'); + const result = sut.run(inputFilename, outputFilename); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 2); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_FileNotFound({ inputFilename: inputFilename })); + assert.deepEqual(compilerTestCallbacks.messages[1], ConverterMessages.Error_UnableToRead({ inputFilename: inputFilename })); }); it('run() return on correct input file extention and unsupperted output file extention', async function () { - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); - const outputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/OutputXName.B'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); + const outputFilename = makePathToFixture('../data/OutputXName.B'); await NodeAssert.doesNotReject(async () => sut.run(inputFilename, outputFilename)); }); }); @@ -110,19 +136,19 @@ describe('KeylayoutToKmnConverter', function () { describe('convert() ', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const read = sut_r.read(inputFilename); - const converted = sut.convert(read); + const converted = sut.convert_bound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); // empty convert_object from unavailable file name const inputFilename_unavailable = makePathToFixture('X.keylayout'); const read_unavailable = sut_r.read(inputFilename_unavailable); - const converted_unavailable = sut.convert(read_unavailable); + const converted_unavailable = sut.convert_bound.convert(read_unavailable, inputFilename_unavailable.replace(/\.keylayout$/, '.kmn')); // empty convert_object from empty filename const inputFilename_empty = makePathToFixture(''); const read_empty = sut_r.read(inputFilename_empty); - const converted_empty = sut.convert(read_empty); + const converted_empty = sut.convert_bound.convert(read_empty, inputFilename_empty.replace(/\.keylayout$/, '.kmn')); it('should return converted array on correct input', async function () { assert.isTrue(converted.arrayOf_Rules.length !== 0); @@ -141,22 +167,22 @@ describe('KeylayoutToKmnConverter', function () { }); it('should return empty on only modifiers as input', async function () { - const converted_mod = sut.convert({ + const converted_mod = sut.convert_bound.convert({ keylayout_filename: '', arrayOf_Modifiers: [['caps'], ['Shift'], ['command']], arrayOf_Rules: [] - }); + }, ''); assert.isTrue((converted_mod.keylayout_filename === '' && converted_mod.arrayOf_Modifiers.length === 0 && converted_mod.arrayOf_Rules.length === 0)); }); it('should return empty on only rules as input', async function () { - const converted_rule = sut.convert({ + const converted_rule = sut.convert_bound.convert({ keylayout_filename: '', arrayOf_Modifiers: [], arrayOf_Rules: [['C0', '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_A', 'A']] - }); + }, ''); assert.isTrue((converted_rule.keylayout_filename === '' && converted_rule.arrayOf_Modifiers.length === 0 && converted_rule.arrayOf_Rules.length === 0)); @@ -228,7 +254,6 @@ describe('KeylayoutToKmnConverter', function () { describe('map_UkeleleKC_To_VK ', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); - [ [0x00, 'K_A'], [0x31, 'K_SPACE'], @@ -239,6 +264,8 @@ describe('KeylayoutToKmnConverter', function () { [0x999, ''], [-1, ''], [null, ''], + [undefined, ''], + [, ''], ].forEach(function (values) { it(("map_UkeleleKC_To_VK(" + values[0] + ")").padEnd(26, " ") + "should return " + "'" + values[1] + "'", async function () { const result = sut.map_UkeleleKC_To_VK(values[0] as number); @@ -270,9 +297,9 @@ describe('KeylayoutToKmnConverter', function () { describe('get_Modifier_array__From__KeyModifier_array ', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const read = sut_r.read(inputFilename); - const converted = sut.convert(read); + const converted = sut.convert_bound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); [ [[['0', 0]], [['', 'shift? caps? ']]], @@ -283,8 +310,6 @@ describe('KeylayoutToKmnConverter', function () { [[['0', -999]], [null]], [[['0']], [null]], - - ].forEach(function (values) { it((values[1][0] !== null) ? ("get_Modifier_array__From__KeyModifier_array(['" + values[0][0][0] + "', '" + values[0][0][1] + "'])").padEnd(68, " ") + " should return ['" + values[1][0][0] + "', '" + values[1][0][1] + "']" : @@ -308,7 +333,7 @@ describe('KeylayoutToKmnConverter', function () { describe('get_KeyModifier_array__From__ActionID ', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const read = sut_r.read(inputFilename); [ @@ -341,7 +366,7 @@ describe('KeylayoutToKmnConverter', function () { describe('get_ActionID__From__ActionNext ', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const read = sut_r.read(inputFilename); [ @@ -374,7 +399,7 @@ describe('KeylayoutToKmnConverter', function () { describe('get_ActionIndex__From__ActionId ', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const read = sut_r.read(inputFilename); [ @@ -402,7 +427,7 @@ describe('KeylayoutToKmnConverter', function () { describe('get_Output__From__ActionId_None ', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const read = sut_r.read(inputFilename); [ @@ -435,7 +460,7 @@ describe('KeylayoutToKmnConverter', function () { describe('get_KeyMBehaviourModOutputArray__from__KeyActionBehaviourOutput_array ', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const read = sut_r.read(inputFilename); /*Italian: @@ -621,7 +646,7 @@ describe('KeylayoutToKmnConverter', function () { describe('get_ActionStateOutput_array__From__ActionState ', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const read = sut_r.read(inputFilename); /* @@ -693,9 +718,9 @@ describe('KeylayoutToKmnConverter', function () { describe('get_ActionOutputBehaviourKeyModi_From__ActionIDStateOutput ', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const read = sut_r.read(inputFilename); - const converted = sut.convert(read); + const converted = sut.convert_bound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); /* Italian: [ ['a1', 'A', true, [ ['a1', 'A', 'a1', '1', 'K_A', 'NCAPS SHIFT'], @@ -751,7 +776,7 @@ describe('KeylayoutToKmnConverter', function () { describe('get_KeyActionOutput_array__From__ActionStateOutput_array ', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const read = sut_r.read(inputFilename); /* Italian: const b6_actionId_arr = [ diff --git a/developer/src/kmc-convert/test/kmn-file-reader.tests.ts b/developer/src/kmc-convert/test/kmn-file-reader.tests.ts index bb46435974..c6bdcca108 100644 --- a/developer/src/kmc-convert/test/kmn-file-reader.tests.ts +++ b/developer/src/kmc-convert/test/kmn-file-reader.tests.ts @@ -1,6 +1,8 @@ /* * Keyman is 2025 copyright (C) SIL International. MIT License. * + * Created by S. Schmitt on 2025-05-12 + * * Tests for KeylayoutToKmnConverter, KeylayoutFileReader, KmnFileWriter * */ @@ -9,7 +11,6 @@ import 'mocha'; import { assert } from 'chai'; import { compilerTestCallbacks, makePathToFixture } from './helpers/index.js'; import { KeylayoutFileReader } from '../src/keylayout-to-kmn/keylayout-file-reader.js'; -import { KeylayoutToKmnConverter } from '../src/keylayout-to-kmn/keylayout-to-kmn-converter.js'; //----------------------------------------------------------------------------------------------------------------------- @@ -24,7 +25,7 @@ describe('KeylayoutFileReader', function () { const sut_r = new KeylayoutFileReader(compilerTestCallbacks); it('read() should return filled array on correct input', async function () { - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const result = sut_r.read(inputFilename); assert.isNotEmpty(result); }); @@ -45,13 +46,13 @@ describe('KeylayoutFileReader', function () { }); it('read() should return empty array on unavailable file name', async function () { - const inputFilename_unavailable = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/X.keylayout'); + const inputFilename_unavailable = makePathToFixture('../data/X.keylayout'); const result = sut_r.read(inputFilename_unavailable); assert.isNull(result); }); it('read() should return empty array on typo in path', async function () { - const result = sut_r.read(makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '|Test.keylayout')); + const result = sut_r.read(makePathToFixture('../data|Test.keylayout')); assert.isNull(result); }); }); diff --git a/developer/src/kmc-convert/test/kmn-file-writer.tests.ts b/developer/src/kmc-convert/test/kmn-file-writer.tests.ts index 2a8dfe9d3d..17c810ed1a 100644 --- a/developer/src/kmc-convert/test/kmn-file-writer.tests.ts +++ b/developer/src/kmc-convert/test/kmn-file-writer.tests.ts @@ -1,6 +1,8 @@ /* * Keyman is 2025 copyright (C) SIL International. MIT License. * + * Created by S. Schmitt on 2025-05-12 + * * Tests for KeylayoutToKmnConverter, KeylayoutFileReader, KmnFileWriter * */ @@ -12,6 +14,7 @@ import { compilerTestCallbacks, compilerTestOptions, makePathToFixture } from '. import { KeylayoutToKmnConverter } from '../src/keylayout-to-kmn/keylayout-to-kmn-converter.js'; import { KmnFileWriter } from '../src/keylayout-to-kmn/kmn-file-writer.js'; import { KeylayoutFileReader } from '../src/keylayout-to-kmn/keylayout-file-reader.js'; +import KEYMAN_VERSION from "@keymanapp/keyman-version"; describe('KmnFileWriter', function () { @@ -20,17 +23,17 @@ describe('KmnFileWriter', function () { }); describe("write() ", function () { - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); const sut_w = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); const read = sut_r.read(inputFilename); - const converted = sut.convert(read); + const converted = sut.convert_bound.convert(read, '/Test.keylayout'.replace(/\.keylayout$/, '.kmn')); // empty convert_object from unavailable file name - const inputFilename_unavailable = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/X.keylayout'); + const inputFilename_unavailable = makePathToFixture('../data/X.keylayout'); const read_unavailable = sut_r.read(inputFilename_unavailable); - const converted_unavailable = sut.convert(read_unavailable); + const converted_unavailable = sut.convert_bound.convert(read_unavailable, inputFilename_unavailable.replace(/\.keylayout$/, '.kmn')); it('write() should return true (no error) if written', async function () { @@ -45,18 +48,99 @@ describe('KmnFileWriter', function () { }); - describe("writeData_Rules() ", function () { - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + describe("writeToString() ", function () { + const inputFilename = makePathToFixture('../data/Test.keylayout'); const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); const sut_w = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); const read = sut_r.read(inputFilename); - const converted = sut.convert(read); + const converted = sut.convert_bound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + + const out_expected_first: string = "c ..................................................................................................................\n" + + "c ..................................................................................................................\n" + + "c Keyman keyboard generated by kmn-convert version: " + KEYMAN_VERSION.VERSION + "\n" + + "c from Ukelele file: "; + + const out_expected_last: string = "\n" + + "c ..................................................................................................................\n" + + "c ..................................................................................................................\n" + + "\n" + + "store(&TARGETS) 'desktop'\n" + + "\n" + + "begin Unicode > use(main)\n\n" + + "group(main) using keys\n\n" + + "\n"; // empty convert_object from unavailable file name - const inputFilename_unavailable = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/X.keylayout'); + //const inputFilename_unavailable = makePathToFixture('../data/X.keylayout'); + const inputFilename_unavailable = '/X.keylayout'; const read_unavailable = sut_r.read(inputFilename_unavailable); - const converted_unavailable = sut.convert(read_unavailable); + const converted_unavailable = sut.convert_bound.convert(read_unavailable, inputFilename_unavailable.replace(/\.keylayout$/, '.kmn')); + + it('writeToString() should return result', async function () { + const result = sut_w.writeToString(converted); + assert.isNotNull(result); + }); + + it('writeToString() should return header in case of missing inputfile', async function () { + const result = sut_w.writeToString(converted_unavailable); + assert.equal(result, ("\n" + out_expected_first + out_expected_last)); + }); + }); + + describe("writeToUint8Array() ", function () { + //const inputFilename = makePathToFixture('../data/Test.keylayout'); + const inputFilename = '/Test.keylayout'; + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sut_r = new KeylayoutFileReader(compilerTestCallbacks); + const sut_w = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); + const read = sut_r.read(inputFilename); + const converted = sut.convert_bound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + + const out_expected_first: string = + "c ..................................................................................................................\n" + + "c ..................................................................................................................\n" + + "c Keyman keyboard generated by kmn-convert version: " + KEYMAN_VERSION.VERSION + "\n" + + "c from Ukelele file: "; + + const out_expected_last: string = "\n" + + "c ..................................................................................................................\n" + + "c ..................................................................................................................\n" + + "\n" + + "store(&TARGETS) 'desktop'\n" + + "\n" + + "begin Unicode > use(main)\n\n" + + "group(main) using keys\n\n" + + "\n"; + + + // empty convert_object from unavailable file name + const inputFilename_unavailable = makePathToFixture('../data/X.keylayout'); + const read_unavailable = sut_r.read(inputFilename_unavailable); + const converted_unavailable = sut.convert_bound.convert(read_unavailable, inputFilename_unavailable.replace(/\.keylayout$/, '.kmn')); + it('writeToUint8Array() should return header in case of missing inputfile', async function () { + const result = sut_w.writeToUint8Array(converted_unavailable); + assert.equal(new TextDecoder().decode(result), ("\n" + out_expected_first + out_expected_last)); + }); + + it('writeToUint8Array() should return result', async function () { + const result = sut_w.writeToUint8Array(converted); + assert.isNotNull(result); + }); + }); + + describe("writeData_Rules() ", function () { + const inputFilename = makePathToFixture('../data/Test.keylayout'); + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sut_r = new KeylayoutFileReader(compilerTestCallbacks); + const sut_w = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); + const read = sut_r.read(inputFilename); + const converted = sut.convert_bound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + + // empty convert_object from unavailable file name + const inputFilename_unavailable = makePathToFixture('../data/X.keylayout'); + const read_unavailable = sut_r.read(inputFilename_unavailable); + const converted_unavailable = sut.convert_bound.convert(read_unavailable, inputFilename_unavailable.replace(/\.keylayout$/, '.kmn')); it('writeData_Rules() should return true (no error) if written', async function () { const result = sut_w.writeData_Rules(converted); @@ -74,24 +158,24 @@ describe('KmnFileWriter', function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); const sut_w = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); + const inputFilename = makePathToFixture('../data/Test.keylayout'); const read = sut_r.read(inputFilename); - const converted = sut.convert(read); + const converted = sut.convert_bound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); // empty convert_object from unavailable file name - const inputFilename_unavailable = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/X.keylayout'); + const inputFilename_unavailable = makePathToFixture('../data/X.keylayout'); const read_unavailable = sut_r.read(inputFilename_unavailable); - const converted_unavailable = sut.convert(read_unavailable); + const converted_unavailable = sut.convert_bound.convert(read_unavailable, inputFilename_unavailable.replace(/\.keylayout$/, '.kmn')); // empty convert_object from empty filename const inputFilename_empty = makePathToFixture(''); const read_empty = sut_r.read(inputFilename_empty); - const converted_empty = sut.convert(read_empty); + const converted_empty = sut.convert_bound.convert(read_empty, inputFilename_empty.replace(/\.keylayout$/, '.kmn')); const out_expected_first: string = "c ..................................................................................................................\n" + "c ..................................................................................................................\n" - + "c Keyman keyboard generated by kmn-convert version: 0.1\n" + + "c Keyman keyboard generated by kmn-convert version: " + KEYMAN_VERSION.VERSION + "\n" + "c from Ukelele file: "; const out_expected_last: string = @@ -99,10 +183,7 @@ describe('KmnFileWriter', function () { + "c ..................................................................................................................\n" + "c ..................................................................................................................\n" + "\n" - + "store(&VERSION) '10.0'\n" - + "store(&TARGETS) 'any'\n" - + "store(&KEYBOARDVERSION) '1.0'\n" - + "store(©RIGHT) '© 2024 SIL International'\n" + + "store(&TARGETS) 'desktop'\n" + "\n" + "begin Unicode > use(main)\n\n" + "group(main) using keys\n\n" @@ -124,30 +205,6 @@ describe('KmnFileWriter', function () { }); }); - describe("reviewRules() ", function () { - const inputFilename = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/Test.keylayout'); - const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); - const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const sut_w = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); - const read = sut_r.read(inputFilename); - const converted = sut.convert(read); - - // empty convert_object from unavailable file name - const inputFilename_unavailable = makePathToFixture('../' + KeylayoutToKmnConverter.DATA_SUBFOLDER + '/X.keylayout'); - const read_unavailable = sut_r.read(inputFilename_unavailable); - const converted_unavailable = sut.convert(read_unavailable); - - it('write() should return true (no error) if written', async function () { - const result = sut_w.write(converted); - assert.isTrue(result); - }); - - it('write() should return false if no inputfile', async function () { - const result = sut_w.write(converted_unavailable); - assert.isFalse(result); - }); - }); - describe('convertToUnicodeCodePoint ', function () { [ ["􏘁", 'U+10F601'], @@ -176,4 +233,35 @@ describe('KmnFileWriter', function () { }); }); }); + + describe('convertToUnicodeCharacter ', function () { + [ + ["a", 'a'], + ["ሴ", 'ሴ'], + ["😎", '😎'], + ["�", undefined], + ["a", 'a'], + ["ሴ", 'ሴ'], + ["😆", '😆'], + ["󴉀", undefined], + ["U+0061;", 'a'], + ["U+1234;", 'ሴ'], + ["U+1F60E", '😎'], + ["U+1000000;", undefined], + ["@", undefined], + ["a", 'a'], + ["ሴ", 'ሴ'], + ['😎', '😎'], + ["W̊", "W̊"], + ["ab", undefined], + ["", ''], + [undefined, undefined], + [null, undefined] + ].forEach(function (values) { + it(('should convert "' + values[0] + '"').padEnd(25, " ") + 'to "' + values[1] + '"', async function () { + const result = util.convertToUnicodeCharacter(values[0] as string); + assert.equal(result, values[1]); + }); + }); + }); }); diff --git a/developer/src/kmc-copy/.eslintrc.cjs b/developer/src/kmc-copy/.eslintrc.cjs index 9c75f8f046..2bdbc1f145 100644 --- a/developer/src/kmc-copy/.eslintrc.cjs +++ b/developer/src/kmc-copy/.eslintrc.cjs @@ -14,8 +14,5 @@ module.exports = { files:"src/**/*.ts", extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } - ], - rules: { - "prefer-const": 1, - }, + ] }; diff --git a/developer/src/kmc-copy/package.json b/developer/src/kmc-copy/package.json index 35e587b5a3..130ada5681 100644 --- a/developer/src/kmc-copy/package.json +++ b/developer/src/kmc-copy/package.json @@ -32,12 +32,10 @@ "devDependencies": { "@keymanapp/developer-test-helpers": "*", "@keymanapp/resources-gosh": "*", - "@types/mocha": "^10.0.0", "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^10.0.0", "typescript": "^5.4.5" }, "mocha": { diff --git a/developer/src/kmc-copy/src/KeymanProjectCopier.ts b/developer/src/kmc-copy/src/KeymanProjectCopier.ts index a751e46731..545163aded 100644 --- a/developer/src/kmc-copy/src/KeymanProjectCopier.ts +++ b/developer/src/kmc-copy/src/KeymanProjectCopier.ts @@ -4,7 +4,23 @@ * Copy a keyboard or lexical model project */ -import { CloudUrls, GitHubUrls, CompilerCallbacks, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter, ValidIds, CompilerBaseOptions } from "@keymanapp/developer-utils"; +import { + CloudUrls, + GitHubUrls, + CompilerCallbacks, + KeymanCompiler, + KeymanCompilerArtifact, + KeymanCompilerArtifacts, + KeymanCompilerResult, + KeymanDeveloperProject, + KeymanDeveloperProjectOptions, + ProjectLoader, + KPJFileWriter, + KpsFileReader, + KpsFileWriter, + ValidIds, + CompilerBaseOptions, +} from "@keymanapp/developer-utils"; import { KeymanFileTypes } from "@keymanapp/common-types"; import { CopierMessages } from "./copier-messages.js"; @@ -124,7 +140,7 @@ export class KeymanProjectCopier implements KeymanCompiler { this.sourceId = this.callbacks.path.basename(projectSource, KeymanFileTypes.Source.Project); // copy project file - const project = await this.loadProjectFromFile(projectSource); + const project = await ProjectLoader.loadProjectFromFile(projectSource, this.callbacks, this.asyncCallbacks); if(!project) { // loadProjectFromFile already reported errors return null; @@ -637,31 +653,6 @@ export class KeymanProjectCopier implements KeymanCompiler { return this.callbacks.path.join(this.outPath, 'source'); } - private async loadProjectFromFile(filename: string): Promise { - const kpjData = await this.asyncCallbacks.fsAsync.readFile(filename); - const reader = new KPJFileReader(this.asyncCallbacks); - let kpj = null; - try { - kpj = reader.read(kpjData); - if(!kpj) { - this.callbacks.reportMessage(CopierMessages.Error_ProjectFileCouldNotBeRead({filename})); - return null; - } - - if(kpj.KeymanDeveloperProject?.Options?.Version && kpj.KeymanDeveloperProject.Options.Version != "1.0" && kpj.KeymanDeveloperProject.Options.Version != "2.0") { - this.callbacks.reportMessage(CopierMessages.Error_UnsupportedProjectVersion({filename, version: kpj.KeymanDeveloperProject.Options.Version})); - return null; - } - reader.validate(kpj); - } catch(e) { - this.callbacks.reportMessage(CopierMessages.Error_InvalidProjectFile({filename, message: (e ?? '').toString()})); - return null; - } - const project = await reader.transform(filename, kpj); - - return project; - } - /** * Write artifacts from a successful compile to disk, via callbacks methods. * The artifacts written will include all files from the project, across diff --git a/developer/src/kmc-copy/src/copier-messages.ts b/developer/src/kmc-copy/src/copier-messages.ts index 976002a35f..cb8d0c83de 100644 --- a/developer/src/kmc-copy/src/copier-messages.ts +++ b/developer/src/kmc-copy/src/copier-messages.ts @@ -92,22 +92,9 @@ export class CopierMessages { `The package source file '${def(o.filename)}' could not be loaded. The file may have an invalid format` ); - static ERROR_UnsupportedProjectVersion = SevError | 0x000D; - static Error_UnsupportedProjectVersion = (o:{filename: string, version: string}) => m(this.ERROR_UnsupportedProjectVersion, - `Project version ${def(o.version)} for '${def(o.filename)}' is not supported by this version of Keyman Developer` - ); - - static ERROR_InvalidProjectFile = SevError | 0x000E; - static Error_InvalidProjectFile = (o:{filename: string, message: string}) => m( - this.ERROR_InvalidProjectFile, - `Project file '${def(o.filename)}' is not valid: ${def(o.message)}` - ); - - static ERROR_ProjectFileCouldNotBeRead = SevError | 0x000F; - static Error_ProjectFileCouldNotBeRead = (o:{filename: string}) => m( - this.ERROR_ProjectFileCouldNotBeRead, - `Project file '${def(o.filename)}' could not be read` - ); + // 0x000D ERROR_UnsupportedProjectVersion: moved to developer-utils-messages + // 0x000E ERROR_InvalidProjectFile: moved to developer-utils-messages + // 0x000F ERROR_ProjectFileCouldNotBeRead: moved to developer-utils-messages static INFO_DryRun = SevInfo | 0x0010; static Info_DryRun = (o:{outPath: string}) => m( diff --git a/developer/src/kmc-copy/tsconfig.json b/developer/src/kmc-copy/tsconfig.json index 1fc0a249be..e54ad9adaa 100644 --- a/developer/src/kmc-copy/tsconfig.json +++ b/developer/src/kmc-copy/tsconfig.json @@ -9,6 +9,7 @@ "paths": { "@keymanapp/keyman-version": ["../../../common/web/keyman-version/keyman-version.mts"], "@keymanapp/common-types": ["../../../common/web/types/src/main"], + "@keymanapp/developer-utils": ["../common/web/utils"], }, }, @@ -21,5 +22,6 @@ "references": [ { "path": "../../../common/web/keyman-version" }, { "path": "../../../common/web/types/" }, + { "path": "../common/web/utils/" }, ] } diff --git a/developer/src/kmc-generate/.eslintrc.cjs b/developer/src/kmc-generate/.eslintrc.cjs index c0198b8452..3d8949501f 100644 --- a/developer/src/kmc-generate/.eslintrc.cjs +++ b/developer/src/kmc-generate/.eslintrc.cjs @@ -14,8 +14,5 @@ module.exports = { files:"src/**/*.ts", extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } - ], - rules: { - "prefer-const": 1, - }, + ] }; diff --git a/developer/src/kmc-generate/package.json b/developer/src/kmc-generate/package.json index daf4f92922..da581a127e 100644 --- a/developer/src/kmc-generate/package.json +++ b/developer/src/kmc-generate/package.json @@ -33,12 +33,10 @@ "devDependencies": { "@keymanapp/developer-test-helpers": "*", "@keymanapp/resources-gosh": "*", - "@types/mocha": "^10.0.0", "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^10.0.0", "typescript": "^5.4.5" }, "mocha": { diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kps b/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kps index 677104a39a..acc50b6d48 100644 --- a/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kps +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kps @@ -38,9 +38,7 @@ $KVKS: - $NAME $ID - $VERSION $PACKAGE_LANGUAGES diff --git a/developer/src/kmc-generate/src/template/ldml-keyboard/source/keyboard.kps b/developer/src/kmc-generate/src/template/ldml-keyboard/source/keyboard.kps index 274bee3c82..6afa4cf2f5 100644 --- a/developer/src/kmc-generate/src/template/ldml-keyboard/source/keyboard.kps +++ b/developer/src/kmc-generate/src/template/ldml-keyboard/source/keyboard.kps @@ -35,9 +35,7 @@ - $NAME $ID - $VERSION $PACKAGE_LANGUAGES diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps index 1e05660d2a..9b27d10a1b 100644 --- a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps @@ -35,9 +35,7 @@ - Sample Project sample - 1.0 English diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps index 1e05660d2a..9b27d10a1b 100644 --- a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps +++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps @@ -35,9 +35,7 @@ - Sample Project sample - 1.0 English diff --git a/developer/src/kmc-keyboard-info/package.json b/developer/src/kmc-keyboard-info/package.json index b97cdcc22a..13af66aae0 100644 --- a/developer/src/kmc-keyboard-info/package.json +++ b/developer/src/kmc-keyboard-info/package.json @@ -33,7 +33,6 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "typescript": "^5.4.5" }, "mocha": { diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts index ddf0f2bcb2..046015e518 100644 --- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts +++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler-messages.ts @@ -3,7 +3,7 @@ import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m const Namespace = CompilerErrorNamespace.KeyboardInfoCompiler; // const SevInfo = CompilerErrorSeverity.Info | Namespace; const SevHint = CompilerErrorSeverity.Hint | Namespace; -// const SevWarn = CompilerErrorSeverity.Warn | Namespace; +const SevWarn = CompilerErrorSeverity.Warn | Namespace; const SevError = CompilerErrorSeverity.Error | Namespace; const SevFatal = CompilerErrorSeverity.Fatal | Namespace; @@ -63,8 +63,29 @@ export class KeyboardInfoCompilerMessages { `The Info.Description field in the package ${def(o.filename)} is required, but is missing or empty.`); static HINT_ScriptDoesNotMatch = SevHint | 0x0011; - static Hint_ScriptDoesNotMatch = (o:{script: string, bcp47:string, commonScript: string}) => m( + static Hint_ScriptDoesNotMatch = (o:{script: string, bcp47:string, commonScript: string, firstBcp47: string}) => m( this.HINT_ScriptDoesNotMatch, - `The script '${def(o.script)}' associated with language tag '${def(o.bcp47)}' does not match the script '${def(o.commonScript)}' for the first language in the package.`); + `The script '${def(o.script)}' associated with language tag '${def(o.bcp47)}' does not match the script '${def(o.commonScript)}' for the first language ('${def(o.firstBcp47)}') in the package.`, + ); + + static WARN_LanguageTagNotFound = SevWarn | 0x0012; + static Warn_LanguageTagNotFound = (o:{bcp47:string, langSubtag:string}) => m( + this.WARN_LanguageTagNotFound, + `Neither the listed BCP 47 tag '${def(o.bcp47)}' nor the language subtag '${def(o.langSubtag)}' were found in langtags.json`, + `The [langtags.json dataset](https://github.com/silnrsi/langtags?tab=readme-ov-file) lists all + the registered language tags. If a tag is not found, then it will not be + searchable in the Keyman keyboard catalogue, and it may indicate that the + tag is incorrect.` + ); + + static WARN_LanguageTagNotFound2 = SevWarn | 0x0013; + static Warn_LanguageTagNotFound2 = (o:{bcp47:string}) => m( + this.WARN_LanguageTagNotFound2, + `The listed BCP 47 tag '${def(o.bcp47)}' was not found in langtags.json`, + `The [langtags.json dataset](https://github.com/silnrsi/langtags?tab=readme-ov-file) lists all + the registered language tags. If a tag is not found, then it will not be + searchable in the Keyman keyboard catalogue, and it may indicate that the + tag is incorrect.` + ); } diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts index 49da3d6684..617b8c131e 100644 --- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts +++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts @@ -4,7 +4,7 @@ */ import { minKeymanVersion } from "./min-keyman-version.js"; -import { KeyboardInfoFile, KeyboardInfoFileIncludes, KeyboardInfoFileLanguageFont, KeyboardInfoFilePlatform } from "./keyboard-info-file.js"; +import { KeyboardInfoFile, KeyboardInfoFileIncludes, KeyboardInfoFileLanguage, KeyboardInfoFileLanguageFont, KeyboardInfoFilePlatform } from "./keyboard-info-file.js"; import { KeymanFileTypes, KmpJsonFile, KmxFileReader, KMX, KeymanTargets } from "@keymanapp/common-types"; import { KeyboardInfoCompilerMessages } from "./keyboard-info-compiler-messages.js"; import { getLangtagByTag } from "@keymanapp/langtags"; @@ -473,6 +473,7 @@ export class KeyboardInfoCompiler implements KeymanCompiler { } let commonScript = null; + let firstBcp47 = null; for(const bcp47 of Object.keys(keyboard_info.languages)) { const language = keyboard_info.languages[bcp47]; @@ -526,49 +527,97 @@ export class KeyboardInfoCompiler implements KeymanCompiler { } } - // - // Add locale description - // - const locale = new Intl.Locale(bcp47); - // DisplayNames.prototype.of will throw a RangeError if it doesn't understand - // the format of the bcp47 tag. This happens with Node 18.14.1, for example, with: - // new Intl.DisplayNames(['en'], {type: 'language'}).of('und-fonipa'); - const mapName = (code: string, dict: Intl.DisplayNames) => { - try { - return dict.of(code); - } catch(e) { - if(e instanceof RangeError) { - return code; - } else { - throw e; - } - } - } - const tag = getLangtagByTag(bcp47) ?? getLangtagByTag(locale.language); - language.languageName = tag ? tag.name : bcp47; - language.regionName = mapName(locale.region, regionNames); - language.scriptName = mapName(locale.script, scriptNames); + ({ commonScript, firstBcp47 } = this.fillLanguageMetadata(language, bcp47, commonScript, firstBcp47)); - language.displayName = language.languageName + ( - (language.scriptName && language.regionName) ? - ` (${language.scriptName}, ${language.regionName})` : - language.scriptName ? - ` (${language.scriptName})` : - language.regionName ? - ` (${language.regionName})` : - '' - ); + } + return true; + } - const resolvedScript = locale.script ?? getLangtagByTag(bcp47)?.script ?? getLangtagByTag(locale.language)?.script ?? undefined; - if(commonScript === null) { - commonScript = resolvedScript; - } else { - if(resolvedScript !== commonScript) { - this.callbacks.reportMessage(KeyboardInfoCompilerMessages.Hint_ScriptDoesNotMatch({commonScript, bcp47, script: resolvedScript})) + private fillLanguageMetadata(language: KeyboardInfoFileLanguage, bcp47: string, commonScript: string, firstBcp47: string) { + // + // Add locale description + // + + // #13160: Workaround for missing 'und' language tag. + // + // `(new Intl.Locale('und')).language` returns `undefined` in V8, as of 31 + // March 2025. This means we cannot rely on `Intl.Locale` to parse the bcp47 + // string for us. The implemented workaround is to replace `und` with a + // known-good language subtag, and then swap it back out in later + // processing. + const isUndefined = bcp47.startsWith('und'); + const searchBcp47 = isUndefined ? ('mul' + bcp47.substring(3)) : bcp47; + const locale = new Intl.Locale(searchBcp47); + const localeLanguage = isUndefined ? 'und' : locale.language; + + // DisplayNames.prototype.of will throw a RangeError if it doesn't understand + // the format of the bcp47 tag. This happens with Node 18.14.1, for example, with: + // new Intl.DisplayNames(['en'], {type: 'language'}).of('und-fonipa'); + const mapName = (code: string, dict: Intl.DisplayNames) => { + try { + return dict.of(code); + } catch(e) { + if(e instanceof RangeError) { + return code; + } else { + throw e; } } } - return true; + const tag = getLangtagByTag(bcp47) ?? getLangtagByTag(localeLanguage); + language.languageName = tag ? tag.name : bcp47; + language.regionName = mapName(locale.region, regionNames); + language.scriptName = mapName(locale.script, scriptNames); + + language.displayName = language.languageName + ( + (language.scriptName && language.regionName) ? + ` (${language.scriptName}, ${language.regionName})` : + language.scriptName ? + ` (${language.scriptName})` : + language.regionName ? + ` (${language.regionName})` : + '' + ); + + if(firstBcp47 === null) { + firstBcp47 = bcp47; + } + + if(tag === undefined) { + if(this.isPrivateUseLanguageSubtag(localeLanguage.toLowerCase())) { + // Private use language subtag [BCP 47 2.2.1.3]. TODO: consider a hint + } else { + if(bcp47 == localeLanguage) { + this.callbacks.reportMessage(KeyboardInfoCompilerMessages.Warn_LanguageTagNotFound2({bcp47})); + } else { + this.callbacks.reportMessage(KeyboardInfoCompilerMessages.Warn_LanguageTagNotFound({ + bcp47, + langSubtag: localeLanguage + })); + } + return { commonScript, firstBcp47 }; + } + } + + const resolvedScript = locale.script ?? tag?.script ?? undefined; + if(commonScript === null) { + commonScript = resolvedScript; + } else { + if(resolvedScript !== commonScript) { + this.callbacks.reportMessage(KeyboardInfoCompilerMessages.Hint_ScriptDoesNotMatch({ + commonScript: commonScript, + bcp47, + script: resolvedScript ?? '(undefined)', + firstBcp47 + })); + } + } + + return { commonScript, firstBcp47 }; + } + + private isPrivateUseLanguageSubtag(tag: string) { + return tag >= 'qaa' && tag <= 'qtz'; } /** @@ -620,4 +669,9 @@ export class KeyboardInfoCompiler implements KeymanCompiler { return result; } + /** @internal */ + public unitTestEndPoints = { + fillLanguageMetadata: this.fillLanguageMetadata.bind(this), + }; + } diff --git a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler-messages.tests.ts b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler-messages.tests.ts index 79e5c488bc..fcd9dfb8c1 100644 --- a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler-messages.tests.ts +++ b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler-messages.tests.ts @@ -296,6 +296,27 @@ describe('KeyboardInfoCompilerMessages', function () { it('should generate ERROR_DescriptionIsMissing if there is no description in .kps', async function() { await testMessage(KeyboardInfoCompilerMessages.ERROR_DescriptionIsMissing,'error_description_is_missing', false); }); + + // WARN_LanguageTagNotFound + + it('should generate WARN_LanguageTagNotFound if a language tag is not found (and no other subtags)', async function() { + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources: KHMER_ANGKOR_SOURCES})); + const result = compiler.unitTestEndPoints.fillLanguageMetadata({}, 'grk-Latn', null, null); + assert.isOk(result); + assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.WARN_LanguageTagNotFound)); + }); + + // WARN_LanguageTagNotFound2 + + it('should generate WARN_LanguageTagNotFound2 if a language tag is not found', async function() { + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources: KHMER_ANGKOR_SOURCES})); + const result = compiler.unitTestEndPoints.fillLanguageMetadata({}, 'grk', null, null); + assert.isOk(result); + assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.WARN_LanguageTagNotFound2)); + }); + }); function nodeCompilerMessage(ncb: TestCompilerCallbacks, code: number): string { diff --git a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts index 7b62b012ed..0bb17e539e 100644 --- a/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts +++ b/developer/src/kmc-keyboard-info/test/keyboard-info-compiler.tests.ts @@ -16,6 +16,12 @@ beforeEach(function() { callbacks.clear(); }); +afterEach(function() { + if(this.currentTest?.isFailed()) { + callbacks.printMessages(); + } +}); + const KHMER_ANGKOR_KPJ = makePathToFixture('khmer_angkor', 'khmer_angkor.kpj'); const KHMER_ANGKOR_JS = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); const KHMER_ANGKOR_KPS = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); @@ -836,3 +842,62 @@ describe('keyboard-info-compiler', function () { assert.deepEqual(result, KHMER_ANGKOR_DISPLAY_FONT_INFO); }); }); + +describe('fillLanguageMetadata', function() { + const tests: { bcp47: string, lang: KeyboardInfoFileLanguage, commonScript: string }[] = [ + + // 'und' language subtag + + { + bcp47: 'und', + lang: { languageName: 'Undetermined', regionName: undefined, scriptName: undefined, displayName: 'Undetermined' }, + commonScript: 'Zyyy', + }, + { + bcp47: 'und-Latn', + lang: { languageName: 'Undetermined', regionName: undefined, scriptName: 'Latin', displayName: 'Undetermined (Latin)' }, + commonScript: 'Latn', + }, + { + bcp47: 'und-MX', + lang: { languageName: 'Undetermined', regionName: 'Mexico', scriptName: undefined, displayName: 'Undetermined (Mexico)' }, + commonScript: 'Zyyy', + }, + { + bcp47: 'und-Khmr-MX', + lang: { languageName: 'Undetermined', regionName: 'Mexico', scriptName: 'Khmer', displayName: 'Undetermined (Khmer, Mexico)' }, + commonScript: 'Khmr', + }, + + // Private use subtags + + { + bcp47: 'qaa-Qabx-QQ', + lang: { languageName: 'qaa-Qabx-QQ', regionName: 'QQ', scriptName: 'Qabx', displayName: 'qaa-Qabx-QQ (Qabx, QQ)' }, + commonScript: 'Qabx', + } + ]; + + for(const test of tests) { + it(`should handle ${test.bcp47} correctly`, async function() { + const compiler = new KeyboardInfoCompiler(); + await compiler.init(callbacks, {sources: KHMER_ANGKOR_SOURCES}); + const lang: KeyboardInfoFileLanguage = {}; + const result = compiler.unitTestEndPoints.fillLanguageMetadata( + lang, + test.bcp47, + null, null + ); + assert.isOk(result); + assert.equal(result.firstBcp47, test.bcp47); + assert.equal(result.commonScript, test.commonScript); + + assert.equal(lang.languageName, test.lang.languageName); + assert.equal(lang.regionName, test.lang.regionName); + assert.equal(lang.scriptName, test.lang.scriptName); + assert.equal(lang.displayName, test.lang.displayName); + + assert.isEmpty(callbacks.messages); + }); + } +}); diff --git a/developer/src/kmc-kmn/.eslintrc.cjs b/developer/src/kmc-kmn/.eslintrc.cjs index 9fb322a1e4..45340a6bd2 100644 --- a/developer/src/kmc-kmn/.eslintrc.cjs +++ b/developer/src/kmc-kmn/.eslintrc.cjs @@ -8,8 +8,5 @@ module.exports = { extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } ], - ignorePatterns: ["test/fixtures/*"], - rules: { - "prefer-const": "off", // TODO: enable this once infrastructure is in place and cleanup the problem cases - }, + ignorePatterns: ["test/fixtures/*"] }; diff --git a/developer/src/kmc-kmn/package.json b/developer/src/kmc-kmn/package.json index 1f5b8bbe9b..34e076b5c0 100644 --- a/developer/src/kmc-kmn/package.json +++ b/developer/src/kmc-kmn/package.json @@ -39,7 +39,6 @@ "@types/sinon-chai": "^3.2.9", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "sinon-chai": "^3.7.0", "typescript": "^5.4.5" }, diff --git a/developer/src/kmc-kmn/src/compiler/compiler.ts b/developer/src/kmc-kmn/src/compiler/compiler.ts index 9859483fb2..21985ad09a 100644 --- a/developer/src/kmc-kmn/src/compiler/compiler.ts +++ b/developer/src/kmc-kmn/src/compiler/compiler.ts @@ -9,7 +9,8 @@ TODO: implement additional interfaces: import { VisualKeyboard, KvkFileReader, LdmlKeyboardTypes, KeymanFileTypes, KvkFileWriter } from '@keymanapp/common-types'; import { CompilerCallbacks, CompilerEvent, CompilerOptions, KeymanCompiler, KeymanCompilerArtifacts, - KeymanCompilerArtifactOptional, KeymanCompilerResult, KeymanCompilerArtifact, KvksFileReader + KeymanCompilerArtifactOptional, KeymanCompilerResult, KeymanCompilerArtifact, KvksFileReader, + CompilerError } from '@keymanapp/developer-utils'; import * as Osk from './osk.js'; import loadWasmHost from '../import/kmcmplib/wasm-host.js'; @@ -217,7 +218,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet } private copyWasmResult(wasm_result: any): KmnCompilerResult { - let result: KmnCompilerResult = { + const result: KmnCompilerResult = { // We cannot Object.assign or {...} on a wasm-defined object, so... artifacts: {}, extra: { @@ -229,10 +230,10 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet }, displayMap: null }; - for(let store of wasm_result.extra.stores) { + for(const store of wasm_result.extra.stores) { result.extra.stores.push({storeType: store.storeType, name: store.name, line: store.line}); } - for(let group of wasm_result.extra.groups) { + for(const group of wasm_result.extra.groups) { result.extra.groups.push({isReadOnly: group.isReadOnly, name: group.name}); } @@ -314,7 +315,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet } }); - let wasm_options = new Module.CompilerOptions(); + const wasm_options = new Module.CompilerOptions(); let wasm_result = null; try { wasm_options.saveDebug = options.saveDebug; @@ -454,9 +455,9 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e})); return null; } - let invalidVkeys: string[] = []; + const invalidVkeys: string[] = []; vk = reader.transform(kvks, invalidVkeys); - for(let invalidVkey of invalidVkeys) { + for(const invalidVkey of invalidVkeys) { this.callbacks.reportMessage(KmnCompilerMessages.Warn_InvalidVkeyInKvksFile({filename, invalidVkey})); } } @@ -470,7 +471,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet Osk.remapVisualKeyboard(vk, displayMap); } - let writer = new KvkFileWriter(); + const writer = new KvkFileWriter(); return { filename: this.callbacks.path.join(this.callbacks.path.dirname(kmxFilename), basename + KeymanFileTypes.Binary.VisualKeyboard), @@ -529,7 +530,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet * @param rangeCount - number of ranges to allocate * @returns UnicodeSet accessor object, or null on failure */ - public parseUnicodeSet(pattern: string, rangeCount: number) : LdmlKeyboardTypes.UnicodeSet | null { + public parseUnicodeSet(pattern: string, rangeCount: number, x?: any) : LdmlKeyboardTypes.UnicodeSet | null { if(!this.verifyInitialized()) { /* c8 ignore next 2 */ // verifyInitialized will set a callback if needed @@ -562,7 +563,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet // rc is negative: it's an error code. this.wasmExports.free(buf); // translate error code into callback - this.callbacks.reportMessage(getUnicodeSetError(rc)); + this.callbacks.reportMessage(getUnicodeSetError(rc, x)); return null; } } @@ -570,7 +571,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet /** * @internal */ - public sizeUnicodeSet(pattern: string) : number { + public sizeUnicodeSet(pattern: string, x?: any) : number { if(!this.verifyInitialized()) { /* c8 ignore next 2 */ return null; @@ -582,7 +583,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet if (rc >= 0) { return rc; } else { - this.callbacks.reportMessage(getUnicodeSetError(rc)); + this.callbacks.reportMessage(getUnicodeSetError(rc, x)); return -1; } } @@ -593,7 +594,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet * @param rc parseUnicodeSet error code * @returns the compiler event */ -function getUnicodeSetError(rc: number) : CompilerEvent { +function getUnicodeSetError(rc: number, x?: any) : CompilerEvent { // from kmcmplib.h const KMCMP_ERROR_SYNTAX_ERR = -1; const KMCMP_ERROR_HAS_STRINGS = -2; @@ -601,16 +602,16 @@ function getUnicodeSetError(rc: number) : CompilerEvent { const KMCMP_FATAL_OUT_OF_RANGE = -4; switch(rc) { case KMCMP_ERROR_SYNTAX_ERR: - return KmnCompilerMessages.Error_UnicodeSetSyntaxError(); + return KmnCompilerMessages.Error_UnicodeSetSyntaxError(x); case KMCMP_ERROR_HAS_STRINGS: - return KmnCompilerMessages.Error_UnicodeSetHasStrings(); + return KmnCompilerMessages.Error_UnicodeSetHasStrings(x); case KMCMP_ERROR_UNSUPPORTED_PROPERTY: - return KmnCompilerMessages.Error_UnicodeSetHasProperties(); + return KmnCompilerMessages.Error_UnicodeSetHasProperties(x); case KMCMP_FATAL_OUT_OF_RANGE: - return KmnCompilerMessages.Fatal_UnicodeSetOutOfRange(); + return KmnCompilerMessages.Fatal_UnicodeSetOutOfRange(x); default: /* c8 ignore next */ - return KmnCompilerMessages.Fatal_UnexpectedException({e: `Unexpected UnicodeSet error code ${rc}`}); + return CompilerError.setFromMetadata(KmnCompilerMessages.Fatal_UnexpectedException({e: `Unexpected UnicodeSet error code ${rc}`}), x); } } diff --git a/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts b/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts index 591083a85c..3274a62ca1 100644 --- a/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts +++ b/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts @@ -1,6 +1,6 @@ import { KeyAddress } from "../kmw-compiler/validate-layout-file.js"; import { kmnfile } from "../kmw-compiler/compiler-globals.js"; -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException, KeymanUrls } from "@keymanapp/developer-utils"; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec as m, CompilerMessageObjectSpec as mx, CompilerMessageDef as def, CompilerMessageSpecWithException, KeymanUrls, ObjectWithMetadata } from "@keymanapp/developer-utils"; const Namespace = CompilerErrorNamespace.KmnCompiler; const SevInfo = CompilerErrorSeverity.Info | Namespace; @@ -12,6 +12,9 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace; // For messages from the KeymanWeb compiler, we need to construct our messages // slightly differently. This could be refactored in the future, as it is not // obvious which messages should use which function. +// Specifically, this could perhaps use CompilerFileCallbacks which provides a +// default filename. +// Then, there could be a CompilerMessageSpecWithLine const mw = (code: number, message: string, o?: {filename?: string, line?: number}) : CompilerEvent => ({ ...m(code, message), filename: o?.filename ?? kmnfile, @@ -104,9 +107,8 @@ export class KmnCompilerMessages { ); static FATAL_UnicodeSetOutOfRange = SevFatal | 0x904; - static Fatal_UnicodeSetOutOfRange = () => CompilerMessageSpecWithException( - this.FATAL_UnicodeSetOutOfRange, - null, + static Fatal_UnicodeSetOutOfRange = (x?: ObjectWithMetadata) => mx( + this.FATAL_UnicodeSetOutOfRange, x, `UnicodeSet buffer was too small`, `Raised when caller to UnicodeSet functions provides an invalid buffer. If you experience this error, it should be reported to the Keyman team for @@ -116,8 +118,8 @@ export class KmnCompilerMessages { // TODO: rename the following functions to Error_UsetHasStrings etc static ERROR_UnicodeSetHasStrings = SevError | 0x905; - static Error_UnicodeSetHasStrings = () => m( - this.ERROR_UnicodeSetHasStrings, + static Error_UnicodeSetHasStrings = (x?: ObjectWithMetadata) => mx( + this.ERROR_UnicodeSetHasStrings, x, `uset contains strings, not allowed`, `The provided uset uses multi-character strings, (\`{}\` notation, e.g. \`[żġħ{ie}{għ}]\`. ). Although full UnicodeSets support strings, LDML @@ -128,8 +130,8 @@ export class KmnCompilerMessages { ); static ERROR_UnicodeSetHasProperties = SevError | 0x906; - static Error_UnicodeSetHasProperties = () => m( - this.ERROR_UnicodeSetHasProperties, + static Error_UnicodeSetHasProperties = (x?: ObjectWithMetadata) => mx( + this.ERROR_UnicodeSetHasProperties, x, `uset contains properties, not allowed`, `The provided uset uses property notation (\`\\p{…}\` or \`[:…:]\`). LDML keyboards do not support Unicode properties in usets, because that would @@ -140,8 +142,8 @@ export class KmnCompilerMessages { ); static ERROR_UnicodeSetSyntaxError = SevError | 0x907; - static Error_UnicodeSetSyntaxError = () => m( - this.ERROR_UnicodeSetSyntaxError, + static Error_UnicodeSetSyntaxError = (x?: ObjectWithMetadata) => mx( + this.ERROR_UnicodeSetSyntaxError, x, `uset had a Syntax Error while parsing`, `The provided uset has a syntax error and could not be parsed. Verify the format of the uset against the specification. 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 a8c1e1b72c..1794f8b6e7 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts @@ -4,7 +4,7 @@ import { KmnCompilerResult } from "../compiler/compiler.js"; export let FTabStop: string; export let nl: string; -export let FCompilerWarningsAsErrors = false; +export const FCompilerWarningsAsErrors = false; export let kmxResult: KmnCompilerResult; export let fk: KMX.KEYBOARD; export let FMnemonic: boolean; 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 12aa38d265..6a6fb83d32 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts @@ -22,7 +22,7 @@ export function JavaScript_Name(i: number, pwszName: string, KeepNameForPersiste ? '' // Potential for overlap in theory but in practice we only use this for named option stores so can never overlap : '_'; // Ensures we cannot overlap numbered instances while(p.length) { - let ch = p.charAt(0); + const ch = p.charAt(0); if(SValidIdentifierCharSet.test(ch)) { // I3681 result += ch; } else { @@ -49,7 +49,7 @@ export function JavaScript_Name(i: number, pwszName: string, KeepNameForPersiste export function JavaScript_Store(fk: KMX.KEYBOARD, line: number, pwsz: string): string { let ch: number, rec: TSentinelRecord, result: string; const wcsentinel: string = String.fromCharCode(KMX.KMXFile.UC_SENTINEL); - let n = pwsz.indexOf(wcsentinel); + const n = pwsz.indexOf(wcsentinel); // Start: plain text store. Always use for < 10.0, conditionally for >= 10.0. if(n < 0 || !isKeyboardVersion10OrLater()) { @@ -129,7 +129,7 @@ export function JavaScript_String(ch: number): string { // I2242 } function JavaScript_Rule(FTabStops: string, FElse: string, fk: KMX.KEYBOARD, fgp: KMX.GROUP, fkp: KMX.KEY): string { - let predicate: string = '1', linecomment: string = '', FIndent: string; + let predicate: string = '1', linecomment: string = ''; let result = ''; if(fkp.Line > 0 && options.saveDebug) { // I4384 @@ -140,7 +140,7 @@ function JavaScript_Rule(FTabStops: string, FElse: string, fk: KMX.KEYBOARD, fgp predicate = JavaScript_ContextMatch(fk, fkp, fkp.dpContext); } - FIndent = FTabStops+FTabStop; + const FIndent = FTabStops+FTabStop; result = `${FTabStops}${FElse}if(${predicate}){${nl}`; if(fgp.fUsingKeys) { @@ -156,7 +156,7 @@ function JavaScript_Rule(FTabStops: string, FElse: string, fk: KMX.KEYBOARD, fgp export function JavaScript_Rules(keyboard: KMX.KEYBOARD, fMnemonic: boolean, fgp: KMX.GROUP): string { - let IsEqualKey = function(k1: KMX.KEY, k2: KMX.KEY): boolean { + const IsEqualKey = function(k1: KMX.KEY, k2: KMX.KEY): boolean { return ( (JavaScript_Key(k1, FMnemonic) == JavaScript_Key(k2, FMnemonic)) && (JavaScript_Shift(k1, FMnemonic) == JavaScript_Shift(k2, FMnemonic)) @@ -166,7 +166,7 @@ export function JavaScript_Rules(keyboard: KMX.KEYBOARD, fMnemonic: boolean, fgp let result = ''; let HasRules = false; - let processed_rule = Array(fgp.keys.length); + const processed_rule = Array(fgp.keys.length); for(let j = 0; j < fgp.keys.length; j++) { processed_rule[j] = false; } @@ -189,8 +189,8 @@ export function JavaScript_Rules(keyboard: KMX.KEYBOARD, fMnemonic: boolean, fgp Counter++; let LocalHasRules = false; - let fkp2 = fgp.keys[j]; - let j2 = j; + const fkp2 = fgp.keys[j]; + const j2 = j; let LocalCounter = 0; while (j < fgp.keys.length) { fkp = fgp.keys[j]; @@ -435,10 +435,9 @@ function GetCodeName(code: number): string { } function CheckStoreForInvalidFunctions(fk: KMX.KEYBOARD, key: KMX.KEY, store: KMX.STORE) { // I1520 - let n: number, rec: TSentinelRecord; + let rec: TSentinelRecord; const wcsentinel = String.fromCharCode(0xFFFF); - - n = store.dpString.indexOf(wcsentinel); + const n = store.dpString.indexOf(wcsentinel); // Disable the check with versions >= 10.0, since we now support deadkeys in stores. if (n >= 0 && !isKeyboardVersion10OrLater()) { @@ -453,12 +452,12 @@ function JavaScript_CompositeContextValue(fk: KMX.KEYBOARD, fkp: KMX.KEY, pwsz: let Result = ''; let InQuotes = false; - let Len = JavaScript_ContextLength(pwsz); + const Len = JavaScript_ContextLength(pwsz); let StartQuotes = -1; let x = 0, Cur = 0; while(x < pwsz.length) { - let rec = ExpandSentinel(fk, pwsz, x); + const rec = ExpandSentinel(fk, pwsz, x); if(rec.IsSentinel) { if(InQuotes) { Result += `",${Cur-StartQuotes})`; @@ -544,7 +543,7 @@ function JavaScript_FullContextValue(fk: KMX.KEYBOARD, fkp: KMX.KEY, pwsz: strin FullContext += ','; } - let rec = ExpandSentinel(fk, pwsz, x); + const rec = ExpandSentinel(fk, pwsz, x); if(rec.IsSentinel) { switch(rec.Code) { case KMX.KMXFile.CODE_ANY: @@ -626,8 +625,7 @@ function isGroupReadOnly(fk: KMX.KEYBOARD, fgp: KMX.GROUP) { } function CallFunctionName(s: string): string { - let n: number; - n = s.indexOf(':'); + const n = s.indexOf(':'); return s.substring(n+1); // not found gives -1, substring(0) is ok :grin: } @@ -640,7 +638,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp let Result = Index; let x = 0; for(let I = 1; I < Index; I++) { - let recContext = ExpandSentinel(fk, pwszContext, x); + const recContext = ExpandSentinel(fk, pwszContext, x); if(isKeyboardVersion10OrLater()) { if(recContext.IsSentinel && [KMX.KMXFile.CODE_IFOPT, KMX.KMXFile.CODE_IFSYSTEMSTORE].includes(recContext.Code)) { @@ -660,7 +658,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp const ContextChar = function(ContextIndex: number, pwszContext: string, xContext: number): string { // I4611 let Index: number; let Result = ''; - let recContext = ExpandSentinel(fk, pwszContext, xContext); + const recContext = ExpandSentinel(fk, pwszContext, xContext); if(recContext.IsSentinel) { if(InQuotes) { // I4611 Result += '");'; @@ -710,17 +708,17 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp let Result = ''; InQuotes = false; - let pwsz = pwszOutput; + const pwsz = pwszOutput; if(fkp != null) { if(isKeyboardVersion10OrLater()) { // KMW >= 10.0 use the full, sentinel-based length for context deletions. len = xstrlen(fkp.dpContext); - let n = len; + const n = len; let x = 0; for(let i = 0; i < n; i++) { - let rec = ExpandSentinel(fk, fkp.dpContext, x); + const rec = ExpandSentinel(fk, fkp.dpContext, x); if(rec.IsSentinel && [KMX.KMXFile.CODE_NUL, KMX.KMXFile.CODE_IFOPT, KMX.KMXFile.CODE_IFSYSTEMSTORE].includes(rec.Code)) { len--; } @@ -746,7 +744,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp } while(x < pwsz.length) { - let rec = ExpandSentinel(fk, pwsz, x); + const rec = ExpandSentinel(fk, pwsz, x); if(rec.IsSentinel) { if(InQuotes) { if(!isGroupReadOnly(fk, fgp)) { @@ -808,7 +806,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp // compiled in-between the original fix and I783 re-fix, and then happened to work due to // their simplicity. - let Index = AdjustIndex(fkp.dpContext, rec.Index.Index); // I3910 + const Index = AdjustIndex(fkp.dpContext, rec.Index.Index); // I3910 if(!isGroupReadOnly(fk, fgp)) { Result += nlt+`k.KIO(${len},this.s${JavaScript_Name(rec.Index.StoreIndex, rec.Index.Store.dpName)},${Index},t);`; @@ -928,7 +926,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp } export function zeroPadHex(n: number, len: number): string { - let result = n.toString(16).toUpperCase(); + const result = n.toString(16).toUpperCase(); if(result.length < len) { return '0'.repeat(len - result.length) + result; } 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 5dcde400ea..b7e36cd581 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler.ts @@ -45,7 +45,7 @@ export function WriteCompiledKeyboard( kmxResult: KmnCompilerResult, FDebug: boolean = false ): string { - let opts: CompilerOptions = { + const opts: CompilerOptions = { shouldAddCompilerVersion: false, saveDebug: FDebug }; @@ -67,7 +67,6 @@ export function WriteCompiledKeyboard( let sBegin_NewContext: string = "", sBegin_PostKeystroke: string = ""; let sLayoutFilename: string = "", sVKDictionary: string = ""; let linecomment: string; // I3438 - let sModifierBitmask: string; let FOptionStores: string; let FKeyboardVersion = "1.0"; let sHelpFileStoreIndex, sEmbedJSStoreIndex, sEmbedCSSStoreIndex; @@ -167,7 +166,7 @@ export function WriteCompiledKeyboard( if (sLayoutFilename != '') { // I3483 sLayoutFilename = callbacks.resolveFilename(kmnfile, sLayoutFilename); - let result = ValidateLayoutFile(keyboard, options.saveDebug, sLayoutFilename, sVKDictionary, kmxResult.displayMap); + const result = ValidateLayoutFile(keyboard, options.saveDebug, sLayoutFilename, sVKDictionary, kmxResult.displayMap); if(!result) { sLayoutFile = ''; return null; @@ -195,7 +194,7 @@ export function WriteCompiledKeyboard( const fMnemonic = vMnemonic == 1; - sModifierBitmask = GetKeyboardModifierBitmask(keyboard, fMnemonic); + const sModifierBitmask = GetKeyboardModifierBitmask(keyboard, fMnemonic); result += `${JavaScript_SetupProlog()}${nl}` + @@ -241,7 +240,7 @@ export function WriteCompiledKeyboard( // Write the stores out FOptionStores = ''; for(let i = 0; i < keyboard.stores.length; i++) { - let fsp = keyboard.stores[i]; + const fsp = keyboard.stores[i]; // I3438 - Save all system stores to the keyboard, for now // I3684 if (!isDebugStore(i)) { @@ -287,14 +286,14 @@ export function WriteCompiledKeyboard( result += WriteBeginStatement(keyboard, 'gpk', rec.Use.GroupIndex); } - let fgp = keyboard.groups[keyboard.startGroup.unicode]; + const fgp = keyboard.groups[keyboard.startGroup.unicode]; result += `${FTabStop}this.gs=function(t,e) {${nl}` + `${FTabStop+FTabStop}return this.g${JavaScript_Name(keyboard.startGroup.unicode, fgp.dpName)}(t,e);${nl}` + `${FTabStop}};${nl}`; // I3681 for(let i = 0; i < keyboard.groups.length; i++) { // I1964 - let fgp = keyboard.groups[i]; + const fgp = keyboard.groups[i]; /* Note on `r` and `m` variables in a group function: @@ -331,7 +330,7 @@ export function WriteCompiledKeyboard( } else { for (let j = 0; j < fgp.keys.length; j++) { // I1964 - let fkp = fgp.keys[j]; + const fkp = fgp.keys[j]; if (!RuleIsExcludedByPlatform(keyboard, fkp)) { result += FTabStop+FTabStop; // I3681 if (HasRules) { @@ -427,9 +426,9 @@ export function WriteCompiledKeyboard( /// function GetKeyboardModifierBitmask(keyboard: KMX.KEYBOARD, fMnemonic: boolean): string { let bitMask = 0; - for(let gp of keyboard.groups) { + for(const gp of keyboard.groups) { if(gp.fUsingKeys) { - for(let kp of gp.keys) { + for(const kp of gp.keys) { if(!RuleIsExcludedByPlatform(keyboard, kp)) { bitMask |= JavaScript_Shift(kp, fMnemonic); } @@ -490,14 +489,14 @@ function JavaScript_SetupEpilog() { function HasSupplementaryPlaneChars() { const suppChar = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; - for(let sp of fk.stores) { + for(const sp of fk.stores) { if(suppChar.test(sp.dpString)) { return true; } } - for(let gp of fk.groups) { - for(let kp of gp.keys) { + for(const gp of fk.groups) { + for(const kp of gp.keys) { if(suppChar.test(kp.dpContext) || suppChar.test(kp.dpOutput)) { return true; } @@ -516,7 +515,7 @@ export function RuleIsExcludedByPlatform(keyboard: KMX.KEYBOARD, fkp: KMX.KEY): let x = 0; while(x < fkp.dpContext.length) { - let rec = ExpandSentinel(keyboard, fkp.dpContext, x); + const rec = ExpandSentinel(keyboard, fkp.dpContext, x); if(rec.IsSentinel && (rec.Code == KMX.KMXFile.CODE_IFSYSTEMSTORE) && (rec.IfSystemStore.dwSystemID == KMX.KMXFile.TSS_PLATFORM) && diff --git a/developer/src/kmc-kmn/src/kmw-compiler/util.ts b/developer/src/kmc-kmn/src/kmw-compiler/util.ts index 7807aa363e..4c775e6c83 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/util.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/util.ts @@ -73,7 +73,7 @@ export interface TSentinelRecord { export function ExpandSentinel(fk: KMX.KEYBOARD, pwsz: string, x: number): TSentinelRecord { - let result: TSentinelRecord = { + const result: TSentinelRecord = { IsSentinel: false }; if(pwsz.charCodeAt(x) == KMX.KMXFile.UC_SENTINEL) { @@ -84,14 +84,14 @@ export function ExpandSentinel(fk: KMX.KEYBOARD, pwsz: string, x: number): TSent switch(result.Code) { case KMX.KMXFile.CODE_ANY: case KMX.KMXFile.CODE_NOTANY: // I3981 - let anyIdx = pwsz.charCodeAt(x) - 1; + const anyIdx = pwsz.charCodeAt(x) - 1; result.Any = { StoreIndex: anyIdx, Store: fk.stores[anyIdx] } break; case KMX.KMXFile.CODE_INDEX: - let indexIdx = pwsz.charCodeAt(x) - 1; + const indexIdx = pwsz.charCodeAt(x) - 1; x++; result.Index = { StoreIndex: indexIdx, @@ -103,14 +103,14 @@ export function ExpandSentinel(fk: KMX.KEYBOARD, pwsz: string, x: number): TSent result.DeadKey = { DeadKey: pwsz.charCodeAt(x) - 1 }; break; case KMX.KMXFile.CODE_USE: - let useIdx = pwsz.charCodeAt(x) - 1; + const useIdx = pwsz.charCodeAt(x) - 1; result.Use = { GroupIndex: useIdx, Group: fk.groups[useIdx] }; break; case KMX.KMXFile.CODE_CALL: - let callIdx = pwsz.charCodeAt(x) - 1; + const callIdx = pwsz.charCodeAt(x) - 1; result.Call = { StoreIndex: callIdx, Store: fk.stores[callIdx] @@ -120,9 +120,9 @@ export function ExpandSentinel(fk: KMX.KEYBOARD, pwsz: string, x: number): TSent result.ContextEx = { Index: pwsz.charCodeAt(x) - 1 }; break; case KMX.KMXFile.CODE_SETOPT: // I3429 - let setIdx1 = pwsz.charCodeAt(x) - 1; + const setIdx1 = pwsz.charCodeAt(x) - 1; x++; - let setIdx2 = pwsz.charCodeAt(x) - 1; + const setIdx2 = pwsz.charCodeAt(x) - 1; result.SetOpt = { StoreIndex1: setIdx1, Store1: fk.stores[setIdx1], @@ -131,9 +131,9 @@ export function ExpandSentinel(fk: KMX.KEYBOARD, pwsz: string, x: number): TSent }; break; case KMX.KMXFile.CODE_SETSYSTEMSTORE: // I3437 - let setsIdx1 = pwsz.charCodeAt(x) - 1; + const setsIdx1 = pwsz.charCodeAt(x) - 1; x++; - let setsIdx2 = pwsz.charCodeAt(x) - 1; + const setsIdx2 = pwsz.charCodeAt(x) - 1; result.SetSystemStore = { dwSystemID: setsIdx1, SystemStore: fk.stores.find(s => s.dwSystemID == setsIdx1) || null, @@ -156,11 +156,11 @@ export function ExpandSentinel(fk: KMX.KEYBOARD, pwsz: string, x: number): TSent }; break; case KMX.KMXFile.CODE_IFOPT: // I3429 - let ifIdx1 = pwsz.charCodeAt(x) - 1; + const ifIdx1 = pwsz.charCodeAt(x) - 1; x++; - let ifNot = pwsz.charCodeAt(x) - 1; + const ifNot = pwsz.charCodeAt(x) - 1; x++; - let ifIdx2 = pwsz.charCodeAt(x) - 1; + const ifIdx2 = pwsz.charCodeAt(x) - 1; result.IfOpt = { StoreIndex1: ifIdx1, Store1: fk.stores[ifIdx1], @@ -170,11 +170,11 @@ export function ExpandSentinel(fk: KMX.KEYBOARD, pwsz: string, x: number): TSent }; break; case KMX.KMXFile.CODE_IFSYSTEMSTORE: // I3430 - let ifsSystemID = pwsz.charCodeAt(x) - 1; + const ifsSystemID = pwsz.charCodeAt(x) - 1; x++; - let ifsNot = pwsz.charCodeAt(x) - 1; + const ifsNot = pwsz.charCodeAt(x) - 1; x++; - let ifsIdx2 = pwsz.charCodeAt(x) - 1; + const ifsIdx2 = pwsz.charCodeAt(x) - 1; result.IfSystemStore = { dwSystemID: ifsSystemID, SystemStore: fk.stores.find(s => s.dwSystemID == ifsSystemID) || null, 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 9bed88f5f8..d651fd3735 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 @@ -32,8 +32,8 @@ function IsValidUnicodeValue(ch: number): boolean { // I4198 enum TKeyIdType { Key_Invalid, Key_Constant, Key_Touch, Key_Unicode, Key_Unicode_Multi }; // I4142 function GetKeyIdUnicodeType(value: string): TKeyIdType { - let values = value.split('_'); - for(let v of values) { + const values = value.split('_'); + for(const v of values) { if(!IsValidUnicodeValue(parseInt(v,16))) { return TKeyIdType.Key_Invalid; } @@ -104,7 +104,7 @@ function CheckKey( // Check that each touch layer has K_LOPT, [K_ROPT,] K_BKSP, K_ENTER // - for(let key of CRequiredKeys) { + for(const key of CRequiredKeys) { if(TRequiredKey[key].toLowerCase() == FId.toLowerCase()) { if(FRequiredKeys.indexOf(key) < 0) { FRequiredKeys.push(key); @@ -140,7 +140,7 @@ function CheckKey( return true; } - let FValid = KeyIdType(FId); + const FValid = KeyIdType(FId); if(FValid == TKeyIdType.Key_Invalid) { callbacks.reportMessage(KmwCompilerMessages.Error_TouchLayoutInvalidIdentifier({keyId: FId, platformName: platformId, layerId: layer.id, address})); @@ -197,9 +197,9 @@ function CheckDictionaryKeyValidity(fk: KMX.KEYBOARD, FDictionary: string[]) { } if([TKeyIdType.Key_Invalid, TKeyIdType.Key_Constant].includes(KeyIdType(FDictionary[i]))) { - for(let fgp of fk.groups) { + for(const fgp of fk.groups) { if(fgp.fUsingKeys) { - for(let fkp of fgp.keys) { + for(const fkp of fgp.keys) { if(JavaScript_Key(fkp, fk.isMnemonic) == i+256) { callbacks.reportMessage(KmwCompilerMessages.Error_InvalidKeyCode({keyId: FDictionary[i]})); } @@ -243,11 +243,11 @@ function TransformSpecialKeys17(FDebug: boolean, sLayoutFile: string): string { } export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFile: string, sVKDictionary: string, displayMap: Osk.PuaMap): VLFOutput { // I4060 // I4139 - let FDictionary: string[] = sVKDictionary.split(/\s+/); + const FDictionary: string[] = sVKDictionary.split(/\s+/); CheckDictionaryKeyValidity(fk, FDictionary); // I4142 - let reader = new TouchLayoutFileReader(); + const reader = new TouchLayoutFileReader(); let data: TouchLayout.TouchLayoutFile; try { if(!callbacks.fs.existsSync(sLayoutFile)) { @@ -275,7 +275,7 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil let FTouchLayoutFont = ''; // I4872 let pid: keyof TouchLayout.TouchLayoutFile; for(pid in data) { - let platform = data[pid]; + const platform = data[pid]; // Test that the font matches on all platforms // I4872 @@ -288,18 +288,18 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil } // Test that all required keys are present - for(let layer of platform.layer) { - let FRequiredKeys: TRequiredKey[] = []; + for(const layer of platform.layer) { + const FRequiredKeys: TRequiredKey[] = []; let rowIndex = 0; - for(let row of layer.row) { + for(const row of layer.row) { rowIndex++; let keyIndex = 0; - for(let key of row.key) { + for(const key of row.key) { keyIndex++; result = CheckKey(pid, platform, layer, key.id, key.text, key.nextlayer, key.sp, FRequiredKeys, FDictionary, {rowIndex, keyIndex}) && result; // I4119 if(key.sk) { let subKeyIndex = 0; - for(let subkey of key.sk) { + for(const subkey of key.sk) { subKeyIndex++; result = CheckKey(pid, platform, layer, subkey.id, subkey.text, subkey.nextlayer, subkey.sp, FRequiredKeys, FDictionary, {rowIndex, keyIndex, subKeyIndex}) && result; @@ -316,7 +316,7 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil if(key.multitap) { let multitapIndex = 0; - for(let subkey of key.multitap) { + for(const subkey of key.multitap) { multitapIndex++; warnGesturesIfNeeded(key.id); result = CheckKey(pid, platform, layer, subkey.id, subkey.text, subkey.nextlayer, subkey.sp, FRequiredKeys, FDictionary, @@ -343,7 +343,7 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil // If not debugging, then this strips out formatting for a big saving in file size // This also normalises any values such as Pad or Width which should be strings - let writer = new TouchLayoutFileWriter({formatted: FDebug}); + const writer = new TouchLayoutFileWriter({formatted: FDebug}); sLayoutFile = writer.compile(data); diff --git a/developer/src/kmc-kmn/src/kmw-compiler/visual-keyboard-compiler.ts b/developer/src/kmc-kmn/src/kmw-compiler/visual-keyboard-compiler.ts index 26fcee82ca..117d0f2aff 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/visual-keyboard-compiler.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/visual-keyboard-compiler.ts @@ -84,13 +84,13 @@ function VisualKeyboardToKLS(FVK: VisualKeyboard.VisualKeyboard): string { keys: string[]; }; - let layers: TLayer[] = []; + const layers: TLayer[] = []; // Discover the layers used in the visual keyboard - for(let key of FVK.keys) { + for(const key of FVK.keys) { if(key.flags & KvkFile.BUILDER_KVK_KEY_FLAGS.kvkkUnicode) { // Find the index of the key in KMW VK arrays - let n = CKeymanWebKeyCodes[key.vkey]; + const n = CKeymanWebKeyCodes[key.vkey]; if(n == 0xFF) { continue; } @@ -110,7 +110,7 @@ function VisualKeyboardToKLS(FVK: VisualKeyboard.VisualKeyboard): string { let result = nl+FTabStop+'this.KV.KLS={'+nl; for(let i = 0; i < layers.length; i++) { - let layer = layers[i]; + const layer = layers[i]; result += `${FTabStop}${FTabStop}"${VKShiftToLayerName(layer.shift)}": [`; for(let j = 0; j < layer.keys.length - 1; j++) { result += '"'+WideQuote(layer.keys[j] ?? '')+'",'; diff --git a/developer/src/kmc-kmn/test/compiler.tests.ts b/developer/src/kmc-kmn/test/compiler.tests.ts index 74c9775f1f..cb190f519d 100644 --- a/developer/src/kmc-kmn/test/compiler.tests.ts +++ b/developer/src/kmc-kmn/test/compiler.tests.ts @@ -65,7 +65,7 @@ describe('Compiler class', function() { assert(compiler.verifyInitialized()); const files = fs.readdirSync(baselineDir); - for(let file of files) { + for(const file of files) { if(file.match(/\.kmx$/)) { const fixtureName = baselineDir + file; const infile = baselineDir + file.replace(/x$/, 'n'); diff --git a/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts b/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts index 5e132e412c..a1008416aa 100644 --- a/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts +++ b/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts @@ -87,7 +87,7 @@ describe('KeymanWeb Compiler', function() { it('should determine the minimum version correctly with U_xxxx_yyyy touch ids', async function() { const filenames = generateTestFilenames('version_u_xxxx_yyyy'); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNotNull(result); assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); // we expect only 1 of the info messages -- for the .kmx target (not 2) @@ -100,7 +100,7 @@ describe('KeymanWeb Compiler', function() { it('should give an error if the minimum version specified in the keyboard does not support U_xxxx_yyyy touch ids', async function() { const filenames = generateTestFilenames('version_u_xxxx_yyyy_14'); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNull(result); assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); @@ -111,7 +111,7 @@ describe('KeymanWeb Compiler', function() { it(`should give warning WARN_ExtendedShiftFlagsNotSupportedInKeymanWeb for v9 keyboards if ${mode} found`, async function() { const filenames = generateTestFilenames(`version_9_${mode}`); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNotNull(result); // only a warning, so output is generated assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); @@ -121,7 +121,7 @@ describe('KeymanWeb Compiler', function() { it(`should select version 10 automatically if ${mode} found`, async function() { const filenames = generateTestFilenames(`version_auto_${mode}`); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNotNull(result); // we expect only 1 of the info messages -- for the .kmx target (not 2) @@ -138,7 +138,7 @@ describe('KeymanWeb Compiler', function() { // Note that the logic being tested here is in kmx compiler.cpp, not kmw compiler const filenames = generateTestFilenames('version_notany'); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNotNull(result); assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); // we expect only 1 of the info messages -- for the .kmx target (not 2) @@ -152,7 +152,7 @@ describe('KeymanWeb Compiler', function() { // Note that the logic being tested here is in kmx compiler.cpp, not kmw compiler const filenames = generateTestFilenames('version_notany_10'); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNull(result); assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); @@ -162,7 +162,7 @@ describe('KeymanWeb Compiler', function() { it('should determine the minimum version correctly with special key caps on normal keys', async function() { const filenames = generateTestFilenames('version_special_key_caps'); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNotNull(result); assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); // we expect only 1 of the info messages -- for the .kmx target (not 2) @@ -175,7 +175,7 @@ describe('KeymanWeb Compiler', function() { it('should give warning WARN_TouchLayoutSpecialLabelOnNormalKey if the minimum version specified in the keyboard does not support special key caps on normal keys', async function() { const filenames = generateTestFilenames('version_special_key_caps_14'); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNotNull(result); assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); @@ -185,7 +185,7 @@ describe('KeymanWeb Compiler', function() { it('should determine the minimum version correctly with v17 gestures', async function() { const filenames = generateTestFilenames('version_gestures'); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNotNull(result); assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); // we expect only 1 of the info messages -- for the .kmx target (not 2) @@ -198,7 +198,7 @@ describe('KeymanWeb Compiler', function() { it('should give warning HINT_TouchLayoutUsesUnsupportedGesturesDownlevel if the minimum version specified in the keyboard does not support special key caps on normal keys', async function() { const filenames = generateTestFilenames('version_gestures_16'); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNotNull(result); assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); @@ -209,7 +209,7 @@ describe('KeymanWeb Compiler', function() { // #12870 const filenames = generateTestFilenames('error_touch_layout_invalid_identifier'); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNull(result); assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); @@ -229,7 +229,7 @@ describe('KeymanWeb Compiler', function() { it('should generate correct index offsets (#12980) for context and context(n) when building a KMW keyboard with v9 target', async function() { const filenames = generateTestFilenames('test_index_12980_v9'); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNotNull(result); const data = new TextDecoder('utf-8').decode(result.artifacts.js.data); @@ -248,7 +248,7 @@ describe('KeymanWeb Compiler', function() { it('should generate correct index offsets (#12980) for context and context(n) when building a KMW keyboard with v10+ target', async function() { const filenames = generateTestFilenames('test_index_12980_v10'); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNotNull(result); const data = new TextDecoder('utf-8').decode(result.artifacts.js.data); @@ -275,10 +275,10 @@ async function run_test_keyboard(kmnCompiler: KmnCompiler, id: string): Promise<{ result: KmnCompilerResult, actualCode: string, actual: ETLResult, expectedCode: string, expected: ETLResult }> { const filenames = generateTestFilenames(id); - let result = await kmnCompiler.run(filenames.source, null); + const result = await kmnCompiler.run(filenames.source, null); assert.isNotNull(result); - let value = { + const value = { result, actualCode: new TextDecoder().decode(result.artifacts.js.data), expectedCode: fs.readFileSync(filenames.fixture, 'utf8'), diff --git a/developer/src/kmc-kmn/test/kmw/util.ts b/developer/src/kmc-kmn/test/kmw/util.ts index 37d89cdf7b..6498a008d3 100644 --- a/developer/src/kmc-kmn/test/kmw/util.ts +++ b/developer/src/kmc-kmn/test/kmw/util.ts @@ -5,7 +5,7 @@ export interface ETLResult { } export function extractTouchLayout(js: string): ETLResult|null { - let m = /KVKL=(?.+?);[\r\n]/ds.exec(js); + const m = /KVKL=(?.+?);[\r\n]/ds.exec(js); if(!m) { return { js: js, @@ -13,7 +13,7 @@ export function extractTouchLayout(js: string): ETLResult|null { }; } - let kvkl = (m).indices.groups.kvkl; + const kvkl = (m).indices.groups.kvkl; return { js: js.substring(0, kvkl[0]) + 'null' + js.substring(kvkl[1]), diff --git a/developer/src/kmc-ldml/.eslintrc.cjs b/developer/src/kmc-ldml/.eslintrc.cjs index 41a487fe41..990bc63f61 100644 --- a/developer/src/kmc-ldml/.eslintrc.cjs +++ b/developer/src/kmc-ldml/.eslintrc.cjs @@ -8,8 +8,5 @@ module.exports = { files:"src/**/*.ts", extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } - ], - rules: { - "prefer-const": "off", // TODO: enable this once infrastructure is in place and cleanup the problem cases - }, + ] }; diff --git a/developer/src/kmc-ldml/package.json b/developer/src/kmc-ldml/package.json index 4abe29b67e..ce0a83e18e 100644 --- a/developer/src/kmc-ldml/package.json +++ b/developer/src/kmc-ldml/package.json @@ -42,7 +42,6 @@ "c8": "^7.12.0", "chalk": "^2.4.2", "common-tags": "^1.8.2", - "mocha": "^8.4.0", "peggy": "^4.2.0", "typescript": "^5.4.5" }, diff --git a/developer/src/kmc-ldml/src/compiler/compiler.ts b/developer/src/kmc-ldml/src/compiler/compiler.ts index e8b38184e9..d66631e0b5 100644 --- a/developer/src/kmc-ldml/src/compiler/compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/compiler.ts @@ -8,7 +8,9 @@ import { CompilerCallbacks, KeymanCompiler, KeymanCompilerResult, KeymanCompilerArtifacts, defaultCompilerOptions, LDMLKeyboardXMLSourceFileReader, LDMLKeyboard, LDMLKeyboardTestDataXMLSourceFile, KMXBuilder, - KeymanCompilerArtifactOptional + KeymanCompilerArtifactOptional, + ResolvingCompilerCallbacks, + KeymanXMLReader, } from "@keymanapp/developer-utils"; import { LdmlCompilerOptions } from './ldml-compiler-options.js'; import { LdmlCompilerMessages } from './ldml-compiler-messages.js'; @@ -94,6 +96,7 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { // uset parser private usetparser?: LdmlKeyboardTypes.UnicodeSetParser = undefined; + private reader?: LDMLKeyboardXMLSourceFileReader; /** * Initialize the compiler, including loading the WASM host for uset parsing. @@ -105,7 +108,9 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { */ async init(callbacks: CompilerCallbacks, options: LdmlCompilerOptions): Promise { this.options = { ...options }; - this.callbacks = callbacks; + this.reader = new LDMLKeyboardXMLSourceFileReader(this.options.readerOptions, callbacks); + // wrap the callbacks so that the eventresolver is called + this.callbacks = new ResolvingCompilerCallbacks(this.reader, this.options, callbacks); return true; } @@ -122,16 +127,16 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { */ async run(inputFilename: string, outputFilename?: string): Promise { - let compilerOptions: LdmlCompilerOptions = { + const compilerOptions: LdmlCompilerOptions = { ...defaultCompilerOptions, ...this.options, }; - let source = this.load(inputFilename); + const source = this.load(inputFilename); if (!source) { return null; } - let kmx = await this.compile(source); + const kmx = await this.compile(source, true); if (!kmx) { return null; } @@ -238,9 +243,9 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { * @returns the source file, or null if invalid */ public load(filename: string): LDMLKeyboardXMLSourceFile | null { - const reader = new LDMLKeyboardXMLSourceFileReader(this.options.readerOptions, this.callbacks); + const reader = this.reader; // load the file from disk into a string - const data = this.callbacks.loadFile(filename); + const data = reader.readFile(filename); if (!data) { this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidFile({ errorText: 'Unable to read XML file' })); return null; @@ -261,6 +266,9 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { return null; } + // record the default filename - for error reporting. + KeymanXMLReader.setDefaultFilename(source, filename); + return source; } @@ -272,7 +280,7 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { * @returns the source file, or null if invalid */ public loadTestData(filename: string): LDMLKeyboardTestDataXMLSourceFile | null { - const reader = new LDMLKeyboardXMLSourceFileReader(this.options.readerOptions, this.callbacks); + const reader = this.reader; const data = this.callbacks.loadFile(filename); if (!data) { this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidFile({ errorText: 'Unable to read XML file' })); @@ -344,6 +352,8 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { * Transforms in-memory LDML keyboard xml file to an intermediate * representation of a .kmx file. * @param source - in-memory representation of LDML keyboard xml file + * @param postValidate - pass true if sections should run a 'validate' phase at the very end. + * Set this to true if you aren't calling validate() separately. * @returns KMXPlusFile intermediate file */ public async compile(source: LDMLKeyboardXMLSourceFile, postValidate?: boolean): Promise { @@ -352,7 +362,7 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { const kmx = new KMXPlusFile(); - for (let section of sections) { + for (const section of sections) { if (!section.validate()) { // TODO-LDML: coverage passed = false; @@ -405,7 +415,7 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { // give all sections a chance to postValidate if (postValidate) { - for (let section of sections) { + for (const section of sections) { if (!section.postValidate(kmx.kmxplus[section.id])) { passed = false; } diff --git a/developer/src/kmc-ldml/src/compiler/disp.ts b/developer/src/kmc-ldml/src/compiler/disp.ts index 03c411ce8d..fd15c8ace7 100644 --- a/developer/src/kmc-ldml/src/compiler/disp.ts +++ b/developer/src/kmc-ldml/src/compiler/disp.ts @@ -12,9 +12,10 @@ import { SubstitutionUse, Substitutions } from "./substitution-tracker.js"; export class DispCompiler extends SectionCompiler { static validateSubstitutions(keyboard: LDMLKeyboard.LKKeyboard, st : Substitutions): boolean { - keyboard.displays?.display?.forEach(({ display, output }) => { - st.addStringAndMarkerSubstitution(SubstitutionUse.match, output); - st.addStringSubstitution(SubstitutionUse.emit, display); + keyboard.displays?.display?.forEach((e) => { + const { display, output } = e; + st.addStringAndMarkerSubstitution(SubstitutionUse.match, output, e); + st.addStringSubstitution(SubstitutionUse.emit, display, e); }); // no marker references in 'id' return true; @@ -25,26 +26,27 @@ export class DispCompiler extends SectionCompiler { } public validate(): boolean { - let valid = true; + const valid = true; const tos = new Set(); const ids = new Set(); if (this.keyboard3.displays?.display) { - for (const { output, keyId } of this.keyboard3.displays?.display) { + for (const display of this.keyboard3.displays?.display) { + const { output, keyId } = display; if ((output && keyId) || (!output && !keyId)) { - this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayNeedsToOrId({ output, keyId })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayNeedsToOrId({ display: display.display }, display)); return false; } else if (output) { if (tos.has(output)) { - this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ output })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ display: display.display }, display)); return false; } else { tos.add(output); } } else if (keyId) { if (ids.has(keyId)) { - this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ keyId })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ display: display.display }, display)); return false; } else { ids.add(keyId); @@ -57,10 +59,10 @@ export class DispCompiler extends SectionCompiler { } public compile(sections: DependencySections): Disp { - let result = new Disp(); + const result = new Disp(); // displayOptions - result.baseCharacter = sections.strs.allocString(this.keyboard3.displays?.displayOptions?.baseCharacter, {unescape: true}); + result.baseCharacter = sections.strs.allocString(this.keyboard3.displays?.displayOptions?.baseCharacter, { unescape: true, x: this.keyboard3?.displays?.displayOptions }); // displays result.disps = this.keyboard3.displays?.display.map(display => ({ @@ -68,11 +70,13 @@ export class DispCompiler extends SectionCompiler { stringVariables: true, markers: true, unescape: true, + x: display, }, sections), - id: sections.strs.allocString(display.keyId), // not escaped, not substituted + id: sections.strs.allocString(display.keyId, { x: display }), // not escaped, not substituted display: sections.strs.allocString(display.display, { stringVariables: true, unescape: true, + x: display, }, sections), })) || []; // TODO-LDML: need coverage for the [] diff --git a/developer/src/kmc-ldml/src/compiler/empty-compiler.ts b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts index 4b9aec7613..79015dca6b 100644 --- a/developer/src/kmc-ldml/src/compiler/empty-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts @@ -1,7 +1,7 @@ import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; import { SectionCompiler } from "./section-compiler.js"; import { util, KMXPlus, LdmlKeyboardTypes } from "@keymanapp/common-types"; -import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils"; +import { CompilerCallbacks, LDMLKeyboard, ObjectWithMetadata } from "@keymanapp/developer-utils"; import { VarsCompiler } from './vars.js'; import { LdmlCompilerMessages } from './ldml-compiler-messages.js'; @@ -33,13 +33,26 @@ export class StrsCompiler extends EmptyCompiler { public postValidate(section?: KMXPlus.Section): boolean { const strs = section; + /** attempt to find a context object for the string */ + function findContextForString(s: string): ObjectWithMetadata { + // try exact match + for(const str of strs.strings) { + if (str.value == s) return str.context; + } + // try substring match + for(const str of strs.strings) { + if (str.value.includes(s)) return str.context; + } + return null; + } + if (strs) { const badStringAnalyzer = new util.BadStringAnalyzer(); const CONTAINS_MARKER_REGEX = new RegExp(LdmlKeyboardTypes.MarkerParser.ANY_MARKER_MATCH); for (let s of strs.allProcessedStrings.values()) { // stop at the first denormalized string if (!util.isNormalized(s)) { - this.callbacks.reportMessage(LdmlCompilerMessages.Warn_StringDenorm({s})); + this.callbacks.reportMessage(LdmlCompilerMessages.Warn_StringDenorm({s}, findContextForString(s))); } // replace all \\uXXXX with the actual code point. // this lets us analyze whether there are PUA, unassigned, etc. @@ -63,16 +76,19 @@ export class StrsCompiler extends EmptyCompiler { const illegals = m.get(util.BadStringType.illegal); if (puas) { const [count, lowestCh] = [puas.size, Array.from(puas.values()).sort((a, b) => a - b)[0]]; - this.callbacks.reportMessage(LdmlCompilerMessages.Hint_PUACharacters({ count, lowestCh })) + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_PUACharacters({ count, lowestCh: util.describeCodepoint(lowestCh) }, + findContextForString(String.fromCodePoint(lowestCh)))); } if (unassigneds) { const [count, lowestCh] = [unassigneds.size, Array.from(unassigneds.values()).sort((a, b) => a - b)[0]]; - this.callbacks.reportMessage(LdmlCompilerMessages.Warn_UnassignedCharacters({ count, lowestCh })) + this.callbacks.reportMessage(LdmlCompilerMessages.Warn_UnassignedCharacters({ count, lowestCh: util.describeCodepoint(lowestCh) }, + findContextForString(String.fromCodePoint(lowestCh)))); } if (illegals) { // do this last, because we will return false. const [count, lowestCh] = [illegals.size, Array.from(illegals.values()).sort((a, b) => a - b)[0]]; - this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalCharacters({ count, lowestCh })) + this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalCharacters({ count, lowestCh: util.describeCodepoint(lowestCh) }, + findContextForString(String.fromCodePoint(lowestCh)))); return false; } } diff --git a/developer/src/kmc-ldml/src/compiler/keys.ts b/developer/src/kmc-ldml/src/compiler/keys.ts index d86d7e2bf5..3c890a5067 100644 --- a/developer/src/kmc-ldml/src/compiler/keys.ts +++ b/developer/src/kmc-ldml/src/compiler/keys.ts @@ -34,10 +34,10 @@ export class KeysCompiler extends SectionCompiler { KeysCompiler.addUsedGestureKeys(layerKeyIds, keyBag, usedKeys); // process each used key. unused keys don't get checked. - for (let keyId of usedKeys.values()) { + for (const keyId of usedKeys.values()) { const key = keyBag.get(keyId); if (!key) continue; // key not found is handled elsewhere. - st.addStringAndMarkerSubstitution(SubstitutionUse.emit, key.output); + st.addStringAndMarkerSubstitution(SubstitutionUse.emit, key.output, key); } return true; } @@ -73,7 +73,7 @@ export class KeysCompiler extends SectionCompiler { this.keyboard3.forms?.form?.forEach((form) => { if (!LDMLKeyboard.ImportStatus.isImpliedImport(form)) { // If it's not an implied import, give a warning. - this.callbacks.reportMessage(LdmlCompilerMessages.Warn_CustomForm({ id: form.id })); + this.callbacks.reportMessage(LdmlCompilerMessages.Warn_CustomForm({ id: form.id }, form)); } }); @@ -100,7 +100,7 @@ export class KeysCompiler extends SectionCompiler { if (!flickHash.has(flickId)) { valid = false; this.callbacks.reportMessage( - LdmlCompilerMessages.Error_MissingFlicks({ flickId, id: keyId }) + LdmlCompilerMessages.Error_MissingFlicks({ id: key.id, flickId: key.flickId }, key) ); } } @@ -109,10 +109,13 @@ export class KeysCompiler extends SectionCompiler { for(const [gestureKeyId, attrs] of gestureKeys.entries()) { const gestureKey = keyBag.get(gestureKeyId); if (gestureKey == null) { - // TODO-LDML: could keep track of already missing keys so we don't warn multiple times on gesture keys + // We don't keep track of already missing keys - might warn multiple times on gesture keys, + // however we leave it to the caller to handle eliding duplicate messages valid = false; this.callbacks.reportMessage( - LdmlCompilerMessages.Error_GestureKeyNotFoundInKeyBag({keyId: gestureKeyId, parentKeyId: keyId, attribute: attrs.join(',')}) + LdmlCompilerMessages.Error_GestureKeyNotFoundInKeyBag({ + keyId: gestureKeyId, parentKeyId: keyId, attribute: attrs.join(',') + }, key) ); } else { usedKeys.add(gestureKeyId); @@ -131,10 +134,15 @@ export class KeysCompiler extends SectionCompiler { if (hardwareLayers.length >= 1) { // validate all errors - for (let layers of hardwareLayers) { - for (let layer of layers.layer) { - valid = - this.validateHardwareLayerForKmap(layers.formId, layer, keyBag) && valid; // note: always validate even if previously invalid results found + for (const layers of hardwareLayers) { + const keymap = this.validateAndGetKeymapFromLayers(layers); + if (!keymap) { + valid = false; + } else { + for (const layer of layers.layer) { + valid = + this.validateHardwareLayerForKmap(keymap, layers, layer, keyBag) && valid; // note: always validate even if previously invalid results found + } } } // TODO-LDML: } else { touch? @@ -143,8 +151,30 @@ export class KeysCompiler extends SectionCompiler { return valid; } + private validateAndGetKeymapFromLayers(layers: LDMLKeyboard.LKLayers) : Constants.KeyMap { + const { formId } = layers; + const badScans = new Set(); + const form = this.getForm(formId); + if (!form) { + this.callbacks.reportMessage( + LdmlCompilerMessages.Error_InvalidHardware({formId}, layers) + ); + return null; + } + const keymap = KeysCompiler.getKeymapFromScancodes(form, badScans); + if (!keymap) { + } else if (badScans.size !== 0) { + const codes = Array.from(badScans.values()).map(n => Number(n).toString(16)).sort().join(' '); + this.callbacks.reportMessage( + LdmlCompilerMessages.Error_InvalidScanCode({ codes, id: form.id }, form) + ); + return null; + } + return keymap; + } + static addKeysFromFlicks(usedFlicks: Set, flickHash: Map, usedKeys: Set) { - for (let flickId of usedFlicks.values()) { + for (const flickId of usedFlicks.values()) { const flick = flickHash.get(flickId); if (!flick) continue; const flickKeys = allUsedKeyIdsInFlick(flick); @@ -174,7 +204,7 @@ export class KeysCompiler extends SectionCompiler { return null; } - let sect = new Keys(sections.strs); + const sect = new Keys(sections.strs); // TODO-LDML: some duplication with validate() const keyBag = this.getKeyBag(); @@ -204,7 +234,7 @@ export class KeysCompiler extends SectionCompiler { } else if (hardwareLayers.length === 1) { const theLayers = hardwareLayers[0]; const { formId } = theLayers; - for (let layer of theLayers.layer) { + for (const layer of theLayers.layer) { this.compileHardwareLayerToKmap(sections, layer, sect, formId); } } // else: TODO-LDML do nothing if only touch layers @@ -265,10 +295,10 @@ export class KeysCompiler extends SectionCompiler { } static addUsedGestureKeys(layerKeyIds: string[], keyBag: Map, usedKeys: Set) { - for (let keyId of layerKeyIds) { + for (const keyId of layerKeyIds) { const key = keyBag.get(keyId); if (!key) continue; - for (let gestureKeyId of allUsedKeyIdsInKey(key).keys()) { + for (const gestureKeyId of allUsedKeyIdsInKey(key).keys()) { usedKeys.add(gestureKeyId); } } @@ -288,21 +318,22 @@ export class KeysCompiler extends SectionCompiler { // only include used flicks in the table // this way, extra unused imported flicks are ignored // in id order, for now - for (let flickId of Array.from(usedFlicks.values()).sort()) { + for (const flickId of Array.from(usedFlicks.values()).sort()) { const flick = flickHash.get(flickId); if (!flick) continue; // already reported by validate() // allocate the in-memory - let flicks: KeysFlicks = new KeysFlicks( - sections.strs.allocString(flickId) + const flicks: KeysFlicks = new KeysFlicks( + sections.strs.allocString(flickId, { x: flick }) ); // add data from each segment - for (let { keyId, directions } of flick.flickSegment) { - const keyIdStr = sections.strs.allocString(keyId); - let directionsList: ListItem = sections.list.allocListFromSpaces( + for (const flickSegment of flick.flickSegment) { + const { keyId, directions } = flickSegment; + const keyIdStr = sections.strs.allocString(keyId, { x: flickSegment }); + const directionsList: ListItem = sections.list.allocListFromSpaces( directions, - { }, + { x: flickSegment }, sections); flicks.flicks.push({ directions: directionsList, @@ -317,7 +348,7 @@ export class KeysCompiler extends SectionCompiler { static getUsedFlicks(layerKeyIds: string[], keyBag: Map) { const usedFlicks = new Set(); - for (let keyId of layerKeyIds) { + for (const keyId of layerKeyIds) { const key = keyBag.get(keyId); if (!key?.flickId) continue; usedFlicks.add(key.flickId); @@ -330,7 +361,7 @@ export class KeysCompiler extends SectionCompiler { // for each used key (whether from layer, gesture, etc.) // push these in id order, for tidiness - for (let keyId of Array.from(usedKeys.values()).sort()) { + for (const keyId of Array.from(usedKeys.values()).sort()) { const key = keyBag.get(keyId); if (!key) continue; // missing key @@ -339,20 +370,20 @@ export class KeysCompiler extends SectionCompiler { if (!!gap) { flags |= constants.keys_key_flags_gap; } - const id = sections.strs.allocString(key.id); + const id = sections.strs.allocString(key.id, { x: key }); const longPress: ListItem = sections.list.allocListFromSpaces( - longPressKeyIds, {}, + longPressKeyIds, { x: key }, sections); const longPressDefault = sections.strs.allocString(longPressDefaultKeyId, - {}, + { x: key }, sections); const multiTap: ListItem = sections.list.allocListFromSpaces( multiTapKeyIds, - {}, + { x: key }, sections); - const keySwitch = sections.strs.allocString(layerId); // 'switch' is a reserved word + const keySwitch = sections.strs.allocString(layerId, { x: key }); // 'switch' is a reserved word const toRaw = output; @@ -365,6 +396,7 @@ export class KeysCompiler extends SectionCompiler { unescape: true, singleOk: true, nfd: true, + x: key, }, sections); if (!to.isOneChar) { @@ -390,14 +422,22 @@ export class KeysCompiler extends SectionCompiler { } public static getKeymapFromForms(forms: LDMLKeyboard.LKForm[], hardware: string, badScans?: Set): Constants.KeyMap { - // seach in reverse form because of overrides - const ldmlForm = [...forms].reverse().find((f) => f.id === hardware); + const ldmlForm = KeysCompiler.findForm(forms, hardware); if (!ldmlForm) { return undefined; } return KeysCompiler.getKeymapFromScancodes(ldmlForm, badScans); } + private getForm(hardware: string) { + return KeysCompiler.findForm(this.keyboard3?.forms.form, hardware); + } + + public static findForm(forms: LDMLKeyboard.LKForm[], hardware: string) { + // seach in reverse form because of overrides + return [...forms].reverse().find((f) => f.id === hardware); + } + public static getKeymapFromScancodes(ldmlForm: LDMLKeyboard.LKForm, badScans?: Set) { const { scanCodes } = ldmlForm; const ldmlScan = scanCodes.map(o => o.codes.split(" ").map(n => Number.parseInt(n, 16))); @@ -413,7 +453,8 @@ export class KeysCompiler extends SectionCompiler { * @returns true if valid */ private validateHardwareLayerForKmap( - hardware: string, + keymap: Constants.KeyMap, + layers: LDMLKeyboard.LKLayers, layer: LDMLKeyboard.LKLayer, keyHash: Map ): boolean { @@ -422,54 +463,38 @@ export class KeysCompiler extends SectionCompiler { const { modifiers } = layer; if (!validModifier(modifiers)) { this.callbacks.reportMessage( - LdmlCompilerMessages.Error_InvalidModifier({ modifiers, layer: layer.id }) + LdmlCompilerMessages.Error_InvalidModifier({ modifiers }, layer) ); valid = false; } - const badScans = new Set(); - const keymap = this.getKeymapFromForm(hardware, badScans); - if (!keymap) { - this.callbacks.reportMessage( - LdmlCompilerMessages.Error_InvalidHardware({ formId: hardware }) - ); - valid = false; - return valid; - } else if (badScans.size !== 0) { - const codes = Array.from(badScans.values()).map(n => Number(n).toString(16)).sort(); - this.callbacks.reportMessage( - LdmlCompilerMessages.Error_InvalidScanCode({ form: hardware, codes }) - ); - valid = false; - return valid; - } - if (layer.row.length > keymap.length) { this.callbacks.reportMessage( - LdmlCompilerMessages.Error_HardwareLayerHasTooManyRows() + LdmlCompilerMessages.Error_HardwareLayerHasTooManyRows(layer) ); valid = false; } for (let y = 0; y < layer.row.length && y < keymap.length; y++) { - const keys = layer.row[y].keys.split(" "); + const row = layer.row[y]; + const keys = row.keys.split(" "); if (keys.length > keymap[y].length) { this.callbacks.reportMessage( LdmlCompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({ row: y + 1, - hardware, - modifiers, - }) + hardware: layers.formId, + modifiers: modifiers || 'none', + }, row) ); valid = false; } let x = -1; - for (let key of keys) { + for (const key of keys) { x++; - let keydef = keyHash.get(key); + const keydef = keyHash.get(key); if (!keydef) { this.callbacks.reportMessage( LdmlCompilerMessages.Error_KeyNotFoundInKeyBag({ @@ -478,14 +503,17 @@ export class KeysCompiler extends SectionCompiler { row: y + 1, layer: layer.id, form: "hardware", - }) + }, row) ); valid = false; continue; } if (!keydef.output && !keydef.gap && !keydef.layerId) { this.callbacks.reportMessage( - LdmlCompilerMessages.Error_KeyMissingToGapOrSwitch({ keyId: key }) + LdmlCompilerMessages.Error_KeyMissingToGapOrSwitch( + { keyId: key }, + keydef, + ) ); valid = false; continue; diff --git a/developer/src/kmc-ldml/src/compiler/layr.ts b/developer/src/kmc-ldml/src/compiler/layr.ts index 6c945a1864..9dff22a5bf 100644 --- a/developer/src/kmc-ldml/src/compiler/layr.ts +++ b/developer/src/kmc-ldml/src/compiler/layr.ts @@ -20,27 +20,39 @@ export class LayrCompiler extends SectionCompiler { let valid = true; let totalLayerCount = 0; let hardwareLayers = 0; - // let touchLayers = 0; + let touchLayers = 0; + const deviceWidths = new Set(); this.keyboard3.layers?.forEach((layers) => { const { formId } = layers; if (formId === 'touch') { - // touchLayers++; - // multiple touch layers are OK + touchLayers++; totalLayerCount += layers.layer?.length; - // TODO-LDML: check that widths are distinct + const { minDeviceWidth } = layers; + if (!minDeviceWidth || + minDeviceWidth < constants.layr_min_minDeviceWidth || + minDeviceWidth > constants.layr_max_minDeviceWidth || + Number.isNaN(Number(minDeviceWidth))) { + valid = false; + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidLayerWidth({minDeviceWidth}, layers)); + } else if (deviceWidths.has(minDeviceWidth)) { + valid = false; + this.callbacks.reportMessage(LdmlCompilerMessages.Error_DuplicateLayerWidth({minDeviceWidth}, layers)); + } else { + deviceWidths.add(minDeviceWidth); + } } else { // hardware hardwareLayers++; if (hardwareLayers > 1) { valid = false; - this.callbacks.reportMessage(LdmlCompilerMessages.Error_ExcessHardware({formId})); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_ExcessHardware({formId}, layers)); } } layers.layer.forEach((layer) => { - const { modifiers, id } = layer; + const { modifiers } = layer; totalLayerCount++; if (!validModifier(modifiers)) { - this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidModifier({ modifiers, layer: id || '' })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidModifier({ modifiers }, layer)); valid = false; } }); @@ -48,7 +60,7 @@ export class LayrCompiler extends SectionCompiler { if (totalLayerCount === 0) { // TODO-LDML: does not validate touch layers yet // no layers seen anywhere valid = false; - this.callbacks.reportMessage(LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement()); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(this.keyboard3?.layers[0])); } return valid; } @@ -57,21 +69,22 @@ export class LayrCompiler extends SectionCompiler { const sect = new Layr(); sect.lists = this.keyboard3.layers.map((layers) => { - const hardware = sections.strs.allocString(layers.formId); + const hardware = sections.strs.allocString(layers.formId, {x:layers}); // Already validated in validate const layerEntries = []; for (const layer of layers.layer) { const rows = layer.row.map((row) => { const erow: LayrRow = { - keys: row.keys.trim().split(/[ \t]+/).map((id) => sections.strs.allocString(id)), + keys: row.keys.trim().split(/[ \t]+/).map((id) => sections.strs.allocString(id, { x: row })), }; - return erow; + // include linenumber info for row + return SectionCompiler.copySymbols(erow, row); }); const mods = translateLayerAttrToModifier(layer); // push a layer entry for each modifier set for (const mod of mods) { layerEntries.push({ - id: sections.strs.allocString(layer.id), + id: sections.strs.allocString(layer.id, {x:layer}), mod, rows, }); diff --git a/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts index 7a925a33c5..f76e0d3b3a 100644 --- a/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts +++ b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts @@ -1,5 +1,4 @@ -import { util } from "@keymanapp/common-types"; -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def } from '@keymanapp/developer-utils'; +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageObjectSpec as mx, CompilerMessageSpec as m, CompilerMessageDef as def, ObjectWithMetadata } from '@keymanapp/developer-utils'; // const SevInfo = CompilerErrorSeverity.Info | CompilerErrorNamespace.LdmlKeyboardCompiler; const SevHint = CompilerErrorSeverity.Hint | CompilerErrorNamespace.LdmlKeyboardCompiler; const SevWarn = CompilerErrorSeverity.Warn | CompilerErrorNamespace.LdmlKeyboardCompiler; @@ -14,52 +13,79 @@ const SevErrorTransform = SevError | 0xF00; */ export class LdmlCompilerMessages { static HINT_NormalizationDisabled = SevHint | 0x0001; - static Hint_NormalizationDisabled = () => m(this.HINT_NormalizationDisabled, `normalization=disabled is not recommended.`); + static Hint_NormalizationDisabled = (x?: ObjectWithMetadata) => mx( + this.HINT_NormalizationDisabled, x, + `normalization=disabled is not recommended.`, + ); static ERROR_InvalidLocale = SevError | 0x0002; - static Error_InvalidLocale = (o:{tag: string}) => m(this.ERROR_InvalidLocale, `Invalid BCP 47 locale form '${def(o.tag)}'`); + static Error_InvalidLocale = (o:{tag: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_InvalidLocale, x, + `Invalid BCP 47 locale form '${def(o.tag)}'`, + ); static ERROR_HardwareLayerHasTooManyRows = SevError | 0x0003; - static Error_HardwareLayerHasTooManyRows = () => m(this.ERROR_HardwareLayerHasTooManyRows, `'hardware' layer has too many rows`); + static Error_HardwareLayerHasTooManyRows = (x?: ObjectWithMetadata) => mx( + this.ERROR_HardwareLayerHasTooManyRows, x, + `'hardware' layer has too many rows`, + ); static ERROR_RowOnHardwareLayerHasTooManyKeys = SevError | 0x0004; - static Error_RowOnHardwareLayerHasTooManyKeys = (o:{row: number, hardware: string, modifiers: string}) => m(this.ERROR_RowOnHardwareLayerHasTooManyKeys, `Row #${def(o.row)} on 'hardware' ${def(o.hardware)} layer for modifier ${o.modifiers || 'none'} has too many keys`); + static Error_RowOnHardwareLayerHasTooManyKeys = (o: { row: number, hardware: string, modifiers: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_RowOnHardwareLayerHasTooManyKeys, x, + `Row #${def(o.row)} on 'hardware' ${def(o.hardware)} layer for modifier ${def(o.modifiers)} has too many keys`, + ); static ERROR_KeyNotFoundInKeyBag = SevError | 0x0005; - static Error_KeyNotFoundInKeyBag = (o:{keyId: string, col: number, row: number, layer: string, form: string}) => - m(this.ERROR_KeyNotFoundInKeyBag, `Key '${def(o.keyId)}' in position #${def(o.col)} on row #${def(o.row)} of layer ${def(o.layer)}, form '${def(o.form)}' not found in key bag`); + static Error_KeyNotFoundInKeyBag = (o: { keyId: string, col: number, row: number, layer: string, form: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_KeyNotFoundInKeyBag, x, + `Key '${def(o.keyId)}' in position #${def(o.col)} on row #${def(o.row)} of layer ${def(o.layer)}, form '${def(o.form)}' not found in key bag`, + ); static HINT_OneOrMoreRepeatedLocales = SevHint | 0x0006; - static Hint_OneOrMoreRepeatedLocales = () => - m(this.HINT_OneOrMoreRepeatedLocales, `After minimization, one or more locales is repeated and has been removed`); + static Hint_OneOrMoreRepeatedLocales = (x?: ObjectWithMetadata) => mx( + this.HINT_OneOrMoreRepeatedLocales, x, + `After minimization, one or more locales is repeated and has been removed`, + ); + // This is the only allowed use of m() vs mx() in this file, all the others take context. static ERROR_InvalidFile = SevError | 0x0007; static Error_InvalidFile = (o:{errorText: string}) => m(this.ERROR_InvalidFile, `The source file has an invalid structure: ${def(o.errorText)}`); static HINT_LocaleIsNotMinimalAndClean = SevHint | 0x0008; - static Hint_LocaleIsNotMinimalAndClean = (o:{sourceLocale: string, locale: string}) => - m(this.HINT_LocaleIsNotMinimalAndClean, `Locale '${def(o.sourceLocale)}' is not minimal or correctly formatted and should be '${def(o.locale)}'`); + static Hint_LocaleIsNotMinimalAndClean = (o:{sourceLocale: string, locale: string}, x?: ObjectWithMetadata) => mx( + this.HINT_LocaleIsNotMinimalAndClean, x, + `Locale '${def(o.sourceLocale)}' is not minimal or correctly formatted and should be '${def(o.locale)}'`, + ); static ERROR_InvalidScanCode = SevError | 0x0009; - static Error_InvalidScanCode = (o:{form?: string, codes?: string[]}) => - m(this.ERROR_InvalidScanCode, `Form '${def(o.form)}' has invalid/unknown scancodes '${def(o.codes?.join(' '))}'`); + static Error_InvalidScanCode = (o: { codes: string, id: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_InvalidScanCode, x, + `Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}'`, + ); static WARN_CustomForm = SevWarn | 0x000A; - static Warn_CustomForm = (o:{id: string}) => - m(this.WARN_CustomForm, `Custom
    element. Key layout may not be as expected.`); + static Warn_CustomForm = (o: { id: string }, x?: ObjectWithMetadata) => mx( + this.WARN_CustomForm, x, + `Custom element. Key layout may not be as expected.`, + ); static ERROR_GestureKeyNotFoundInKeyBag = SevError | 0x000B; - static Error_GestureKeyNotFoundInKeyBag = (o:{keyId: string, parentKeyId: string, attribute: string}) => - m(this.ERROR_GestureKeyNotFoundInKeyBag, `Key '${def(o.keyId)}' not found in key bag, referenced from other '${def(o.parentKeyId)}' in ${def(o.attribute)}`); + static Error_GestureKeyNotFoundInKeyBag = (o:{keyId: string, parentKeyId: string, attribute: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_GestureKeyNotFoundInKeyBag, x, + `Key '${def(o.keyId)}' not found in key bag, referenced from other '${def(o.parentKeyId)}' in ${def(o.attribute)}`, + ); static HINT_NoDisplayForMarker = SevHint | 0x000C; - static Hint_NoDisplayForMarker = (o: { id: string }) => - m(this.HINT_NoDisplayForMarker, `Key element with id "${def(o.id)}" has only marker output, but there is no matching display element by output or keyId. Keycap may be blank.`); + static Hint_NoDisplayForMarker = (o: { id: string }, x?: ObjectWithMetadata) => mx( + this.HINT_NoDisplayForMarker, x, + `Key element with id "${def(o.id)}" has only marker output, but there is no matching display element by output or keyId. Keycap may be blank.`, + ); static ERROR_InvalidVersion = SevError | 0x000D; - static Error_InvalidVersion = (o:{version: string}) => m( - this.ERROR_InvalidVersion, + static Error_InvalidVersion = (o: { version: string; }, x?: ObjectWithMetadata) => mx( + this.ERROR_InvalidVersion, x, `Version number '${def(o.version)}' must be a semantic version format string with 'major.minor.patch' components.`, `The version number in the LDML keyboard file must be a [semantic version](https://semver.org) (semver) string. This string has a format of three @@ -72,168 +98,216 @@ export class LdmlCompilerMessages { ); static ERROR_MustBeAtLeastOneLayerElement = SevError | 0x000E; - static Error_MustBeAtLeastOneLayerElement = () => - m(this.ERROR_MustBeAtLeastOneLayerElement, `The source file must contain at least one layer element.`); + static Error_MustBeAtLeastOneLayerElement = (x?: ObjectWithMetadata) => mx( + this.ERROR_MustBeAtLeastOneLayerElement, x, + `The source file must contain at least one layer element.`, + ); static HINT_NoDisplayForSwitch = SevHint | 0x000F; - static Hint_NoDisplayForSwitch = (o: { id: string }) => - m(this.HINT_NoDisplayForSwitch, `Key element with id "${def(o.id)}" is a layer switch key, but there is no matching display element by keyId. Keycap may be blank.`); - - /** annotate the to= or id= entry */ - private static outputOrKeyId(o:{output?: string, keyId?: string}) { - if (o.output && o.keyId) { - return `output='${o.output}' keyId='${o.keyId}'`; - } else if(o.keyId) { - return `keyId='${o.keyId}'`; - } else if (o.output) { - return `output='${o.output}'`; - } else { - return ''; - } - } + static Hint_NoDisplayForSwitch = (o: { id: string }, x?: ObjectWithMetadata) => mx( + this.HINT_NoDisplayForSwitch, x, + `Key element with id "${def(o.id)}" is a layer switch key, but there is no matching display element by keyId. Keycap may be blank.`, + ); static ERROR_DisplayIsRepeated = SevError | 0x0010; - static Error_DisplayIsRepeated = (o:{output?: string, keyId?: string}) => - m(this.ERROR_DisplayIsRepeated, `display ${LdmlCompilerMessages.outputOrKeyId(o)} has more than one display entry.`); + static Error_DisplayIsRepeated = (o:{display?: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_DisplayIsRepeated, x, + `display display='${def(o.display)}' refers to the same keyId or output as another entry.`, + ); static ERROR_KeyMissingToGapOrSwitch = SevError | 0x0011; - static Error_KeyMissingToGapOrSwitch = (o:{keyId: string}) => - m(this.ERROR_KeyMissingToGapOrSwitch, `key id='${def(o.keyId)}' must have either output=, gap=, or layerId=.`); + static Error_KeyMissingToGapOrSwitch = (o:{keyId: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_KeyMissingToGapOrSwitch, x, + `key id='${def(o.keyId)}' must have either output=, gap=, or layerId=.`, + ); static ERROR_ExcessHardware = SevError | 0x0012; - static Error_ExcessHardware = (o:{formId: string}) => m(this.ERROR_ExcessHardware, - `layers formId=${def(o.formId)}: Can only have one non-'touch' element`); + static Error_ExcessHardware = (o:{formId: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_ExcessHardware, x, + `layers formId=${def(o.formId)}: Can only have one non-'touch' element`, + ); static ERROR_InvalidHardware = SevError | 0x0013; - static Error_InvalidHardware = (o:{formId: string}) => m(this.ERROR_InvalidHardware, - `layers has invalid value formId=${def(o.formId)}`); - - private static layerIdOrEmpty(layer : string) { - if (layer) { - return ` on layer id=${def(layer)}`; - } else { - return ''; - } - } + static Error_InvalidHardware = (o: { formId: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_InvalidHardware, x, + `layers has invalid value formId=${def(o.formId)}`, + ); static ERROR_InvalidModifier = SevError | 0x0014; - static Error_InvalidModifier = (o:{layer: string, modifiers: string}) => m(this.ERROR_InvalidModifier, - `layer has invalid modifiers='${def(o.modifiers)}'` + LdmlCompilerMessages.layerIdOrEmpty(o.layer)); + static Error_InvalidModifier = (o:{modifiers: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_InvalidModifier, x, + `layer has invalid modifiers='${def(o.modifiers)}'`, + ); static ERROR_MissingFlicks = SevError | 0x0015; - static Error_MissingFlicks = (o:{flickId: string, id: string}) => m(this.ERROR_MissingFlicks, - `key id=${def(o.id)} refers to missing flickId=${def(o.flickId)}`); + static Error_MissingFlicks = (o: {id: string, flickId: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_MissingFlicks, x, + `key id=${def(o.id)} refers to missing flickId=${def(o.flickId)}`, + ); static ERROR_DuplicateVariable = SevError | 0x0016; - static Error_DuplicateVariable = (o:{ids: string}) => m(this.ERROR_DuplicateVariable, - `duplicate variables: id=${def(o.ids)}`); + static Error_DuplicateVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_DuplicateVariable, x, + `duplicate variable: id=${def(o.id)}`, + ); // Not hit due to XML parsing static ERROR_InvalidTransformsType = SevError | 0x0018; - static Error_InvalidTransformsType = (o:{types: string[]}) => - m(this.ERROR_InvalidTransformsType, `Invalid transforms types: '${def(o.types?.join(','))}'`); + static Error_InvalidTransformsType = (o:{type: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_InvalidTransformsType, x, + `Invalid transforms type: '${def(o.type)}'`, + ); static ERROR_DuplicateTransformsType = SevError | 0x0019; - static Error_DuplicateTransformsType = (o:{types: string[]}) => - m(this.ERROR_DuplicateTransformsType, `Duplicate transforms types: '${def(o.types?.join(','))}'`); + static Error_DuplicateTransformsType = (o:{type: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_DuplicateTransformsType, x, + `Duplicate transforms type: '${def(o.type)}'`, + ); static ERROR_MixedTransformGroup = SevError | 0x001A; - static Error_MixedTransformGroup = () => - m(this.ERROR_MixedTransformGroup, `transformGroup cannot contain both reorder and transform elements`); + static Error_MixedTransformGroup = (x?: ObjectWithMetadata) => mx( + this.ERROR_MixedTransformGroup, x, + `transformGroup cannot contain both reorder and transform elements`, + ); static ERROR_EmptyTransformGroup = SevError | 0x001B; - static Error_EmptyTransformGroup = () => - m(this.ERROR_EmptyTransformGroup, `transformGroup must have either reorder or transform elements`); + static Error_EmptyTransformGroup = (x?: ObjectWithMetadata) => mx( + this.ERROR_EmptyTransformGroup, x, + `transformGroup must have either reorder or transform elements`, + ); static ERROR_MissingStringVariable = SevError | 0x001C; - static Error_MissingStringVariable = (o:{id: string}) => - m(this.ERROR_MissingStringVariable, `Reference to undefined string variable: \${${def(o.id)}}`); + static Error_MissingStringVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_MissingStringVariable, x, + `Reference to undefined string variable: \${${def(o.id)}}`, + ); static ERROR_MissingSetVariable = SevError | 0x001D; - static Error_MissingSetVariable = (o:{id: string}) => - m(this.ERROR_MissingSetVariable, `Reference to undefined set variable: \$[${def(o.id)}]`); + static Error_MissingSetVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_MissingSetVariable, x, + `Reference to undefined set variable: \$[${def(o.id)}]`, + ); static ERROR_MissingUnicodeSetVariable = SevError | 0x001E; - static Error_MissingUnicodeSetVariable = (o:{id: string}) => - m(this.ERROR_MissingUnicodeSetVariable, `Reference to undefined UnicodeSet variable: \$[${def(o.id)}]`); + static Error_MissingUnicodeSetVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_MissingUnicodeSetVariable, x, + `Reference to undefined UnicodeSet variable: \$[${def(o.id)}]`, + ); static ERROR_NeedSpacesBetweenSetVariables = SevError | 0x001F; - static Error_NeedSpacesBetweenSetVariables = (o:{item: string}) => - m(this.ERROR_NeedSpacesBetweenSetVariables, `Need spaces between set variables: ${def(o.item)}`); + static Error_NeedSpacesBetweenSetVariables = (o:{item: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_NeedSpacesBetweenSetVariables, x, + `Need spaces between set variables: ${def(o.item)}`, + ); static ERROR_CantReferenceSetFromUnicodeSet = SevError | 0x0020; - static Error_CantReferenceSetFromUnicodeSet = (o:{id: string}) => - m(this.ERROR_CantReferenceSetFromUnicodeSet, `Illegal use of set variable from within UnicodeSet: \$[${def(o.id)}]`); + static Error_CantReferenceSetFromUnicodeSet = (o:{id: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_CantReferenceSetFromUnicodeSet, x, + `Illegal use of set variable from within UnicodeSet: \$[${def(o.id)}]` + ); static ERROR_MissingMarkers = SevError | 0x0021; - static Error_MissingMarkers = (o: { ids: string[] }) => - m(this.ERROR_MissingMarkers, `Markers used for matching but not defined: ${def(o.ids?.join(','))}`); + static Error_MissingMarkers = (o: { ids: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_MissingMarkers, x, + `Markers used for matching but not defined: ${def(o.ids)}` + ); static ERROR_DisplayNeedsToOrId = SevError | 0x0022; - static Error_DisplayNeedsToOrId = (o:{output?: string, keyId?: string}) => - m(this.ERROR_DisplayNeedsToOrId, `display ${LdmlCompilerMessages.outputOrKeyId(o)} needs output= or keyId=, but not both`); + static Error_DisplayNeedsToOrId = (o:{display?: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_DisplayNeedsToOrId, x, + `display display='${def(o.display)}' needs output= or keyId=, but not both`, + ); static HINT_PUACharacters = SevHint | 0x0023; - static Hint_PUACharacters = (o: { count: number, lowestCh: number }) => - m(this.HINT_PUACharacters, `File contains ${def(o.count)} PUA character(s), including ${util.describeCodepoint(o.lowestCh)}`); + static Hint_PUACharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx( + this.HINT_PUACharacters, x, + `File contains ${def(o.count)} PUA character(s), including ${def(o.lowestCh)}`, + ); static WARN_UnassignedCharacters = SevWarn | 0x0024; - static Warn_UnassignedCharacters = (o: { count: number, lowestCh: number }) => - m(this.WARN_UnassignedCharacters, `File contains ${def(o.count)} unassigned character(s), including ${util.describeCodepoint(o.lowestCh)}`); + static Warn_UnassignedCharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx( + this.WARN_UnassignedCharacters, x, + `File contains ${def(o.count)} unassigned character(s), including ${def(o.lowestCh)}`, + ); static ERROR_IllegalCharacters = SevError | 0x0025; - static Error_IllegalCharacters = (o: { count: number, lowestCh: number }) => - m(this.ERROR_IllegalCharacters, `File contains ${def(o.count)} illegal character(s), including ${util.describeCodepoint(o.lowestCh)}`); + static Error_IllegalCharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_IllegalCharacters, x, + `File contains ${def(o.count)} illegal character(s), including ${def(o.lowestCh) }`, + ); static HINT_CharClassImplicitDenorm = SevHint | 0x0026; - static Hint_CharClassImplicitDenorm = (o: { lowestCh: number }) => - m(this.HINT_CharClassImplicitDenorm, `File has character classes which span non-NFD character(s), including ${util.describeCodepoint(o.lowestCh)}. These will not match any text.`); + static Hint_CharClassImplicitDenorm = (o: { lowestCh: number }, x?: ObjectWithMetadata) => mx( + this.HINT_CharClassImplicitDenorm, x, + `File has character classes which span non-NFD character(s), including ${def(o.lowestCh)}. These will not match any text.`, + ); static WARN_CharClassExplicitDenorm = SevWarn | 0x0027; - static Warn_CharClassExplicitDenorm = (o: { lowestCh: number }) => - m(this.WARN_CharClassExplicitDenorm, `File has character classes which include non-NFD characters(s), including ${util.describeCodepoint(o.lowestCh)}. These will not match any text.`); + static Warn_CharClassExplicitDenorm = (o: { lowestCh: number }, x?: ObjectWithMetadata) => mx( + this.WARN_CharClassExplicitDenorm, x, + `File has character classes which include non-NFD characters(s), including ${def(o.lowestCh)}. These will not match any text.`, + ); - static ERROR_UnparseableReorderSet = SevError | 0x0028; - static Error_UnparseableReorderSet = (o: { from: string, set: string }) => - m(this.ERROR_UnparseableReorderSet, `Illegal UnicodeSet "${def(o.set)}" in reorder "${def(o.from)}`); + // Available: 0x0028 static ERROR_InvalidVariableIdentifier = SevError | 0x0029; - static Error_InvalidVariableIdentifier = (o: { id: string }) => m( - this.ERROR_InvalidVariableIdentifier, + static Error_InvalidVariableIdentifier = (o: { id: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_InvalidVariableIdentifier, x, `Invalid variable identifier "${def(o.id)}". Identifiers must be between 1 and 32 characters, and can use A-Z, a-z, 0-9, and _.`, ); static ERROR_InvalidMarkerIdentifier = SevError | 0x002A; - static Error_InvalidMarkerIdentifier = (o: { id: string }) => m( - this.ERROR_InvalidMarkerIdentifier, + static Error_InvalidMarkerIdentifier = (o: { id: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_InvalidMarkerIdentifier, x, `Invalid marker identifier "\m{${def(o.id)}}". Identifiers must be between 1 and 32 characters, and can use A-Z, a-z, 0-9, and _.`, ); static WARN_StringDenorm = SevWarn | 0x002B; - static Warn_StringDenorm = (o: { s: string }) => - m(this.WARN_StringDenorm, `File contains string "${def(o.s)}" that is neither NFC nor NFD.`); + static Warn_StringDenorm = (o: { s: string }, x?: ObjectWithMetadata) => mx( + this.WARN_StringDenorm, x, + `File contains string "${def(o.s)}" that is neither NFC nor NFD.`, + ); - // Available: 0x02C-0x2F + static ERROR_DuplicateLayerWidth = SevError | 0x002C; + static Error_DuplicateLayerWidth = (o: { minDeviceWidth: number }, x?: ObjectWithMetadata) => mx( + this.ERROR_DuplicateLayerWidth, x, + `Two or more layers have minDeviceWidth=${def(o.minDeviceWidth)}`, + `Touch layers must have distinct widths.` + ); + + static ERROR_InvalidLayerWidth = SevError | 0x002D; + static Error_InvalidLayerWidth = (o: { minDeviceWidth: number }, x?: ObjectWithMetadata) => mx( + this.ERROR_InvalidLayerWidth, x, + `Invalid Layers minDeviceWidth=${def(o.minDeviceWidth)}`, + `Width must be between 1-999 (millimeters), inclusive.` // sync with layr_max_minDeviceWidth / layr_max_maxDeviceWidth (from spec) + ); + + // Available: 0x02E-0x2F static ERROR_InvalidQuadEscape = SevError | 0x0030; - static Error_InvalidQuadEscape = (o: { cp: number }) => - m(this.ERROR_InvalidQuadEscape, `Invalid escape "\\u${util.hexQuad(o?.cp || 0)}". Hint: Use "\\u{${def(o?.cp?.toString(16))}}"`); + static Error_InvalidQuadEscape = (o: { cp: string, recommended: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_InvalidQuadEscape, x, + `Invalid escape "${def(o.cp)}"`, + `**Hint**: Use "${def(o.recommended)}"`, + ); // // Transform syntax errors begin at ...F00 (SevErrorTransform) // This is a bit of a catch-all and represents messages bubbling up from the underlying regex engine static ERROR_UnparseableTransformFrom = SevErrorTransform | 0x00; - static Error_UnparseableTransformFrom = (o: { from: string, message: string }) => - m(this.ERROR_UnparseableTransformFrom, `Invalid transform from="${def(o.from)}": "${def(o.message)}"`); + static Error_UnparseableTransformFrom = (o: { from: string, message: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_UnparseableTransformFrom, x, + `Invalid transform from="${def(o.from)}": "${def(o.message)}"`, + ); //------------------------------------------------------------------------------| // max length of detail message lines (checked by verifyCompilerMessagesObject) | //------------------------------------------------------------------------------| static ERROR_IllegalTransformDollarsign = SevErrorTransform | 0x01; - static Error_IllegalTransformDollarsign = (o: { from: string }) => m( - this.ERROR_IllegalTransformDollarsign, + static Error_IllegalTransformDollarsign = (o: { from: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_IllegalTransformDollarsign, x, `Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, ` **Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a variable name, the variable name may not be valid (A-Z, a-z, 0-9, _, 32 @@ -241,38 +315,36 @@ export class LdmlCompilerMessages { `); static ERROR_TransformFromMatchesNothing = SevErrorTransform | 0x02; - static Error_TransformFromMatchesNothing = (o: { from: string }) => m( - this.ERROR_TransformFromMatchesNothing, + static Error_TransformFromMatchesNothing = (o: { from: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_TransformFromMatchesNothing, x, `Invalid transfom from="${def(o.from)}": Matches an empty string.` ); static ERROR_IllegalTransformPlus = SevErrorTransform | 0x03; - static Error_IllegalTransformPlus = (o: { from: string }) => m( - this.ERROR_IllegalTransformPlus, + static Error_IllegalTransformPlus = (o: { from: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_IllegalTransformPlus, x, `Invalid transform from="${def(o.from)}": Unescaped plus (+) is not valid transform syntax.`, ` **Hint**: Use \`\\+\` to match a literal plus. `); static ERROR_IllegalTransformAsterisk = SevErrorTransform | 0x04; - static Error_IllegalTransformAsterisk = (o: { from: string }) =>m( - this.ERROR_IllegalTransformAsterisk, + static Error_IllegalTransformAsterisk = (o: { from: string }, x?: ObjectWithMetadata) =>mx( + this.ERROR_IllegalTransformAsterisk, x, `Invalid transform from="${def(o.from)}": Unescaped asterisk (*) is not valid transform syntax.`, ` **Hint**: Use \`\\*\` to match a literal asterisk. `); static ERROR_IllegalTransformToUset = SevErrorTransform | 0x05; - static Error_IllegalTransformToUset = (o: { to: string }) => m( - this.ERROR_IllegalTransformToUset, + static Error_IllegalTransformToUset = (o: { to: string }, x?: ObjectWithMetadata) => mx( + this.ERROR_IllegalTransformToUset, x, `Invalid transform to="${def(o.to)}": Set variable (\\$[…]) cannot be used in 'to=' unless part of a map.`, ` **Hint**: If a map was meant, must use the form \`\`. `); static ERROR_UnparseableTransformTo = SevErrorTransform | 0x06; - static Error_UnparseableTransformTo = (o: {to: string, message: string}) => m( - this.ERROR_UnparseableTransformTo, + static Error_UnparseableTransformTo = (o: {to: string, message: string}, x?: ObjectWithMetadata) => mx( + this.ERROR_UnparseableTransformTo, x, `Invalid transform to="${def(o.to)}": "${def(o.message)}"`, ); - - } diff --git a/developer/src/kmc-ldml/src/compiler/linter-keycaps.ts b/developer/src/kmc-ldml/src/compiler/linter-keycaps.ts index 8f1006c54a..9612190fdf 100644 --- a/developer/src/kmc-ldml/src/compiler/linter-keycaps.ts +++ b/developer/src/kmc-ldml/src/compiler/linter-keycaps.ts @@ -35,13 +35,13 @@ export class LinterKeycaps extends Linter { const disp = this.findDisp(id.value, to.value); if (!disp) { - this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForSwitch({ id: id.value })); + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForSwitch({ id: id.value }, key)); } } else if (to.value !== '' && nonMarkerOutput === '') { // has output, but only markers const disp = this.findDisp(id.value, to.value); if (!disp) { - this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForMarker({ id: id.value })); + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForMarker({ id: id.value }, key)); } } } diff --git a/developer/src/kmc-ldml/src/compiler/loca.ts b/developer/src/kmc-ldml/src/compiler/loca.ts index fea8cc4338..3cc222e613 100644 --- a/developer/src/kmc-ldml/src/compiler/loca.ts +++ b/developer/src/kmc-ldml/src/compiler/loca.ts @@ -1,6 +1,6 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlus } from '@keymanapp/common-types'; -import { LDMLKeyboard } from '@keymanapp/developer-utils'; +import { LDMLKeyboard, ObjectWithMetadata } from '@keymanapp/developer-utils'; import { SectionCompiler } from "./section-compiler.js"; import { LdmlCompilerMessages } from "./ldml-compiler-messages.js"; @@ -21,17 +21,27 @@ export class LocaCompiler extends SectionCompiler { */ private getLocales = (keyboard: LKKeyboard) => - [keyboard.locale].concat(Array.isArray(keyboard.locales?.locale) ? keyboard.locales.locale.map(v => v.id) : []) + [keyboard.locale].concat(Array.isArray(keyboard.locales?.locale) ? keyboard.locales.locale.map(v => v.id) : []); + + /** + * Attempt to locate context metadata for a locale, for error messages + * @param loc locale tag + * @returns context metadata or null + */ + private contextForLocale(loc: string) : ObjectWithMetadata { + if (loc == this.keyboard3.locale) return this.keyboard3; + return this.keyboard3?.locales; + } public validate(): boolean { let valid = true; const locales = this.getLocales(this.keyboard3); - for(let tag of locales) { + for(const tag of locales) { try { new Intl.Locale(tag); } catch(e) { if(e instanceof RangeError) { - this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidLocale({tag})); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidLocale({tag}, this.contextForLocale(tag))); valid = false; } else { /* c8 ignore next 2 */ @@ -43,7 +53,7 @@ export class LocaCompiler extends SectionCompiler { } public compile(sections: DependencySections): Loca { - let result = new Loca(); + const result = new Loca(); // This also minimizes locales according to Remove Likely Subtags algorithm: // https://www.unicode.org/reports/tr35/#Likely_Subtags @@ -51,7 +61,8 @@ export class LocaCompiler extends SectionCompiler { const locales = sourceLocales.map((sourceLocale: string) => { const locale = new Intl.Locale(sourceLocale).minimize().toString(); if(locale != sourceLocale) { - this.callbacks.reportMessage(LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale, locale})); + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({ sourceLocale, locale }, + this.contextForLocale(sourceLocale))); } return locale; }); @@ -60,10 +71,10 @@ export class LocaCompiler extends SectionCompiler { // yet include `getCanonicalLocales` but node 16 does include it so we can // safely use it. Also well supported in modern browsers. const canonicalLocales = (Intl as any).getCanonicalLocales(locales) as string[]; - result.locales = canonicalLocales.map(locale => sections.strs.allocString(locale)); + result.locales = canonicalLocales.map(locale => sections.strs.allocString(locale, {x: this.contextForLocale(locale)})); if(result.locales.length < locales.length) { - this.callbacks.reportMessage(LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales()); + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales(this.keyboard3?.locales)); } return result; diff --git a/developer/src/kmc-ldml/src/compiler/meta.ts b/developer/src/kmc-ldml/src/compiler/meta.ts index 66cc7cf20d..e6cd6371ee 100644 --- a/developer/src/kmc-ldml/src/compiler/meta.ts +++ b/developer/src/kmc-ldml/src/compiler/meta.ts @@ -28,11 +28,11 @@ export class MetaCompiler extends SectionCompiler { if(versionNumber !== undefined) { if(versionNumber.match(/^[=v]/i)) { // semver ignores a preceding '=' or 'v' - this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVersion({ version: versionNumber })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVersion({ version: versionNumber }, this.keyboard3)); return false; } if(!semver.parse(versionNumber, {loose: false})) { - this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVersion({ version: versionNumber })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVersion({ version: versionNumber }, this.keyboard3)); return false; } } @@ -41,7 +41,7 @@ export class MetaCompiler extends SectionCompiler { private validateNormalization(normalization?: string) { if (normalization === 'disabled') { - this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NormalizationDisabled()); + this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NormalizationDisabled(this.keyboard3.settings)); } return true; } @@ -52,13 +52,19 @@ export class MetaCompiler extends SectionCompiler { } public compile(sections: DependencySections): Meta { - let result = new Meta(); - result.author = sections.strs.allocString(this.keyboard3.info?.author); - result.conform = sections.strs.allocString(this.keyboard3.conformsTo); - result.layout = sections.strs.allocString(this.keyboard3.info?.layout); - result.name = sections.strs.allocString(this.keyboard3.info?.name); - result.indicator = sections.strs.allocString(this.keyboard3.info?.indicator); - result.version = sections.strs.allocString(this.keyboard3.version?.number ?? "0.0.0"); + const result = new Meta(); + result.author = sections.strs.allocString(this.keyboard3.info?.author, + {x: this.keyboard3.info}); + result.conform = sections.strs.allocString(this.keyboard3.conformsTo, + {x: this.keyboard3}); + result.layout = sections.strs.allocString(this.keyboard3.info?.layout, + {x: this.keyboard3.info}); + result.name = sections.strs.allocString(this.keyboard3.info?.name, + {x: this.keyboard3.info}); + result.indicator = sections.strs.allocString(this.keyboard3.info?.indicator, + {x: this.keyboard3.info}); + result.version = sections.strs.allocString(this.keyboard3.version?.number ?? "0.0.0", + {x: this.keyboard3.version}); result.settings = (this.keyboard3.settings?.normalization == "disabled" ? KeyboardSettings.normalizationDisabled : 0); return result; diff --git a/developer/src/kmc-ldml/src/compiler/substitution-tracker.ts b/developer/src/kmc-ldml/src/compiler/substitution-tracker.ts index 6f07475490..61daefa0cf 100644 --- a/developer/src/kmc-ldml/src/compiler/substitution-tracker.ts +++ b/developer/src/kmc-ldml/src/compiler/substitution-tracker.ts @@ -1,4 +1,5 @@ import { LdmlKeyboardTypes } from "@keymanapp/common-types"; +import { ObjectWithMetadata } from "@keymanapp/developer-utils"; /** * Verb for SubstitutionTracker.add() @@ -14,7 +15,7 @@ export enum SubstitutionUse { variable, } -type SubstitutionSet = Set; +type SubstitutionSet = Map; /** Tracks usage of markers */ export class SubstitutionTracker { @@ -28,10 +29,10 @@ export class SubstitutionTracker { all: SubstitutionSet; constructor() { - this.emitted = new Set(); - this.consumed = new Set(); - this.matched = new Set(); - this.all = new Set(); + this.emitted = new Map(); + this.consumed = new Map(); + this.matched = new Map(); + this.all = new Map(); } /** @@ -39,32 +40,33 @@ export class SubstitutionTracker { * @param verb what kind of use we are adding * @param markers list of substitutions to add */ - add(verb: SubstitutionUse, markers: string[]) { + add(verb: SubstitutionUse, markers: string[], x?: ObjectWithMetadata) { if (!markers.length) { return; // skip if empty } + x = x || {}; // need at least an empty object if (verb == SubstitutionUse.emit) { markers.forEach((m) => { - this.emitted.add(m); - this.all.add(m); + this.emitted.set(m, x); + this.all.set(m,x); }); } else if (verb == SubstitutionUse.consume) { markers.forEach((m) => { - this.consumed.add(m); - this.all.add(m); + this.consumed.set(m,x); + this.all.set(m,x); }); } else if (verb == SubstitutionUse.match) { markers.forEach((m) => { - this.matched.add(m); - this.all.add(m); + this.matched.set(m,x); + this.all.set(m,x); }); } else if (verb == SubstitutionUse.variable) { markers.forEach((m) => { // we don't know, so add it to all three - this.matched.add(m); - this.emitted.add(m); - this.consumed.add(m); - this.all.add(m); + this.matched.set(m,x); + this.emitted.set(m,x); + this.consumed.set(m,x); + this.all.set(m,x); }); /* c8 skip next 3 */ } else { @@ -75,27 +77,27 @@ export class SubstitutionTracker { /** rollup of several substitution types */ export class Substitutions { - addSetAndStringSubtitution(verb: SubstitutionUse, str?: string) { - this.set.add(verb, LdmlKeyboardTypes.VariableParser.allSetReferences(str)); - this.addStringAndMarkerSubstitution(verb, str); + addSetAndStringSubtitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) { + this.set.add(verb, LdmlKeyboardTypes.VariableParser.allSetReferences(str), x); + this.addStringAndMarkerSubstitution(verb, str, x); } /** add a string that can have string var substitutions or markers */ - addStringAndMarkerSubstitution(verb: SubstitutionUse, str?: string) { - this.addMarkers(verb, str); - this.addStringSubstitution(verb, str); + addStringAndMarkerSubstitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) { + this.addMarkers(verb, str, x); + this.addStringSubstitution(verb, str, x); } - addStringSubstitution(verb: SubstitutionUse, str?: string) { - this.string.add(verb, LdmlKeyboardTypes.VariableParser.allStringReferences(str)); + addStringSubstitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) { + this.string.add(verb, LdmlKeyboardTypes.VariableParser.allStringReferences(str), x); } /** add a string that's just markers */ - addMarkers(verb: SubstitutionUse, str?: string) { - this.markers.add(verb, LdmlKeyboardTypes.MarkerParser.allReferences(str)); - LdmlKeyboardTypes.MarkerParser.allBrokenReferences(str).forEach(m => this.badMarkers.add(m)); + addMarkers(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) { + this.markers.add(verb, LdmlKeyboardTypes.MarkerParser.allReferences(str), x); + LdmlKeyboardTypes.MarkerParser.allBrokenReferences(str).forEach(m => this.badMarkers.set(m, x)); } // all valid markers markers: SubstitutionTracker; // all invalid markers - badMarkers: Set; + badMarkers: SubstitutionSet; // all valid set ids set: SubstitutionTracker; // all valid string ids @@ -108,6 +110,6 @@ export class Substitutions { this.set = new SubstitutionTracker(); this.string = new SubstitutionTracker(); this.uset = new SubstitutionTracker(); - this.badMarkers = new Set(); + this.badMarkers = new Map(); } } diff --git a/developer/src/kmc-ldml/src/compiler/tran.ts b/developer/src/kmc-ldml/src/compiler/tran.ts index 95d79dd468..1cc9bd0289 100644 --- a/developer/src/kmc-ldml/src/compiler/tran.ts +++ b/developer/src/kmc-ldml/src/compiler/tran.ts @@ -1,6 +1,6 @@ import { constants, SectionIdent } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlus, LdmlKeyboardTypes, util } from '@keymanapp/common-types'; -import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils"; +import { CompilerCallbacks, LDMLKeyboard, ObjectWithMetadata } from "@keymanapp/developer-utils"; import { SectionCompiler } from "./section-compiler.js"; import Bksp = KMXPlus.Bksp; @@ -18,6 +18,7 @@ import { verifyValidAndUnique } from "../util/util.js"; import { LdmlCompilerMessages } from "./ldml-compiler-messages.js"; import { Substitutions, SubstitutionUse } from "./substitution-tracker.js"; import { transform_from_parse, transform_to_parse } from "../util/abnf/abnf.js"; +import { StrsOptions } from "../../../../../common/web/types/src/kmx/kmx-plus/kmx-plus.js"; type TransformCompilerType = 'simple' | 'backspace'; @@ -26,22 +27,24 @@ export abstract class TransformCompiler transforms.transformGroup.forEach(transformGroup => { - transformGroup.transform?.forEach(({ to, from }) => { - st.addSetAndStringSubtitution(SubstitutionUse.consume, from); - st.addSetAndStringSubtitution(SubstitutionUse.emit, to); + transformGroup.transform?.forEach((transform) => { + const { to, from } = transform; + st.addSetAndStringSubtitution(SubstitutionUse.consume, from, transform); + st.addSetAndStringSubtitution(SubstitutionUse.emit, to, transform); const mapFrom = LdmlKeyboardTypes.VariableParser.CAPTURE_SET_REFERENCE.exec(from); const mapTo = LdmlKeyboardTypes.VariableParser.MAPPED_SET_REFERENCE.exec(to || ''); if (mapFrom) { // add the 'from' as a match - st.set.add(SubstitutionUse.consume, [mapFrom[1]]); + st.set.add(SubstitutionUse.consume, [mapFrom[1]], transform); } if (mapTo) { // add the 'from' as a match - st.set.add(SubstitutionUse.emit, [mapTo[1]]); + st.set.add(SubstitutionUse.emit, [mapTo[1]], transform); } }); - transformGroup.reorder?.forEach(({ before }) => { - st.addStringSubstitution(SubstitutionUse.consume, before); + transformGroup.reorder?.forEach((reorder) => { + const { before } = reorder; + st.addStringSubstitution(SubstitutionUse.consume, before, reorder); }); })); return true; @@ -55,37 +58,37 @@ export abstract class TransformCompiler type); + if (transforms && this.id == "tran") { + // Only run this part in the main "tran" compiler. + // we use this map to get back to a context object for the error message + const typeToObject = new Map(); + transforms.forEach(t => typeToObject.set(t.type, t)); + const types = transforms.map(({type}) => type); if (!verifyValidAndUnique(types, - types => reportMessage(LdmlCompilerMessages.Error_DuplicateTransformsType({ types })), - new Set(['simple', 'backspace']), - types => reportMessage(LdmlCompilerMessages.Error_InvalidTransformsType({ types })))) { + types => types.forEach(type => + reportMessage(LdmlCompilerMessages.Error_DuplicateTransformsType({ type }, typeToObject.get(type))) + ), + new Set(ALLOWED_TYPES), + types => types.forEach(type => + reportMessage(LdmlCompilerMessages.Error_InvalidTransformsType({ type }, typeToObject.get(type))) + ) + )) { valid = false; } - // check for mixed groups - let mixed = false; - let empty = false; - transforms.forEach(({transformGroup}) => transformGroup.forEach((transformGroup) => { + transforms.forEach(({ type, transformGroup }) => transformGroup.forEach((transformGroup) => { + if (this.type != type) return; // only validate bskp in bksp, tran in tran, etc. if (transformGroup.reorder?.length && transformGroup.transform?.length) { - mixed = true; - } - if (!transformGroup.reorder?.length && !transformGroup.transform?.length) { - empty = true; + valid = false; + reportMessage(LdmlCompilerMessages.Error_MixedTransformGroup(transformGroup)); + } else if (!transformGroup.reorder?.length && !transformGroup.transform?.length) { + valid = false; + reportMessage(LdmlCompilerMessages.Error_EmptyTransformGroup(transformGroup)); } })); - if (mixed) { - valid = false; - reportMessage(LdmlCompilerMessages.Error_MixedTransformGroup()); // report this once - } - if (empty) { - valid = false; - reportMessage(LdmlCompilerMessages.Error_EmptyTransformGroup()); // report this once - } // TODO-LDML: linting here should check for identical from, but this involves a double-parse which is ugly // TODO-LDML: unicodesets means that either we fully parse them and verify conflicting rules or the linting is imperfect @@ -96,14 +99,14 @@ export abstract class TransformCompiler DTD - this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVariableIdentifier({id})); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVariableIdentifier({id}, x)); return false; } return true; @@ -53,8 +53,8 @@ export class VarsCompiler extends SectionCompiler { const variables = this.keyboard3?.variables; // Check for duplicate ids - const allIds = new Set(); - const dups = new Set(); + const allIds = new Map(); + const dups = new Set(); const allStrings = new Set(); const allSets = new Set(); @@ -63,46 +63,49 @@ export class VarsCompiler extends SectionCompiler { /** * add an ID to check for duplicates * @param id id + * @param x context */ - function addId(id: string): void { + function addId(id: string, x: LDMLKeyboard.Variable): void { if (allIds.has(id)) { dups.add(id); } else { - allIds.add(id); + allIds.set(id, x); } } if (variables) { // Strings - for (const { id, value } of variables.string) { - if(!this.validateIdentifier(id)) { + for (const e of variables.string) { + const { id, value } = e; + if(!this.validateIdentifier(id, e)) { valid = false; continue; } - addId(id); + addId(id, e); const stringrefs = LdmlKeyboardTypes.VariableParser.allStringReferences(value); for(const ref of stringrefs) { if(!allStrings.has(ref)) { valid = false; - this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingStringVariable({id: ref})); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingStringVariable({id: ref}, e)); allStrings.add(ref); // avoids multiple reports of same missing variable } } - st.string.add(SubstitutionUse.variable, stringrefs); + st.string.add(SubstitutionUse.variable, stringrefs, e); allStrings.add(id); - st.addMarkers(SubstitutionUse.variable, value); + st.addMarkers(SubstitutionUse.variable, value, e); } // Sets - for (const { id, value } of variables.set) { + for (const e of variables.set) { + const { id, value } = e; if(!this.validateIdentifier(id)) { valid = false; continue; } - addId(id); + addId(id, e); allSets.add(id); // check for illegal references, here. const stringrefs = LdmlKeyboardTypes.VariableParser.allStringReferences(value); - st.string.add(SubstitutionUse.variable, stringrefs); + st.string.add(SubstitutionUse.variable, stringrefs, e); // Now split into spaces. const items: string[] = LdmlKeyboardTypes.VariableParser.setSplitter(value); @@ -111,32 +114,33 @@ export class VarsCompiler extends SectionCompiler { if (setrefs.length > 1) { // this is the form $[seta]$[setb] valid = false; - this.callbacks.reportMessage(LdmlCompilerMessages.Error_NeedSpacesBetweenSetVariables({ item })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_NeedSpacesBetweenSetVariables({ item }, e)); } else { - st.set.add(SubstitutionUse.variable, setrefs); // the reference to a 'map' + st.set.add(SubstitutionUse.variable, setrefs, e); // the reference to a 'map' } // TODO-LDML: Are there other illegal cases here? what about "x$[set]"? } } // UnicodeSets - for (const { id, value } of variables.uset) { + for (const e of variables.uset) { + const { id, value } = e; if(!this.validateIdentifier(id)) { valid = false; continue; } - addId(id); + addId(id, e); allUnicodeSets.add(id); const stringrefs = LdmlKeyboardTypes.VariableParser.allStringReferences(value); - st.string.add(SubstitutionUse.variable, stringrefs); + st.string.add(SubstitutionUse.variable, stringrefs, e); const setrefs = LdmlKeyboardTypes.VariableParser.allSetReferences(value); for (const id2 of setrefs) { if (!allUnicodeSets.has(id2)) { valid = false; if (allSets.has(id2)) { // $[set] in a UnicodeSet must be another UnicodeSet. - this.callbacks.reportMessage(LdmlCompilerMessages.Error_CantReferenceSetFromUnicodeSet({ id: id2 })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_CantReferenceSetFromUnicodeSet({ id: id2 }, e)); } else { - st.uset.add(SubstitutionUse.variable, [id2]); + st.uset.add(SubstitutionUse.variable, [id2], e); } } } @@ -144,31 +148,33 @@ export class VarsCompiler extends SectionCompiler { } // one report if any dups if (dups.size > 0) { - this.callbacks.reportMessage(LdmlCompilerMessages.Error_DuplicateVariable({ - ids: Array.from(dups.values()).sort().join(', ') - })); + for (const id of dups.values()) { + this.callbacks.reportMessage(LdmlCompilerMessages.Error_DuplicateVariable({ + id + }, allIds.get(id))); + } valid = false; } // check for any missing vars - for (const id of st.set.all) { + for (const [id,x] of st.set.all.entries()) { // note: we check the uset list also. we don't know until later which was // intended. collisions are handled separately. if (!allSets.has(id) && !allUnicodeSets.has(id)) { valid = false; - this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingSetVariable({ id })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingSetVariable({ id }, x)); } } - for (const id of st.string.all) { + for (const [id,x] of st.string.all.entries()) { if (!allStrings.has(id)) { valid = false; - this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingStringVariable({ id })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingStringVariable({ id }, x)); } } - for (const id of st.uset.all) { + for (const [id,x] of st.uset.all.entries()) { if (!allUnicodeSets.has(id)) { valid = false; - this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingUnicodeSetVariable({ id })); + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingUnicodeSetVariable({ id }, x)); } } @@ -192,13 +198,13 @@ export class VarsCompiler extends SectionCompiler { // see if there are any matched-but-not-emitted markers const matchedNotEmitted : Set = new Set(); const mt = st.markers; - for (const m of mt.matched.values()) { + for (const m of mt.matched.keys()) { if (m === LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID) continue; // match-all marker if (!mt.emitted.has(m)) { matchedNotEmitted.add(m); } } - for (const m of mt.consumed.values()) { + for (const m of mt.consumed.keys()) { if (m === LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID) continue; // match-all marker if (!mt.emitted.has(m)) { matchedNotEmitted.add(m); @@ -207,7 +213,9 @@ export class VarsCompiler extends SectionCompiler { // report once if (matchedNotEmitted.size > 0) { - this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingMarkers({ ids: Array.from(matchedNotEmitted.values()).sort() })); + const ids = Array.from(matchedNotEmitted.values()).sort(); + const x = mt.all.get(ids[0]); // get the FIRST object for context + this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingMarkers({ ids: ids.join(',')}, x)); valid = false; } @@ -216,19 +224,24 @@ export class VarsCompiler extends SectionCompiler { if (!!st.badMarkers.size) { valid = false; - st.badMarkers.forEach(id => - this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidMarkerIdentifier({ id }))); + for (const [id, x] of st.badMarkers.entries()) { + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidMarkerIdentifier({ id }, x)); + } } return valid; } validateSubstitutions(keyboard: LDMLKeyboard.LKKeyboard, st : Substitutions) : boolean { - keyboard?.variables?.string?.forEach(({value}) => - st.markers.add(SubstitutionUse.variable, LdmlKeyboardTypes.MarkerParser.allReferences(value))); + keyboard?.variables?.string?.forEach((string) => { + const {value} = string; + st.markers.add(SubstitutionUse.variable, LdmlKeyboardTypes.MarkerParser.allReferences(value), string); + }); // get markers mentioned in a set - keyboard?.variables?.set?.forEach(({ value }) => - LdmlKeyboardTypes.VariableParser.setSplitter(value).forEach(v => st.markers.add(SubstitutionUse.match, LdmlKeyboardTypes.MarkerParser.allReferences(v)))); + keyboard?.variables?.set?.forEach((set) => { + const { value } = set; + LdmlKeyboardTypes.VariableParser.setSplitter(value).forEach(v => st.markers.add(SubstitutionUse.match, LdmlKeyboardTypes.MarkerParser.allReferences(v), set)); + }); return true; } @@ -254,8 +267,16 @@ export class VarsCompiler extends SectionCompiler { const mt = st.markers; // collect all markers, excluding the match-all - const allMarkers : string[] = Array.from(mt.all).filter(m => m !== LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID).sort(); - result.markers = sections.list.allocList(allMarkers, {}, sections); + const allMarkers : string[] = Array.from(mt.all.keys()).filter(m => m !== LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID).sort(); + result.markers = sections.list.allocList(allMarkers, { + // pass the first object in the listr + x: mt.all.get(allMarkers[0]||'') + // however, this is the string with the marker *id*. So, from the StrsCompiler + // point of view, the value of this context would be if the marker ID had, say, + // invalid Unicode in it for ERROR_IllegalCharacters - but, that wouldn't be a + // valid Marker ID either. + // So it's not likely that this context will be used, but it's available. + }, sections); // sets need to be added late, because they can refer to markers variables?.set?.forEach((e) => diff --git a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts index 7a4d5d37f0..480ac5add5 100644 --- a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts @@ -49,7 +49,7 @@ export class LdmlKeyboardVisualKeyboardCompiler { * false if no VK was generated for this keyboard */ public compile(source: KMXPlus.KMXPlusData, keyboardId: string): VisualKeyboard.VisualKeyboard | boolean | null { - let result = new VisualKeyboard.VisualKeyboard(); + const result = new VisualKeyboard.VisualKeyboard(); /* TODO-LDML: consider VisualKeyboardHeaderFlags.kvkhUseUnderlying kvkhDisplayUnderlying kvkhAltGr kvkh102 */ result.header.flags = 0; @@ -60,13 +60,13 @@ export class LdmlKeyboardVisualKeyboardCompiler { let hasVisualKeyboard = false; - for(let layersList of source.layr.lists) { + for(const layersList of source.layr.lists) { const formId = layersList.hardware.value; if(formId == 'touch') { continue; } - for(let layer of layersList.layers) { + for(const layer of layersList.layers) { const res = this.compileHardwareLayer(source, result, layer, formId); if(res === false) { // failed to compile the layer @@ -105,10 +105,10 @@ export class LdmlKeyboardVisualKeyboardCompiler { let result = true; let y = -1; - for(let row of layer.rows) { + for(const row of layer.rows) { y++; let x = -1; - for(let key of row.keys) { + for(const key of row.keys) { x++; const keydef: KMXPlus.KeysKeys = findLast(source.keys?.keys, (kd: KMXPlus.KeysKeys) => kd.id.value == key.value); @@ -117,7 +117,9 @@ export class LdmlKeyboardVisualKeyboardCompiler { if (!keydef || !kmap || text === null) { this.callbacks.reportMessage( - LdmlCompilerMessages.Error_KeyNotFoundInKeyBag({ keyId: key.value, layer: layerId, row: y, col: x, form: hardware }) + LdmlCompilerMessages.Error_KeyNotFoundInKeyBag({ + keyId: key.value, layer: layerId, row: y, col: x, form: hardware + }, row) ); result = false; } else { diff --git a/developer/src/kmc-ldml/src/util/util.ts b/developer/src/kmc-ldml/src/util/util.ts index dbb5dc0531..316e8bd5d7 100644 --- a/developer/src/kmc-ldml/src/util/util.ts +++ b/developer/src/kmc-ldml/src/util/util.ts @@ -120,15 +120,15 @@ export function allUsedKeyIdsInFlick(flick? : LDMLKeyboard.LKFlick) : Set void, - allowed?: Set, - onInvalid?: (invalids: string[]) => void) +export function verifyValidAndUnique( + values: T[], + onDuplicate: (duplicates: T[]) => void, + allowed?: Set, + onInvalid?: (invalids: T[]) => void) : boolean { - const dups: string[] = []; - const invalids: string[] = []; - const seen = new Set(); + const dups: T[] = []; + const invalids: T[] = []; + const seen = new Set(); for (const value of values) { if (allowed && !allowed.has(value)) { invalids.push(value); @@ -140,7 +140,7 @@ export function verifyValidAndUnique( } } - function dedupedSortedArray(values: string[]) : string[] { + function dedupedSortedArray(values: T[]) : T[] { return Array.from(new Set(values)).sort(); } @@ -171,7 +171,7 @@ function translateModifierSubsetToLayer(modifiers: string) : number { throw Error(`translateModifierSubsetToLayer only takes a single subset of the modifiers`); } let mod = constants.keys_mod_none; - for (let str of modifiers.split(' ')) { + for (const str of modifiers.split(' ')) { const submod = constants.keys_mod_map.get(str); mod |= submod; } @@ -188,8 +188,8 @@ function translateModifierSubsetToLayer(modifiers: string) : number { export function validModifier(modifier?: string) : boolean { if (!modifier) return true; // valid to have no modifier, == none // TODO-LDML: enforce illegal combinations per spec. - for (let sub of modifier.trim().split(',')) { - for (let str of sub.trim().split(' ')) { + for (const sub of modifier.trim().split(',')) { + for (const str of sub.trim().split(' ')) { if (!constants.keys_mod_map.has(str)) { return false; } diff --git a/developer/src/kmc-ldml/test/compiler-e2e.tests.ts b/developer/src/kmc-ldml/test/compiler-e2e.tests.ts index 2277e1c2a1..7c05d39d22 100644 --- a/developer/src/kmc-ldml/test/compiler-e2e.tests.ts +++ b/developer/src/kmc-ldml/test/compiler-e2e.tests.ts @@ -1,11 +1,13 @@ import 'mocha'; import {assert} from 'chai'; import hextobin from '@keymanapp/hextobin'; -import {compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture} from './helpers/index.js'; +import {compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture, scrubContextFromMessages} from './helpers/index.js'; import { compareXml } from './helpers/compareXml.js'; import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js'; import { kmxToXml } from '../src/util/serialize.js'; import { writeFileSync } from 'node:fs'; +import { LdmlCompilerMessages } from '../src/main.js'; +import { util } from '@keymanapp/common-types'; /** Overall compiler tests */ describe('compiler-tests', function() { @@ -24,7 +26,7 @@ describe('compiler-tests', function() { const binaryFilename = makePathToFixture('basic.txt'); // Compare output - let expected = await hextobin(binaryFilename, undefined, {silent:true}); + const expected = await hextobin(binaryFilename, undefined, {silent:true}); // now compare it to use with run() // Let's build basic.xml @@ -42,6 +44,25 @@ describe('compiler-tests', function() { assert.isNotNull(kvk?.data); }); + it('should-validate-on-run compiling sections/strs/invalid-illegal.xml', async function() { + this.timeout(4000); + const inputFilename = makePathToFixture('sections/strs/invalid-illegal.xml'); + + // should fail validation + const k = new LdmlKeyboardCompiler(); + await k.init(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); + + const runOutput = await k.run(inputFilename, "invalid-illegal.kmx"); // need the exact name passed to build-fixtures + assert.isNull(runOutput, "Expect invalid-illegal to fail to run()"); + assert.sameDeepMembers( scrubContextFromMessages(compilerTestCallbacks.messages), [ + // copied from strs.tests.ts + // validation messages + LdmlCompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: util.describeCodepoint(0xFDD0) }), + LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }), + ]); + }); + + it('should-serialize-kmx', async function() { this.timeout(4000); // Let's build basic.xml diff --git a/developer/src/kmc-ldml/test/disp.tests.ts b/developer/src/kmc-ldml/test/disp.tests.ts index 31d79ac5e2..3bad95e339 100644 --- a/developer/src/kmc-ldml/test/disp.tests.ts +++ b/developer/src/kmc-ldml/test/disp.tests.ts @@ -1,7 +1,7 @@ import 'mocha'; import {assert} from 'chai'; import { DispCompiler } from '../src/compiler/disp.js'; -import { compilerTestCallbacks, loadSectionFixture, testCompilationCases } from './helpers/index.js'; +import { compilerTestCallbacks, loadSectionFixture, testCompilationCases, withOffset } from './helpers/index.js'; import { KMXPlus } from '@keymanapp/common-types'; import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; @@ -11,14 +11,14 @@ describe('disp', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while it('should compile minimal disp', async function() { - let disp = await loadSectionFixture(DispCompiler, 'sections/disp/minimal.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/minimal.xml', compilerTestCallbacks) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.ok(disp?.disps); assert.equal(disp.disps.length, 0); }); it('should compile typical disp', async function() { - let disp = await loadSectionFixture(DispCompiler, 'sections/disp/typical.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/typical.xml', compilerTestCallbacks) as Disp; assert.sameDeepMembers(compilerTestCallbacks.messages, []); assert.ok(disp?.disps); @@ -27,7 +27,7 @@ describe('disp', function () { assert.equal(disp.disps[0].display?.value, '`'); }); it('should compile maximal disp', async function() { - let disp = await loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(disp?.baseCharacter?.value, 'x'); assert.ok(disp?.disps); @@ -40,7 +40,7 @@ describe('disp', function () { assert.equal(disp.disps[2].display?.value, '(g)'); }); it('should compile escaped disp', async function() { - let disp = await loadSectionFixture(DispCompiler, 'sections/disp/escaped.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/escaped.xml', compilerTestCallbacks) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(disp?.baseCharacter?.value, 'x'); assert.ok(disp?.disps); @@ -51,41 +51,41 @@ describe('disp', function () { assert.equal(disp.disps[1].display?.value, '(f)'); }); it('should compile options-only disp', async function() { - let disp = await loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(disp?.baseCharacter?.value, 'x'); assert.ok(disp?.disps); }); it('should compile disp without converting markers', async function() { - let disp = await loadSectionFixture(DispCompiler, 'sections/disp/not-a-marker.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/not-a-marker.xml', compilerTestCallbacks) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.ok(disp?.disps); assert.equal(disp.disps.length, 1); assert.equal(disp.disps[0].display?.value, '\\m{hat}'); }); it('should reject duplicate tos', async function() { - let disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupto.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupto.xml', compilerTestCallbacks) as Disp; assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ output: 'e' })); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ display: '(e)' }, withOffset(330))); }); it('should reject duplicate ids', async function() { - let disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupid.xml', compilerTestCallbacks) as Disp;1 + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupid.xml', compilerTestCallbacks) as Disp;1 assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ keyId: 'e' })); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ display: '(e)' }, withOffset(329))); }); it('should reject if neither to nor id', async function() { - let disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-none.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-none.xml', compilerTestCallbacks) as Disp; assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ display: '(f)'}, withOffset(182))); }); it('should reject if both to and id', async function() { - let disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-both.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-both.xml', compilerTestCallbacks) as Disp; assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ output: 'e', keyId: 'e' })); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ display: '(e)' }, withOffset(182))); }); testCompilationCases(DispCompiler, [ { diff --git a/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-0.xml b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-0.xml new file mode 100644 index 0000000000..3054afceb7 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-0.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-1024.xml b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-1024.xml new file mode 100644 index 0000000000..d1d34b598f --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-1024.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-1500.xml b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-1500.xml new file mode 100644 index 0000000000..5000e27304 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-1500.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-x.xml b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-x.xml new file mode 100644 index 0000000000..fec6f8d26f --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-bad-width-x.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/layr/error-dup-width.xml b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-dup-width.xml new file mode 100644 index 0000000000..695e2dbde0 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/layr/error-dup-width.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/layr/invalid-missing-layer2.xml b/developer/src/kmc-ldml/test/fixtures/sections/layr/invalid-missing-layer2.xml index c8a38602ea..331cd7e9e7 100644 --- a/developer/src/kmc-ldml/test/fixtures/sections/layr/invalid-missing-layer2.xml +++ b/developer/src/kmc-ldml/test/fixtures/sections/layr/invalid-missing-layer2.xml @@ -5,4 +5,5 @@ + diff --git a/developer/src/kmc-ldml/test/helpers/compareXml.ts b/developer/src/kmc-ldml/test/helpers/compareXml.ts index f245da51a9..057986bdb2 100644 --- a/developer/src/kmc-ldml/test/helpers/compareXml.ts +++ b/developer/src/kmc-ldml/test/helpers/compareXml.ts @@ -1,6 +1,6 @@ import {assert} from 'chai'; import {readFileSync} from 'node:fs'; -import { KeymanXMLReader } from "@keymanapp/developer-utils"; +import { KeymanXMLReader, SymbolUtils } from "@keymanapp/developer-utils"; /** * @@ -21,5 +21,5 @@ export function compareXml(actual : string, expect: string, mutator?: (input: an const actualParsed = mutator(reader.parse(actualStr)); const expectParsed = mutator(reader.parse(expectStr)); - assert.deepEqual(actualParsed, expectParsed); + assert.deepEqual(SymbolUtils.removeSymbols(actualParsed), SymbolUtils.removeSymbols(expectParsed)); } diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index 7b67ee4e4c..967287cf87 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import { fileURLToPath } from 'url'; import { SectionCompiler, SectionCompilerNew } from '../../src/compiler/section-compiler.js'; import { util, KMXPlus, LdmlKeyboardTypes } from '@keymanapp/common-types'; -import { CompilerEvent, compilerEventFormat, CompilerCallbacks, LDMLKeyboardXMLSourceFileReader, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboard, } from "@keymanapp/developer-utils"; +import { CompilerEvent, compilerEventFormat, CompilerCallbacks, LDMLKeyboardXMLSourceFileReader, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboard, KeymanXMLMetadata, KeymanXMLReader, CompilerError } from "@keymanapp/developer-utils"; import { LdmlKeyboardCompiler } from '../../src/main.js'; // make sure main.js compiles import { assert } from 'chai'; import { KMXPlusMetadataCompiler } from '../../src/compiler/metadata-compiler.js'; @@ -80,7 +80,7 @@ export async function loadSectionFixture(compilerClass: SectionCompilerNew, file return null; } - let sections: DependencySections = { + const sections: DependencySections = { usetparser: await getTestUnicodeSetParser(callbacks) }; @@ -144,7 +144,7 @@ export async function compileKeyboard(inputFilename: string, options: LdmlCompil const valid = await k.validate(source); if (validateMessages) { - assert.sameDeepMembers(compilerTestCallbacks.messages, validateMessages, "validation messages mismatch"); + assert.sameDeepMembers(scrubContextFromMessages(compilerTestCallbacks.messages), validateMessages, "validation messages mismatch"); assert.notEqual(valid, expectFailValidate, 'validation failure'); } else { checkMessages(); @@ -155,7 +155,7 @@ export async function compileKeyboard(inputFilename: string, options: LdmlCompil const kmx = await k.compile(source); if (compileMessages) { - assert.sameDeepMembers(compilerTestCallbacks.messages, compileMessages, "compiler messages mismatch"); + assert.sameDeepMembers(scrubContextFromMessages(compilerTestCallbacks.messages), compileMessages, "compiler messages mismatch"); } else { checkMessages(); } @@ -252,6 +252,31 @@ export interface CompilationCase { * Optional, if true, postValidate() must return false. (must be != postValidate()) */ postValidateFail?: boolean; + /** + * retain offset (line number) information. otherwise, scrub it to reduce testing noise. + * only tests that specifically are checking offsets will set this to true + */ + retainOffsetInMessages?: boolean; +} + +/** + * Scrub 'context' from messages. to simplify unit tests + * @param messages input array of messages + * @returns copy of messages + */ +export function scrubContextFromMessages(messages: CompilerEvent[]): CompilerEvent[] { + return messages.map(m => { + const scrubbed = Object.assign({}, m); + // Turn this on once all messages have offsets, see messages.tests.ts + // if (!scrubbed.offset) { + // throw Error(`Error, no offset detected in message ${CompilerError.formatEvent(m)}`); + // } + delete scrubbed.offset; + delete scrubbed.line; + delete scrubbed.filename; + delete scrubbed.column; + return scrubbed; + }); } /** @@ -263,7 +288,7 @@ export interface CompilationCase { export function testCompilationCases(compiler: SectionCompilerNew, cases : CompilationCase[], dependencies?: (SectionCompilerNew)[]) { // we need our own callbacks rather than using the global so messages don't get mixed const callbacks = new TestCompilerCallbacks(); - for (let testcase of cases) { + for (const testcase of cases) { const expectFailure = testcase.throws || !!(testcase.errors); // if true, we expect this to fail const testHeading = expectFailure ? `should fail to compile: ${testcase.subpath}`: `should compile: ${testcase.subpath}`; @@ -274,27 +299,32 @@ export function testCompilationCases(compiler: SectionCompilerNew, cases : Compi assert.throws(async () => await loadSectionFixture(compiler, testcase.subpath, callbacks, testcase.dependencies || dependencies), testcase.throws, 'expected exception from compilation'); return; } - let section = await loadSectionFixture(compiler, testcase.subpath, callbacks, testcase.dependencies || dependencies); - const testcaseErrors = matchCompilerEventsOrBoolean(callbacks.messages, testcase.errors); - const testcaseWarnings = matchCompilerEvents(callbacks.messages, testcase.warnings); + const section = await loadSectionFixture(compiler, testcase.subpath, callbacks, testcase.dependencies || dependencies); + let messagesToCheck = callbacks.messages; + // scrub offsets from messages to reduce churn in the test casws + if (!testcase.retainOffsetInMessages && callbacks.messages) { + messagesToCheck = scrubContextFromMessages(callbacks.messages); + } + const testcaseErrors = matchCompilerEventsOrBoolean(messagesToCheck, testcase.errors); + const testcaseWarnings = matchCompilerEvents(messagesToCheck, testcase.warnings); // if we expected errors or warnings, show them if (testcaseErrors && testcaseErrors !== true) { - assert.includeDeepMembers(callbacks.messages, testcaseErrors, 'expected errors to be included'); + assert.includeDeepMembers(messagesToCheck, testcaseErrors, 'expected errors to be included'); } if (testcaseErrors && testcase.strictErrors) { - assert.sameDeepMembers(callbacks.messages, testcaseErrors, 'expected same errors to be included'); + assert.sameDeepMembers(messagesToCheck, testcaseErrors, 'expected same errors to be included'); } if (testcaseWarnings) { - assert.includeDeepMembers(callbacks.messages, testcaseWarnings, 'expected warnings to be included'); + assert.includeDeepMembers(messagesToCheck, testcaseWarnings, 'expected warnings to be included'); } else if (!expectFailure) { // no warnings, so expect zero messages - assert.sameDeepMembers(callbacks.messages, [], 'expected zero messages but got ' + callbacks.messages); + assert.sameDeepMembers(messagesToCheck, [], 'expected zero messages but got ' + callbacks.messages); } - + if (expectFailure) { assert.isNull(section, 'expected compilation result failure (null)'); } else { - assert.isNotNull(section, `failed with ${compilerEventFormat(callbacks.messages)}`); + assert.isNotNull(section, `failed with ${CompilerError.formatEvent(callbacks.messages)}`); } // run the user-supplied callback if any @@ -327,3 +357,18 @@ const dontEscape = /[a-zA-Z0-9\.${}\[\]-]/; export function hex_str(s?: string) : string { return [...s].map(ch => dontEscape.test(ch) ? ch : util.escapeRegexChar(ch)).join(''); } + +/** + * Return an object simulating an XML object with an offset number + * For use in calling message functions + * @param c number for the offset setting + * @param x if set, this object will be used as the base object instead of {} + */ +export function withOffset(c: number, x?: any) : KeymanXMLMetadata { + // set metadata on an empty object + const o = Object.assign({}, x); + KeymanXMLReader.setMetaData(o, { + startIndex: c + }); + return o; +} diff --git a/developer/src/kmc-ldml/test/keys.tests.ts b/developer/src/kmc-ldml/test/keys.tests.ts index cfb37e6baf..3c3c842810 100644 --- a/developer/src/kmc-ldml/test/keys.tests.ts +++ b/developer/src/kmc-ldml/test/keys.tests.ts @@ -1,7 +1,7 @@ import 'mocha'; import { assert } from 'chai'; import { KeysCompiler } from '../src/compiler/keys.js'; -import { assertCodePoints, compilerTestCallbacks, loadSectionFixture, testCompilationCases } from './helpers/index.js'; +import { assertCodePoints, compilerTestCallbacks, loadSectionFixture, testCompilationCases, withOffset } from './helpers/index.js'; import { KMXPlus, Constants, LdmlKeyboardTypes } from '@keymanapp/common-types'; import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; import { constants } from '@keymanapp/ldml-keyboard-constants'; @@ -9,6 +9,7 @@ import { MetaCompiler } from '../src/compiler/meta.js'; const keysDependencies = [ ...BASIC_DEPENDENCIES, MetaCompiler ]; import Keys = KMXPlus.Keys; import { BASIC_DEPENDENCIES } from '../src/compiler/empty-compiler.js'; +import { LDMLKeyboard } from '@keymanapp/developer-utils'; const K = Constants.USVirtualKeyCodes; describe('keys', function () { @@ -219,7 +220,7 @@ describe('keys.kmap', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while it('should compile minimal kmap data', async function() { - let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/minimal.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/minimal.xml', compilerTestCallbacks, keysDependencies) as Keys; assert.isNotNull(keys); assert.equal(compilerTestCallbacks.messages.length, 0); // skip reserved (gap) keys @@ -311,7 +312,7 @@ describe('keys.kmap', function () { { subpath: 'sections/keys/invalid-bad-modifier.xml', errors: [ - LdmlCompilerMessages.Error_InvalidModifier({layer:'base',modifiers:'altR-shift'}), + LdmlCompilerMessages.Error_InvalidModifier({ modifiers: 'altR-shift' }), ] }, { @@ -330,7 +331,11 @@ describe('keys.kmap', function () { // warning on custom form subpath: 'sections/layr/warn-custom-us-form.xml', warnings: [ - LdmlCompilerMessages.Warn_CustomForm({id: "us"}), + // most tests will want to leave retainOffsetInMessages: false to + // not require maintaining the offset here, which will break if + // the XML changes. + // However, it's worthwhile having at least one test that verifies in this way. + LdmlCompilerMessages.Warn_CustomForm({id: "us"}, withOffset(367)), ], callback: (sect, subpath, callbacks) => { const keys = sect as Keys; @@ -355,6 +360,9 @@ describe('keys.kmap', function () { }, ]); }, + // Note: Most tests will NOT want to set this. + // We set this here to test the test mechanism. + retainOffsetInMessages: true, }, { // warning on a custom unknown form - but no error! @@ -392,7 +400,7 @@ describe('keys.kmap', function () { LdmlCompilerMessages.Warn_CustomForm({id: "us"}), ], errors: [ - LdmlCompilerMessages.Error_InvalidScanCode({ form: "us", codes: ['ff'] }), + LdmlCompilerMessages.Error_InvalidScanCode({ codes: 'ff', id: 'us' }), ], }, { @@ -401,7 +409,7 @@ describe('keys.kmap', function () { LdmlCompilerMessages.Warn_CustomForm({id: "zzz"}), ], errors: [ - LdmlCompilerMessages.Error_InvalidScanCode({ form: "zzz", codes: ['ff'] }), + LdmlCompilerMessages.Error_InvalidScanCode({ codes: 'ff', id: "zzz" }), ], }, { @@ -438,36 +446,39 @@ describe('keys.kmap', function () { ], keysDependencies); it('should reject layouts with too many hardware rows', async function() { - let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-rows.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-rows.xml', compilerTestCallbacks, keysDependencies) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_HardwareLayerHasTooManyRows()); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_HardwareLayerHasTooManyRows(withOffset(276))); }); it('should reject layouts with too many hardware keys', async function() { - let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-keys.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-keys.xml', compilerTestCallbacks, keysDependencies) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({row: 1, hardware: 'us', modifiers: 'none'})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({ row: 1, hardware: 'us', modifiers: 'none' }, withOffset(785) as LDMLKeyboard.LKRow)); }); it('should reject layouts with undefined keys', async function() { - let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-undefined-key.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-undefined-key.xml', compilerTestCallbacks, keysDependencies) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_KeyNotFoundInKeyBag({col: 1, form: 'hardware', keyId: 'foo', layer: 'base', row: 1})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_KeyNotFoundInKeyBag({col: 1, form: 'hardware', keyId: 'foo', layer: 'base', row: 1}, withOffset(271) as LDMLKeyboard.LKRow)); }); it('should reject layouts with invalid keys', async function() { - let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-key-missing-attrs.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-key-missing-attrs.xml', compilerTestCallbacks, keysDependencies) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_KeyMissingToGapOrSwitch({keyId: 'Q'})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_KeyMissingToGapOrSwitch( + {keyId: 'Q'}, + withOffset(188) + )); }); it('should accept layouts with gap/switch keys', async function() { - let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/gap-switch.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/gap-switch.xml', compilerTestCallbacks, keysDependencies) as Keys; assert.isNotNull(keys); assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(keys.keys.length, 4 + KeysCompiler.reserved_count); diff --git a/developer/src/kmc-ldml/test/layr.tests.ts b/developer/src/kmc-ldml/test/layr.tests.ts index 8aeb604171..dc0334c0b1 100644 --- a/developer/src/kmc-ldml/test/layr.tests.ts +++ b/developer/src/kmc-ldml/test/layr.tests.ts @@ -2,7 +2,7 @@ import 'mocha'; import { assert } from 'chai'; import { LayrCompiler } from '../src/compiler/layr.js'; import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; -import { compilerTestCallbacks, testCompilationCases } from './helpers/index.js'; +import { compilerTestCallbacks, testCompilationCases, withOffset } from './helpers/index.js'; import { KMXPlus } from '@keymanapp/common-types'; import { constants } from '@keymanapp/ldml-keyboard-constants'; @@ -96,8 +96,7 @@ describe('layr', function () { subpath: 'sections/keys/invalid-bad-modifier.xml', errors: [ LdmlCompilerMessages.Error_InvalidModifier({ - layer: 'base', - modifiers: 'altR-shift' + modifiers: 'altR-shift', }), ], }, @@ -108,7 +107,14 @@ describe('layr', function () { { // missing layer element subpath: 'sections/layr/invalid-missing-layer.xml', + errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(withOffset(258))], + retainOffsetInMessages: true, + }, + { + // missing layer element + subpath: 'sections/layr/invalid-missing-layer2.xml', errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement()], + retainOffsetInMessages: true, }, { // keep in sync with similar test in test-keys.ts @@ -132,7 +138,7 @@ describe('layr', function () { { subpath: 'sections/layr/error-bogus-modifiers.xml', errors: [ - LdmlCompilerMessages.Error_InvalidModifier({ layer: '', modifiers: 'caps bogus'}), + LdmlCompilerMessages.Error_InvalidModifier({ modifiers: 'caps bogus' }), ] }, { @@ -160,5 +166,18 @@ describe('layr', function () { } }, }, + { + subpath: 'sections/layr/error-dup-width.xml', + errors: [ + LdmlCompilerMessages.Error_DuplicateLayerWidth({ minDeviceWidth: 120}), + ] + }, + ...[0, 1024, 1500, `x` as unknown as number].map(minDeviceWidth => ({ + subpath: `sections/layr/error-bad-width-${minDeviceWidth}.xml`, + errors: [ + // + LdmlCompilerMessages.Error_InvalidLayerWidth({ minDeviceWidth }), + ] + })), ]); }); diff --git a/developer/src/kmc-ldml/test/loca.tests.ts b/developer/src/kmc-ldml/test/loca.tests.ts index 4dedc2bfd4..287ee62b00 100644 --- a/developer/src/kmc-ldml/test/loca.tests.ts +++ b/developer/src/kmc-ldml/test/loca.tests.ts @@ -1,7 +1,7 @@ import 'mocha'; import { assert } from 'chai'; import { LocaCompiler } from '../src/compiler/loca.js'; -import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js'; +import { compilerTestCallbacks, loadSectionFixture, withOffset } from './helpers/index.js'; import { KMXPlus } from '@keymanapp/common-types'; import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; @@ -11,7 +11,7 @@ describe('loca', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while it('should compile minimal loca data', async function() { - let loca = await loadSectionFixture(LocaCompiler, 'sections/loca/minimal.xml', compilerTestCallbacks) as Loca; + const loca = await loadSectionFixture(LocaCompiler, 'sections/loca/minimal.xml', compilerTestCallbacks) as Loca; assert.isObject(loca); assert.equal(compilerTestCallbacks.messages.length, 0); @@ -21,15 +21,15 @@ describe('loca', function () { }); it('should compile multiple locales', async function() { - let loca = await loadSectionFixture(LocaCompiler, 'sections/loca/multiple.xml', compilerTestCallbacks) as Loca; + const loca = await loadSectionFixture(LocaCompiler, 'sections/loca/multiple.xml', compilerTestCallbacks) as Loca; assert.isObject(loca); // Note: multiple.xml includes fr-FR twice, with differing case, which should be canonicalized assert.equal(compilerTestCallbacks.messages.length, 4); - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-FR', locale: 'fr'})); - assert.deepEqual(compilerTestCallbacks.messages[1], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'km-khmr-kh', locale: 'km'})); - assert.deepEqual(compilerTestCallbacks.messages[2], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-fr', locale: 'fr'})); - assert.deepEqual(compilerTestCallbacks.messages[3], LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales()); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-FR', locale: 'fr'}, withOffset(136))); + assert.deepEqual(compilerTestCallbacks.messages[1], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'km-khmr-kh', locale: 'km'}, withOffset(136))); + assert.deepEqual(compilerTestCallbacks.messages[2], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-fr', locale: 'fr'}, withOffset(136))); + assert.deepEqual(compilerTestCallbacks.messages[3], LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales(withOffset(136))); // Original is 6 locales, now five minimized in the results assert.equal(loca.locales.length, 5); @@ -41,11 +41,11 @@ describe('loca', function () { }); it('should reject structurally invalid locales', async function() { - let loca = await loadSectionFixture(LocaCompiler, 'sections/loca/invalid-locale.xml', compilerTestCallbacks) as Loca; + const loca = await loadSectionFixture(LocaCompiler, 'sections/loca/invalid-locale.xml', compilerTestCallbacks) as Loca; assert.isNull(loca); assert.equal(compilerTestCallbacks.messages.length, 1); // We'll only test one invalid BCP 47 tag to verify that we are properly calling BCP 47 validation routines. // Furthermore, we are testing BCP 47 structure, not the validity of each subtag -- we must assume the author knows of new subtags! - assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_InvalidLocale({tag:'en-*'})); + assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_InvalidLocale({tag:'en-*'}, withOffset(40))); }) }); diff --git a/developer/src/kmc-ldml/test/messages.tests.ts b/developer/src/kmc-ldml/test/messages.tests.ts index 041e1ed214..a7d06006cb 100644 --- a/developer/src/kmc-ldml/test/messages.tests.ts +++ b/developer/src/kmc-ldml/test/messages.tests.ts @@ -1,10 +1,73 @@ import 'mocha'; +import {expect} from 'chai'; import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; -import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; +import { CompilerErrorNamespace, CompilerEvent } from '@keymanapp/developer-utils'; +import { withOffset } from './helpers/index.js'; describe('LdmlCompilerMessages', function () { it('should have a valid LdmlCompilerMessages object', function() { return verifyCompilerMessagesObject(LdmlCompilerMessages, CompilerErrorNamespace.LdmlKeyboardCompiler); }); + it('should have offset (line) reporting on all messages', function() { + const m = LdmlCompilerMessages as Record; + const keys = Object.keys(LdmlCompilerMessages); + /** all fns */ + let total = 0; + /** does not take line numbers */ + const noLines = new Set(); + /** takes line numbers */ + let lines = 0; + const fakeOffsetNumber = 1234; + const fakeOffsetObject = withOffset(fakeOffsetNumber); + for(const key of keys) { + // exclude this one, does not need line numbers + if (key == 'Error_InvalidFile') continue; + if (typeof m[key] == 'function') { + total++; + const f = m[key] as Function; + // console.log(`${f.name}: ${f.length}`); + if (f.length === 0) { // Error_foo() + noLines.add(key); + continue; + } + // now try to call it + let resp : CompilerEvent; + if (f.length === 1) { // Error_foo(x) + resp = f(fakeOffsetObject); + } else if(f.length >= 2) { // Error_foo(o, x) + resp = f({}, fakeOffsetObject); + } + expect(resp).to.be.ok; // should get an object one way or another + if(resp.offset) { + lines++; + expect(resp.offset).to.equal(fakeOffsetNumber, `Offset number round trip for error ${f.name} did not work, check the message function`); + } else { + // did not get a column number back + noLines.add(key); + } + } + } + expect(lines).to.not.be.equal(0, `None of ${total} messages had offset reporting.`); + expect(Array.from(noLines.values())).to.deep.equal([], `${noLines.size}/${total} ${Number((noLines.size/total)*100).toFixed(0)}% of message(s) did not have detectable offset (line number) reporting`); + }); + it('Should only have simple ${def(o...)} interpolation', () => { + const m = LdmlCompilerMessages as Record; + const keys = Object.keys(LdmlCompilerMessages); + const complexInterpolation : string[] = []; + for(const key of keys) { + // exclude this one, does not need line numbers + if (key == 'Error_InvalidFile') continue; + if (typeof m[key] == 'function') { + const f = m[key] as Function; + const s = f.toString(); + // scan for interpolation + // Interpolation.. A non-escaped ${ that's NOT followed by `def(o.` triggers this error. + if (/.*`.*(? { }), [constants.keys_mod_alt | constants.keys_mod_shift]); }); it('should round trip each possible modifier', () => { - for(let str of constants.keys_mod_map.keys()) { + for(const str of constants.keys_mod_map.keys()) { const layer : LDMLKeyboard.LKLayer = { id: str, modifiers: `${str}`, @@ -213,7 +213,7 @@ describe('test of util/util.ts', () => { } }); it('should round trip each possible modifier with altL', () => { - for(let str of constants.keys_mod_map.keys()) { + for(const str of constants.keys_mod_map.keys()) { const layer : LDMLKeyboard.LKLayer = { id: str, modifiers: `${str} altL`, @@ -225,14 +225,14 @@ describe('test of util/util.ts', () => { }); describe('isValidModifier()', () => { it('should treat falsy values as valid', () => { - for(let str of [ + for(const str of [ null, undefined, '', 'none' ]) { assert.ok(validModifier(str), `validModifier(${JSON.stringify(str)})`); } }); it('should treat bad values as invalid', () => { - for(let str of [ + for(const str of [ 'asdfasdf', 'shift asdfasdf', 'altR-shift', 'altR-shift shift' ]) { assert.notOk(validModifier(str), `validModifier(${JSON.stringify(str)})`); diff --git a/developer/src/kmc-ldml/test/vars.tests.ts b/developer/src/kmc-ldml/test/vars.tests.ts index 69f08787b6..bb0ee5d389 100644 --- a/developer/src/kmc-ldml/test/vars.tests.ts +++ b/developer/src/kmc-ldml/test/vars.tests.ts @@ -112,13 +112,14 @@ describe('vars', function () { { subpath: 'sections/vars/dup0.xml', errors: [ - LdmlCompilerMessages.Error_DuplicateVariable({ids: 'y'}) + LdmlCompilerMessages.Error_DuplicateVariable({id: 'y'}) ], }, { subpath: 'sections/vars/dup1.xml', errors: [ - LdmlCompilerMessages.Error_DuplicateVariable({ids: 'upper, y'}) + LdmlCompilerMessages.Error_DuplicateVariable({id: 'upper'}), + LdmlCompilerMessages.Error_DuplicateVariable({id: 'y'}), ], }, { @@ -237,7 +238,7 @@ describe('vars', function () { 'doesnt_exist_1', 'doesnt_exist_2', 'doesnt_exist_3', - ] + ].join(',') }), ], }, diff --git a/developer/src/kmc-ldml/test/visual-keyboard-compiler.tests.ts b/developer/src/kmc-ldml/test/visual-keyboard-compiler.tests.ts index 77a8482090..a6f0de9250 100644 --- a/developer/src/kmc-ldml/test/visual-keyboard-compiler.tests.ts +++ b/developer/src/kmc-ldml/test/visual-keyboard-compiler.tests.ts @@ -36,7 +36,7 @@ describe('visual-keyboard-compiler', function() { checkMessages(); assert.isTrue(valid, 'k.validate should not have failed'); - let kmx = await k.compile(source); + const kmx = await k.compile(source); assert(kmx, 'k.compile should not have failed'); const keyboardId = path.basename(inputFilename, '.xml'); @@ -57,7 +57,7 @@ describe('visual-keyboard-compiler', function() { assert.isNotNull(code); // Compare output - let expected = await hextobin(binaryFilename, undefined, {silent:true}); + const expected = await hextobin(binaryFilename, undefined, {silent:true}); assert.deepEqual(code, expected); }); diff --git a/developer/src/kmc-model-info/.eslintrc.cjs b/developer/src/kmc-model-info/.eslintrc.cjs index 494ae6b00a..cfde91ef8e 100644 --- a/developer/src/kmc-model-info/.eslintrc.cjs +++ b/developer/src/kmc-model-info/.eslintrc.cjs @@ -10,8 +10,5 @@ module.exports = { files: "src/**/*.ts", extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } - ], - rules: { - "prefer-const": "off", // TODO: enable this once infrastructure is in place and cleanup the problem cases - }, + ] }; diff --git a/developer/src/kmc-model-info/package.json b/developer/src/kmc-model-info/package.json index 3a07f49ba1..4c715ccf6f 100644 --- a/developer/src/kmc-model-info/package.json +++ b/developer/src/kmc-model-info/package.json @@ -38,7 +38,6 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "typescript": "^5.4.5" }, "mocha": { diff --git a/developer/src/kmc-model-info/src/model-info-compiler.ts b/developer/src/kmc-model-info/src/model-info-compiler.ts index a5fb62cbcd..2ab2d63da6 100644 --- a/developer/src/kmc-model-info/src/model-info-compiler.ts +++ b/developer/src/kmc-model-info/src/model-info-compiler.ts @@ -153,7 +153,7 @@ export class ModelInfoCompiler implements KeymanCompiler { } - let model_info: ModelInfoFile = { + const model_info: ModelInfoFile = { languages: [], }; diff --git a/developer/src/kmc-model-info/test/model-info-compiler.tests.ts b/developer/src/kmc-model-info/test/model-info-compiler.tests.ts index 729eca3ac9..4b0a85624e 100644 --- a/developer/src/kmc-model-info/test/model-info-compiler.tests.ts +++ b/developer/src/kmc-model-info/test/model-info-compiler.tests.ts @@ -42,7 +42,7 @@ describe('model-info-compiler', function () { assert.isNotNull(result); const actual = JSON.parse(new TextDecoder().decode(result.artifacts.model_info.data)); - let expected = JSON.parse(fs.readFileSync(buildModelInfoFilename, 'utf-8')); + const expected = JSON.parse(fs.readFileSync(buildModelInfoFilename, 'utf-8')); // `lastModifiedDate` is dependent on time of run (not worth mocking) delete actual['lastModifiedDate']; diff --git a/developer/src/kmc-model/.eslintrc.cjs b/developer/src/kmc-model/.eslintrc.cjs index efa3d1a96a..5a22cc17fd 100644 --- a/developer/src/kmc-model/.eslintrc.cjs +++ b/developer/src/kmc-model/.eslintrc.cjs @@ -11,9 +11,5 @@ module.exports = { files: "src/**/*.ts", extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], }, - ], - rules: { - "prefer-const": "off", // TODO: enable this once infrastructure is in place and cleanup the problem cases - "no-var": "off", // TODO: enable this once infrastructure is in place and cleanup the problem cases - }, + ] }; diff --git a/developer/src/kmc-model/package.json b/developer/src/kmc-model/package.json index 87cf768ebf..8dff9fef1e 100644 --- a/developer/src/kmc-model/package.json +++ b/developer/src/kmc-model/package.json @@ -41,8 +41,7 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "esbuild": "^0.25.0", - "mocha": "^10.0.0" + "esbuild": "^0.25.0" }, "mocha": { "spec": "build/test/**/*.tests.js", diff --git a/developer/src/kmc-model/src/build-trie.ts b/developer/src/kmc-model/src/build-trie.ts index 366c4f8871..e65b2fc42c 100644 --- a/developer/src/kmc-model/src/build-trie.ts +++ b/developer/src/kmc-model/src/build-trie.ts @@ -26,10 +26,10 @@ export function createTrieDataStructure(filenames: string[], searchTermToKey?: ( throw new ModelCompilerError(ModelCompilerMessages.Error_SearchTermToKeyMustBeExplicitlySpecified()); } // Make one big word list out of all of the filenames provided. - let wordlist: WordList = {}; + const wordlist: WordList = {}; filenames.forEach(filename => parseWordListFromFilename(wordlist, filename)); - let trie = Trie.buildTrie(wordlist, searchTermToKey as Trie.SearchTermToKey); + const trie = Trie.buildTrie(wordlist, searchTermToKey as Trie.SearchTermToKey); return JSON.stringify(trie); } @@ -90,15 +90,15 @@ export function parseWordListFromContents(wordlist: WordList, contents: string): function _parseWordList(wordlist: WordList, source: WordListSource): void { const TAB = "\t"; - let wordsSeenInThisFile = new Set(); + const wordsSeenInThisFile = new Set(); - for (let [lineno, line] of source.lines()) { + for (const [lineno, srcLine] of source.lines()) { ModelCompilerMessageContext.line = lineno; // Remove the byte-order mark (BOM) from the beginning of the string. // Because `contents` can be the concatenation of several files, we have to remove // the BOM from every possible start of file -- i.e., beginning of every line. - line = line.replace(/^\uFEFF/, '').trim(); + const line = srcLine.replace(/^\uFEFF/, '').trim(); if (line.startsWith('#') || line === "") { continue; // skip comments and empty lines @@ -108,7 +108,7 @@ function _parseWordList(wordlist: WordList, source: WordListSource): void { let [wordform, countText] = line.split(TAB); // Clean the word form. - let original = wordform; + const original = wordform; wordform = wordform.normalize('NFC'); if (original !== wordform) { @@ -182,7 +182,7 @@ class WordListFromFilename { */ function* enumerateLines(lines: string[]): Generator { let i = 1; - for (let line of lines) { + for (const line of lines) { yield [i, line]; i++; } @@ -294,7 +294,7 @@ namespace Trie { * @returns A JSON-serialiable object that can be given to the TrieModel constructor. */ export function buildTrie(wordlist: WordList, keyFunction: SearchTermToKey): object { - let root = new Trie(keyFunction).buildFromWordList(wordlist).root; + const root = new Trie(keyFunction).buildFromWordList(wordlist).root; return { totalWeight: sumWeights(root), root: root @@ -316,8 +316,8 @@ namespace Trie { * @param words a list of word and count pairs. */ buildFromWordList(words: WordList): Trie { - for (let [wordform, weight] of Object.entries(words)) { - let key = this.toKey(wordform); + for (const [wordform, weight] of Object.entries(words)) { + const key = this.toKey(wordform); addUnsorted(this.root, { key, weight, content: wordform }, 0); } sortTrie(this.root); @@ -402,10 +402,10 @@ namespace Trie { * @param depth depth of the trie at this level. */ function convertLeafToInternalNode(leaf: Leaf, depth: number): void { - let entries = leaf.entries; + const entries = leaf.entries; // Alias the current node, as the desired type. - let internal = ( leaf) as InternalNode; + const internal = ( leaf) as InternalNode; internal.type = 'internal'; delete leaf.entries; @@ -413,7 +413,7 @@ namespace Trie { internal.children = {}; // Convert the old values array into the format for interior nodes. - for (let item of entries) { + for (const item of entries) { let char: string; if (depth < item.key.length) { char = item.key[depth]; @@ -444,7 +444,7 @@ namespace Trie { node.entries.sort(function (a, b) { return b.weight - a.weight; }); } else { // We MUST recurse and sort children before returning. - for (let char of node.values) { + for (const char of node.values) { sortTrie(node.children[char]); } diff --git a/developer/src/kmc-model/src/join-word-breaker-decorator.ts b/developer/src/kmc-model/src/join-word-breaker-decorator.ts index add997d9c7..9c5be53548 100644 --- a/developer/src/kmc-model/src/join-word-breaker-decorator.ts +++ b/developer/src/kmc-model/src/join-word-breaker-decorator.ts @@ -15,7 +15,7 @@ export function decorateWithJoin(breaker: WordBreakingFunction, joiners: string[ const delimiters = joiners.concat(); return function (input: string): Span[] { - let originalSpans = breaker(input); + const originalSpans = breaker(input); // Implements a finite-state transducer (FST) where: // - Transductions are pushed onto a stack @@ -28,8 +28,8 @@ export function decorateWithJoin(breaker: WordBreakingFunction, joiners: string[ // (hence the for-loop over the input tape) // - each state is a JavaScript callback (function) let state = emptyStack; - let stack: Span[] = []; - for (let span of originalSpans) { + const stack: Span[] = []; + for (const span of originalSpans) { state = state(span); } @@ -83,10 +83,10 @@ export function decorateWithJoin(breaker: WordBreakingFunction, joiners: string[ /****************** Helpers ******************/ function concatLastSpanInStackWith(span: Span) { - let lastIndex = stack.length - 1; + const lastIndex = stack.length - 1; - let top = stack[lastIndex]; - let joinedSpan = concatenateSpans(top, span); + const top = stack[lastIndex]; + const joinedSpan = concatenateSpans(top, span); stack[lastIndex] = joinedSpan; } } @@ -120,7 +120,7 @@ export function decorateWithJoin(breaker: WordBreakingFunction, joiners: string[ * When Array.prototype.include() doesn't exist: */ function includes(haystack: T[], needle: T): boolean { - for (let item of haystack) { + for (const item of haystack) { if (item === needle) return true; } diff --git a/developer/src/kmc-model/src/lexical-model-compiler.ts b/developer/src/kmc-model/src/lexical-model-compiler.ts index 922646a3db..54b50ba503 100644 --- a/developer/src/kmc-model/src/lexical-model-compiler.ts +++ b/developer/src/kmc-model/src/lexical-model-compiler.ts @@ -81,9 +81,9 @@ export class LexicalModelCompiler implements KeymanCompiler { */ async run(inputFilename: string, outputFilename?: string): Promise { try { - let modelSource = this.loadFromFilename(inputFilename); - let containingDirectory = callbacks.path.dirname(inputFilename); - let code = this.generateLexicalModelCode('', modelSource, containingDirectory); + const modelSource = this.loadFromFilename(inputFilename); + const containingDirectory = callbacks.path.dirname(inputFilename); + const code = this.generateLexicalModelCode('', modelSource, containingDirectory); const result: LexicalModelCompilerResult = { artifacts: { js: { @@ -129,11 +129,11 @@ export class LexicalModelCompiler implements KeymanCompiler { if(!data) { throw new ModelCompilerError(ModelCompilerMessages.Error_ModelFileNotFound({filename})); } - let sourceCode = new TextDecoder().decode(data); + const sourceCode = new TextDecoder().decode(data); // Compile the module to JavaScript code. // NOTE: transpile module does a very simple TS to JS compilation. // It DOES NOT check for types! - let compilationOutput = ts.transpile(sourceCode, { + const compilationOutput = ts.transpile(sourceCode, { // Our runtime only supports ES3 with Node/CommonJS modules on Android 5.0. // When we drop Android 5.0 support, we can update this to a `ScriptTarget` // matrix against target version of Keyman, here and in @@ -142,11 +142,11 @@ export class LexicalModelCompiler implements KeymanCompiler { module: ts.ModuleKind.CommonJS, }); // Turn the module into a function in which we can inject a global. - let moduleCode = '(function(exports){' + compilationOutput + '})'; + const moduleCode = '(function(exports){' + compilationOutput + '})'; // Run the module; its exports will be assigned to `moduleExports`. - let moduleExports: Partial = {}; - let module = eval(moduleCode); + const moduleExports: Partial = {}; + const module = eval(moduleCode); module(moduleExports); if (!moduleExports['__esModule'] || !moduleExports['default']) { @@ -179,7 +179,7 @@ export class LexicalModelCompiler implements KeymanCompiler { switch(modelSource.format) { case "custom-1.0": - let sources: string[] = modelSource.sources.map(function(source) { + const sources: string[] = modelSource.sources.map(function(source) { return new TextDecoder().decode(callbacks.loadFile(callbacks.path.join(sourcePath, source))); }); func += this.transpileSources(sources).join('\n'); @@ -191,9 +191,9 @@ export class LexicalModelCompiler implements KeymanCompiler { // Convert all relative path names to paths relative to the enclosing // directory. This way, we'll read the files relative to the model.ts // file, rather than the current working directory. - let filenames = modelSource.sources.map(filename => callbacks.path.join(sourcePath, filename)); + const filenames = modelSource.sources.map(filename => callbacks.path.join(sourcePath, filename)); - let definitions = new ModelDefinitions(modelSource); + const definitions = new ModelDefinitions(modelSource); func += definitions.compileDefinitions(); @@ -203,7 +203,7 @@ export class LexicalModelCompiler implements KeymanCompiler { createTrieDataStructure(filenames, definitions.searchTermToKey) }, {\n`; - let wordBreakerSourceCode = compileWordBreaker(normalizeWordBreakerSpec(modelSource.wordBreaker)); + const wordBreakerSourceCode = compileWordBreaker(normalizeWordBreakerSpec(modelSource.wordBreaker)); func += ` wordBreaker: ${wordBreakerSourceCode},\n`; // START - the lexical mapping option block @@ -275,7 +275,7 @@ function compileJoinDecorator(spec: WordBreakerSpec, existingWordBreakerCode: st // The decorator will run IMMEDIATELY when the model is loaded, // by the LMLayer returning the decorated word breaker to the // LMLayer model. - let joinerExpr: string = JSON.stringify(spec.joinWordsAt) + const joinerExpr: string = JSON.stringify(spec.joinWordsAt) return `(${decorateWithJoin.toString()}(${existingWordBreakerCode}, ${joinerExpr}))`; } diff --git a/developer/src/kmc-model/src/model-defaults.ts b/developer/src/kmc-model/src/model-defaults.ts index 374d7688d7..41a1a04964 100644 --- a/developer/src/kmc-model/src/model-defaults.ts +++ b/developer/src/kmc-model/src/model-defaults.ts @@ -1,3 +1,9 @@ +// Since methods in this file get written out via .toString to actual models, +// and `var` is needed to support the initial version of Chrome loaded onto +// the oldest Android devices we support, we relax the no-var rule here. + +/* eslint no-var: 0 */ + import { LexicalModelTypes } from '@keymanapp/common-types'; import CasingForm = LexicalModelTypes.CasingForm; import CasingFunction = LexicalModelTypes.CasingFunction; diff --git a/developer/src/kmc-model/src/model-definitions.ts b/developer/src/kmc-model/src/model-definitions.ts index 9525233d82..0ba3d3e435 100644 --- a/developer/src/kmc-model/src/model-definitions.ts +++ b/developer/src/kmc-model/src/model-definitions.ts @@ -76,7 +76,7 @@ export class ModelDefinitions { if(modelSource.applyCasing) { this.model.applyCasing = modelSource.applyCasing; - let _this = this; + const _this = this; // Since the defined casing function may expect to take our default implementation // as a parameter, we can define the full implementation via closure capture. @@ -111,7 +111,7 @@ export class ModelDefinitions { this.applyCasing = this.defaults.applyCasing; } - let _this = this; + const _this = this; this.searchTermToKey = function(text: string) { return _this.model.searchTermToKey(text, _this.applyCasing); } diff --git a/developer/src/kmc-model/src/script-overrides-decorator.ts b/developer/src/kmc-model/src/script-overrides-decorator.ts index c1216fa960..c3eb8d7b82 100644 --- a/developer/src/kmc-model/src/script-overrides-decorator.ts +++ b/developer/src/kmc-model/src/script-overrides-decorator.ts @@ -21,15 +21,15 @@ export function decorateWithScriptOverrides(breaker: WordBreakingFunction, optio const HAS_SOUTHEAST_ASIAN_LETTER = /[\u0E01-\u0E3A\u0E40-\u0E4E\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0EDC-\u0EDF\u1000-\u103F\u1050-\u108F\u109A-\u109D\u1780-\u17D3\u17D7\u17DC\u17DD\u30A1-\u30FA\u30FC-\u30FF]/; return function enhancedBreaker(phrase: string): Span[] { - let originalSpans = breaker(phrase); + const originalSpans = breaker(phrase); if (originalSpans.length === 0) { return []; } - let outputSpans = [originalSpans.shift()]; - for (let currentSpan of originalSpans) { - let previousSpan = lastFrom(outputSpans); + const outputSpans = [originalSpans.shift()]; + for (const currentSpan of originalSpans) { + const previousSpan = lastFrom(outputSpans); if (spansAreBackToBack(previousSpan, currentSpan) && hasSouthEastAsianLetter(previousSpan) && diff --git a/developer/src/kmc-model/test/compile-model-with-pseudoclosure.tests.ts b/developer/src/kmc-model/test/compile-model-with-pseudoclosure.tests.ts index b243b51f9c..c4d3334b6a 100644 --- a/developer/src/kmc-model/test/compile-model-with-pseudoclosure.tests.ts +++ b/developer/src/kmc-model/test/compile-model-with-pseudoclosure.tests.ts @@ -13,7 +13,7 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { const PATH = makePathToFixture(MODEL_ID); describe('specifying custom methods: applyCasing and searchTermToKey', function () { - let casingWithPrependedSymbols: LexicalModelTypes.CasingFunction = function(casingName: LexicalModelTypes.CasingForm, text: string, defaultApplyCasing: LexicalModelTypes.CasingFunction) { + const casingWithPrependedSymbols: LexicalModelTypes.CasingFunction = function(casingName: LexicalModelTypes.CasingForm, text: string, defaultApplyCasing: LexicalModelTypes.CasingFunction) { switch(casingName) { // Use of symbols, and of the `casingName` name, exist to serve as regex targets. case 'lower': @@ -28,9 +28,9 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { }; it('variant 1: applyCasing prepends symbols, searchTermToKey removes them', async function() { - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.tsv'], languageUsesCasing: true, // applyCasing won't appear without this! @@ -62,7 +62,7 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { assert.match(code, /'\^'/); assert.match(code, /§/); - let modelInitIndex = code.indexOf('LMLayerWorker.loadModel'); + const modelInitIndex = code.indexOf('LMLayerWorker.loadModel'); let modelInitCode = code.substring(modelInitIndex); // Chop off the IIFE terminator. Not strictly necessary, but whatever. modelInitCode = modelInitCode.substring(0, modelInitCode.lastIndexOf('\n})();')) @@ -77,15 +77,15 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { assert.match(modelInitCode, /['"]§['"]/); // Make sure it compiles! - let compilation = compileModelSourceCode(code); + const compilation = compileModelSourceCode(code); assert.isFalse(compilation.hasSyntaxError); assert.isNotNull(compilation.exportedModel); }); it('variant 2: applyCasing prepends symbols, searchTermToKey keeps them', async function() { - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.tsv'], languageUsesCasing: true, // applyCasing won't appear without this! @@ -111,7 +111,7 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { assert.match(code, /'\+'/); assert.match(code, /'\^'/); - let modelInitIndex = code.indexOf('LMLayerWorker.loadModel'); + const modelInitIndex = code.indexOf('LMLayerWorker.loadModel'); let modelInitCode = code.substring(modelInitIndex); // Chop off the IIFE terminator. Not strictly necessary, but whatever. modelInitCode = modelInitCode.substring(0, modelInitCode.lastIndexOf('\n})();')) @@ -122,15 +122,15 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { assert.match(modelInitCode, /['"]-['"]/); // Make sure it compiles! - let compilation = compileModelSourceCode(code); + const compilation = compileModelSourceCode(code); assert.isFalse(compilation.hasSyntaxError); assert.isNotNull(compilation.exportedModel); }); it('variant 3: applyCasing prepends symbols, default searchTermToKey', async function() { - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.tsv'], languageUsesCasing: true, // applyCasing won't appear without this! @@ -150,7 +150,7 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { assert.match(code, /'\+'/); assert.match(code, /'\^'/); - let modelInitIndex = code.indexOf('LMLayerWorker.loadModel'); + const modelInitIndex = code.indexOf('LMLayerWorker.loadModel'); let modelInitCode = code.substring(modelInitIndex); // Chop off the IIFE terminator. Not strictly necessary, but whatever. modelInitCode = modelInitCode.substring(0, modelInitCode.lastIndexOf('\n})();')) @@ -161,7 +161,7 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { assert.match(modelInitCode, /['"]-['"]/); // Make sure it compiles! - let compilation = compileModelSourceCode(code); + const compilation = compileModelSourceCode(code); assert.isFalse(compilation.hasSyntaxError); assert.isNotNull(compilation.exportedModel); }); @@ -169,9 +169,9 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { describe('relying on default applyCasing + searchTermToKey', function() { it('languageUsesCasing: true', async function() { - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.tsv'], languageUsesCasing: true, // applyCasing should appear! @@ -188,15 +188,15 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { assert.match(code, /languageUsesCasing/); // Make sure it compiles! - let compilation = compileModelSourceCode(code); + const compilation = compileModelSourceCode(code); assert.isFalse(compilation.hasSyntaxError); assert.isNotNull(compilation.exportedModel); }); it('languageUsesCasing: false', async function() { - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.tsv'], languageUsesCasing: false, // applyCasing should not appear! @@ -214,15 +214,15 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { assert.match(code, /languageUsesCasing/); // Make sure it compiles! - let compilation = compileModelSourceCode(code); + const compilation = compileModelSourceCode(code); assert.isFalse(compilation.hasSyntaxError); assert.isNotNull(compilation.exportedModel); }); it('languageUsesCasing: undefined', async function() { - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.tsv'] // languageUsesCasing: undefined @@ -241,7 +241,7 @@ describe('LexicalModelCompiler - pseudoclosure compilation + use', function () { assert.notMatch(code, /languageUsesCasing/); // Make sure it compiles! - let compilation = compileModelSourceCode(code); + const compilation = compileModelSourceCode(code); assert.isFalse(compilation.hasSyntaxError); assert.isNotNull(compilation.exportedModel); }); diff --git a/developer/src/kmc-model/test/compile-model.tests.ts b/developer/src/kmc-model/test/compile-model.tests.ts index 7de9d6d5da..e5c9e86c15 100644 --- a/developer/src/kmc-model/test/compile-model.tests.ts +++ b/developer/src/kmc-model/test/compile-model.tests.ts @@ -7,7 +7,7 @@ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { KeymanFileTypes } from '@keymanapp/common-types'; describe('LexicalModelCompiler', function () { - let callbacks = new TestCompilerCallbacks(); + const callbacks = new TestCompilerCallbacks(); this.timeout(5000); @@ -21,8 +21,8 @@ describe('LexicalModelCompiler', function () { 'example.qaa.scriptusesspaces', ]; - for (let modelID of MODELS) { - let modelPath = makePathToFixture(modelID, modelID + KeymanFileTypes.Source.Model); + for (const modelID of MODELS) { + const modelPath = makePathToFixture(modelID, modelID + KeymanFileTypes.Source.Model); it(`should compile ${modelID}`, async function () { const compiler = new LexicalModelCompiler(); @@ -32,8 +32,8 @@ describe('LexicalModelCompiler', function () { assert.isNotNull(result); const decoder = new TextDecoder(); const code = decoder.decode(result.artifacts.js.data); - let r = compileModelSourceCode(code); - let compilation = r as CompilationResult; + const r = compileModelSourceCode(code); + const compilation = r as CompilationResult; assert.isFalse(compilation.hasSyntaxError, 'model code had syntax error'); assert.isNull(compilation.error, `compilation error: ${compilation.error}`); diff --git a/developer/src/kmc-model/test/compile-trie.tests.ts b/developer/src/kmc-model/test/compile-trie.tests.ts index d36df694f4..81de56987e 100644 --- a/developer/src/kmc-model/test/compile-trie.tests.ts +++ b/developer/src/kmc-model/test/compile-trie.tests.ts @@ -21,14 +21,14 @@ describe('LexicalModelCompiler', function () { const MODEL_ID = 'example.qaa.trivial'; const PATH = makePathToFixture(MODEL_ID); - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.tsv'] }, PATH) as string; - let result = compileModelSourceCode(code); + const result = compileModelSourceCode(code); assert.isFalse(result.hasSyntaxError); assert.isNotNull(result.exportedModel); assert.equal(result.modelConstructorName, 'TrieModel'); @@ -45,14 +45,14 @@ describe('LexicalModelCompiler', function () { const MODEL_ID = 'example.qaa.utf16le'; const PATH = makePathToFixture(MODEL_ID); - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.txt'] }, PATH) as string; - let result = compileModelSourceCode(code); + const result = compileModelSourceCode(code); assert.isFalse(result.hasSyntaxError); assert.isNotNull(result.exportedModel); assert.equal(result.modelConstructorName, 'TrieModel'); @@ -67,9 +67,9 @@ describe('LexicalModelCompiler', function () { const MODEL_ID = 'example.qaa.trivial'; const PATH = makePathToFixture(MODEL_ID); - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.tsv'], // This is a possible word breaking function: @@ -78,7 +78,7 @@ describe('LexicalModelCompiler', function () { } }, PATH) as string; - let result = compileModelSourceCode(code); + const result = compileModelSourceCode(code); assert.isFalse(result.hasSyntaxError, `Syntax error in ${code}`); assert.isNotNull(result.exportedModel); assert.equal(result.modelConstructorName, 'TrieModel'); @@ -91,14 +91,14 @@ describe('LexicalModelCompiler', function () { const MODEL_ID = 'example.qaa.smp'; const PATH = makePathToFixture(MODEL_ID); - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.tsv'] }, PATH) as string; - let result = compileModelSourceCode(code); + const result = compileModelSourceCode(code); assert.isFalse(result.hasSyntaxError); assert.isNotNull(result.exportedModel); assert.equal(result.modelConstructorName, 'TrieModel'); @@ -106,7 +106,7 @@ describe('LexicalModelCompiler', function () { // Test every character in the string to make sure we don't have // unpaired surrogates which destroy everything. // We can assume that the first and last chars are not SMP - for(var i = 1; i < code.length - 1; i++) { + for(let i = 1; i < code.length - 1; i++) { assert.notEqual(0xFFFD, code.charCodeAt(i)); if(code.charCodeAt(i) >= 0xD800 && code.charCodeAt(i) < 0xDC00) { assert.isTrue((code.charCodeAt(i+1) >= 0xDC00 && code.charCodeAt(i+1) < 0xE000), @@ -125,9 +125,9 @@ describe('LexicalModelCompiler', function () { it('should include the source code of its search term to key function', async function () { const MODEL_ID = 'example.qaa.trivial'; const PATH = makePathToFixture(MODEL_ID); - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.tsv'] // NOTE: we intentionally OMIT the searchTermToKey function @@ -152,13 +152,13 @@ describe('createTrieDataStructure()', function () { it('uses the provided searchTermToKey function', function () { // check if the expected key is in the resultant data structure. // N.B., we assume the wordlist contains the wordform "turtles" - let lowercaseSourceCode = createTrieDataStructure([WORDLIST_FILENAME], (wf) => { + const lowercaseSourceCode = createTrieDataStructure([WORDLIST_FILENAME], (wf) => { return wf.toLowerCase() }) assert.match(lowercaseSourceCode, /"key":\s*"turtles"/); assert.notMatch(lowercaseSourceCode, /"key":\s*"TURTLES"/); - let uppercaseSourceCode = createTrieDataStructure([WORDLIST_FILENAME], (wf) => { + const uppercaseSourceCode = createTrieDataStructure([WORDLIST_FILENAME], (wf) => { return wf.toUpperCase() }) assert.match(uppercaseSourceCode, /"key":\s*"TURTLES"/); @@ -171,7 +171,7 @@ describe('createTrieDataStructure()', function () { // // Is pretty much a JSON-encoded Trie spec, stringifying { root: Node, totalWeight: number } // as used to initialize the Trie. - let sourceCode = createTrieDataStructure([WORDLIST_FILENAME], (wf) => wf); + const sourceCode = createTrieDataStructure([WORDLIST_FILENAME], (wf) => wf); // Simple first-pass check: the signs of #11073 are not present. assert.notMatch(sourceCode, /undefined/); @@ -203,7 +203,7 @@ describe('createTrieDataStructure()', function () { }; // Using each word as a prediction prefix, attempt to get a suggestion corresponding to each. - for(let word of words) { + for(const word of words) { const rawSuggestions = model.predict({insert: '', deleteLeft: 0}, contextFromWord(word)); const suggestions = rawSuggestions.map((entry) => entry.sample.displayAs); suggestions.forEach((suggestion) => set.add(suggestion)); diff --git a/developer/src/kmc-model/test/default-apply-case.tests.ts b/developer/src/kmc-model/test/default-apply-case.tests.ts index 09d1f49d7a..429f89dc7e 100644 --- a/developer/src/kmc-model/test/default-apply-case.tests.ts +++ b/developer/src/kmc-model/test/default-apply-case.tests.ts @@ -57,7 +57,7 @@ describe('The default applyCasing() function', function () { ['1234.?!', '1234.?!'] ]; - for (let [input, expected] of testCases) { + for (const [input, expected] of testCases) { it(`should lowercase '${input}' as '${expected}'`, function() { assert.equal(defaultApplyCasing('lower', input), expected); }); @@ -88,7 +88,7 @@ describe('The default applyCasing() function', function () { ['1234.?!', '1234.?!'] ]; - for (let [input, expected] of testCases) { + for (const [input, expected] of testCases) { it(`should uppercase '${input}' as '${expected}'`, function() { assert.equal(defaultApplyCasing('upper', input), expected); }); @@ -119,7 +119,7 @@ describe('The default applyCasing() function', function () { ['1234.?!', '1234.?!'] ]; - for (let [input, expected] of testCases) { + for (const [input, expected] of testCases) { it(`should initial-case '${input}' as '${expected}'`, function() { assert.equal(defaultApplyCasing('initial', input), expected); }); diff --git a/developer/src/kmc-model/test/default-search-term-to-key.tests.ts b/developer/src/kmc-model/test/default-search-term-to-key.tests.ts index 48cf2ef1f5..d3119b961a 100644 --- a/developer/src/kmc-model/test/default-search-term-to-key.tests.ts +++ b/developer/src/kmc-model/test/default-search-term-to-key.tests.ts @@ -49,7 +49,7 @@ describe('The default searchTermToKey() function', function () { ['DŽ', 'DZ'], ]; - for (let [input, expected] of testCases) { + for (const [input, expected] of testCases) { it(`should normalize '${input}' to '${expected}'`, function() { assert.equal(defaultSearchTermToKey(input), expected); }); @@ -102,7 +102,7 @@ describe('The default searchTermToKey() function', function () { // as U+0130's default handling is... not ideal in Turkish. // // Instead, we can get a simple-enough test with inverted casing. - let customCasing = function(caseToApply: LexicalModelTypes.CasingForm, + const customCasing = function(caseToApply: LexicalModelTypes.CasingForm, text: string, defaultApplyCasing: LexicalModelTypes.CasingFunction): string { switch(caseToApply) { @@ -117,11 +117,11 @@ describe('The default searchTermToKey() function', function () { } } - let customCasingClosure = function(caseToApply: LexicalModelTypes.CasingForm, text: string): string { + const customCasingClosure = function(caseToApply: LexicalModelTypes.CasingForm, text: string): string { return customCasing(caseToApply, text, defaultApplyCasing); } - for (let [input, expected] of testCases) { + for (const [input, expected] of testCases) { it(`should normalize '${input}' to '${expected}'`, function() { assert.equal(defaultCasedSearchTermToKey(input, customCasingClosure as LexicalModelTypes.CasingFunction), expected); }); @@ -173,7 +173,7 @@ describe('The default searchTermToKey() function', function () { ['DŽ', 'dz'], ]; - for (let [input, expected] of testCases) { + for (const [input, expected] of testCases) { it(`should normalize '${input}' to '${expected}'`, function() { assert.equal(defaultCasedSearchTermToKey(input, defaultApplyCasing), expected); }); diff --git a/developer/src/kmc-model/test/helpers/index.ts b/developer/src/kmc-model/test/helpers/index.ts index d58e0834d1..4f80d76e46 100644 --- a/developer/src/kmc-model/test/helpers/index.ts +++ b/developer/src/kmc-model/test/helpers/index.ts @@ -60,7 +60,7 @@ export function compileModelSourceCode(code: string) { }; } - let fakeLMLayerWorker = { + const fakeLMLayerWorker = { loadModel(model: object) { exportedModel = model; } @@ -76,7 +76,7 @@ export function compileModelSourceCode(code: string) { // // ...we can intercept the name "WhateverModel" and assign it to // `modelConstructorName`. - let modelsNamespace = new Proxy({}, { + const modelsNamespace = new Proxy({}, { get(_target, property) { if (typeof property !== 'string') throw new Error(`Don't know how to handle non-string property: ${String(property)}`); @@ -98,7 +98,7 @@ export function compileModelSourceCode(code: string) { // wordBreaker["someWordBreaker"] // // ...we can intercept the name "someWordBreaker". - let wordBreakerNamespace = new Proxy({}, { + const wordBreakerNamespace = new Proxy({}, { get(_target, property) { if (typeof property !== 'string') throw new Error(`Don't know how to handle non-string property: ${String(property)}`); diff --git a/developer/src/kmc-model/test/join-word-breaker.tests.ts b/developer/src/kmc-model/test/join-word-breaker.tests.ts index e40d2ccdc0..50cab25a82 100644 --- a/developer/src/kmc-model/test/join-word-breaker.tests.ts +++ b/developer/src/kmc-model/test/join-word-breaker.tests.ts @@ -5,7 +5,7 @@ import { LexicalModelTypes } from '@keymanapp/common-types'; describe('The join word breaker decorator', function () { it('should decorate an existing word breaker', function () { - let breakWords = decorateWithJoin(defaultWordBreaker, ['-']); + const breakWords = decorateWithJoin(defaultWordBreaker, ['-']); assert.isFunction(breakWords); }); @@ -79,12 +79,12 @@ describe('The join word breaker decorator', function () { ], ] - for (let [phrase, joiners, unjoined, expected] of TEST_CASES) { + for (const [phrase, joiners, unjoined, expected] of TEST_CASES) { it(`should break «${[phrase]}» as [${expected.join(' ;; ')}]`, function () { - let breakWords = decorateWithJoin(defaultWordBreaker, joiners); - let unjoinedResult = defaultWordBreaker(phrase).map(onlyText); + const breakWords = decorateWithJoin(defaultWordBreaker, joiners); + const unjoinedResult = defaultWordBreaker(phrase).map(onlyText); assert.deepEqual(unjoinedResult, unjoined); - let actualResult = breakWords(phrase).map(onlyText); + const actualResult = breakWords(phrase).map(onlyText); assert.deepEqual(actualResult, expected); }); } diff --git a/developer/src/kmc-model/test/model-definitions.tests.ts b/developer/src/kmc-model/test/model-definitions.tests.ts index 4eaccfb792..3dc8b81db3 100644 --- a/developer/src/kmc-model/test/model-definitions.tests.ts +++ b/developer/src/kmc-model/test/model-definitions.tests.ts @@ -9,13 +9,13 @@ describe('Model definition pseudoclosures', function () { describe('languageUsesCasing == true', function() { // We don't need a complete spec for this, given the (currently) limited range of what // the ModelPseudoclosure covers. - let modelSource: LexicalModelSource = { + const modelSource: LexicalModelSource = { languageUsesCasing: true, sources: [], format: 'trie-1.0' }; - let pseudoclosure = new ModelDefinitions(modelSource); + const pseudoclosure = new ModelDefinitions(modelSource); const testCases: [string, string, string][] = [ // Note: not written the Turkish way. Turns out 'İ'.toLowerCase() decomposes the result, @@ -45,7 +45,7 @@ describe('Model definition pseudoclosures', function () { ["‘", "‘", "'"] ]; - for (let [input, cased, keyed] of testCases) { + for (const [input, cased, keyed] of testCases) { it(`should case '${input}' as '${cased}'`, function() { assert.equal(pseudoclosure.applyCasing('lower', input), cased); }); @@ -59,13 +59,13 @@ describe('Model definition pseudoclosures', function () { describe('languageUsesCasing == false', function() { // We don't need a complete spec for this, given the (currently) limited range of what // the ModelPseudoclosure covers. - let modelSource: LexicalModelSource = { + const modelSource: LexicalModelSource = { languageUsesCasing: false, sources: [], format: 'trie-1.0' }; - let pseudoclosure = new ModelDefinitions(modelSource); + const pseudoclosure = new ModelDefinitions(modelSource); const testCases: [string, string][] = [ // Note: not written the Turkish way. Turns out 'İ'.toLowerCase() decomposes the result, @@ -91,7 +91,7 @@ describe('Model definition pseudoclosures', function () { ["‘", "'"] ]; - for (let [input, keyed] of testCases) { + for (const [input, keyed] of testCases) { it(`should key '${input}' as '${keyed}'`, function() { assert.equal(pseudoclosure.searchTermToKey(input), keyed); }); @@ -101,12 +101,12 @@ describe('Model definition pseudoclosures', function () { describe('Pre 14.0 defaults (languageUsesCasing == undefined)', function() { // We don't need a complete spec for this, given the (currently) limited range of what // the ModelPseudoclosure covers. - let modelSource: LexicalModelSource = { + const modelSource: LexicalModelSource = { sources: [], format: 'trie-1.0' }; - let pseudoclosure = new ModelDefinitions(modelSource); + const pseudoclosure = new ModelDefinitions(modelSource); const testCases: [string, string][] = [ // Note: not written the Turkish way. Turns out 'İ'.toLowerCase() decomposes the result, @@ -129,7 +129,7 @@ describe('Model definition pseudoclosures', function () { ['1234.?!', '1234.?!'] ]; - for (let [input, keyed] of testCases) { + for (const [input, keyed] of testCases) { it(`should key '${input}' as '${keyed}'`, function() { assert.equal(pseudoclosure.searchTermToKey(input), keyed); }); @@ -139,7 +139,7 @@ describe('Model definition pseudoclosures', function () { describe('Model-defined applyCasing + (dependent) searchTermToKey', function() { // Note: this test only implements enough Turkish-related stuff to facilitate // a functional test. Not guaranteed to be sufficient for actual Turkish use. - let turkishCasing = function(form: LexicalModelTypes.CasingForm, text: string, defaultApplyCasing: (form: LexicalModelTypes.CasingForm, text: string) => string): string { + const turkishCasing = function(form: LexicalModelTypes.CasingForm, text: string, defaultApplyCasing: (form: LexicalModelTypes.CasingForm, text: string) => string): string { switch(form) { case 'lower': return defaultApplyCasing(form, text @@ -156,7 +156,7 @@ describe('Model definition pseudoclosures', function () { } } - let modelSource: LexicalModelSource = { + const modelSource: LexicalModelSource = { languageUsesCasing: true, applyCasing: turkishCasing, searchTermToKey: function(wordform: string, applyCasing: LexicalModelTypes.CasingFunction): string { @@ -170,7 +170,7 @@ describe('Model definition pseudoclosures', function () { format: 'trie-1.0' }; - let pseudoclosure = new ModelDefinitions(modelSource); + const pseudoclosure = new ModelDefinitions(modelSource); const testCases: [string, string, string][] = [ ['İstanbul', 'istanbul', 'istanbul'], @@ -186,7 +186,7 @@ describe('Model definition pseudoclosures', function () { ['1234.?!', '1234.?!', '1234.?!'] ]; - for (let [input, cased, keyed] of testCases) { + for (const [input, cased, keyed] of testCases) { it(`should case '${input}' as '${cased}'`, function() { assert.equal(pseudoclosure.applyCasing('lower', input), cased); }); diff --git a/developer/src/kmc-model/test/override-script-defaults.tests.ts b/developer/src/kmc-model/test/override-script-defaults.tests.ts index 5c325e4da1..3f08904c4d 100644 --- a/developer/src/kmc-model/test/override-script-defaults.tests.ts +++ b/developer/src/kmc-model/test/override-script-defaults.tests.ts @@ -7,7 +7,7 @@ const THIN_SPACE = "\u2009"; describe('The script overrides word breaker decorator', function () { it('should decorate an existing word breaker', function () { - let breakWords = decorateWithScriptOverrides(defaultWordBreaker, 'break-words-at-spaces') + const breakWords = decorateWithScriptOverrides(defaultWordBreaker, 'break-words-at-spaces') assert.isFunction(breakWords); }); @@ -19,11 +19,11 @@ describe('The script overrides word breaker decorator', function () { const expectedNumSpans = phraseSpans.length; it(`should break «${[phrase]}» as ${expectedNumSpans} spans`, function () { - let breakWords = decorateWithScriptOverrides(defaultWordBreaker, 'break-words-at-spaces'); - let defaultResult = defaultWordBreaker(phrase); + const breakWords = decorateWithScriptOverrides(defaultWordBreaker, 'break-words-at-spaces'); + const defaultResult = defaultWordBreaker(phrase); assert.isAbove(defaultResult.length, expectedNumSpans); - let actualResult = breakWords(phrase); + const actualResult = breakWords(phrase); assert.lengthOf(actualResult, expectedNumSpans); assert.deepEqual(actualResult.map(grabText), phraseSpans); }); diff --git a/developer/src/kmc-model/test/parse-wordlist.tests.ts b/developer/src/kmc-model/test/parse-wordlist.tests.ts index 81245649f1..ada01fbc2b 100644 --- a/developer/src/kmc-model/test/parse-wordlist.tests.ts +++ b/developer/src/kmc-model/test/parse-wordlist.tests.ts @@ -22,7 +22,7 @@ const SENCOTEN_WORDLIST = { describe('parsing a word list', function () { - let testCallbacks = new TestCompilerCallbacks(); + const testCallbacks = new TestCompilerCallbacks(); beforeEach(function () { testCallbacks.clear(); @@ -34,18 +34,18 @@ describe('parsing a word list', function () { }); it('should remove the UTF-8 byte order mark from files', function () { - let word = 'hello'; - let count = 1; - let expected: WordList = {}; + const word = 'hello'; + const count = 1; + const expected: WordList = {}; expected[word] = count; - let file = `# this is a comment\n${word}\t${count}`; - let withoutBOM: WordList = {}; + const file = `# this is a comment\n${word}\t${count}`; + const withoutBOM: WordList = {}; parseWordListFromContents(withoutBOM, file); assert.deepEqual(withoutBOM, expected, "expected regular file to parse properly"); assert.isEmpty(testCallbacks.messages); - let withBOM: WordList = {}; + const withBOM: WordList = {}; parseWordListFromContents(withBOM, `${BOM}${file}`) assert.deepEqual(withBOM, expected, "expected BOM to be ignored"); assert.isEmpty(testCallbacks.messages); @@ -54,7 +54,7 @@ describe('parsing a word list', function () { it('should read word lists in UTF-8', function () { // N.B.: this is the format exported by Google Drive when selecting "TSV". const filename = makePathToFixture('example.qaa.sencoten', 'wordlist.tsv'); - let wordlist: WordList = {}; + const wordlist: WordList = {}; parseWordListFromFilename(wordlist, filename); assert.deepEqual(wordlist, SENCOTEN_WORDLIST); @@ -65,7 +65,7 @@ describe('parsing a word list', function () { // N.B.: this is the format exported by MS Excel when selecting // "UTF-16" text (tested on Excel for macOS). const filename = makePathToFixture('example.qaa.utf16le', 'wordlist.txt'); - let wordlist: WordList = {}; + const wordlist: WordList = {}; parseWordListFromFilename(wordlist, filename); assert.deepEqual(wordlist, SENCOTEN_WORDLIST); @@ -75,7 +75,7 @@ describe('parsing a word list', function () { it('should NOT read word lists in UTF-16 big-endian (with BOM)', function () { // N.B.: Does anything output this format...? const filename = makePathToFixture('example.qaa.utf16be', 'wordlist.txt'); - let wordlist: WordList = {}; + const wordlist: WordList = {}; assert.throws(() => { parseWordListFromFilename(wordlist, filename); }, 'UTF-16BE is unsupported'); @@ -101,7 +101,7 @@ describe('parsing a word list', function () { for(let i = 0; i < words.length; i++) { file += `${words[i]}\t${i+1}\n`; } - let repeatedWords: WordList = {}; + const repeatedWords: WordList = {}; parseWordListFromContents(repeatedWords, file); assert.deepEqual(repeatedWords, expected); diff --git a/developer/src/kmc-model/test/punctuation.tests.ts b/developer/src/kmc-model/test/punctuation.tests.ts index f3ad64433e..c3679f08d9 100644 --- a/developer/src/kmc-model/test/punctuation.tests.ts +++ b/developer/src/kmc-model/test/punctuation.tests.ts @@ -13,9 +13,9 @@ describe('LexicalModelCompiler', function () { it('should compile punctuation into the generated code', async function () { const callbacks = new TestCompilerCallbacks(); - let compiler = new LexicalModelCompiler(); + const compiler = new LexicalModelCompiler(); assert.isTrue(await compiler.init(callbacks, null)); - let code = compiler.generateLexicalModelCode(MODEL_ID, { + const code = compiler.generateLexicalModelCode(MODEL_ID, { format: 'trie-1.0', sources: ['wordlist.tsv'], punctuation: { @@ -31,7 +31,7 @@ describe('LexicalModelCompiler', function () { assert.match(code, /\u1680/); // Make sure it compiles! - let compilation = compileModelSourceCode(code); + const compilation = compileModelSourceCode(code); assert.isFalse(compilation.hasSyntaxError); assert.isNotNull(compilation.exportedModel); }); diff --git a/developer/src/kmc-model/tools/create-override-script-regexp.ts b/developer/src/kmc-model/tools/create-override-script-regexp.ts index 38f6ade0a6..63c39aeea5 100644 --- a/developer/src/kmc-model/tools/create-override-script-regexp.ts +++ b/developer/src/kmc-model/tools/create-override-script-regexp.ts @@ -9,8 +9,8 @@ * If you need to add more Unicode blocks, customize SPACELESS_SCRIPT_BLOCKS. */ -import {readFileSync} from "fs"; -import * as path from "path"; +import {readFileSync} from "node:fs"; +import * as path from "node:path"; // Where to find UnicodeData.txt and Blocks.txt const UCD_DIR = path.join("..", "..", "..", "..", "resources", "standards-data", "unicode-character-database"); diff --git a/developer/src/kmc-package/.eslintrc.cjs b/developer/src/kmc-package/.eslintrc.cjs index 41a487fe41..990bc63f61 100644 --- a/developer/src/kmc-package/.eslintrc.cjs +++ b/developer/src/kmc-package/.eslintrc.cjs @@ -8,8 +8,5 @@ module.exports = { files:"src/**/*.ts", extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], } - ], - rules: { - "prefer-const": "off", // TODO: enable this once infrastructure is in place and cleanup the problem cases - }, + ] }; diff --git a/developer/src/kmc-package/package.json b/developer/src/kmc-package/package.json index 93525e7330..5e2df84fc6 100644 --- a/developer/src/kmc-package/package.json +++ b/developer/src/kmc-package/package.json @@ -40,7 +40,6 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "typescript": "^5.4.5" }, "mocha": { diff --git a/developer/src/kmc-package/src/compiler/cp1252.ts b/developer/src/kmc-package/src/compiler/cp1252.ts index e9b04de519..c23a760014 100644 --- a/developer/src/kmc-package/src/compiler/cp1252.ts +++ b/developer/src/kmc-package/src/compiler/cp1252.ts @@ -7,7 +7,7 @@ */ export function transcodeToCP1252(s: string): Uint8Array { buildUnicodeToCP1252(); - let result = new Uint8Array(s.length); + const result = new Uint8Array(s.length); for(let i = 0; i < s.length; i++) { result[i] = UNICODE_CP1252[s.charCodeAt(i)] ?? '?'.charCodeAt(0); } @@ -23,7 +23,7 @@ function buildUnicodeToCP1252() { } } -let UNICODE_CP1252: {[index:number]: number} = {} +const UNICODE_CP1252: {[index:number]: number} = {} // Source: https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT // _undefined_ values have been mapped to the same USV even though this isn't a diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index 04eae4afd8..726ab28df9 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -13,7 +13,7 @@ import { PackageCompilerMessages } from './package-compiler-messages.js'; import { PackageMetadataCollector } from './package-metadata-collector.js'; import { KmpInfWriter } from './kmp-inf-writer.js'; import { transcodeToCP1252 } from './cp1252.js'; -import { MIN_LM_FILEVERSION_KMP_JSON, PackageVersionValidator } from './package-version-validator.js'; +import { DEFAULT_KEYBOARD_VERSION, MIN_LM_FILEVERSION_KMP_JSON, PackageVersionValidator } from './package-version-validator.js'; import { PackageKeyboardTargetValidator } from './package-keyboard-target-validator.js'; import { PackageMetadataUpdater } from './package-metadata-updater.js'; import { markdownToHTML } from './markdown.js'; @@ -292,10 +292,10 @@ export class KmpCompiler implements KeymanCompiler { kmp.keyboards = kps.Keyboards.Keyboard.map((keyboard: KpsFile.KpsFileKeyboard) => ({ displayFont: keyboard.DisplayFont ? this.callbacks.path.basename(this.normalizePath(keyboard.DisplayFont)) : undefined, oskFont: keyboard.OSKFont ? this.callbacks.path.basename(this.normalizePath(keyboard.OSKFont)) : undefined, - name:keyboard.Name?.trim(), + name: '', // filled by PackageMetadataUpdater id:keyboard.ID?.trim(), - version:keyboard.Version?.trim(), - rtl:keyboard.RTL == 'True' ? true : undefined, + version: DEFAULT_KEYBOARD_VERSION, // filled by PackageMetadataUpdater + rtl: false, // filled by PackageMetadataUpdater languages: keyboard.Languages?.Language?.length ? this.kpsLanguagesToKmpLanguages(keyboard.Languages.Language) : [], @@ -323,7 +323,7 @@ export class KmpCompiler implements KeymanCompiler { if(kps.LexicalModels?.LexicalModel?.length) { kmp.lexicalModels = kps.LexicalModels.LexicalModel.map((model: KpsFile.KpsFileLexicalModel) => ({ - name:model.Name.trim(), + name:model.ID.trim(), id:model.ID.trim(), languages: model.Languages?.Language?.length ? this.kpsLanguagesToKmpLanguages(model.Languages.Language) : [] @@ -389,7 +389,7 @@ export class KmpCompiler implements KeymanCompiler { })); // Remove default values - for(let item of kmp.startMenu.items) { + for(const item of kmp.startMenu.items) { if(item.icon == '') delete item.icon; if(item.location == 'psmelStartMenu') delete item.location; if(item.arguments == '') delete item.arguments; @@ -407,7 +407,7 @@ export class KmpCompiler implements KeymanCompiler { // Helper functions private kpsInfoToKmpInfo(kpsInfo: KpsFile.KpsFileInfo): KmpJsonFile.KmpJsonFileInfo { - let kmpInfo: KmpJsonFile.KmpJsonFileInfo = {}; + const kmpInfo: KmpJsonFile.KmpJsonFileInfo = {}; const keys: [(keyof KpsFile.KpsFileInfo), (keyof KmpJsonFile.KmpJsonFileInfo), boolean][] = [ ['Author','author',false], @@ -418,7 +418,7 @@ export class KmpCompiler implements KeymanCompiler { ['Description','description',true], ]; - for (let [src,dst,isMarkdown] of keys) { + for (const [src,dst,isMarkdown] of keys) { if (kpsInfo[src]?._ && (kpsInfo[src]._.trim() || kpsInfo[src].$?.URL)) { kmpInfo[dst] = { description: (kpsInfo[src]._ ?? '').trim() diff --git a/developer/src/kmc-package/src/compiler/kmp-inf-writer.ts b/developer/src/kmc-package/src/compiler/kmp-inf-writer.ts index 037f34f4ae..d7b6f8bab6 100644 --- a/developer/src/kmc-package/src/compiler/kmp-inf-writer.ts +++ b/developer/src/kmc-package/src/compiler/kmp-inf-writer.ts @@ -64,7 +64,7 @@ export class KmpInfWriter { } this.addSection('StartMenuEntries'); let hasIcon = false, hasLocation = false; - for(let item of this.data.startMenu.items) { + for(const item of this.data.startMenu.items) { this.addString(item.name, `"${item.filename}","${item.arguments ?? ''}"`); hasIcon = hasIcon || !!item.icon; hasLocation = hasLocation || (item.location??'psmelStartMenu') != 'psmelStartMenu'; @@ -75,7 +75,7 @@ export class KmpInfWriter { if(hasIcon) { this.addSection('StartMenuEntries_Icon'); - for(let item of this.data.startMenu.items) { + for(const item of this.data.startMenu.items) { if(item.icon) { this.addString(item.name, item.icon); } @@ -84,7 +84,7 @@ export class KmpInfWriter { if(hasLocation) { this.addSection('StartMenuEntries_Location'); - for(let item of this.data.startMenu.items) { + for(const item of this.data.startMenu.items) { if(item.location != 'psmelStartMenu') { this.addString(item.name, '1'); // psmelDesktop ordinal = 1 is the only other supported value } diff --git a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts index 5a088be128..d98ff747e0 100644 --- a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts +++ b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts @@ -144,7 +144,7 @@ export class PackageCompilerMessages { `The source file '${def(o.filename)}' should not be included in the package; instead include the compiled result.` ); - // 0x001E was ERROR_InvalidPackageFile, now CommonTypesMessages.Error_InvalidPackageFile + // 0x001E was ERROR_InvalidPackageFile, now DeveloperUtilsMessages.Error_InvalidPackageFile static ERROR_FileRecordIsMissingName = SevError | 0x001F; static Error_FileRecordIsMissingName = (o:{description:string}) => m(this.ERROR_FileRecordIsMissingName, @@ -191,7 +191,7 @@ export class PackageCompilerMessages { `The format for version numbers should be number[.number[.number]]. Each number component should be an integer, without leading zeroes.` ); - + static ERROR_PackageMustNotContainItself = SevError | 0x0028; static Error_PackageMustNotContainItself = (o:{outputFilename: string}) => m( this.ERROR_PackageMustNotContainItself, `The package may not include a .kmp file of the same name '${def(o.outputFilename)}'.`, ` diff --git a/developer/src/kmc-package/src/compiler/package-keyboard-target-validator.ts b/developer/src/kmc-package/src/compiler/package-keyboard-target-validator.ts index 06f36c7727..40faefadba 100644 --- a/developer/src/kmc-package/src/compiler/package-keyboard-target-validator.ts +++ b/developer/src/kmc-package/src/compiler/package-keyboard-target-validator.ts @@ -8,7 +8,7 @@ export class PackageKeyboardTargetValidator { constructor(private callbacks: CompilerCallbacks) {} public verifyAllTargets(kmp: KmpJsonFile.KmpJsonFile, metadata: KeyboardMetadataCollection): void { - for(let keyboard of Object.keys(metadata)) { + for(const keyboard of Object.keys(metadata)) { let hasJS = true; if(metadata[keyboard].data.targets) { diff --git a/developer/src/kmc-package/src/compiler/package-metadata-collector.ts b/developer/src/kmc-package/src/compiler/package-metadata-collector.ts index 89a8f35b09..8a2425a8e9 100644 --- a/developer/src/kmc-package/src/compiler/package-metadata-collector.ts +++ b/developer/src/kmc-package/src/compiler/package-metadata-collector.ts @@ -17,13 +17,13 @@ export class PackageMetadataCollector { constructor(private callbacks: CompilerCallbacks) {} public collectKeyboardMetadata(kpsFilename: string, kmp: KmpJsonFile.KmpJsonFile): KeyboardMetadataCollection { - let result: KeyboardMetadataCollection = {}; + const result: KeyboardMetadataCollection = {}; if(!kmp.keyboards) { return result; } - for(let keyboard of kmp.keyboards) { + for(const keyboard of kmp.keyboards) { result[keyboard.id] = this.getKeyboardFileData(kpsFilename, kmp, keyboard); if(!result[keyboard.id]) { // Warnings or errors will have been raised by getKeyboardFileData diff --git a/developer/src/kmc-package/src/compiler/package-metadata-updater.ts b/developer/src/kmc-package/src/compiler/package-metadata-updater.ts index 943210fa6a..9a926f57ad 100644 --- a/developer/src/kmc-package/src/compiler/package-metadata-updater.ts +++ b/developer/src/kmc-package/src/compiler/package-metadata-updater.ts @@ -3,7 +3,7 @@ import { KeyboardMetadataCollection } from './package-metadata-collector.js'; export class PackageMetadataUpdater { public updatePackage(metadata: KeyboardMetadataCollection) { - for(let id of Object.keys(metadata)) { + for(const id of Object.keys(metadata)) { const keyboard = metadata[id]; keyboard.keyboard.name = keyboard.data.keyboardName; keyboard.keyboard.rtl = keyboard.data.isRtl ? true : undefined; diff --git a/developer/src/kmc-package/src/compiler/package-validation.ts b/developer/src/kmc-package/src/compiler/package-validation.ts index ae5fd331f5..725822be82 100644 --- a/developer/src/kmc-package/src/compiler/package-validation.ts +++ b/developer/src/kmc-package/src/compiler/package-validation.ts @@ -32,7 +32,7 @@ export class PackageValidation { } private checkForDuplicatedOrNonMinimalLanguages(resourceType: 'keyboard'|'model', id: string, languages: KmpJsonFile.KmpJsonFileLanguage[]): boolean { - let minimalTags: {[tag: string]: string} = {}; + const minimalTags: {[tag: string]: string} = {}; if(languages.length == 0) { if(resourceType == 'keyboard') { @@ -43,7 +43,7 @@ export class PackageValidation { } } - for(let lang of languages) { + for(const lang of languages) { let locale; try { locale = new Intl.Locale(lang.id); @@ -99,7 +99,7 @@ export class PackageValidation { this.callbacks.reportMessage(PackageCompilerMessages.Warn_PackageNameDoesNotFollowLexicalModelConventions({filename})); } - for(let model of kmpJson.lexicalModels) { + for(const model of kmpJson.lexicalModels) { if(!this.checkForDuplicatedOrNonMinimalLanguages('model', model.id, model.languages)) { return false; } @@ -119,7 +119,7 @@ export class PackageValidation { this.callbacks.reportMessage(PackageCompilerMessages.Warn_PackageNameDoesNotFollowKeyboardConventions({filename})); } - for(let keyboard of kmpJson.keyboards) { + for(const keyboard of kmpJson.keyboards) { if(!this.checkForDuplicatedOrNonMinimalLanguages('keyboard', keyboard.id, keyboard.languages)) { return false; } @@ -131,7 +131,7 @@ export class PackageValidation { } private checkContentFiles(kmpJson: KmpJsonFile.KmpJsonFile, outputFilename: string): boolean { - for(let file of kmpJson.files) { + for(const file of kmpJson.files) { if(!this.checkContentFile(file, outputFilename)) { return false; } diff --git a/developer/src/kmc-package/src/compiler/package-version-validator.ts b/developer/src/kmc-package/src/compiler/package-version-validator.ts index 7250b6bef4..2a33d13654 100644 --- a/developer/src/kmc-package/src/compiler/package-version-validator.ts +++ b/developer/src/kmc-package/src/compiler/package-version-validator.ts @@ -13,9 +13,9 @@ export class PackageVersionValidator { public getMinKeymanVersion(keyboardMetadata: KeyboardMetadataCollection): string { let minKeymanVersion = MIN_KBD_FILEVERSION_KMP_JSON; - for(let id of Object.keys(keyboardMetadata)) { + for(const id of Object.keys(keyboardMetadata)) { // minKeymanVersion is always a float so float comparison is safe - let kbdMinKeymanVersion = parseFloat(keyboardMetadata[id].data.minKeymanVersion); + const kbdMinKeymanVersion = parseFloat(keyboardMetadata[id].data.minKeymanVersion); if(kbdMinKeymanVersion > 14.0) { // Keyman for Windows 14 and earlier only accepted version 7.0 for keyboard // packages, so we must not write any other version in order to allow @@ -54,11 +54,11 @@ export class PackageVersionValidator { return true; } - let result = true; + const result = true; // We now know we have at least one keyboard in the package - for(let keyboard of kmp.keyboards) { + for(const keyboard of kmp.keyboards) { const data = keyboardMetadata[keyboard.id]; if(!data) { // Warnings or errors will have been raised by getKeyboardFileData diff --git a/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.intermediate.json b/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.intermediate.json index 7c9d139bd4..f074fcc320 100644 --- a/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.intermediate.json +++ b/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.intermediate.json @@ -31,7 +31,7 @@ ], "lexicalModels": [ { - "name": "SENĆOŦEN dictionary", + "name": "example.qaa.sencoten", "id": "example.qaa.sencoten", "languages": [ { diff --git a/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.json b/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.json index dacbc3ee67..af54b89ec4 100644 --- a/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.json +++ b/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.json @@ -31,7 +31,7 @@ ], "lexicalModels": [ { - "name": "SENĆOŦEN dictionary", + "name": "example.qaa.sencoten", "id": "example.qaa.sencoten", "languages": [ { diff --git a/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.zipped.json b/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.zipped.json index b36605dc41..69b2b218a3 100644 --- a/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.zipped.json +++ b/developer/src/kmc-package/test/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.zipped.json @@ -28,7 +28,7 @@ ], "lexicalModels": [ { - "name": "SENĆOŦEN dictionary", + "name": "example.qaa.sencoten", "id": "example.qaa.sencoten", "languages": [ { diff --git a/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.intermediate.json b/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.intermediate.json index 160c7e74b4..54666d7264 100644 --- a/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.intermediate.json +++ b/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.intermediate.json @@ -35,7 +35,7 @@ ], "lexicalModels": [ { - "name": "SENĆOŦEN dictionary", + "name": "withfolders.qaa.sencoten", "id": "withfolders.qaa.sencoten", "languages": [ { diff --git a/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.zipped.json b/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.zipped.json index a79910220a..7d73fd14ed 100644 --- a/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.zipped.json +++ b/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.zipped.json @@ -33,7 +33,7 @@ ], "lexicalModels": [ { - "name": "SENĆOŦEN dictionary", + "name": "withfolders.qaa.sencoten", "id": "withfolders.qaa.sencoten", "languages": [ { diff --git a/developer/src/kmc-package/test/kmp-inf-writer.tests.ts b/developer/src/kmc-package/test/kmp-inf-writer.tests.ts index f852fc69b8..094e729445 100644 --- a/developer/src/kmc-package/test/kmp-inf-writer.tests.ts +++ b/developer/src/kmc-package/test/kmp-inf-writer.tests.ts @@ -20,8 +20,8 @@ describe('KmpInfWriter', function () { const fixtureANSI = new TextDecoder('cp1252').decode(fixtureKmpInf); const testANSI = new TextDecoder('cp1252').decode(test); - let testItems = parseInf(testANSI); - let fixtureItems = parseInf(fixtureANSI); + const testItems = parseInf(testANSI); + const fixtureItems = parseInf(fixtureANSI); assert.deepEqual(testItems, fixtureItems); }); }); @@ -36,8 +36,9 @@ type InfFile = {[name:string]: Section}; * which never repeats */ function parseInf(inf: string): InfFile { - let items = inf.replaceAll(/\r\n/g, '\n').split('\n'); - let sections: InfFile = {}, newSection: Section = {}; + const items = inf.replaceAll(/\r\n/g, '\n').split('\n'); + const sections: InfFile = {}; + let newSection: Section = {}; sections[0] = newSection; for(let item of items) { item = item.trim(); diff --git a/developer/src/kmc-package/test/package-compiler.tests.ts b/developer/src/kmc-package/test/package-compiler.tests.ts index a9777a2a08..76d00d99be 100644 --- a/developer/src/kmc-package/test/package-compiler.tests.ts +++ b/developer/src/kmc-package/test/package-compiler.tests.ts @@ -37,7 +37,7 @@ describe('KmpCompiler', function () { callbacks.clear(); }); - for (let modelID of MODELS) { + for (const modelID of MODELS) { const kpsPath = modelID.includes('withfolders') ? makePathToFixture(modelID, 'source', `${modelID}.model.kps`) : makePathToFixture(modelID, `${modelID}.model.kps`); const jsPath = modelID.includes('withfolders') ? @@ -119,7 +119,7 @@ describe('KmpCompiler', function () { const zip = JSZip(); - let jszip = await zip.loadAsync(kmpData); + const jszip = await zip.loadAsync(kmpData); assert.isNotNull(jszip.file('kmp.json')); // kmp.json should be present // kmp file should contain the following files const expectedFiles = [ @@ -135,7 +135,7 @@ describe('KmpCompiler', function () { expectedFiles.sort(), 'khmer_angkor.kmp file should have exactly the expected files'); - let kmpJsonData = JSON.parse(await jszip.file('kmp.json').async('string')); + const kmpJsonData = JSON.parse(await jszip.file('kmp.json').async('string')); assert.deepEqual(kmpJsonData, kmpJsonFixture); }); @@ -149,12 +149,12 @@ describe('KmpCompiler', function () { const kpsPath = makePathToFixture('kmp.json', 'ahom_star.kps'); const kmpJsonRefPath = makePathToFixture('kmp.json', 'kmp.json'); - let kmpJsonActual = kmpCompiler.transformKpsToKmpObject(kpsPath); + const kmpJsonActual = kmpCompiler.transformKpsToKmpObject(kpsPath); if(kmpJsonActual == null) { callbacks.printMessages(); assert.isNotNull(kmpJsonActual); } - let kmpJsonFixture = JSON.parse(fs.readFileSync(kmpJsonRefPath, 'utf-8')); + const kmpJsonFixture = JSON.parse(fs.readFileSync(kmpJsonRefPath, 'utf-8')); assert.isNotNull(kmpJsonFixture); // Blank out system.keymanDeveloperVersion which will vary @@ -178,12 +178,12 @@ describe('KmpCompiler', function () { debugger; - let kmpJsonActual = kmpCompiler.transformKpsToKmpObject(kpsPath); + const kmpJsonActual = kmpCompiler.transformKpsToKmpObject(kpsPath); if(kmpJsonActual == null) { callbacks.printMessages(); assert.isNotNull(kmpJsonActual); } - let kmpJsonFixture = JSON.parse(fs.readFileSync(kmpJsonRefPath, 'utf-8')); + const kmpJsonFixture = JSON.parse(fs.readFileSync(kmpJsonRefPath, 'utf-8')); assert.isNotNull(kmpJsonFixture); // Blank out system.keymanDeveloperVersion which will vary diff --git a/developer/src/kmc-package/test/windows-package-installer-compiler.tests.ts b/developer/src/kmc-package/test/windows-package-installer-compiler.tests.ts index e1fde65647..f61b65d8aa 100644 --- a/developer/src/kmc-package/test/windows-package-installer-compiler.tests.ts +++ b/developer/src/kmc-package/test/windows-package-installer-compiler.tests.ts @@ -23,7 +23,7 @@ describe('WindowsPackageInstallerCompiler', function () { }; const callbacks = new TestCompilerCallbacks(); - let compiler = new WindowsPackageInstallerCompiler(); + const compiler = new WindowsPackageInstallerCompiler(); assert.isTrue(await compiler.init(callbacks, {sources})); const result = await compiler.run(kpsPath, null); diff --git a/developer/src/kmc/.eslintrc.cjs b/developer/src/kmc/.eslintrc.cjs index 01ff867bb8..1e8b540611 100644 --- a/developer/src/kmc/.eslintrc.cjs +++ b/developer/src/kmc/.eslintrc.cjs @@ -2,8 +2,5 @@ module.exports = { parserOptions: { project: ["./tsconfig.json", "./test/tsconfig.json"], }, - ignorePatterns: ["build-bundler.js"], - rules: { - "prefer-const": "off", // TODO: enable this once infrastructure is in place and cleanup the problem cases - }, + ignorePatterns: ["build-bundler.js"] }; diff --git a/developer/src/kmc/build.sh b/developer/src/kmc/build.sh index 274787cdad..05a1bdfcfc 100755 --- a/developer/src/kmc/build.sh +++ b/developer/src/kmc/build.sh @@ -88,7 +88,7 @@ function do_bundle() { sentry-cli sourcemaps inject \ --org keyman \ --project keyman-developer \ - --release "$VERSION_GIT_TAG" \ + --release "$KEYMAN_VERSION_GIT_TAG" \ build/dist/ "${SOURCEMAP_PATHS[@]}" # Manually copy over kmcmplib module diff --git a/developer/src/kmc/package.json b/developer/src/kmc/package.json index f4647ba1a3..7e04cae164 100644 --- a/developer/src/kmc/package.json +++ b/developer/src/kmc/package.json @@ -62,7 +62,6 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "esbuild": "^0.25.0", - "mocha": "^8.4.0", "typescript": "^5.4.5" }, "mocha": { diff --git a/developer/src/kmc/src/commands/analyze.ts b/developer/src/kmc/src/commands/analyze.ts index b0699977c3..ff38aa0194 100644 --- a/developer/src/kmc/src/commands/analyze.ts +++ b/developer/src/kmc/src/commands/analyze.ts @@ -19,13 +19,13 @@ interface AnalysisActivityOptions extends CompilerBaseOptions { }; export function declareAnalyze(program: Command) { - let command = program.command('analyze'); + const command = program.command('analyze'); declareOskCharUse(command); declareOskRewrite(command); } function declareOskCharUse(command: Command) { - let subCommand = command.command('osk-char-use [infile...]'); + const subCommand = command.command('osk-char-use [infile...]'); BaseOptions.addAll(subCommand); subCommand .description('Analyze On Screen Keyboards for character usage') @@ -51,7 +51,7 @@ function declareOskCharUse(command: Command) { } function declareOskRewrite(command: Command) { - let subCommand = command.command('osk-rewrite-from-char-use [infile...]'); + const subCommand = command.command('osk-rewrite-from-char-use [infile...]'); subCommand .description('Rewrite On Screen Keyboard files from source mapping; given file.ext, output is written to file-pua.ext') .addOption(new Option('-m, --mapping-file ', 'JSON mapping file to read from').makeOptionMandatory()) @@ -88,7 +88,7 @@ function commanderOptionsToAnalyzeOptions(options: any): AnalysisActivityOptions async function analyze(action: (callbacks: CompilerCallbacks, filenames: string[], options: AnalysisActivityOptions)=>Promise, filenames: string[], commanderOptions: any): Promise { const options = commanderOptionsToAnalyzeOptions(commanderOptions); - let callbacks = new NodeCompilerCallbacks(options); + const callbacks = new NodeCompilerCallbacks(options); try { return await action(callbacks, filenames, options); } catch(e) { @@ -129,7 +129,7 @@ async function analyzeOskRewritePua(callbacks: CompilerCallbacks, filenames: str return false; } const mappedFilenames = Object.keys(analyzer.data); - for(let filename of mappedFilenames) { + for(const filename of mappedFilenames) { const ext = path.extname(filename); const outputFilename = filename.substring(0, filename.length - ext.length) + '-pua' + ext; fs.writeFileSync(outputFilename, analyzer.data[filename]); diff --git a/developer/src/kmc/src/commands/build.ts b/developer/src/kmc/src/commands/build.ts index 39e0e20578..20f1092a9f 100644 --- a/developer/src/kmc/src/commands/build.ts +++ b/developer/src/kmc/src/commands/build.ts @@ -103,7 +103,7 @@ async function buildFile(filenames: string[], _options: any, commander: any): Pr return await exitProcess(1); } - for(let filename of filenames) { + for(const filename of filenames) { if(!await build(filename, commanderOptions.outFile, callbacks, options)) { // Once a file fails to build, we bail on subsequent builds return await exitProcess(1); @@ -135,7 +135,7 @@ async function build(filename: string, outfile: string, parentCallbacks: NodeCom builder = new BuildProject(); } else { // Otherwise, if it's one of our known file extensions, we build it - let extensions: string[] = []; + const extensions: string[] = []; builder = buildActivities.find(build => { extensions.push(build.sourceExtension); return filename.toLowerCase().endsWith(build.sourceExtension); diff --git a/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts b/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts index 2e522c99fd..f6a44ced5f 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts @@ -52,13 +52,13 @@ export class BuildModelInfo extends BuildActivity { return false; } - let kmpCompiler = new KmpCompiler(); + const kmpCompiler = new KmpCompiler(); if(!await kmpCompiler.init(callbacks, options)) { // Errors will have been emitted by KmpCompiler return false; } - let kmpJsonData = kmpCompiler.transformKpsToKmpObject(project.resolveInputFilePath(kps)); + const kmpJsonData = kmpCompiler.transformKpsToKmpObject(project.resolveInputFilePath(kps)); if(!kmpJsonData) { // Errors will have been emitted by KmpCompiler return false; diff --git a/developer/src/kmc/src/commands/buildClasses/BuildProject.ts b/developer/src/kmc/src/commands/buildClasses/BuildProject.ts index 4ae9f8d9ce..82e1347aa5 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildProject.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildProject.ts @@ -8,7 +8,7 @@ import { buildActivities, buildKeyboardInfoActivity, buildModelInfoActivity } fr import { InfrastructureMessages } from '../../messages/infrastructureMessages.js'; import { loadProject } from '../../util/projectLoader.js'; import { ExtendedCompilerOptions } from '../../util/extendedCompilerOptions.js'; -import { getOption } from '@keymanapp/developer-utils'; +import { getOption } from '../../util/options.js'; export class BuildProject extends BuildActivity { public get name(): string { return 'Project'; } @@ -21,7 +21,7 @@ export class BuildProject extends BuildActivity { return false; } - let builder = new ProjectBuilder(infile, callbacks, options); + const builder = new ProjectBuilder(infile, callbacks, options); return builder.run(); } } @@ -52,7 +52,7 @@ class ProjectBuilder { } // Go through the various file types and build them - for(let builder of buildActivities) { + for(const builder of buildActivities) { if(builder.sourceExtension == KeymanFileTypes.Source.Project) { // We don't support nested projects continue; @@ -97,7 +97,7 @@ class ProjectBuilder { } let result = true; - for(let file of this.project.files) { + for(const file of this.project.files) { if(file.fileType.toLowerCase() == activity.sourceExtension) { result = await this.buildTarget(file, activity) && result; } diff --git a/developer/src/kmc/src/commands/buildTestData/index.ts b/developer/src/kmc/src/commands/buildTestData/index.ts index 220bc4b26f..f0c0ad9c53 100644 --- a/developer/src/kmc/src/commands/buildTestData/index.ts +++ b/developer/src/kmc/src/commands/buildTestData/index.ts @@ -12,7 +12,7 @@ import { dirname } from 'node:path'; export async function buildTestData(infile: string, _options: any, commander: any): Promise { const options: CommandLineBuildBaseOptions = commander.optsWithGlobals(); - let compilerOptions: kmcLdml.LdmlCompilerOptions = { + const compilerOptions: kmcLdml.LdmlCompilerOptions = { ...defaultCompilerOptions, ...options, saveDebug: false, @@ -25,7 +25,7 @@ export async function buildTestData(infile: string, _options: any, commander: an const callbacks = new NodeCompilerCallbacks(options); - let testData = await loadTestData(infile, callbacks, compilerOptions); + const testData = await loadTestData(infile, callbacks, compilerOptions); if (!testData || callbacks.hasFailureMessage()) { await exitProcess(1); } diff --git a/developer/src/kmc/src/commands/convert.ts b/developer/src/kmc/src/commands/convert.ts index c03b0c5427..2dbbf71c31 100644 --- a/developer/src/kmc/src/commands/convert.ts +++ b/developer/src/kmc/src/commands/convert.ts @@ -13,7 +13,7 @@ import { Converter, ConverterOptions } from '@keymanapp/kmc-convert'; import { CompilerCallbacks } from '@keymanapp/developer-utils'; export function declareConvert(program: Command) { - let command = program.command('convert '); + const command = program.command('convert '); BaseOptions.addLogLevel(command); BaseOptions.addLogFormat(command); command diff --git a/developer/src/kmc/src/commands/messageCommand.ts b/developer/src/kmc/src/commands/messageCommand.ts index d0feac380e..57f7095d2c 100644 --- a/developer/src/kmc/src/commands/messageCommand.ts +++ b/developer/src/kmc/src/commands/messageCommand.ts @@ -154,7 +154,7 @@ function initialize(options: any): MessageOptions { } function allMessageDetails(): CompilerMessageDetail[] { - let result: CompilerMessageDetail[] = []; + const result: CompilerMessageDetail[] = []; messageNamespaceKeys.forEach((namespace: CompilerErrorNamespace) => { const ms = messageSources[namespace] as CompilerMessageSource; diff --git a/developer/src/kmc/src/kmc.ts b/developer/src/kmc/src/kmc.ts index c246fecdf6..d552b1711e 100644 --- a/developer/src/kmc/src/kmc.ts +++ b/developer/src/kmc/src/kmc.ts @@ -6,7 +6,8 @@ import { Command, Option } from 'commander'; import { declareBuild } from './commands/build.js'; import { declareAnalyze } from './commands/analyze.js'; -import { KeymanSentry, loadOptions } from '@keymanapp/developer-utils'; +import { loadOptions } from './util/options.js'; +import { KeymanSentry } from './util/KeymanSentry.js'; import KEYMAN_VERSION from "@keymanapp/keyman-version"; import { TestKeymanSentry } from './util/TestKeymanSentry.js'; import { exitProcess } from './util/sysexits.js'; diff --git a/developer/src/kmc/src/kmlmp.ts b/developer/src/kmc/src/kmlmp.ts index a1346ac0d3..d5fdb20c16 100644 --- a/developer/src/kmc/src/kmlmp.ts +++ b/developer/src/kmc/src/kmlmp.ts @@ -30,19 +30,19 @@ if (!inputFilename) { exitDueToUsageError('Must provide a lexical model package source file.'); } -let outputFilename: string = program.opts().outFile ? program.opts().outFile : inputFilename.replace(/\.kps$/, ".kmp"); +const outputFilename: string = program.opts().outFile ? program.opts().outFile : inputFilename.replace(/\.kps$/, ".kmp"); // // Run the compiler // const callbacks = new NodeCompilerCallbacks({logLevel: 'info'}); -let kmpCompiler = new KmpCompiler(); +const kmpCompiler = new KmpCompiler(); if(!await kmpCompiler.init(callbacks, null)) { process.exit(1); } -let result = await kmpCompiler.run(inputFilename, outputFilename); +const result = await kmpCompiler.run(inputFilename, outputFilename); if(!result) { process.exit(1); } diff --git a/developer/src/kmc/src/messages/infrastructureMessages.ts b/developer/src/kmc/src/messages/infrastructureMessages.ts index 3e1f4f30c6..d384aa7d86 100644 --- a/developer/src/kmc/src/messages/infrastructureMessages.ts +++ b/developer/src/kmc/src/messages/infrastructureMessages.ts @@ -38,9 +38,7 @@ export class InfrastructureMessages { static Info_FileNotBuiltSuccessfully = (o:{filename:string,relativeFilename:string}) => ({filename:o.filename, ...m(this.INFO_FileNotBuiltSuccessfully, `${def(o.relativeFilename)} failed to build.`)}); - static ERROR_InvalidProjectFile = SevError | 0x0008; - static Error_InvalidProjectFile = (o:{message:string}) => m(this.ERROR_InvalidProjectFile, - `Project file is not valid: ${def(o.message)}`); + // 0x0008: ERROR_InvalidProjectFile moved to DeveloperUtilsMessages static HINT_FilenameHasDifferingCase = SevHint | 0x0009; static Hint_FilenameHasDifferingCase = (o:{reference:string, filename:string}) => m(this.HINT_FilenameHasDifferingCase, @@ -84,9 +82,7 @@ export class InfrastructureMessages { static Error_InvalidProjectFolder = (o:{folderName:string}) => m(this.ERROR_InvalidProjectFolder, `The folder ${def(o.folderName)} does not appear to be a Keyman Developer project.`); - static ERROR_UnsupportedProjectVersion = SevError | 0x0013; - static Error_UnsupportedProjectVersion = (o:{version:string}) => m(this.ERROR_UnsupportedProjectVersion, - `Project version ${def(o.version)} is not supported by this version of Keyman Developer.`); + // 0x0013: ERROR_UnsupportedProjectVersion moved to developer-utils-messages static HINT_ProjectIsVersion10 = SevHint | 0x0014; static Hint_ProjectIsVersion10 = () => m(this.HINT_ProjectIsVersion10, diff --git a/developer/src/kmc/src/messages/messageNamespaces.ts b/developer/src/kmc/src/messages/messageNamespaces.ts index 67a9e460f2..cbfc2533fc 100644 --- a/developer/src/kmc/src/messages/messageNamespaces.ts +++ b/developer/src/kmc/src/messages/messageNamespaces.ts @@ -1,4 +1,4 @@ -import { CommonTypesMessages, CompilerErrorNamespace } from '@keymanapp/developer-utils'; +import { DeveloperUtilsMessages, CompilerErrorNamespace } from '@keymanapp/developer-utils'; import { AnalyzerMessages } from '@keymanapp/kmc-analyze'; import { CopierMessages } from '@keymanapp/kmc-copy'; import { GeneratorMessages } from '@keymanapp/kmc-generate'; @@ -14,7 +14,7 @@ import { ConverterMessages } from '@keymanapp/kmc-convert'; // Maps every compiler error namespace to the corresponding implementation const messageNamespaces: Record = { [CompilerErrorNamespace.LdmlKeyboardCompiler]: LdmlCompilerMessages, - [CompilerErrorNamespace.CommonTypes]: CommonTypesMessages, + [CompilerErrorNamespace.DeveloperUtils]: DeveloperUtilsMessages, [CompilerErrorNamespace.KmnCompiler]: KmnCompilerMessages, [CompilerErrorNamespace.ModelCompiler]: ModelCompilerMessages, [CompilerErrorNamespace.PackageCompiler]: PackageCompilerMessages, @@ -40,7 +40,7 @@ export type CompilerMessageSource = { // TODO: consolidate with messageNamespaces above export const messageSources: Record = { [CompilerErrorNamespace.LdmlKeyboardCompiler]: { module: 'kmc-ldml', class: LdmlCompilerMessages }, - [CompilerErrorNamespace.CommonTypes]: { module: 'common-types', class: CommonTypesMessages }, + [CompilerErrorNamespace.DeveloperUtils]: { module: 'developer-utils', class: DeveloperUtilsMessages }, [CompilerErrorNamespace.KmnCompiler]: { module: 'kmc-kmn', class: KmnCompilerMessages }, [CompilerErrorNamespace.ModelCompiler]: { module: 'kmc-model', class: ModelCompilerMessages }, [CompilerErrorNamespace.PackageCompiler]: { module: 'kmc-package', class: PackageCompilerMessages }, diff --git a/developer/src/kmc/src/util/KeymanSentry.ts b/developer/src/kmc/src/util/KeymanSentry.ts new file mode 100644 index 0000000000..9ee05ecb7f --- /dev/null +++ b/developer/src/kmc/src/util/KeymanSentry.ts @@ -0,0 +1,104 @@ +// TODO: this is duplicated in server.js +import Sentry from "@sentry/node"; +import KEYMAN_VERSION from "@keymanapp/keyman-version"; +import { getOption } from "./options.js"; + +export type SentryNodeOptions = Sentry.NodeOptions; + +/** + * Maximum delay on shutdown of process to send pending events + * to Sentry, in msec + */ +const CLOSE_TIMEOUT = 2000; + +let isInit = false; + +export class KeymanSentry { + + static isEnabled() { + if(process.argv.includes('--no-error-reporting')) { + return false; + } + if(process.argv.includes('--error-reporting')) { + return true; + } + + return getOption('automatically report errors', true); + } + + static init(options?: SentryNodeOptions) { + options = options ?? {}; + Sentry.init({ + ...options, + dsn: 'https://39b25a09410349a58fe12aaf721565af@o1005580.ingest.sentry.io/5983519', // Keyman Developer + environment: KEYMAN_VERSION.VERSION_ENVIRONMENT, + release: KEYMAN_VERSION.VERSION_GIT_TAG, + }); + isInit = true; + } + + private static writeSentryMessage(eventId: string) { + process.stderr.write(` + This error has been automatically reported to the Keyman team. + Identifier: ${eventId} + Application: Keyman Developer + Reported at: https://sentry.io/organizations/keyman/projects/keyman-developer/events/${eventId}/ + `); + } + + static async reportException(e: any, silent: boolean = true) { + if(isInit) { + const eventId = await Sentry.captureException(e); + if(!silent) { + this.writeSentryMessage(eventId); + } + return eventId; + } + return null; + } + + static captureMessage(message: string) { + if(isInit) { + return Sentry.captureMessage(message); + } else { + return null; + } + } + + /** + * capture an exception for Sentry; note that in local environments, this will normally + * throw the exception rather than sending to Sentry + * @param e + * @param force if true, reports to Sentry even in local environments + */ + static async captureException(e: any, force?: boolean): Promise { + if(isInit) { + // For local development, we don't want to bury the trace; we need the cast to avoid + // TS2367 (comparison appears to be unintentional) + if(!force && (KEYMAN_VERSION.VERSION_ENVIRONMENT as string) == 'local') { + throw e; + } + + const eventId = Sentry.captureException(e); + process.stderr.write(` + Fatal error: ${(e??'').toString()} + `); + this.writeSentryMessage(eventId); + await this.close(); + + process.exit(1); + } else { + throw e; + } + } + + static async close() { + if(isInit) { + try { + await Sentry.close(CLOSE_TIMEOUT); + } catch(e) { + // ignore any errors here + } + } + } +} \ No newline at end of file diff --git a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts index 94465d4413..a92314aa5f 100644 --- a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts +++ b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts @@ -13,7 +13,7 @@ import { CompilerCallbacks, CompilerEvent, import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; import chalk from 'chalk'; import supportsColor from 'supports-color'; -import { KeymanSentry } from '@keymanapp/developer-utils'; +import { KeymanSentry } from './KeymanSentry.js'; import { fileURLToPath } from 'url'; const color = chalk.default; @@ -45,6 +45,8 @@ export class NodeCompilerCallbacks implements CompilerCallbacks { messages: CompilerEvent[] = []; messageCount = 0; messageFilename: string = ''; + /** cache of the contentes of the text */ + messageFiletext: string = ''; maxLogMessages = MaxMessagesDefault; constructor(private options: CompilerCallbackOptions) { @@ -55,6 +57,7 @@ export class NodeCompilerCallbacks implements CompilerCallbacks { this.messages = []; this.messageCount = 0; this.messageFilename = ''; + this.messageFiletext = ''; } /** @@ -154,6 +157,7 @@ export class NodeCompilerCallbacks implements CompilerCallbacks { // Reset max message limit when a new file is being processed this.messageFilename = event.filename; this.messageCount = 0; + this.messageFiletext = ''; } const disable = CompilerFileCallbacks.applyMessageOverridesToEvent(event, this.options.messageOverrides); diff --git a/developer/src/kmc/src/util/TestKeymanSentry.ts b/developer/src/kmc/src/util/TestKeymanSentry.ts index b8572516bd..34e41e64d3 100644 --- a/developer/src/kmc/src/util/TestKeymanSentry.ts +++ b/developer/src/kmc/src/util/TestKeymanSentry.ts @@ -1,6 +1,6 @@ import { KmnCompiler } from "@keymanapp/kmc-kmn"; import { NodeCompilerCallbacks } from "./NodeCompilerCallbacks.js"; -import { KeymanSentry, SentryNodeOptions } from '@keymanapp/developer-utils'; +import { KeymanSentry, SentryNodeOptions } from './KeymanSentry.js'; const cli = process.argv.join(' '); diff --git a/developer/src/kmc/src/util/extendedCompilerOptions.ts b/developer/src/kmc/src/util/extendedCompilerOptions.ts index 9a2e482f68..53f68288f0 100644 --- a/developer/src/kmc/src/util/extendedCompilerOptions.ts +++ b/developer/src/kmc/src/util/extendedCompilerOptions.ts @@ -56,7 +56,7 @@ function commanderOptionsMessagesToCompilerOptionsMessages(messages: any, callba } const result: CompilerMessageOverrideMap = {}; - for(let message of messages) { + for(const message of messages) { const override = commanderOptionsMessageToCompilerOptionsMessage(message, callbacks); if(!override) { return null; diff --git a/developer/src/kmc/src/util/kmcSentryOptions.ts b/developer/src/kmc/src/util/kmcSentryOptions.ts index c6c47f732f..b0def8be66 100644 --- a/developer/src/kmc/src/util/kmcSentryOptions.ts +++ b/developer/src/kmc/src/util/kmcSentryOptions.ts @@ -1,6 +1,6 @@ import Sentry from "@sentry/node"; import * as process from "node:process"; -import { SentryNodeOptions } from "@keymanapp/developer-utils"; +import { SentryNodeOptions } from "./KeymanSentry.js"; Sentry.setContext('Command Line', { // obfusate parameters with longer paths e.g. from 'c:/users/name/a/b' to diff --git a/developer/src/common/web/utils/src/utils/options.ts b/developer/src/kmc/src/util/options.ts similarity index 100% rename from developer/src/common/web/utils/src/utils/options.ts rename to developer/src/kmc/src/util/options.ts diff --git a/developer/src/kmc/src/util/projectLoader.ts b/developer/src/kmc/src/util/projectLoader.ts index 1cd944397f..b870a9fd68 100644 --- a/developer/src/kmc/src/util/projectLoader.ts +++ b/developer/src/kmc/src/util/projectLoader.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as fs from 'fs'; import { KeymanFileTypes } from "@keymanapp/common-types"; -import { CompilerCallbacks, KeymanDeveloperProject, KPJFileReader } from "@keymanapp/developer-utils"; +import { CompilerCallbacks, KeymanDeveloperProject, ProjectLoader } from "@keymanapp/developer-utils"; import { InfrastructureMessages } from "../messages/infrastructureMessages.js"; export const isProject = (filename: string): boolean => @@ -26,7 +26,7 @@ export async function loadProject(infile: string, callbacks: CompilerCallbacks) } const project = callbacks.fs.existsSync(infile) ? - await loadProjectFromFile(infile, callbacks) : + await ProjectLoader.loadProjectFromFile(infile, callbacks) : await loadDefaultProjectFromFolder(infile, callbacks); return project; @@ -42,21 +42,3 @@ async function loadDefaultProjectFromFolder(infile: string, callbacks: CompilerC return project; } -async function loadProjectFromFile(infile: string, callbacks: CompilerCallbacks): Promise { - const kpjData = await callbacks.fsAsync.readFile(infile); - const reader = new KPJFileReader(callbacks); - let kpj = null; - try { - kpj = reader.read(kpjData); - if(kpj.KeymanDeveloperProject?.Options?.Version && kpj.KeymanDeveloperProject.Options.Version != "1.0" && kpj.KeymanDeveloperProject.Options.Version != "2.0") { - callbacks.reportMessage(InfrastructureMessages.Error_UnsupportedProjectVersion({version: kpj.KeymanDeveloperProject.Options.Version})); - return null; - } - reader.validate(kpj); - } catch(e) { - callbacks.reportMessage(InfrastructureMessages.Error_InvalidProjectFile({message: (e??'').toString()})); - return null; - } - const project = await reader.transform(infile, kpj); - return project; -} diff --git a/developer/src/kmc/src/util/projectRunner.ts b/developer/src/kmc/src/util/projectRunner.ts index 50ebb41418..e32ea2923a 100644 --- a/developer/src/kmc/src/util/projectRunner.ts +++ b/developer/src/kmc/src/util/projectRunner.ts @@ -18,7 +18,7 @@ async function runProject(callbacks: CompilerCallbacks, filename: string, callba } export async function runOnFiles(callbacks: CompilerCallbacks, filenames: string[], callback: (filename:string)=>Promise): Promise { - for(let filename of filenames) { + for(const filename of filenames) { const result = isProject(filename) ? await runProject(callbacks, filename, callback) : await callback(filename); diff --git a/developer/src/kmc/src/util/sysexits.ts b/developer/src/kmc/src/util/sysexits.ts index b656d538e4..7256dd4e77 100644 --- a/developer/src/kmc/src/util/sysexits.ts +++ b/developer/src/kmc/src/util/sysexits.ts @@ -1,4 +1,4 @@ -import { KeymanSentry } from "@keymanapp/developer-utils"; +import { KeymanSentry } from './KeymanSentry.js'; /** * Exit codes defined in : diff --git a/developer/src/kmc/test/build.tests.ts b/developer/src/kmc/test/build.tests.ts index 5bb382efc3..f8356db54b 100644 --- a/developer/src/kmc/test/build.tests.ts +++ b/developer/src/kmc/test/build.tests.ts @@ -1,5 +1,5 @@ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; -import { clearOptions } from '@keymanapp/developer-utils'; +import { clearOptions } from '../src/util/options.js'; import { assert } from 'chai'; import 'mocha'; import { BuildProject } from '../src/commands/buildClasses/BuildProject.js'; diff --git a/developer/src/kmc/test/getLastGitCommitDate.tests.ts b/developer/src/kmc/test/getLastGitCommitDate.tests.ts index 917cfdc7ad..2aa88aa5fa 100644 --- a/developer/src/kmc/test/getLastGitCommitDate.tests.ts +++ b/developer/src/kmc/test/getLastGitCommitDate.tests.ts @@ -5,17 +5,20 @@ import { expectedGitDateFormat, getLastGitCommitDate } from '../src/util/getLast describe('getLastGitCommitDate', function () { it('should return a valid date for a folder in this repo', async function() { + this.timeout(5000); // getLastGitCommitDate depends on git which can sometimes take longer const path = makePathToFixture('.'); const date = getLastGitCommitDate(path); assert.match(date, expectedGitDateFormat); }); it('should return null for a folder outside the repo', async function() { + this.timeout(5000); // getLastGitCommitDate depends on git which can sometimes take longer const date = getLastGitCommitDate('/'); assert.isNull(date); }); it('should return a valid date for a specific file in the repo', async function() { + this.timeout(5000); // getLastGitCommitDate depends on git which can sometimes take longer const path = makePathToFixture('get-last-git-commit-date/README.md'); const date = getLastGitCommitDate(path); // The expected date was manually extracted using the following command, with msec appended: diff --git a/developer/src/kmc/test/infrastructureMessages.tests.ts b/developer/src/kmc/test/infrastructureMessages.tests.ts index 225742bd92..68edaa7a10 100644 --- a/developer/src/kmc/test/infrastructureMessages.tests.ts +++ b/developer/src/kmc/test/infrastructureMessages.tests.ts @@ -8,9 +8,9 @@ import { NodeCompilerCallbacks } from '../src/util/NodeCompilerCallbacks.js'; import { KeymanFileTypes } from '@keymanapp/common-types'; import { unitTestEndpoints } from '../src/commands/build.js'; import { KmnCompilerMessages } from '@keymanapp/kmc-kmn'; -import { clearOptions } from '@keymanapp/developer-utils'; +import { clearOptions } from '../src/util/options.js'; import { loadProject } from '../src/util/projectLoader.js'; -import { CompilerErrorNamespace, CompilerEvent, defaultCompilerOptions, CompilerOptions} from '@keymanapp/developer-utils'; +import { CompilerErrorNamespace, CompilerEvent, defaultCompilerOptions, CompilerOptions, DeveloperUtilsMessages} from '@keymanapp/developer-utils'; import { analyzeUnitTestEndpoints } from '../src/commands/analyze.js'; import { BuildKeyboardInfo } from '../src/commands/buildClasses/BuildKeyboardInfo.js'; import { BuildModelInfo } from '../src/commands/buildClasses/BuildModelInfo.js'; @@ -71,12 +71,12 @@ describe('InfrastructureMessages', function () { 'ERROR_OutFileNotValidForProjects not generated, instead got: '+JSON.stringify(ncb.messages,null,2)); }); - // ERROR_InvalidProjectFile + // ERROR_InvalidProjectFile - test stays here for now though referencing developer-messages it('should generate ERROR_InvalidProjectFile if a project file is invalid', async function() { const ncb = new NodeCompilerCallbacks({logLevel: 'silent'}); await unitTestEndpoints.build(makePathToFixture('invalid-projects', 'error_invalid_project_file.kpj'), null, ncb, {}); - assert.isTrue(ncb.hasMessage(InfrastructureMessages.ERROR_InvalidProjectFile), + assert.isTrue(ncb.hasMessage(DeveloperUtilsMessages.ERROR_InvalidProjectFile), 'ERROR_InvalidProjectFile not generated, instead got: '+JSON.stringify(ncb.messages,null,2)); }); @@ -202,14 +202,14 @@ describe('InfrastructureMessages', function () { assertMessagesEqual(ncb.messages, expectedMessages); }); - // ERROR_UnsupportedProjectVersion + // ERROR_UnsupportedProjectVersion (test stays here for now though referencing developer-utils) it('should generate ERROR_UnsupportedProjectVersion if a project file with an unsupported version is loaded', async function() { const ncb = new NodeCompilerCallbacks({logLevel: 'silent'}); const filename = makePathToFixture('invalid-projects', 'error_unsupported_project_version.kpj'); const expectedMessages = [ InfrastructureMessages.INFO_BuildingFile, - InfrastructureMessages.ERROR_UnsupportedProjectVersion, + DeveloperUtilsMessages.ERROR_UnsupportedProjectVersion, InfrastructureMessages.INFO_ProjectNotBuiltSuccessfully ]; await unitTestEndpoints.build(filename, null, ncb, {compilerWarningsAsErrors: true}); diff --git a/developer/src/kmc/test/project-build.tests.ts b/developer/src/kmc/test/project-build.tests.ts index 3b10d8857c..87a627b749 100644 --- a/developer/src/kmc/test/project-build.tests.ts +++ b/developer/src/kmc/test/project-build.tests.ts @@ -4,7 +4,7 @@ import 'mocha'; import { BuildProject } from '../src/commands/buildClasses/BuildProject.js'; import { makePathToFixture } from './helpers/index.js'; import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js'; -import { clearOptions } from '@keymanapp/developer-utils'; +import { clearOptions } from '../src/util/options.js'; const callbacks = new TestCompilerCallbacks(); @@ -13,7 +13,7 @@ describe('BuildProject', function () { clearOptions(); const builder = new BuildProject(); const path = makePathToFixture('relative_paths', 'k_000___null_keyboard.kpj'); - let result = await builder.build(path, null, callbacks, { + const result = await builder.build(path, null, callbacks, { shouldAddCompilerVersion: false, compilerWarningsAsErrors: true, saveDebug: false, diff --git a/developer/src/kmcmplib/build.sh b/developer/src/kmcmplib/build.sh index aaf7184676..77036cc990 100755 --- a/developer/src/kmcmplib/build.sh +++ b/developer/src/kmcmplib/build.sh @@ -92,12 +92,10 @@ fi do_action clean # Note, we have a 'global' configure and also a per-arch configure -if builder_start_action configure; then - # Import our standard compiler defines; this is copied from - # /resources/build/meson/standard.meson.build by build.sh, because meson doesn't - # allow us to reference a file outside its root - mkdir -p "$THIS_SCRIPT_PATH/resources" - cp "$KEYMAN_ROOT/resources/build/meson/standard.meson.build" "$THIS_SCRIPT_PATH/resources/meson.build" +if builder_has_action configure; then + # Import our standard compiler defines + source "$KEYMAN_ROOT/resources/build/meson/standard_meson_build.inc.sh" + standard_meson_build # We have to checkout the keyboards repo in a 'configure' action because # otherwise meson will not get the right list of keyboard source files, @@ -105,8 +103,6 @@ if builder_start_action configure; then if should_do_full_test; then checkout_keyboards fi - - builder_finish_action success configure fi do_action configure diff --git a/developer/src/kmcmplib/getversion.bat b/developer/src/kmcmplib/getversion.bat deleted file mode 100644 index dcab02e4d9..0000000000 --- a/developer/src/kmcmplib/getversion.bat +++ /dev/null @@ -1,3 +0,0 @@ -@echo off -REM Get the version number from VERSION.md -more ..\..\..\VERSION.md diff --git a/developer/src/kmcmplib/getversion.sh b/developer/src/kmcmplib/getversion.sh deleted file mode 100755 index cd02c07929..0000000000 --- a/developer/src/kmcmplib/getversion.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# Get the version number from VERSION.md -VERSIONFILE=../../../VERSION.md -cat $VERSIONFILE diff --git a/developer/src/kmcmplib/meson.build b/developer/src/kmcmplib/meson.build index d494e884d9..0493c504f4 100644 --- a/developer/src/kmcmplib/meson.build +++ b/developer/src/kmcmplib/meson.build @@ -5,7 +5,7 @@ # project('kmcmplib', 'cpp', 'c', - version: run_command(find_program('getversion.bat', 'getversion.sh'), check: true).stdout().strip(), + version: files('../../../VERSION.md'), license: 'MIT', default_options : ['buildtype=release', 'cpp_std=c++14', diff --git a/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas index 1ba224d50b..dc274d082d 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas @@ -274,9 +274,7 @@ begin // Add metadata about the keyboard pk := TPackageKeyboard.Create(kps); - pk.Name := Name; pk.ID := ID; - pk.Version := Version; kps.Keyboards.Add(pk); SetPackageLanguageMetadata(kps, pk.Languages); diff --git a/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas index 77d93d8df9..6f5bfc7041 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.LdmlKeyboardProjectTemplate.pas @@ -233,9 +233,7 @@ begin // Add metadata about the keyboard pk := TPackageKeyboard.Create(kps); - pk.Name := Name; pk.ID := ID; - pk.Version := Version; kps.Keyboards.Add(pk); SetPackageLanguageMetadata(kps, pk.Languages); diff --git a/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas index 6edd42dfa2..42ac45d082 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas @@ -178,7 +178,6 @@ begin // Add metadata about the lexical model plm := TPackageLexicalModel.Create(kps); - plm.Name := Name; plm.ID := ID; kps.LexicalModels.Add(plm); diff --git a/developer/src/server/build.sh b/developer/src/server/build.sh index e0e82e3617..6741bb68de 100755 --- a/developer/src/server/build.sh +++ b/developer/src/server/build.sh @@ -52,6 +52,7 @@ function configure_server() { verify_npm_setup # See https://github.com/bubenshchykov/ngrok/issues/254, https://github.com/bubenshchykov/ngrok/pull/255 # TODO: this is horrible; is there a way we can avoid this? + rm -f "$KEYMAN_ROOT"/node_modules/ngrok/bin/ngrok rm -f "$KEYMAN_ROOT"/node_modules/ngrok/bin/ngrok.exe } @@ -128,6 +129,7 @@ function installer_server() { pushd "$PRODBUILDTEMP" npm install --omit=dev --omit=optional # See https://github.com/bubenshchykov/ngrok/issues/254, https://github.com/bubenshchykov/ngrok/pull/255 + rm -f node_modules/ngrok/bin/ngrok rm -f node_modules/ngrok/bin/ngrok.exe popd diff --git a/developer/src/server/package.json b/developer/src/server/package.json index 8619adceec..99cd6bb260 100644 --- a/developer/src/server/package.json +++ b/developer/src/server/package.json @@ -13,7 +13,7 @@ "@sentry/node": "^7.57.0", "chalk": "^4.1.2", "express": "^4.20.0", - "multer": "^1.4.5-lts.1", + "multer": "^2.0.1", "ngrok": "^5.0.0-beta.2", "open": "^8.4.0", "restructure": "^3.0.1", @@ -21,7 +21,7 @@ "semver": "^7.5.4", "ws": "^8.17.1", "xmlbuilder": "~11.0.0" -}, + }, "optionalDependencies": { "node-hide-console-window": "^2.2.0" }, @@ -30,11 +30,10 @@ "@keymanapp/resources-gosh": "*", "@types/express": "^4.17.13", "@types/mocha": "^9.1.0", - "@types/multer": "^1.4.7", + "@types/multer": "^1.4.12", "@types/node": "^20.4.1", "@types/ws": "^8.2.2", "copyfiles": "^2.4.1", - "mocha": "^10.0.0", "node-gyp": "^10.2.0", "tsc-watch": "^4.5.0", "typescript": "^5.4.5" diff --git a/developer/src/common/web/utils/src/utils/KeymanSentry.ts b/developer/src/server/src/KeymanSentry.ts similarity index 98% rename from developer/src/common/web/utils/src/utils/KeymanSentry.ts rename to developer/src/server/src/KeymanSentry.ts index dfe62bfa3d..9dba30a0d4 100644 --- a/developer/src/common/web/utils/src/utils/KeymanSentry.ts +++ b/developer/src/server/src/KeymanSentry.ts @@ -1,3 +1,4 @@ +// TODO: this is duplicated in kmc! import Sentry from "@sentry/node"; import KEYMAN_VERSION from "@keymanapp/keyman-version"; import { getOption } from "./options.js"; diff --git a/developer/src/server/src/index.ts b/developer/src/server/src/index.ts index 677c5a7cd5..85de2fcce6 100644 --- a/developer/src/server/src/index.ts +++ b/developer/src/server/src/index.ts @@ -5,12 +5,13 @@ import chalk from 'chalk'; import express from 'express'; import multer from 'multer'; import * as ws from 'ws'; -import { KeymanSentry, loadOptions } from '@keymanapp/developer-utils'; +import { KeymanSentry } from './KeymanSentry.js'; import { configuration } from './config.js'; import { environment } from './environment.js'; import setupRoutes from './routes.js'; import { shutdown } from './shutdown.js'; import { initTray } from './tray.js'; +import { loadOptions } from './options.js'; const options = { ngrokLog: false, // Set this to true if you need to see ngrok logs in the console diff --git a/developer/src/server/src/load-json-file.ts b/developer/src/server/src/load-json-file.ts index 28916cd83b..174a22a650 100644 --- a/developer/src/server/src/load-json-file.ts +++ b/developer/src/server/src/load-json-file.ts @@ -1,4 +1,4 @@ -import { KeymanSentry } from '@keymanapp/developer-utils'; +import { KeymanSentry } from './KeymanSentry.js'; import { readFileSync, existsSync } from 'fs'; export function loadJsonFile(filename: string): any { diff --git a/developer/src/server/src/options.ts b/developer/src/server/src/options.ts new file mode 100644 index 0000000000..45799ba5f2 --- /dev/null +++ b/developer/src/server/src/options.ts @@ -0,0 +1,96 @@ +// TODO: this is duplicated in kmc +import * as os from 'node:os'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +export interface KeymanDeveloperOptions { + "use tab char"?: boolean; + "link font sizes"?: boolean; + "indent size"?: number; + "use old debugger"?: boolean; + "editor theme"?: string; + "debugger break when exiting line"?: boolean; + "debugger single step after break"?: boolean; + "debugger show store offset"?: boolean; + "debugger recompile with debug info"?: boolean; + "debugger auto reset before compilng"?: boolean; + "auto save before compiling"?: boolean; + "osk auto save before importing"?: boolean; + "web host port"?: number; + "server keep alive"?: boolean; + "server use local addresses"?: boolean; + "server ngrok token"?: string; + "server ngrok region"?: string; + "server use ngrok"?: boolean; + "server show console window"?: boolean; + "char map disable database lookups"?: boolean; + "char map auto lookup"?: boolean; + "open keyboard files in source view"?: boolean; + "display theme"?: string; + "external editor path"?: string; + "smtp server"?: string; + "test email addresses"?: string; + "web ladder length"?: number; + "default project path"?: string; + "automatically report errors"?: boolean; + "automatically report usage"?: boolean; + "toolbar visible"?: boolean; + "active project"?: string; + "prompt to upgrade projects"?: boolean; +}; + +type KeymanDeveloperOption = keyof KeymanDeveloperOptions; + +// Default has no options set, and unit tests will use the defaults (won't call +// `loadOptions()`) +let options: KeymanDeveloperOptions = {}; + +// We only load the options from disk once on first use +let optionsLoaded = false; + +export async function loadOptions(): Promise { + if(optionsLoaded) { + return options; + } + + options = {}; + try { + const optionsFile = path.join(os.homedir(), '.keymandeveloper', 'options.json'); + if(fs.existsSync(optionsFile)) { + for(let i = 0; i < 5; i++) { + try { + const data = JSON.parse(fs.readFileSync(optionsFile, 'utf-8')); + if(typeof data == 'object') { + options = data; + } + break; + } catch(e) { + if(e?.code == 'EBUSY') { + await new Promise(resolve => setTimeout(resolve, 500)); + } else { + // give up, nothing to report here + break; + } + } + } + } + } catch(e) { + // Nothing to report here, sadly -- because we cannot rely on Sentry at this + // low level. + options = {}; + } + optionsLoaded = true; + return options; +} + +export function getOption(valueName: T, defaultValue: KeymanDeveloperOptions[T]): KeymanDeveloperOptions[T] { + return options[valueName] ?? defaultValue; +} + +/** + * unit tests will clear options before running, for consistency + */ +export function clearOptions() { + options = {}; + optionsLoaded = true; +} \ No newline at end of file diff --git a/developer/src/server/src/shutdown.ts b/developer/src/server/src/shutdown.ts index d33b035bfc..13f0c15349 100644 --- a/developer/src/server/src/shutdown.ts +++ b/developer/src/server/src/shutdown.ts @@ -1,4 +1,4 @@ -import { KeymanSentry } from '@keymanapp/developer-utils'; +import { KeymanSentry } from './KeymanSentry.js'; export async function shutdown(code?: number): Promise { await KeymanSentry.close(); diff --git a/developer/src/server/src/site/lib/sentry/init.js.in b/developer/src/server/src/site/lib/sentry/init.js.in index 21ceba901b..dbaf0ea5c1 100644 --- a/developer/src/server/src/site/lib/sentry/init.js.in +++ b/developer/src/server/src/site/lib/sentry/init.js.in @@ -9,8 +9,8 @@ Sentry.init({ dsn: 'https://39b25a09410349a58fe12aaf721565af@o1005580.ingest.sentry.io/5983519', // Keyman Developer - environment: '$VERSION_ENVIRONMENT', - release: '$VERSION_GIT_TAG' + environment: '$KEYMAN_VERSION_ENVIRONMENT', + release: '$KEYMAN_VERSION_GIT_TAG' }); function keymanEnableDiagnostics() { diff --git a/developer/src/server/src/version-data.ts b/developer/src/server/src/version-data.ts index 79e930da2a..05f266c5c6 100644 --- a/developer/src/server/src/version-data.ts +++ b/developer/src/server/src/version-data.ts @@ -1,18 +1,18 @@ /* Version information from build-utils.sh: - # VERSION: Full current build version, e.g. "14.0.1" - # [VERSION_WIN: Full current build version for Windows, e.g. "14.0.1.0"] - # VERSION_RELEASE: Current release version, e.g. "14.0" - # VERSION_MAJOR: Major version, e.g. "14" - # VERSION_MINOR: Minor version, e.g. "0" - # VERSION_PATCH: Patch version, e.g. "1" - # TIER: Current tier, one of "alpha", "beta" or "stable" - # VERSION_TAG: Tier + Pull Request + Location of build [-alpha|-beta][-test[-1234]][-local] - # VERSION_WITH_TAG: e.g. "14.0.1-alpha-test-1234" or "14.0.5-beta-local" or "14.0.1-alpha-test" - # [KEYMAN_ROOT: fully resolved root path of Keyman repository] - # VERSION_ENVIRONMENT: One of: local, test, alpha, beta, stable - # VERSION_GIT_TAG: Git tag for the release, "release@$VERSION_WITH_TAG", e.g. "release@14.0.1-alpha-test-1234" + # KEYMAN_VERSION: Full current build version, e.g. "14.0.1" + # [KEYMAN_VERSION_WIN: Full current build version for Windows, e.g. "14.0.1.0"] + # KEYMAN_VERSION_RELEASE: Current release version, e.g. "14.0" + # KEYMAN_VERSION_MAJOR: Major version, e.g. "14" + # KEYMAN_VERSION_MINOR: Minor version, e.g. "0" + # KEYMAN_VERSION_PATCH: Patch version, e.g. "1" + # KEYMAN_TIER: Current tier, one of "alpha", "beta" or "stable" + # KEYMAN_VERSION_TAG: Tier + Pull Request + Location of build [-alpha|-beta][-test[-1234]][-local] + # KEYMAN_VERSION_WITH_TAG: e.g. "14.0.1-alpha-test-1234" or "14.0.5-beta-local" or "14.0.1-alpha-test" + # [KEYMAN_ROOT: fully resolved root path of Keyman repository] + # KEYMAN_VERSION_ENVIRONMENT: One of: local, test, alpha, beta, stable + # KEYMAN_VERSION_GIT_TAG: Git tag for the release, "release@$KEYMAN_VERSION_WITH_TAG", e.g. "release@14.0.1-alpha-test-1234" */ export interface Environment { diff --git a/developer/src/server/src/win32/trayicon/addon-src/package-lock.json b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json index 09ab72cd15..4f2af24f7c 100644 --- a/developer/src/server/src/win32/trayicon/addon-src/package-lock.json +++ b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json @@ -136,9 +136,10 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1191,9 +1192,9 @@ } }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "requires": { "balanced-match": "^1.0.0" } diff --git a/developer/src/test/auto/package-info/PackageInfoTest.pas b/developer/src/test/auto/package-info/PackageInfoTest.pas index ed02e0ed13..db96940819 100644 --- a/developer/src/test/auto/package-info/PackageInfoTest.pas +++ b/developer/src/test/auto/package-info/PackageInfoTest.pas @@ -278,9 +278,7 @@ begin Assert.AreEqual(k1.Keyboards.Count, k2.Keyboards.Count); for i := 0 to k1.Keyboards.Count - 1 do begin - Assert.AreEqual(k1.Keyboards[i].Name, k2.Keyboards[i].Name); Assert.AreEqual(k1.Keyboards[i].ID, k2.Keyboards[i].ID); - Assert.AreEqual(k1.Keyboards[i].Version, k2.Keyboards[i].Version); Assert.IsTrue( ( Assigned(k1.Keyboards[i].OSKFont) and diff --git a/developer/src/tike/child/UfrmPackageEditor.dfm b/developer/src/tike/child/UfrmPackageEditor.dfm index 38ead060eb..eed5cb5f91 100644 --- a/developer/src/tike/child/UfrmPackageEditor.dfm +++ b/developer/src/tike/child/UfrmPackageEditor.dfm @@ -184,7 +184,7 @@ inherited frmPackageEditor: TfrmPackageEditor Top = 0 Width = 965 Height = 622 - ActivePage = pageDetails + ActivePage = pageLexicalModels Align = alClient Images = modActionsMain.ilEditorPages MultiLine = True @@ -236,7 +236,7 @@ inherited frmPackageEditor: TfrmPackageEditor Width = 546 Height = 32 AutoSize = False - Caption = + Caption = 'A typical package will need keyboards, fonts, and documentation.' + ' You shouldn'#39't typically add source files. Also, don'#39't add any s' + 'tandard Keyman files (such as keyman.exe) here.' @@ -261,7 +261,7 @@ inherited frmPackageEditor: TfrmPackageEditor object lblFileType: TLabel Left = 256 Top = 107 - Width = 47 + Width = 46 Height = 13 Caption = 'File Type:' FocusControl = editFilePath @@ -390,30 +390,11 @@ inherited frmPackageEditor: TfrmPackageEditor end object lblKeyboardFiles: TLabel Left = 15 - Top = 516 + Top = 492 Width = 26 Height = 13 Anchors = [akLeft, akBottom] Caption = 'Files:' - ExplicitTop = 341 - end - object lblKeyboardDescription: TLabel - Left = 15 - Top = 489 - Width = 62 - Height = 13 - Anchors = [akLeft, akBottom] - Caption = 'Description:' - ExplicitTop = 314 - end - object lblKeyboardVersion: TLabel - Left = 15 - Top = 568 - Width = 41 - Height = 13 - Anchors = [akLeft, akBottom] - Caption = 'Version:' - ExplicitTop = 393 end object lblKeyboardOSKFont: TLabel Left = 260 @@ -437,15 +418,6 @@ inherited frmPackageEditor: TfrmPackageEditor Caption = 'Languages' FocusControl = gridKeyboardLanguages end - object lblKeyboardRTL: TLabel - Left = 15 - Top = 595 - Width = 75 - Height = 13 - Anchors = [akLeft, akBottom] - Caption = 'Is right-to-left:' - ExplicitTop = 420 - end object lblKeyboardExamples: TLabel Left = 260 Top = 363 @@ -478,46 +450,24 @@ inherited frmPackageEditor: TfrmPackageEditor TabOrder = 0 OnClick = lbKeyboardsClick end - object editKeyboardDescription: TEdit + object memoKeyboardFiles: TMemo Left = 96 - Top = 486 + Top = 489 Width = 148 - Height = 21 + Height = 70 TabStop = False Anchors = [akLeft, akBottom] ParentColor = True ReadOnly = True TabOrder = 1 end - object memoKeyboardFiles: TMemo - Left = 96 - Top = 513 - Width = 148 - Height = 46 - TabStop = False - Anchors = [akLeft, akBottom] - ParentColor = True - ReadOnly = True - TabOrder = 2 - end - object editKeyboardVersion: TEdit - Left = 96 - Top = 565 - Width = 148 - Height = 21 - TabStop = False - Anchors = [akLeft, akBottom] - ParentColor = True - ReadOnly = True - TabOrder = 3 - end object cbKeyboardOSKFont: TComboBox Left = 339 Top = 72 Width = 174 Height = 21 Style = csDropDownList - TabOrder = 5 + TabOrder = 2 OnClick = cbKeyboardOSKFontClick end object cbKeyboardDisplayFont: TComboBox @@ -526,7 +476,7 @@ inherited frmPackageEditor: TfrmPackageEditor Width = 174 Height = 21 Style = csDropDownList - TabOrder = 6 + TabOrder = 3 OnClick = cbKeyboardDisplayFontClick end object gridKeyboardLanguages: TStringGrid @@ -540,7 +490,7 @@ inherited frmPackageEditor: TfrmPackageEditor FixedCols = 0 RowCount = 9 Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goColSizing, goRowSelect] - TabOrder = 7 + TabOrder = 4 OnClick = gridKeyboardLanguagesClick OnDblClick = gridKeyboardLanguagesDblClick ColWidths = ( @@ -553,7 +503,7 @@ inherited frmPackageEditor: TfrmPackageEditor Width = 73 Height = 25 Caption = '&Add...' - TabOrder = 8 + TabOrder = 5 OnClick = cmdKeyboardAddLanguageClick end object cmdKeyboardRemoveLanguage: TButton @@ -562,27 +512,16 @@ inherited frmPackageEditor: TfrmPackageEditor Width = 72 Height = 25 Caption = '&Remove' - TabOrder = 10 + TabOrder = 7 OnClick = cmdKeyboardRemoveLanguageClick end - object editKeyboardRTL: TEdit - Left = 96 - Top = 592 - Width = 148 - Height = 21 - TabStop = False - Anchors = [akLeft, akBottom] - ParentColor = True - ReadOnly = True - TabOrder = 4 - end object cmdKeyboardEditLanguage: TButton Left = 339 Top = 327 Width = 73 Height = 25 Caption = 'Ed&it...' - TabOrder = 9 + TabOrder = 6 OnClick = cmdKeyboardEditLanguageClick end object gridKeyboardExamples: TStringGrid @@ -596,7 +535,7 @@ inherited frmPackageEditor: TfrmPackageEditor FixedCols = 0 RowCount = 9 Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goColSizing, goRowSelect] - TabOrder = 11 + TabOrder = 8 OnClick = gridKeyboardExamplesClick OnDblClick = gridKeyboardExamplesDblClick ColWidths = ( @@ -612,7 +551,7 @@ inherited frmPackageEditor: TfrmPackageEditor Height = 25 Anchors = [akLeft, akBottom] Caption = 'Add...' - TabOrder = 12 + TabOrder = 9 OnClick = cmdKeyboardAddExampleClick end object cmdKeyboardEditExample: TButton @@ -622,7 +561,7 @@ inherited frmPackageEditor: TfrmPackageEditor Height = 25 Anchors = [akLeft, akBottom] Caption = 'Ed&it...' - TabOrder = 13 + TabOrder = 10 OnClick = cmdKeyboardEditExampleClick end object cmdKeyboardRemoveExample: TButton @@ -632,7 +571,7 @@ inherited frmPackageEditor: TfrmPackageEditor Height = 25 Anchors = [akLeft, akBottom] Caption = '&Remove' - TabOrder = 14 + TabOrder = 11 OnClick = cmdKeyboardRemoveExampleClick end object cmdKeyboardWebOSKFonts: TButton @@ -641,7 +580,7 @@ inherited frmPackageEditor: TfrmPackageEditor Width = 75 Height = 25 Caption = 'Web fonts...' - TabOrder = 15 + TabOrder = 12 OnClick = cmdKeyboardWebOSKFontsClick end object cmdKeyboardWebDisplayFonts: TButton @@ -650,7 +589,7 @@ inherited frmPackageEditor: TfrmPackageEditor Width = 75 Height = 25 Caption = 'Web fonts...' - TabOrder = 16 + TabOrder = 13 OnClick = cmdKeyboardWebDisplayFontsClick end end @@ -700,16 +639,9 @@ inherited frmPackageEditor: TfrmPackageEditor Height = 13 Caption = 'Filename:' end - object lblLexicalModelDescription: TLabel - Left = 261 - Top = 102 - Width = 62 - Height = 13 - Caption = 'Description:' - end object lblLexicalModelLanguages: TLabel - Left = 260 - Top = 181 + Left = 261 + Top = 101 Width = 59 Height = 13 Caption = 'Languages:' @@ -724,27 +656,18 @@ inherited frmPackageEditor: TfrmPackageEditor TabOrder = 0 OnClick = lbLexicalModelsClick end - object editLexicalModelDescription: TEdit - Left = 342 - Top = 99 - Width = 275 - Height = 21 - TabStop = False - TabOrder = 1 - OnChange = editLexicalModelDescriptionChange - end object gridLexicalModelLanguages: TStringGrid Left = 260 - Top = 200 + Top = 120 Width = 593 - Height = 376 + Height = 456 Anchors = [akLeft, akTop, akRight, akBottom] ColCount = 2 DefaultRowHeight = 16 FixedCols = 0 RowCount = 9 Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goColSizing, goRowSelect] - TabOrder = 2 + TabOrder = 1 OnClick = gridLexicalModelLanguagesClick OnDblClick = gridLexicalModelLanguagesDblClick ColWidths = ( @@ -758,7 +681,7 @@ inherited frmPackageEditor: TfrmPackageEditor Height = 25 Anchors = [akLeft, akBottom] Caption = '&Add...' - TabOrder = 3 + TabOrder = 2 OnClick = cmdLexicalModelLanguageAddClick end object cmdLexicalModelLanguageRemove: TButton @@ -768,7 +691,7 @@ inherited frmPackageEditor: TfrmPackageEditor Height = 25 Anchors = [akLeft, akBottom] Caption = '&Remove' - TabOrder = 4 + TabOrder = 3 OnClick = cmdLexicalModelLanguageRemoveClick end object cmdLexicalModelLanguageEdit: TButton @@ -778,18 +701,9 @@ inherited frmPackageEditor: TfrmPackageEditor Height = 25 Anchors = [akLeft, akBottom] Caption = 'Ed&it...' - TabOrder = 5 + TabOrder = 4 OnClick = cmdLexicalModelLanguageEditClick end - object chkLexicalModelRTL: TCheckBox - Left = 342 - Top = 126 - Width = 97 - Height = 17 - Caption = 'Is Right-to-left' - TabOrder = 6 - OnClick = chkLexicalModelRTLClick - end object editLexicalModelFilename: TEdit Left = 342 Top = 72 @@ -798,7 +712,7 @@ inherited frmPackageEditor: TfrmPackageEditor TabStop = False ParentColor = True ReadOnly = True - TabOrder = 7 + TabOrder = 5 end end end @@ -944,7 +858,7 @@ inherited frmPackageEditor: TfrmPackageEditor object lblDescriptionMarkdown: TLabel Left = 114 Top = 497 - Width = 215 + Width = 214 Height = 13 Caption = 'Markdown accepted, no embedded HTML' Transparent = True @@ -1342,7 +1256,7 @@ inherited frmPackageEditor: TfrmPackageEditor Top = 48 Width = 551 Height = 13 - Caption = + Caption = 'Compiling the package takes all the files you have selected and ' + 'compresses them into a single package file.' end @@ -1575,7 +1489,7 @@ inherited frmPackageEditor: TfrmPackageEditor object Label5: TLabel Left = 9 Top = 73 - Width = 82 + Width = 81 Height = 13 Caption = 'Target filename:' end diff --git a/developer/src/tike/child/UfrmPackageEditor.pas b/developer/src/tike/child/UfrmPackageEditor.pas index 3044f5aa82..f3dcc71f09 100644 --- a/developer/src/tike/child/UfrmPackageEditor.pas +++ b/developer/src/tike/child/UfrmPackageEditor.pas @@ -138,12 +138,8 @@ type Label2: TLabel; Label3: TLabel; lblKeyboardFiles: TLabel; - lblKeyboardDescription: TLabel; lbKeyboards: TListBox; - editKeyboardDescription: TEdit; memoKeyboardFiles: TMemo; - lblKeyboardVersion: TLabel; - editKeyboardVersion: TEdit; lblKeyboardOSKFont: TLabel; cbKeyboardOSKFont: TComboBox; cbKeyboardDisplayFont: TComboBox; @@ -153,8 +149,6 @@ type cmdKeyboardAddLanguage: TButton; cmdKeyboardRemoveLanguage: TButton; chkFollowKeyboardVersion: TCheckBox; - lblKeyboardRTL: TLabel; - editKeyboardRTL: TEdit; cmdKeyboardEditLanguage: TButton; panBuildMobile: TPanel; lblDebugHostCaption: TLabel; @@ -172,15 +166,12 @@ type lblLexlicalModels: TLabel; lblLexicalModelsSubtitle: TLabel; lblLexicalModelFilename: TLabel; - lblLexicalModelDescription: TLabel; lblLexicalModelLanguages: TLabel; lbLexicalModels: TListBox; - editLexicalModelDescription: TEdit; gridLexicalModelLanguages: TStringGrid; cmdLexicalModelLanguageAdd: TButton; cmdLexicalModelLanguageRemove: TButton; cmdLexicalModelLanguageEdit: TButton; - chkLexicalModelRTL: TCheckBox; editLexicalModelFilename: TEdit; imgQRCode: TImage; panOpenInExplorer: TPanel; @@ -1628,10 +1619,7 @@ begin k := SelectedKeyboard; if not Assigned(k) then begin - editKeyboardDescription.Text := ''; - editKeyboardVersion.Text := ''; memoKeyboardFiles.Text := ''; - editKeyboardRTL.Text := ''; cbKeyboardOSKFont.ItemIndex := -1; cbKeyboardDisplayFont.ItemIndex := -1; gridKeyboardLanguages.RowCount := 1; @@ -1645,12 +1633,6 @@ begin // Details memoKeyboardFiles.Text := ''; - editKeyboardDescription.Text := k.Name; - editKeyboardVersion.Text := k.Version; - - if k.RTL - then editKeyboardRTL.Text := 'True' - else editKeyboardRTL.Text := 'False (or not .js format)'; for i := 0 to pack.Files.Count - 1 do if SameText(TKeyboardUtils.KeyboardFileNameToID(pack.Files[i].FileName), k.ID) then @@ -1779,12 +1761,8 @@ var e: Boolean; begin e := lbKeyboards.ItemIndex >= 0; - lblKeyboardDescription.Enabled := e; - editKeyboardDescription.Enabled := e; lblKeyboardFiles.Enabled := e; memoKeyboardFiles.Enabled := e; - lblKeyboardVersion.Enabled := e; - editKeyboardVersion.Enabled := e; lblKeyboardOSKFont.Enabled := e; cbKeyboardOSKFont.Enabled := e; lblKeyboardDisplayFont.Enabled := e; @@ -2330,9 +2308,7 @@ begin lm := SelectedLexicalModel; if not Assigned(lm) then begin - editLexicalModelDescription.Text := ''; editLexicalModelFilename.Text := ''; - chkLexicalModelRTL.Checked := False; gridLexicalModelLanguages.RowCount := 1; EnableLexicalModelTabControls; Exit; @@ -2340,9 +2316,6 @@ begin // Details - editLexicalModelDescription.Text := lm.Name; - chkLexicalModelRTL.Checked := lm.RTL; - for i := 0 to pack.Files.Count - 1 do if TLexicalModelUtils.LexicalModelFileNameToID(pack.Files[i].FileName) = lm.ID then begin @@ -2364,13 +2337,10 @@ var e: Boolean; begin e := lbLexicalModels.ItemIndex >= 0; - lblLexicalModelDescription.Enabled := e; - editLexicalModelDescription.Enabled := e; lblLexicalModelFilename.Enabled := e; editLexicalModelFilename.Enabled := e; lblLexicalModelLanguages.Enabled := e; cmdLexicalModelLanguageAdd.Enabled := e; - chkLexicalModelRTL.Enabled := e; e := e and (gridLexicalModelLanguages.Row > 0); gridLexicalModelLanguages.Enabled := e; @@ -2436,7 +2406,6 @@ begin lm := SelectedLexicalModel; Assert(Assigned(lm)); - lm.Name := editLexicalModelDescription.Text; Modified := True; end; @@ -2448,7 +2417,6 @@ begin Exit; lm := SelectedLexicalModel; Assert(Assigned(lm)); - lm.RTL := chkLexicalModelRTL.Checked; Modified := True; end; diff --git a/developer/src/tike/xml/app/lib/sentry/init.js.in b/developer/src/tike/xml/app/lib/sentry/init.js.in index 21ceba901b..dbaf0ea5c1 100644 --- a/developer/src/tike/xml/app/lib/sentry/init.js.in +++ b/developer/src/tike/xml/app/lib/sentry/init.js.in @@ -9,8 +9,8 @@ Sentry.init({ dsn: 'https://39b25a09410349a58fe12aaf721565af@o1005580.ingest.sentry.io/5983519', // Keyman Developer - environment: '$VERSION_ENVIRONMENT', - release: '$VERSION_GIT_TAG' + environment: '$KEYMAN_VERSION_ENVIRONMENT', + release: '$KEYMAN_VERSION_GIT_TAG' }); function keymanEnableDiagnostics() { diff --git a/developer/src/tike/xml/app/package.json b/developer/src/tike/xml/app/package.json index d0696c3986..ecb8d845a3 100644 --- a/developer/src/tike/xml/app/package.json +++ b/developer/src/tike/xml/app/package.json @@ -6,6 +6,7 @@ "url": "https://github.com/keymanapp/keyman/issues" }, "homepage": "https://keyman.com/", + "scripts": {}, "dependencies": { "monaco-editor": "0.15.6" } diff --git a/developer/src/tools/sentry-upload-difs.sh b/developer/src/tools/sentry-upload-difs.sh index 24a014bc3d..f06cbba6e0 100755 --- a/developer/src/tools/sentry-upload-difs.sh +++ b/developer/src/tools/sentry-upload-difs.sh @@ -80,7 +80,7 @@ upload_sourcemap() { --no-dedupe \ --org keyman \ --project keyman-developer \ - --release "$VERSION_GIT_TAG" \ + --release "$KEYMAN_VERSION_GIT_TAG" \ --ext js --ext mjs --ext ts --ext map \ "$smpath" } diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 3316073d52..b135674088 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -1,32 +1,16 @@ # -# Code owners - this lists first the primary dev, then second, the platform advocate for each area -# - in addition, it lists additional reviewers +# Code owners - this lists the primary dev for each area # -# @darcywong00 @mcdurdin @ermshiperete @rc-swag @SabineSIL @sgschantz - -/android/ @darcywong00 @sgschantz - -/common/ @mcdurdin @rc-swag @srl295 -/core/ @mcdurdin @rc-swag @srl295 -/common/lexical-model-types/ @jahorton @mcdurdin -/common/schemas/ @mcdurdin @jahorton -/common/test/ @mcdurdin @ermshiperete -/common/web/ @jahorton @mcdurdin - -/developer/ @mcdurdin @darcywong00 @SabineSIL @markcsinclair @srl295 -/docs/ @mcdurdin @jahorton -/ios/ @sgschantz @jahorton -/linux/ @ermshiperete @darcywong00 -/mac/ @sgschantz @SabineSIL - -/oem/firstvoices/android/ @darcywong00 @sgschantz -/oem/firstvoices/common/ @mcdurdin @rc-swag -/oem/firstvoices/ios/ @sgschantz @jahorton -/oem/firstvoices/windows/ @rc-swag @ermshiperete -/resources/ @mcdurdin @jahorton @srl295 - -# Web is currently shared between Eberhard and Joshua: -/web/ @ermshiperete @jahorton -/web/src/engine/predictive-text/ @jahorton @mcdurdin - -/windows/ @rc-swag @ermshiperete +## Note: Per 2025-April CODEOWNERS is not used, please add reviews manually +## — contact team owners if you're unsure. +# +# +#/android/ @darcywong00 +#/common/ @mcdurdin +#/core/ @mcdurdin +#/developer/ @mcdurdin +#/ios/ +#/linux/ @ermshiperete +#/mac/ @sgschantz +#/web/ @ermshiperete +#/windows/ @rc-swag diff --git a/docs/build/linux-ubuntu.md b/docs/build/linux-ubuntu.md index 98e15185bf..82537b1b79 100644 --- a/docs/build/linux-ubuntu.md +++ b/docs/build/linux-ubuntu.md @@ -6,6 +6,7 @@ On Linux, you can build the following projects: - [Keyman Core](#keyman-core) (aka core) - [Keyman for Linux](#keyman-for-linux) +- [Keyman Developer (node modules only)](#keyman-developer-node-modules-only) - [Keyman Web](#keyman-web) - [Keyman for Android](#keyman-for-android) - - + Maximum="$(var.KEYMAN_VERSION)" IncludeMaximum="no" MigrateFeatures="yes" Property="VERSION11FOUND" /> + + @@ -88,7 +88,7 @@ - + diff --git a/package-lock.json b/package-lock.json index e1f8d2285b..0b952d7fa5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-n": "^15.7.0", "eslint-plugin-promise": "^6.1.1", - "mocha": "^10.0.0", + "mocha": "^11.2.2", "mocha-teamcity-reporter": "^4.0.0", "playwright": "^1.46.1", "sinon": "^17.0.1", @@ -110,7 +110,6 @@ }, "devDependencies": { "@keymanapp/resources-gosh": "*", - "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", "typescript": "^5.4.5" } @@ -161,7 +160,6 @@ "c8": "^7.12.0", "chalk": "^2.4.2", "hexy": "^0.3.4", - "mocha": "^8.4.0", "typescript": "^5.4.5" } }, @@ -207,101 +205,6 @@ "dev": true, "license": "MIT" }, - "common/web/types/node_modules/js-yaml": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "common/web/types/node_modules/minimatch": { - "version": "3.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "common/web/types/node_modules/mocha": { - "version": "8.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "common/web/types/node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "common/web/types/node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "common/web/types/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, "common/web/types/node_modules/supports-color": { "version": "5.5.0", "dev": true, @@ -341,9 +244,8 @@ "license": "MIT", "dependencies": { "@keymanapp/common-types": "*", - "@sentry/node": "^7.57.0", "eventemitter3": "^5.0.0", - "fast-xml-parser": "^5.0.9", + "fast-xml-parser": "^5.2.2", "path-browserify": "^1.0.1", "restructure": "^3.0.1", "sax": ">=0.6.0", @@ -357,111 +259,9 @@ "@types/semver": "^7.3.12", "c8": "^7.12.0", "git-diff": "^2.0.6", - "mocha": "^8.4.0", "typescript": "^5.4.5" } }, - "developer/src/common/web/utils/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "developer/src/common/web/utils/node_modules/js-yaml": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", - "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "developer/src/common/web/utils/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "developer/src/common/web/utils/node_modules/mocha": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", - "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "developer/src/common/web/utils/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "developer/src/common/web/utils/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "developer/src/kmc": { "name": "@keymanapp/kmc", "license": "MIT", @@ -495,7 +295,6 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "esbuild": "^0.25.0", - "mocha": "^8.4.0", "typescript": "^5.4.5" } }, @@ -513,7 +312,6 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "typescript": "^5.4.5" } }, @@ -564,107 +362,6 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "developer/src/kmc-analyze/node_modules/js-yaml": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", - "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "developer/src/kmc-analyze/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "developer/src/kmc-analyze/node_modules/mocha": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", - "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "developer/src/kmc-analyze/node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "developer/src/kmc-analyze/node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "developer/src/kmc-analyze/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "developer/src/kmc-analyze/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -700,7 +397,6 @@ "@types/semver": "^7.3.12", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "typescript": "^5.4.5" } }, @@ -766,123 +462,6 @@ "node": ">=4" } }, - "developer/src/kmc-convert/node_modules/js-yaml": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", - "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "developer/src/kmc-convert/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "developer/src/kmc-convert/node_modules/mocha": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", - "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "developer/src/kmc-convert/node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "developer/src/kmc-convert/node_modules/mocha/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "developer/src/kmc-convert/node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "developer/src/kmc-convert/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "developer/src/kmc-convert/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -907,22 +486,13 @@ "devDependencies": { "@keymanapp/developer-test-helpers": "*", "@keymanapp/resources-gosh": "*", - "@types/mocha": "^10.0.0", "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^10.0.0", "typescript": "^5.4.5" } }, - "developer/src/kmc-copy/node_modules/@types/mocha": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.8.tgz", - "integrity": "sha512-HfMcUmy9hTMJh66VNcmeC9iVErIZJli2bszuXc6julh5YGuRb/W5OnkHjwLNYdFlMis0sY3If5SEAp+PktdJjw==", - "dev": true, - "license": "MIT" - }, "developer/src/kmc-copy/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -1003,21 +573,13 @@ "devDependencies": { "@keymanapp/developer-test-helpers": "*", "@keymanapp/resources-gosh": "*", - "@types/mocha": "^10.0.0", "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^10.0.0", "typescript": "^5.4.5" } }, - "developer/src/kmc-generate/node_modules/@types/mocha": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz", - "integrity": "sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==", - "dev": true - }, "developer/src/kmc-generate/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -1121,7 +683,6 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "typescript": "^5.4.5" } }, @@ -1172,107 +733,6 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "developer/src/kmc-keyboard-info/node_modules/js-yaml": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", - "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "developer/src/kmc-keyboard-info/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "developer/src/kmc-keyboard-info/node_modules/mocha": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", - "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "developer/src/kmc-keyboard-info/node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "developer/src/kmc-keyboard-info/node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "developer/src/kmc-keyboard-info/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "developer/src/kmc-keyboard-info/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1311,7 +771,6 @@ "@types/sinon-chai": "^3.2.9", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "sinon-chai": "^3.7.0", "typescript": "^5.4.5" } @@ -1363,107 +822,6 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "developer/src/kmc-kmn/node_modules/js-yaml": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", - "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "developer/src/kmc-kmn/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "developer/src/kmc-kmn/node_modules/mocha": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", - "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "developer/src/kmc-kmn/node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "developer/src/kmc-kmn/node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "developer/src/kmc-kmn/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "developer/src/kmc-kmn/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1506,7 +864,6 @@ "c8": "^7.12.0", "chalk": "^2.4.2", "common-tags": "^1.8.2", - "mocha": "^8.4.0", "peggy": "^4.2.0", "typescript": "^5.4.5" } @@ -1558,107 +915,6 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "developer/src/kmc-ldml/node_modules/js-yaml": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", - "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "developer/src/kmc-ldml/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "developer/src/kmc-ldml/node_modules/mocha": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", - "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "developer/src/kmc-ldml/node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "developer/src/kmc-ldml/node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "developer/src/kmc-ldml/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "developer/src/kmc-ldml/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1695,8 +951,7 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "esbuild": "^0.25.0", - "mocha": "^10.0.0" + "esbuild": "^0.25.0" } }, "developer/src/kmc-model-info": { @@ -1711,7 +966,6 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "typescript": "^5.4.5" } }, @@ -1757,101 +1011,6 @@ "dev": true, "license": "MIT" }, - "developer/src/kmc-model-info/node_modules/js-yaml": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "developer/src/kmc-model-info/node_modules/minimatch": { - "version": "3.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "developer/src/kmc-model-info/node_modules/mocha": { - "version": "8.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "developer/src/kmc-model-info/node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "developer/src/kmc-model-info/node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "developer/src/kmc-model-info/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, "developer/src/kmc-model-info/node_modules/supports-color": { "version": "5.5.0", "dev": true, @@ -1947,7 +1106,6 @@ "@types/node": "^20.4.1", "c8": "^7.12.0", "chalk": "^2.4.2", - "mocha": "^8.4.0", "typescript": "^5.4.5" } }, @@ -1993,101 +1151,6 @@ "dev": true, "license": "MIT" }, - "developer/src/kmc-package/node_modules/js-yaml": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "developer/src/kmc-package/node_modules/minimatch": { - "version": "3.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "developer/src/kmc-package/node_modules/mocha": { - "version": "8.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "developer/src/kmc-package/node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "developer/src/kmc-package/node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "developer/src/kmc-package/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, "developer/src/kmc-package/node_modules/supports-color": { "version": "5.5.0", "dev": true, @@ -2165,101 +1228,6 @@ "version": "1.1.3", "license": "MIT" }, - "developer/src/kmc/node_modules/js-yaml": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "developer/src/kmc/node_modules/minimatch": { - "version": "3.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "developer/src/kmc/node_modules/mocha": { - "version": "8.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "developer/src/kmc/node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "developer/src/kmc/node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "developer/src/kmc/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, "developer/src/kmc/node_modules/supports-color": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", @@ -2279,7 +1247,7 @@ "@sentry/node": "^7.57.0", "chalk": "^4.1.2", "express": "^4.20.0", - "multer": "^1.4.5-lts.1", + "multer": "^2.0.1", "ngrok": "^5.0.0-beta.2", "open": "^8.4.0", "restructure": "^3.0.1", @@ -2293,11 +1261,10 @@ "@keymanapp/resources-gosh": "*", "@types/express": "^4.17.13", "@types/mocha": "^9.1.0", - "@types/multer": "^1.4.7", + "@types/multer": "^1.4.12", "@types/node": "^20.4.1", "@types/ws": "^8.2.2", "copyfiles": "^2.4.1", - "mocha": "^10.0.0", "node-gyp": "^10.2.0", "tsc-watch": "^4.5.0", "typescript": "^5.4.5" @@ -2311,35 +1278,14 @@ "dev": true, "license": "MIT" }, - "developer/src/server/node_modules/busboy": { - "version": "1.6.0", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "developer/src/server/node_modules/multer": { - "version": "1.4.5-lts.1", + "developer/src/server/node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dev": true, "license": "MIT", "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "developer/src/server/node_modules/streamsearch": { - "version": "1.1.0", - "engines": { - "node": ">=10.0.0" + "@types/express": "*" } }, "node_modules/@75lb/deep-merge": { @@ -2373,26 +1319,28 @@ } }, "node_modules/@actions/github": { - "version": "2.2.0", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", + "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", "license": "MIT", "dependencies": { - "@actions/http-client": "^1.0.3", - "@octokit/graphql": "^4.3.1", - "@octokit/rest": "^16.43.1" - } - }, - "node_modules/@actions/github/node_modules/@actions/http-client": { - "version": "1.0.11", - "license": "MIT", - "dependencies": { - "tunnel": "0.0.6" + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.2.2", + "@octokit/plugin-rest-endpoint-methods": "^10.4.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "undici": "^5.28.5" } }, "node_modules/@actions/http-client": { - "version": "2.0.1", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", "license": "MIT", "dependencies": { - "tunnel": "^0.0.6" + "tunnel": "^0.0.6", + "undici": "^5.25.4" } }, "node_modules/@babel/code-frame": { @@ -2512,280 +1460,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", @@ -2803,119 +1477,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3011,6 +1572,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3742,155 +2312,161 @@ } }, "node_modules/@octokit/auth-token": { - "version": "2.5.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", "license": "MIT", - "dependencies": { - "@octokit/types": "^6.0.3" + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/core": { - "version": "3.6.0", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", + "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "license": "MIT", - "peer": true, "dependencies": { - "@octokit/auth-token": "^2.4.4", - "@octokit/graphql": "^4.5.8", - "@octokit/request": "^5.6.3", - "@octokit/request-error": "^2.0.5", - "@octokit/types": "^6.0.3", + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/endpoint": { - "version": "6.0.12", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", "license": "MIT", "dependencies": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", + "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/graphql": { - "version": "4.8.0", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", "license": "MIT", "dependencies": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/openapi-types": { - "version": "11.2.0", + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "1.1.2", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", "license": "MIT", "dependencies": { - "@octokit/types": "^2.0.1" + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" } }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "2.16.2", + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "license": "MIT", "dependencies": { - "@types/node": ">= 8" - } - }, - "node_modules/@octokit/plugin-request-log": { - "version": "1.0.4", - "license": "MIT", - "peerDependencies": { - "@octokit/core": ">=3" + "@octokit/openapi-types": "^20.0.0" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "2.4.0", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", "license": "MIT", "dependencies": { - "@octokit/types": "^2.0.1", - "deprecation": "^2.3.1" + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" } }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "2.16.2", + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "license": "MIT", "dependencies": { - "@types/node": ">= 8" + "@octokit/openapi-types": "^20.0.0" } }, "node_modules/@octokit/request": { - "version": "5.6.3", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", "license": "MIT", "dependencies": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.16.1", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/request-error": { - "version": "2.1.0", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", "license": "MIT", "dependencies": { - "@octokit/types": "^6.0.3", + "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" - } - }, - "node_modules/@octokit/rest": { - "version": "16.43.2", - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^2.4.0", - "@octokit/plugin-paginate-rest": "^1.1.1", - "@octokit/plugin-request-log": "^1.0.0", - "@octokit/plugin-rest-endpoint-methods": "2.4.0", - "@octokit/request": "^5.2.0", - "@octokit/request-error": "^1.0.2", - "atob-lite": "^2.0.0", - "before-after-hook": "^2.0.0", - "btoa-lite": "^1.0.0", - "deprecation": "^2.0.0", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "lodash.uniq": "^4.5.0", - "octokit-pagination-methods": "^1.1.0", - "once": "^1.4.0", - "universal-user-agent": "^4.0.0" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/request-error": { - "version": "1.2.1", - "license": "MIT", - "dependencies": { - "@octokit/types": "^2.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/types": { - "version": "2.16.2", - "license": "MIT", - "dependencies": { - "@types/node": ">= 8" - } - }, - "node_modules/@octokit/rest/node_modules/universal-user-agent": { - "version": "4.0.1", - "license": "ISC", - "dependencies": { - "os-name": "^3.1.0" + }, + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/types": { - "version": "6.34.0", + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^11.2.0" + "@octokit/openapi-types": "^24.2.0" } }, "node_modules/@peggyjs/from-mem": { @@ -3928,19 +2504,20 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.2.tgz", - "integrity": "sha512-hZ/JhxPIceWaGSEzUZp83/8M49CoxlkuThfTR7t4AoCu5+ZvJ3vktLm60Otww2TXeROB5igiZ8D9oPQh6ckBVg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "progress": "2.0.3", - "proxy-agent": "6.4.0", - "semver": "7.6.0", - "tar-fs": "3.0.5", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.2" + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" @@ -3954,6 +2531,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3963,6 +2541,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -3973,12 +2552,13 @@ } }, "node_modules/@puppeteer/browsers/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3994,18 +2574,24 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "MIT" + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4018,6 +2604,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4032,6 +2619,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4044,6 +2632,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -4062,6 +2651,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -4416,6 +3006,100 @@ "node": ">=0.6.19" } }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.55.0.tgz", + "integrity": "sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/browser-utils/node_modules/@sentry/core": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz", + "integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.55.0.tgz", + "integrity": "sha512-cP3BD/Q6pquVQ+YL+rwCnorKuTXiS9KXW8HNKu4nmmBAyf7urjs+F6Hr1k9MXP5yQ8W3yK7jRWd09Yu6DHWOiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback/node_modules/@sentry/core": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz", + "integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.55.0.tgz", + "integrity": "sha512-roCDEGkORwolxBn8xAKedybY+Jlefq3xYmgN2fr3BTnsXjSYOPC7D1/mYqINBat99nDtvgFvNfRcZPiwwZ1hSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.0", + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.55.0.tgz", + "integrity": "sha512-nIkfgRWk1091zHdu4NbocQsxZF1rv1f7bbp3tTIlZYbrH62XVZosx5iHAuZG0Zc48AETLE7K4AX9VGjvQj8i9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "8.55.0", + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/core": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz", + "integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay/node_modules/@sentry/core": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz", + "integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, "node_modules/@sentry-internal/tracing": { "version": "7.108.0", "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.108.0.tgz", @@ -4460,26 +3144,6 @@ "node": ">=8" } }, - "node_modules/@sentry/browser": { - "version": "5.30.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/core": "5.30.0", - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/browser/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/@sentry/cli": { "version": "2.31.0", "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.31.0.tgz", @@ -4624,65 +3288,6 @@ "node": ">=10" } }, - "node_modules/@sentry/core": { - "version": "5.30.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/hub": "5.30.0", - "@sentry/minimal": "5.30.0", - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/core/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/@sentry/hub": { - "version": "5.30.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/hub/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/@sentry/minimal": { - "version": "5.30.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/hub": "5.30.0", - "@sentry/types": "5.30.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/minimal/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/@sentry/node": { "version": "7.108.0", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.108.0.tgz", @@ -4728,32 +3333,6 @@ "node": ">=8" } }, - "node_modules/@sentry/types": { - "version": "5.30.0", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/utils": { - "version": "5.30.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/types": "5.30.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/utils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "license": "MIT", @@ -4785,7 +3364,8 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@tsconfig/node10": { "version": "1.0.8", @@ -5051,14 +3631,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/multer": { - "version": "1.4.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/node": { "version": "20.12.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", @@ -5445,11 +4017,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "dev": true, - "license": "ISC" - }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -5478,10 +4045,11 @@ } }, "node_modules/@web/dev-server": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@web/dev-server/-/dev-server-0.4.5.tgz", - "integrity": "sha512-R11sODOLFcm51f2uir51KE4QXRSYakDaeBeJdrUutPCmYUDEk86GjYBR3R1wslimnwGPIjhFDsXNMfASxYfgAQ==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@web/dev-server/-/dev-server-0.4.6.tgz", + "integrity": "sha512-jj/1bcElAy5EZet8m2CcUdzxT+CRvUjIXGh8Lt7vxtthkN9PzY9wlhWx/9WOs5iwlnG1oj0VGo6f/zvbPO0s9w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.11", "@types/command-line-args": "^5.0.0", @@ -5493,7 +4061,7 @@ "command-line-usage": "^7.0.1", "debounce": "^1.2.0", "deepmerge": "^4.2.2", - "ip": "^2.0.1", + "internal-ip": "^6.2.0", "nanocolors": "^0.2.1", "open": "^8.0.2", "portfinder": "^1.0.32" @@ -5507,15 +4075,16 @@ } }, "node_modules/@web/dev-server-core": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.2.tgz", - "integrity": "sha512-Q/0jpF13Ipk+qGGQ+Yx/FW1TQBYazpkfgYHHo96HBE7qv4V4KKHqHglZcSUxti/zd4bToxX1cFTz8dmbTlb8JA==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.5.tgz", + "integrity": "sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==", "dev": true, + "license": "MIT", "dependencies": { "@types/koa": "^2.11.6", "@types/ws": "^7.4.0", "@web/parse5-utils": "^2.1.0", - "chokidar": "^3.4.3", + "chokidar": "^4.0.1", "clone": "^2.1.2", "es-module-lexer": "^1.0.0", "get-stream": "^6.0.0", @@ -5529,7 +4098,7 @@ "mime-types": "^2.1.27", "parse5": "^6.0.1", "picomatch": "^2.2.2", - "ws": "^7.4.2" + "ws": "^7.5.10" }, "engines": { "node": ">=18.0.0" @@ -5544,6 +4113,22 @@ "@types/node": "*" } }, + "node_modules/@web/dev-server-core/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@web/dev-server-core/node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -5583,11 +4168,26 @@ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, - "node_modules/@web/dev-server-core/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "node_modules/@web/dev-server-core/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@web/dev-server-core/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8.3.0" }, @@ -5605,15 +4205,15 @@ } }, "node_modules/@web/dev-server-esbuild": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@web/dev-server-esbuild/-/dev-server-esbuild-1.0.2.tgz", - "integrity": "sha512-ak5mKt7L0H/Fa470Ku7p9A1eI32DNyFGM83jDkJviBO8r3lM00O5hVFW1K+UIYNC5EyanLyPxTqgtIuQEyMYcQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@web/dev-server-esbuild/-/dev-server-esbuild-1.0.4.tgz", + "integrity": "sha512-ia1LxBwwRiQBYhJ7/RtLenHyPjzle3SvTw3jOZaeGv8UGXVPOkQV8fR05caOtW/DPPZaZovNAybzRKVnNiYIZg==", "dev": true, + "license": "MIT", "dependencies": { "@mdn/browser-compat-data": "^4.0.0", - "@web/dev-server-core": "^0.7.0", - "esbuild": "^0.19.11", - "get-tsconfig": "^4.7.2", + "@web/dev-server-core": "^0.7.4", + "esbuild": "^0.25.0", "parse5": "^6.0.1", "ua-parser-js": "^1.0.33" }, @@ -5621,76 +4221,6 @@ "node": ">=18.0.0" } }, - "node_modules/@web/dev-server-esbuild/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@web/dev-server-esbuild/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@web/dev-server-esbuild/node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" - } - }, "node_modules/@web/dev-server-esbuild/node_modules/parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -5869,10 +4399,11 @@ } }, "node_modules/@web/test-runner-core": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.2.tgz", - "integrity": "sha512-G0D3mv9jvR+5xILENchPP9v1ZjBf3QVlzarMLR5jedCNbgntzcayF0LeW5wh5uyafGZJH28cYm9jGrJvGipoPQ==", + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.4.tgz", + "integrity": "sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.11", "@types/babel__code-frame": "^7.0.2", @@ -5882,15 +4413,15 @@ "@types/istanbul-lib-coverage": "^2.0.3", "@types/istanbul-reports": "^3.0.0", "@web/browser-logs": "^0.4.0", - "@web/dev-server-core": "^0.7.2", - "chokidar": "^3.4.3", + "@web/dev-server-core": "^0.7.3", + "chokidar": "^4.0.1", "cli-cursor": "^3.1.0", "co-body": "^6.1.0", "convert-source-map": "^2.0.0", "debounce": "^1.2.0", "dependency-graph": "^0.11.0", "globby": "^11.0.1", - "ip": "^2.0.1", + "internal-ip": "^6.2.0", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.0.2", @@ -5905,6 +4436,22 @@ "node": ">=18.0.0" } }, + "node_modules/@web/test-runner-core/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@web/test-runner-core/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5912,9 +4459,9 @@ "dev": true }, "node_modules/@web/test-runner-core/node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -5922,6 +4469,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -5929,6 +4477,20 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/@web/test-runner-core/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@web/test-runner-core/node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -6212,14 +4774,6 @@ } } }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -6247,14 +4801,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -6268,20 +4814,10 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/append-field": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, "node_modules/arg": { @@ -6391,6 +4927,7 @@ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.0.1" }, @@ -6431,10 +4968,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, - "node_modules/atob-lite": { - "version": "2.0.0", - "license": "MIT" - }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -6448,10 +4981,11 @@ } }, "node_modules/b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", - "dev": true + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/balanced-match": { "version": "1.0.0", @@ -6459,49 +4993,80 @@ "license": "MIT" }, "node_modules/bare-events": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", - "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", "dev": true, + "license": "Apache-2.0", "optional": true }, "node_modules/bare-fs": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.0.tgz", - "integrity": "sha512-TNFqa1B4N99pds2a5NYHR15o0ZpdNKbAeKTE/+G6ED/UeOavv8RY3dr/Fu99HW3zU3pXpo2kDNO8Sjsm2esfOw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.2.tgz", + "integrity": "sha512-8wSeOia5B7LwD4+h465y73KOdj5QHsbbuoUfPBi+pXgFJIPuG7SsiOdJuijWMyfid49eD+WivpfY7KT8gbAzBA==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-events": "^2.0.0", - "bare-path": "^2.0.0", - "bare-stream": "^1.0.0" + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } } }, "node_modules/bare-os": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", - "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", "dev": true, - "optional": true + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } }, "node_modules/bare-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.1.tgz", - "integrity": "sha512-OHM+iwRDRMDBsSW7kl3dO62JyHdBKO3B25FB9vNQBPcGHMo4+eA8Yj41Lfbk3pS/seDY+siNge0LdRTulAau/A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-os": "^2.1.0" + "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-1.0.0.tgz", - "integrity": "sha512-KhNUoDL40iP4gFaLSsoGE479t0jHijfYdIcxRn/XtezA2BaUD0NRf/JGRpsMq6dMNM+SrCrB0YSSo/5wBY4rOQ==", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "streamx": "^2.16.1" + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } } }, "node_modules/base64-js": { @@ -6522,29 +5087,25 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/before-after-hook": { - "version": "2.2.2", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "license": "Apache-2.0" }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -6589,20 +5150,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "dev": true, @@ -6629,10 +5176,6 @@ "dev": true, "license": "ISC" }, - "node_modules/btoa-lite": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -6652,6 +5195,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -6689,6 +5233,17 @@ "semver": "^7.0.0" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -6992,26 +5547,6 @@ "resolved": "resources/tools/check-markdown", "link": true }, - "node_modules/chokidar": { - "version": "3.5.1", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.1" - } - }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -7052,14 +5587,15 @@ } }, "node_modules/chromium-bidi": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.17.tgz", - "integrity": "sha512-BqOuIWUgTPj8ayuBFJUYCCuwIcwjBsb3/614P7tt1bEPJ4i1M0kCdIl0Wi9xhtswBXnfO2bTpTMkHD71H8rJMg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "3.0.1", "urlpattern-polyfill": "10.0.0", - "zod": "3.22.4" + "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" @@ -7174,14 +5710,6 @@ "type-is": "^1.6.16" } }, - "node_modules/code-point-at": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "license": "MIT", @@ -7318,18 +5846,34 @@ "license": "MIT" }, "node_modules/concat-stream": { - "version": "1.6.2", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", "engines": [ - "node >= 0.8" + "node >= 6.0" ], "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^2.2.2", + "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "license": "MIT", @@ -7353,6 +5897,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "license": "MIT" @@ -7460,6 +6013,7 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14" } @@ -7582,6 +6136,82 @@ "node": ">=0.10.0" } }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/default-gateway/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-gateway/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "license": "MIT", @@ -7633,6 +6263,7 @@ "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", "dev": true, + "license": "MIT", "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", @@ -7675,6 +6306,8 @@ }, "node_modules/deprecation": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "license": "ISC" }, "node_modules/destroy": { @@ -7687,10 +6320,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1273771", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1273771.tgz", - "integrity": "sha512-QDbb27xcTVReQQW/GHJsdQqGKwYBE7re7gxehj467kKP2DKuYBUj6i2k5LRiAC66J1yZG/9gsxooz/s9pcm0Og==", - "dev": true + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/diff": { "version": "5.0.0", @@ -7751,6 +6385,7 @@ }, "node_modules/encoding": { "version": "0.1.13", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7759,6 +6394,7 @@ }, "node_modules/encoding/node_modules/iconv-lite": { "version": "0.6.3", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -8398,6 +7034,7 @@ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -8414,15 +7051,6 @@ "source-map": "~0.6.1" } }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/eslint": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", @@ -8731,10 +7359,11 @@ } }, "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -8885,15 +7514,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8966,15 +7586,6 @@ "node": ">=0.10" } }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -8987,11 +7598,12 @@ "node": ">=4.0" } }, - "node_modules/esrecurse/node_modules/estraverse": { + "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -9037,88 +7649,6 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" }, - "node_modules/execa": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/execa/node_modules/get-stream": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/path-key": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/execa/node_modules/semver": { - "version": "5.7.1", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/execa/node_modules/shebang-command": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/shebang-regex": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/which": { - "version": "1.3.1", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -9126,36 +7656,37 @@ "dev": true }, "node_modules/express": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", - "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", - "serve-static": "1.16.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -9164,18 +7695,16 @@ }, "engines": { "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -9197,39 +7726,17 @@ "node": ">= 0.8" } }, - "node_modules/express/node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/finalhandler/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/express/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/express/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -9261,7 +7768,8 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.2.12", @@ -9280,22 +7788,11 @@ } }, "node_modules/fast-json-patch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-2.2.1.tgz", - "integrity": "sha512-4j5uBaTnsYAV5ebkidvxiLUYOwjQ+JSFljeqfTxCrH9bDmlCQaOJFS84oDJ2rAXZq2yskmk3ORfoP9DCwqFNig==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", "dev": true, - "dependencies": { - "fast-deep-equal": "^2.0.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/fast-json-patch/node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", - "dev": true + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", @@ -9310,9 +7807,9 @@ "dev": true }, "node_modules/fast-xml-parser": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.0.9.tgz", - "integrity": "sha512-2mBwCiuW3ycKQQ6SOesSB8WeF+fIGb6I/GG5vU5/XEptwFFhp9PE8b9O7fbs2dpq9fXn4ULR3UsfydNUCntf5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.2.tgz", + "integrity": "sha512-ZaCmslH75Jkfowo/x44Uq8KT5SutC5BFxHmY61nmTXPccw11PVuIXKUqC2hembMkJ3nPwTkQESXiUlsKutCbMg==", "funding": [ { "type": "github", @@ -9321,7 +7818,7 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.0.5" + "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -9367,6 +7864,57 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-replace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", @@ -9648,27 +8196,28 @@ } }, "node_modules/get-uri": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", - "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", "dev": true, + "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4", - "fs-extra": "^11.2.0" + "debug": "^4.3.4" }, "engines": { "node": ">= 14" } }, "node_modules/get-uri/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -9679,19 +8228,12 @@ } } }, - "node_modules/get-uri/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } + "license": "MIT" }, "node_modules/git-diff": { "version": "2.0.6", @@ -9897,14 +8439,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/growl": { - "version": "1.10.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.x" - } - }, "node_modules/has": { "version": "1.0.3", "dev": true, @@ -10163,6 +8697,16 @@ "node": ">= 6" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -10192,7 +8736,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.1", @@ -10290,6 +8835,25 @@ "node": ">=0.10.0" } }, + "node_modules/internal-ip": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", + "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-gateway": "^6.0.0", + "ipaddr.js": "^1.9.1", + "is-ip": "^3.1.0", + "p-event": "^4.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/internal-ip?sponsor=1" + } + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -10312,12 +8876,6 @@ "node": ">= 0.10" } }, - "node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true - }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -10337,6 +8895,16 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "license": "MIT", @@ -10370,17 +8938,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -10475,17 +9032,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-generator-function": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", @@ -10513,6 +9059,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-regex": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -10570,13 +9129,6 @@ "node": ">=8" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -10611,13 +9163,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -10718,6 +9263,7 @@ }, "node_modules/isexe": { "version": "2.0.0", + "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -10951,17 +9497,6 @@ "json5": "lib/cli.js" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jszip": { "version": "3.9.1", "license": "(MIT OR GPL-3.0-or-later)", @@ -10997,10 +9532,11 @@ } }, "node_modules/koa": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", - "integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", + "integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "^1.3.5", "cache-content-type": "^1.0.0", @@ -11237,6 +9773,7 @@ }, "node_modules/lodash.get": { "version": "4.4.2", + "dev": true, "license": "MIT" }, "node_modules/lodash.isequal": { @@ -11257,25 +9794,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.set": { - "version": "4.3.2", - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/log-update": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", @@ -11390,16 +9908,6 @@ "node": ">=10" } }, - "node_modules/macos-release": { - "version": "2.5.0", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -11481,6 +9989,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -11498,12 +10013,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -11708,7 +10224,8 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp": { "version": "0.5.6", @@ -11721,43 +10238,39 @@ } }, "node_modules/mocha": { - "version": "10.0.0", + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.2.2.tgz", + "integrity": "sha512-VlSBxrPYHK4YNOEbFdkCxHQbZMoNzBkoPprqtZRW6311EUF/DlSxoycE2e/2NtRk4WKkIXzyrXDTrlikJMWgbw==", "dev": true, "license": "MIT", "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", "mocha": "bin/mocha.js" }, "engines": { - "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/mocha-teamcity-reporter": { @@ -11771,38 +10284,65 @@ "mocha": ">=6" } }, - "node_modules/mocha/node_modules/chokidar": { - "version": "3.5.3", + "node_modules/mocha/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/mocha/node_modules/debug": { - "version": "4.3.4", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -11813,10 +10353,15 @@ } } }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", + "node_modules/mocha/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, - "license": "MIT" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } }, "node_modules/mocha/node_modules/escape-string-regexp": { "version": "4.0.0", @@ -11829,34 +10374,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", + "node_modules/mocha/node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": "*" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/mocha/node_modules/log-symbols": { @@ -11875,7 +10454,9 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "license": "ISC", "dependencies": { @@ -11885,49 +10466,76 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/mocha/node_modules/ms": { "version": "2.1.3", "dev": true, "license": "MIT" }, - "node_modules/mocha/node_modules/nanoid": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/mocha/node_modules/readdirp": { - "version": "3.6.0", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/mocha/node_modules/serialize-javascript": { - "version": "6.0.0", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } }, + "node_modules/mocha/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "dev": true, @@ -11943,31 +10551,69 @@ } }, "node_modules/mocha/node_modules/workerpool": { - "version": "6.2.1", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true, "license": "Apache-2.0" }, + "node_modules/mocha/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ms": { "version": "2.1.2", "license": "MIT" }, + "node_modules/multer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", + "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/nanocolors": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.13.tgz", "integrity": "sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==", "dev": true }, - "node_modules/nanoid": { - "version": "3.1.20", - "dev": true, - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -11986,6 +10632,7 @@ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4.0" } @@ -12012,10 +10659,6 @@ "hpagent": "^0.1.2" } }, - "node_modules/nice-try": { - "version": "1.0.5", - "license": "MIT" - }, "node_modules/nise": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", @@ -12054,10 +10697,11 @@ "dev": true }, "node_modules/nise/node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", - "dev": true + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" }, "node_modules/node-cleanup": { "version": "2.1.2", @@ -12066,6 +10710,7 @@ }, "node_modules/node-fetch": { "version": "2.6.7", + "dev": true, "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" @@ -12257,14 +10902,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/normalize-url": { "version": "6.1.0", "license": "MIT", @@ -12275,31 +10912,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nwsapi": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", @@ -12308,6 +10920,8 @@ }, "node_modules/object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12368,10 +10982,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/octokit-pagination-methods": { - "version": "1.1.0", - "license": "MIT" - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -12443,17 +11053,6 @@ "node": ">= 0.8.0" } }, - "node_modules/os-name": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "macos-release": "^2.2.0", - "windows-release": "^3.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/p-cancelable": { "version": "2.1.1", "license": "MIT", @@ -12461,8 +11060,25 @@ "node": ">=8" } }, + "node_modules/p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-finally": { "version": "1.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -12511,44 +11127,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pac-proxy-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", - "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", - "pac-resolver": "^7.0.0", - "socks-proxy-agent": "^8.0.2" + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" }, "engines": { "node": ">= 14" } }, "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, + "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/pac-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -12560,23 +11189,32 @@ } }, "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { "node": ">= 14" } }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/pac-resolver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, + "license": "MIT", "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" @@ -12696,9 +11334,10 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -12752,10 +11391,11 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -12885,43 +11525,43 @@ } }, "node_modules/proxy-agent": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", - "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.3", + "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.1", + "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.2" + "socks-proxy-agent": "^8.0.5" }, "engines": { "node": ">= 14" } }, "node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, + "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -12933,12 +11573,13 @@ } }, "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { @@ -12950,10 +11591,18 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "dev": true, @@ -12997,28 +11646,30 @@ } }, "node_modules/puppeteer-core": { - "version": "22.7.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.7.0.tgz", - "integrity": "sha512-9Q+L3VD7cfhXnxv6AqwTHsVb/+/uXENByhPrgvwQ49wvzQwtf1d6b7v6gpoG3tpRdwYjxoV1eHTD8tFahww09g==", + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.2.2", - "chromium-bidi": "0.5.17", - "debug": "4.3.4", - "devtools-protocol": "0.0.1273771", - "ws": "8.16.0" + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" }, "engines": { "node": ">=18" } }, "node_modules/puppeteer-core/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -13029,33 +11680,20 @@ } } }, - "node_modules/puppeteer-core/node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } + "license": "MIT" }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -13090,12 +11728,6 @@ } ] }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, "node_modules/quick-lru": { "version": "5.1.1", "license": "MIT", @@ -13153,17 +11785,6 @@ "version": "5.1.2", "license": "MIT" }, - "node_modules/readdirp": { - "version": "3.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/rechoir": { "version": "0.6.2", "dev": true, @@ -13549,81 +12170,26 @@ "node": ">= 0.8" } }, - "node_modules/serialize-javascript": { - "version": "5.0.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", - "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/serve-static/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "node_modules/serve-static/node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/serve-static/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static/node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -13717,6 +12283,7 @@ }, "node_modules/signal-exit": { "version": "3.0.7", + "dev": true, "license": "ISC" }, "node_modules/sinon": { @@ -13854,12 +12421,13 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", - "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "^7.1.1", + "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" }, @@ -13868,24 +12436,23 @@ } }, "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, + "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/socks-proxy-agent/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -13896,6 +12463,13 @@ } } }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "dev": true, @@ -13977,14 +12551,23 @@ "node": ">=10" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/streamx": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", - "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", + "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", "dev": true, + "license": "MIT", "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" @@ -14009,19 +12592,6 @@ "node": ">=0.6.19" } }, - "node_modules/string-width": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", @@ -14119,17 +12689,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", @@ -14161,11 +12720,14 @@ "node": ">=4" } }, - "node_modules/strip-eof": { - "version": "1.0.0", + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, "node_modules/strip-json-comments": { @@ -14184,9 +12746,9 @@ "link": true }, "node_modules/strnum": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz", - "integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.0.tgz", + "integrity": "sha512-w0S//9BqZZGw0L0Y8uLSelFGnDJgTyyNQLmSlPnVz43zPAiqu3w4t8J8sDqqANOGeZIZ/9jWuPguYcEnsoHv4A==", "funding": [ { "type": "github", @@ -14288,17 +12850,18 @@ } }, "node_modules/tar-fs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", - "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", + "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { - "bare-fs": "^2.1.1", - "bare-path": "^2.1.0" + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, "node_modules/tar-stream": { @@ -14306,6 +12869,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, + "license": "MIT", "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", @@ -14370,6 +12934,16 @@ "node": ">=8" } }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -14436,6 +13010,7 @@ }, "node_modules/tr46": { "version": "0.0.3", + "dev": true, "license": "MIT" }, "node_modules/ts-api-utils": { @@ -14572,13 +13147,13 @@ } }, "node_modules/tsx": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.0.tgz", - "integrity": "sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==", + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.23.0", + "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -14591,403 +13166,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" - } - }, "node_modules/tsx/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -15069,6 +13247,8 @@ }, "node_modules/typedarray": { "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, "node_modules/typescript": { @@ -15162,11 +13342,24 @@ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, + "license": "MIT", "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -15197,17 +13390,11 @@ } }, "node_modules/universal-user-agent": { - "version": "6.0.0", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", "license": "ISC" }, - "node_modules/universalify": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "license": "MIT", @@ -15245,7 +13432,8 @@ "version": "10.0.0", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -15324,6 +13512,7 @@ }, "node_modules/webidl-conversions": { "version": "3.0.1", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/whatwg-encoding": { @@ -15361,6 +13550,7 @@ }, "node_modules/whatwg-url": { "version": "5.0.0", + "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -15417,27 +13607,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.3", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, - "node_modules/windows-release": { - "version": "3.3.3", - "license": "MIT", - "dependencies": { - "execa": "^1.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -15456,11 +13625,6 @@ "node": ">=12.17" } }, - "node_modules/workerpool": { - "version": "6.1.0", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/wrap-ansi": { "version": "7.0.0", "license": "MIT", @@ -15579,9 +13743,10 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -15813,10 +13978,11 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -15826,7 +13992,7 @@ "license": "MIT", "dependencies": { "@actions/core": "^1.9.1", - "@actions/github": "^2.1.0", + "@actions/github": "^6.0.1", "emoji-regex": "^10.3.0", "typescript": "^5.4.5", "yargs": "^17.7.2" @@ -16054,10 +14220,36 @@ "@zip.js/zip.js": "^2.7.32", "c8": "^7.12.0", "jsdom": "^23.0.1", - "mocha": "^10.0.0", "tsx": "^4.19.0" } }, + "web/node_modules/@sentry/browser": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.55.0.tgz", + "integrity": "sha512-1A31mCEWCjaMxJt6qGUK+aDnLDcK6AwLAZnqpSchNysGni1pSn1RWSmk9TBF8qyTds5FH8B31H480uxMPUJ7Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.0", + "@sentry-internal/feedback": "8.55.0", + "@sentry-internal/replay": "8.55.0", + "@sentry-internal/replay-canvas": "8.55.0", + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "web/node_modules/@sentry/core": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz", + "integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, "web/src/engine/common/web-utils": { "name": "@keymanapp/web-utils", "license": "MIT", @@ -16065,7 +14257,6 @@ "@keymanapp/keyman-version": "*", "@keymanapp/resources-gosh": "*", "c8": "^7.12.0", - "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", "typescript": "^5.4.5" } @@ -16078,7 +14269,6 @@ }, "devDependencies": { "@keymanapp/resources-gosh": "*", - "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", "promise-status-async": "^1.2.10", "typescript": "^5.4.5" @@ -16098,7 +14288,6 @@ "@keymanapp/web-utils": "*", "@types/mocha": "^7.0.2", "c8": "^7.12.0", - "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", "typescript": "^5.4.5" } @@ -16118,7 +14307,6 @@ "@keymanapp/resources-gosh": "*", "@types/mocha": "^7.0.2", "c8": "^7.12.0", - "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", "typescript": "^5.4.5" } @@ -16137,7 +14325,6 @@ "devDependencies": { "@keymanapp/common-types": "*", "@keymanapp/resources-gosh": "*", - "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", "typescript": "^5.4.5" } @@ -16160,7 +14347,6 @@ "@keymanapp/resources-gosh": "*", "c8": "^7.12.0", "combine-source-map": "^0.8.0", - "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", "typescript": "^5.4.5" } @@ -16171,7 +14357,7 @@ "license": "MIT", "dependencies": { "@keymanapp/keyman-version": "*", - "@sentry/browser": "^5.27.4" + "@sentry/browser": "^8.55.0" }, "devDependencies": { "typescript": "^5.4.5" diff --git a/package.json b/package.json index ab3bb19139..1e7b65507c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-n": "^15.7.0", "eslint-plugin-promise": "^6.1.1", - "mocha": "^10.0.0", + "mocha": "^11.2.2", "mocha-teamcity-reporter": "^4.0.0", "playwright": "^1.46.1", "sinon": "^17.0.1", @@ -69,6 +69,11 @@ "@keymanapp/keyman-version": "file:common/web/keyman-version", "@keymanapp/ldml-keyboard-constants": "file:core/include/ldml" }, + "overrides": { + "ajv-cli": { + "fast-json-patch": "^3.1.1" + } + }, "engines": { "node": "20.16.0" } diff --git a/resources/build/README.md b/resources/build/README.md index ead7975b8b..6a9271a5e4 100644 --- a/resources/build/README.md +++ b/resources/build/README.md @@ -12,13 +12,13 @@ including this file... ## Various tools -### build-utils.sh +### build-utils.sh, builder.inc.sh -See [build-utils.md](build-utils.md) +See [builder.md](../../docs/builder.md) -### jq-win64 +### jq/jq-win64 -jq-win64.exe is used by various builds for json query. +jq resp. jq-win64.exe is used by various builds for json query. Official source: . [License](jq-license.txt) diff --git a/resources/build/_builder_nvm.sh b/resources/build/_builder_nvm.sh index a1bb9a4cc1..063aec2404 100755 --- a/resources/build/_builder_nvm.sh +++ b/resources/build/_builder_nvm.sh @@ -41,8 +41,10 @@ type -t nvm >/dev/null || { } } -nvm use "$REQUIRED_NODE_VERSION" || \ - (nvm install "$REQUIRED_NODE_VERSION" && nvm use "$REQUIRED_NODE_VERSION") +if ! nvm use "${REQUIRED_NODE_VERSION}"; then + nvm install "${REQUIRED_NODE_VERSION}" + nvm use "${REQUIRED_NODE_VERSION}" +fi # Beware the hardcoded path below -- it should already be in the system PATH diff --git a/resources/build/build-utils-ci.inc.sh b/resources/build/build-utils-ci.inc.sh index a67f285254..b4fc88c252 100644 --- a/resources/build/build-utils-ci.inc.sh +++ b/resources/build/build-utils-ci.inc.sh @@ -7,38 +7,6 @@ . "$KEYMAN_ROOT/resources/build/jq.inc.sh" -# -# Returns 0 if current build is running in CI, as a pull request test, or as a -# mainline branch test, or as a release build -# -builder_is_ci_build() { - if builder_is_ci_release_build || builder_is_ci_test_build; then - return 0 - fi - return 1 -} - -# -# Returns 0 if current build is running as a release build in CI -# -builder_is_ci_release_build() { - if [[ "$VERSION_ENVIRONMENT" =~ ^alpha|beta|stable$ ]]; then - return 0 - fi - return 1 -} - -# -# Returns 0 if current build is running in CI, as a pull request test, or as a -# mainline branch test -# -builder_is_ci_test_build() { - if [[ "$VERSION_ENVIRONMENT" == test ]]; then - return 0 - fi - return 1 -} - # # Returns 0 if current build is in CI and triggered from a pull request. If it # returns 0, then a call is made to GitHub to get pull request details, and the @@ -115,7 +83,7 @@ function builder_pull_has_label() { function builder_publish_npm() { if builder_has_option --npm-publish; then # Require --dry-run if local or test to avoid accidentally clobbering npm packages - if [[ $VERSION_ENVIRONMENT =~ local|test ]] && ! builder_has_option --dry-run; then + if [[ $KEYMAN_VERSION_ENVIRONMENT =~ local|test ]] && ! builder_has_option --dry-run; then builder_die "publish --npm-publish must use --dry-run flag for local or test builds" fi _builder_publish_npm_package publish @@ -126,9 +94,9 @@ function builder_publish_npm() { function _builder_publish_npm_package() { local action=$1 - local dist_tag=$TIER dry_run= + local dist_tag=$KEYMAN_TIER dry_run= - if [[ $TIER == stable ]]; then + if [[ $KEYMAN_TIER == stable ]]; then dist_tag=latest fi @@ -164,7 +132,7 @@ function _builder_write_npm_version() { # git tagging. :) if ! "$JQ" -e '.version' package.json > /dev/null; then pushd "$KEYMAN_ROOT" > /dev/null - npm version --allow-same-version --no-git-tag-version --no-commit-hooks --workspaces "$VERSION_WITH_TAG" + npm version --allow-same-version --no-git-tag-version --no-commit-hooks --workspaces "$KEYMAN_VERSION_WITH_TAG" popd > /dev/null fi @@ -174,12 +142,12 @@ function _builder_write_npm_version() { # the local package.json files, so they do need to be restored afterwards find "$KEYMAN_ROOT" -name "package.json" -not -path '*/node_modules/*' -print0 | \ while IFS= read -r -d '' line; do - cat "$line" | "$JQ" --arg VERSION_WITH_TAG "$VERSION_WITH_TAG" \ + cat "$line" | "$JQ" --arg KEYMAN_VERSION_WITH_TAG "$KEYMAN_VERSION_WITH_TAG" \ ' . + - (try { dependencies: (.dependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $VERSION_WITH_TAG) | from_entries) } catch {}) + - (try { devDependencies: (.devDependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $VERSION_WITH_TAG) | from_entries) } catch {}) + - (try { optionalDependencies: (.optionalDependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $VERSION_WITH_TAG) | from_entries) } catch {}) + (try { dependencies: (.dependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $KEYMAN_VERSION_WITH_TAG) | from_entries) } catch {}) + + (try { devDependencies: (.devDependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $KEYMAN_VERSION_WITH_TAG) | from_entries) } catch {}) + + (try { optionalDependencies: (.optionalDependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $KEYMAN_VERSION_WITH_TAG) | from_entries) } catch {}) ' > "${line}_" mv -f "${line}_" "$line" done @@ -243,4 +211,4 @@ function _builder_publish_cache_package_json() { function builder_publish_cleanup() { rm -f "$KEYMAN_ROOT/builder_package_publish.json" -} \ No newline at end of file +} diff --git a/resources/build/build-utils.sh b/resources/build/build-utils.sh index 2cbbcbfba9..4996845c5a 100755 --- a/resources/build/build-utils.sh +++ b/resources/build/build-utils.sh @@ -1,19 +1,19 @@ #!/usr/bin/env bash # -# This script sets build environment variables. VERSION vars are also exported: -# VERSION: Full current build version, e.g. "14.0.1" -# VERSION_WIN: Full current build version for Windows, e.g. "14.0.1.0" -# VERSION_RELEASE: Current release version, e.g. "14.0" -# VERSION_MAJOR: Major version, e.g. "14" -# VERSION_MINOR: Minor version, e.g. "0" -# VERSION_PATCH: Patch version, e.g. "1" -# TIER: Current tier, one of "alpha", "beta" or "stable" -# VERSION_TAG: Tier + Pull Request + Location of build [-alpha|-beta][-test[-1234]][-local] -# VERSION_WITH_TAG: e.g. "14.0.1-alpha-test-1234" or "14.0.5-beta-local" or "14.0.1-alpha-test" -# VERSION_GIT_TAG: Git tag for the release, "release@$VERSION_WITH_TAG", e.g. "release@14.0.1-alpha-test-1234" +# This script sets build environment variables. KEYMAN_VERSION vars are also exported: +# KEYMAN_VERSION: Full current build version, e.g. "14.0.1" +# KEYMAN_VERSION_WIN: Full current build version for Windows, e.g. "14.0.1.0" +# KEYMAN_VERSION_RELEASE: Current release version, e.g. "14.0" +# KEYMAN_VERSION_MAJOR: Major version, e.g. "14" +# KEYMAN_VERSION_MINOR: Minor version, e.g. "0" +# KEYMAN_VERSION_PATCH: Patch version, e.g. "1" +# KEYMAN_TIER: Current tier, one of "alpha", "beta" or "stable" +# KEYMAN_VERSION_TAG: Tier + Pull Request + Location of build [-alpha|-beta][-test[-1234]][-local] +# KEYMAN_VERSION_WITH_TAG: e.g. "14.0.1-alpha-test-1234" or "14.0.5-beta-local" or "14.0.1-alpha-test" +# KEYMAN_VERSION_GIT_TAG: Git tag for the release, "release@$KEYMAN_VERSION_WITH_TAG", e.g. "release@14.0.1-alpha-test-1234" # KEYMAN_ROOT: fully resolved root path of Keyman repository -# VERSION_ENVIRONMENT: One of: local, test, alpha, beta, stable -# UPLOAD_SENTRY: true - if VERSION_ENVIRONMENT is one of alpha, beta, stable +# KEYMAN_VERSION_ENVIRONMENT: One of: local, test, alpha, beta, stable +# UPLOAD_SENTRY: true - if KEYMAN_VERSION_ENVIRONMENT is one of alpha, beta, stable # false - if local, test. Indicates if debug artifacts should be uploaded to Sentry # BUILDER_OS: win|mac|linux -- current build environment # @@ -57,20 +57,20 @@ function findKeymanRoot() { } function findVersion() { - local VERSION_MD="$KEYMAN_ROOT/VERSION.md" - VERSION=$(builder_trim $(<"$VERSION_MD")) - [[ "$VERSION" =~ ^([[:digit:]]+)\.([[:digit:]]+)\.([[:digit:]]+)$ ]] && { - VERSION_MAJOR="${BASH_REMATCH[1]}" - VERSION_MINOR="${BASH_REMATCH[2]}" - VERSION_PATCH="${BASH_REMATCH[3]}" - VERSION_RELEASE="$VERSION_MAJOR.$VERSION_MINOR" + local KEYMAN_VERSION_MD="$KEYMAN_ROOT/VERSION.md" + KEYMAN_VERSION=$(builder_trim $(<"$KEYMAN_VERSION_MD")) + [[ "$KEYMAN_VERSION" =~ ^([[:digit:]]+)\.([[:digit:]]+)\.([[:digit:]]+)$ ]] && { + KEYMAN_VERSION_MAJOR="${BASH_REMATCH[1]}" + KEYMAN_VERSION_MINOR="${BASH_REMATCH[2]}" + KEYMAN_VERSION_PATCH="${BASH_REMATCH[3]}" + KEYMAN_VERSION_RELEASE="$KEYMAN_VERSION_MAJOR.$KEYMAN_VERSION_MINOR" } || { echo "Invalid VERSION.md file: expected major.minor.patch"; exit 1; } # Used for Windows, which requires a four part version string - VERSION_WIN="$VERSION.0" + KEYMAN_VERSION_WIN="$KEYMAN_VERSION.0" # # Build a tag to append to the version string. This is not assigned @@ -78,70 +78,71 @@ function findVersion() { # display string and in TeamCity configuration # - if [ "$TIER" == "alpha" ] || [ "$TIER" == "beta" ]; then - VERSION_TAG="-$TIER" + if [ "$KEYMAN_TIER" == "alpha" ] || [ "$KEYMAN_TIER" == "beta" ]; then + KEYMAN_VERSION_TAG="-$KEYMAN_TIER" else - VERSION_TAG= + KEYMAN_VERSION_TAG= fi - if [[ -z "${TEAMCITY_VERSION-}" && -z "${GITHUB_ACTIONS-}" && -z "${KEYMAN_PKG_BUILD-}" ]]; then + if ! builder_is_running_on_teamcity && ! builder_is_running_on_gha \ + && [[ -z "${KEYMAN_PKG_BUILD-}" ]]; then # Local dev machine, not TeamCity or GitHub Action and not .deb package build - VERSION_TAG="$VERSION_TAG-local" - VERSION_ENVIRONMENT=local + KEYMAN_VERSION_TAG="$KEYMAN_VERSION_TAG-local" + KEYMAN_VERSION_ENVIRONMENT=local elif [ -n "${TEAMCITY_PR_NUMBER-}" ]; then # On TeamCity: are we running a pull request build or a master/beta/stable build? - VERSION_ENVIRONMENT="test" + KEYMAN_VERSION_ENVIRONMENT="test" # Note TEAMCITY_PR_NUMBER can also be 'master', 'beta', or 'stable-x.y' # This indicates we are running a Test build. if [[ $TEAMCITY_PR_NUMBER =~ ^(master|beta|stable(-[0-9]+\.[0-9]+)?)$ ]]; then - VERSION_TAG="$VERSION_TAG-test" + KEYMAN_VERSION_TAG="$KEYMAN_VERSION_TAG-test" else - VERSION_TAG="$VERSION_TAG-test-$TEAMCITY_PR_NUMBER" + KEYMAN_VERSION_TAG="$KEYMAN_VERSION_TAG-test-$TEAMCITY_PR_NUMBER" fi - elif [ -n "${GITHUB_ACTIONS-}" ] && ${GHA_TEST_BUILD-}; then - VERSION_ENVIRONMENT="test" + elif builder_is_running_on_gha && ${GHA_TEST_BUILD-}; then + KEYMAN_VERSION_ENVIRONMENT="test" # Note GHA_BRANCH can be 'master', 'beta', or 'stable-x.y' # This indicates we are running a Test build. if [[ ${GHA_BRANCH-} =~ ^(master|beta|stable(-[0-9]+\.[0-9]+)?)$ ]]; then - VERSION_TAG="${VERSION_TAG}-test" + KEYMAN_VERSION_TAG="${KEYMAN_VERSION_TAG}-test" else - VERSION_TAG="${VERSION_TAG}-test-${GHA_BRANCH-unset}" + KEYMAN_VERSION_TAG="${KEYMAN_VERSION_TAG}-test-${GHA_BRANCH-unset}" fi else - VERSION_ENVIRONMENT="$TIER" + KEYMAN_VERSION_ENVIRONMENT="$KEYMAN_TIER" fi - VERSION_WITH_TAG="$VERSION$VERSION_TAG" - VERSION_GIT_TAG="release@$VERSION_WITH_TAG" + KEYMAN_VERSION_WITH_TAG="$KEYMAN_VERSION$KEYMAN_VERSION_TAG" + KEYMAN_VERSION_GIT_TAG="release@$KEYMAN_VERSION_WITH_TAG" - readonly VERSION - readonly VERSION_MAJOR - readonly VERSION_MINOR - readonly VERSION_PATCH - readonly VERSION_RELEASE - readonly VERSION_WIN - readonly VERSION_TAG - readonly VERSION_WITH_TAG - readonly VERSION_ENVIRONMENT - readonly VERSION_GIT_TAG + readonly KEYMAN_VERSION + readonly KEYMAN_VERSION_MAJOR + readonly KEYMAN_VERSION_MINOR + readonly KEYMAN_VERSION_PATCH + readonly KEYMAN_VERSION_RELEASE + readonly KEYMAN_VERSION_WIN + readonly KEYMAN_VERSION_TAG + readonly KEYMAN_VERSION_WITH_TAG + readonly KEYMAN_VERSION_ENVIRONMENT + readonly KEYMAN_VERSION_GIT_TAG # Export version strings so places like version.gradle can access them - export VERSION - export VERSION_MAJOR - export VERSION_MINOR - export VERSION_PATCH - export VERSION_RELEASE - export VERSION_WIN - export VERSION_TAG - export VERSION_WITH_TAG - export VERSION_ENVIRONMENT - export VERSION_GIT_TAG + export KEYMAN_VERSION + export KEYMAN_VERSION_MAJOR + export KEYMAN_VERSION_MINOR + export KEYMAN_VERSION_PATCH + export KEYMAN_VERSION_RELEASE + export KEYMAN_VERSION_WIN + export KEYMAN_VERSION_TAG + export KEYMAN_VERSION_WITH_TAG + export KEYMAN_VERSION_ENVIRONMENT + export KEYMAN_VERSION_GIT_TAG } function findTier() { - local TIER_MD="$KEYMAN_ROOT/TIER.md" - TIER=$(builder_trim $(<"$TIER_MD")) - [[ "$TIER" =~ ^(alpha|beta|stable)$ ]] || { + local KEYMAN_TIER_MD="$KEYMAN_ROOT/TIER.md" + KEYMAN_TIER=$(builder_trim $(<"$KEYMAN_TIER_MD")) + [[ "$KEYMAN_TIER" =~ ^(alpha|beta|stable)$ ]] || { echo "Invalid TIER.md file: expected alpha, beta or stable." exit 1; } @@ -150,39 +151,40 @@ function findTier() { function printBuildNumberForTeamCity() { if [ ! -z "${TEAMCITY_VERSION-}" ]; then if [ ! -z "${TEAMCITY_PR_NUMBER-}" ]; then - echo "##teamcity[buildNumber '$VERSION_WITH_TAG']" + echo "##teamcity[buildNumber '$KEYMAN_VERSION_WITH_TAG']" else # For alpha/beta builds, for now we don't append the # version tag as buildNumber is used in the delivery # of the build version. We may improve this in the # future. - echo "##teamcity[buildNumber '$VERSION']" + echo "##teamcity[buildNumber '$KEYMAN_VERSION']" fi fi } function printVersionUtilsDebug() { - echo "KEYMAN_ROOT: $KEYMAN_ROOT" - echo "VERSION: $VERSION" - echo "VERSION_WIN: $VERSION_WIN" - echo "VERSION_RELEASE: $VERSION_RELEASE" - echo "VERSION_MAJOR: $VERSION_MAJOR" - echo "VERSION_MINOR: $VERSION_MINOR" - echo "VERSION_PATCH: $VERSION_PATCH" - echo "TIER: $TIER" - echo "VERSION_TAG: $VERSION_TAG" - echo "VERSION_WITH_TAG: $VERSION_WITH_TAG" - echo "VERSION_GIT_TAG: $VERSION_GIT_TAG" - echo "VERSION_ENVIRONMENT: $VERSION_ENVIRONMENT" + echo "KEYMAN_ROOT: $KEYMAN_ROOT" + echo "KEYMAN_VERSION: $KEYMAN_VERSION" + echo "KEYMAN_VERSION_WIN: $KEYMAN_VERSION_WIN" + echo "KEYMAN_VERSION_RELEASE: $KEYMAN_VERSION_RELEASE" + echo "KEYMAN_VERSION_MAJOR: $KEYMAN_VERSION_MAJOR" + echo "KEYMAN_VERSION_MINOR: $KEYMAN_VERSION_MINOR" + echo "KEYMAN_VERSION_PATCH: $KEYMAN_VERSION_PATCH" + echo "KEYMAN_TIER: $KEYMAN_TIER" + echo "KEYMAN_VERSION_TAG: $KEYMAN_VERSION_TAG" + echo "KEYMAN_VERSION_WITH_TAG: $KEYMAN_VERSION_WITH_TAG" + echo "KEYMAN_VERSION_GIT_TAG: $KEYMAN_VERSION_GIT_TAG" + echo "KEYMAN_VERSION_ENVIRONMENT: $KEYMAN_VERSION_ENVIRONMENT" } function findShouldSentryRelease() { - # Default, for 'test' or 'local' environment, or in case $VERSION_ENVIRONMENT is improperly specified. + # Default, for 'test' or 'local' environment, or in case + # $KEYMAN_VERSION_ENVIRONMENT is improperly specified. # (May be overridden by -upload-sentry in supporting build scripts.) UPLOAD_SENTRY=false # Default: override to `true` for release builds. - case $VERSION_ENVIRONMENT in + case $KEYMAN_VERSION_ENVIRONMENT in # Actual release tiers alpha | beta | stable) UPLOAD_SENTRY=true @@ -236,12 +238,12 @@ function exportEnvironmentDefinitionScript() { echo "# This file redefines critical environment variables for import to Xcode build phases." >> "$ENVIRONMENT_SH" echo "" >> "$ENVIRONMENT_SH" - # Defining variables for VERSION here will leave static definitions that don't automatically update when a user + # Defining variables for KEYMAN_VERSION here will leave static definitions that don't automatically update when a user # changes branches; some branches are 'similar enough' to not require full command-line based rebuilds. - # We want that VERSION number to properly mirror the state of its branch during development so that it matches + # We want that KEYMAN_VERSION number to properly mirror the state of its branch during development so that it matches # any error reports that get logged to Sentry. # - # As a result, we explicitly do NOT define VERSION or VERSION_TAG as part of ENVIRONMENT_SH. + # As a result, we explicitly do NOT define KEYMAN_VERSION or KEYMAN_VERSION_TAG as part of ENVIRONMENT_SH. echo "# Required for successful dSYM upload for Sentry error reporting" >> "$ENVIRONMENT_SH" echo "export SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-}" >> "$ENVIRONMENT_SH" @@ -267,17 +269,17 @@ replaceVersionStrings() { local outfile=$2 sed " - s/\$VERSION_WIN/$VERSION_WIN/g; - s/\$VERSION_RELEASE/$VERSION_RELEASE/g; - s/\$VERSION_MAJOR/$VERSION_MAJOR/g; - s/\$VERSION_MINOR/$VERSION_MINOR/g; - s/\$VERSION_PATCH/$VERSION_PATCH/g; - s/\$TIER/$TIER/g; - s/\$VERSION_TAG/$VERSION_TAG/g; - s/\$VERSION_WITH_TAG/$VERSION_WITH_TAG/g; - s/\$VERSION_GIT_TAG/$VERSION_GIT_TAG/g; - s/\$VERSION_ENVIRONMENT/$VERSION_ENVIRONMENT/g; - s/\$VERSION/$VERSION/g; + s/\$KEYMAN_VERSION_WIN/$KEYMAN_VERSION_WIN/g; + s/\$KEYMAN_VERSION_RELEASE/$KEYMAN_VERSION_RELEASE/g; + s/\$KEYMAN_VERSION_MAJOR/$KEYMAN_VERSION_MAJOR/g; + s/\$KEYMAN_VERSION_MINOR/$KEYMAN_VERSION_MINOR/g; + s/\$KEYMAN_VERSION_PATCH/$KEYMAN_VERSION_PATCH/g; + s/\$KEYMAN_TIER/$KEYMAN_TIER/g; + s/\$KEYMAN_VERSION_TAG/$KEYMAN_VERSION_TAG/g; + s/\$KEYMAN_VERSION_WITH_TAG/$KEYMAN_VERSION_WITH_TAG/g; + s/\$KEYMAN_VERSION_GIT_TAG/$KEYMAN_VERSION_GIT_TAG/g; + s/\$KEYMAN_VERSION_ENVIRONMENT/$KEYMAN_VERSION_ENVIRONMENT/g; + s/\$KEYMAN_VERSION/$KEYMAN_VERSION/g; " "$infile" > "$outfile" } @@ -287,39 +289,39 @@ replaceVersionStrings_Mkver() { local infile=$1 local outfile=$2 - # Note that $VERSION differs between the two functions! + # Note that $KEYMAN_VERSION differs between the two functions! # We should be deprecating all the mkver strings sed " - s/\$VersionWin/$VERSION_WIN/g; - s/\$VersionRelease/$VERSION_RELEASE/g; - s/\$VersionMajor/$VERSION_MAJOR/g; - s/\$VersionMinor/$VERSION_MINOR/g; - s/\$VersionPatch/$VERSION_PATCH/g; - s/\$Tier/$TIER/g; - s/\$Tag/$VERSION_TAG/g; - s/\$VersionWithTag/$VERSION_WITH_TAG/g; - s/\$VersionGitTag/$VERSION_GIT_TAG/g; - s/\$VersionRc/$VERSION_MAJOR,$VERSION_MINOR,$VERSION_PATCH,0/g; - s/\$Environment/$VERSION_ENVIRONMENT/g; - s/\$Version/$VERSION/g; - s/\$VERSIONNUM/$VERSION_MAJOR,$VERSION_MINOR,$VERSION_PATCH,0/g; - s/\$RELEASE_MAJOR/$VERSION_MAJOR/g; - s/\$RELEASE_MINOR/$VERSION_MINOR/g; - s/\$RELEASE/$VERSION_RELEASE/g; + s/\$VersionWin/$KEYMAN_VERSION_WIN/g; + s/\$VersionRelease/$KEYMAN_VERSION_RELEASE/g; + s/\$VersionMajor/$KEYMAN_VERSION_MAJOR/g; + s/\$VersionMinor/$KEYMAN_VERSION_MINOR/g; + s/\$VersionPatch/$KEYMAN_VERSION_PATCH/g; + s/\$Tier/$KEYMAN_TIER/g; + s/\$Tag/$KEYMAN_VERSION_TAG/g; + s/\$VersionWithTag/$KEYMAN_VERSION_WITH_TAG/g; + s/\$VersionGitTag/$KEYMAN_VERSION_GIT_TAG/g; + s/\$VersionRc/$KEYMAN_VERSION_MAJOR,$KEYMAN_VERSION_MINOR,$KEYMAN_VERSION_PATCH,0/g; + s/\$Environment/$KEYMAN_VERSION_ENVIRONMENT/g; + s/\$Version/$KEYMAN_VERSION/g; + s/\$KEYMAN_VERSIONNUM/$KEYMAN_VERSION_MAJOR,$KEYMAN_VERSION_MINOR,$KEYMAN_VERSION_PATCH,0/g; + s/\$RELEASE_MAJOR/$KEYMAN_VERSION_MAJOR/g; + s/\$RELEASE_MINOR/$KEYMAN_VERSION_MINOR/g; + s/\$RELEASE/$KEYMAN_VERSION_RELEASE/g; - s/\$VERSION_WIN/$VERSION_WIN/g; - s/\$VERSION_RELEASE/$VERSION_RELEASE/g; - s/\$VERSION_MAJOR/$VERSION_MAJOR/g; - s/\$VERSION_MINOR/$VERSION_MINOR/g; - s/\$VERSION_PATCH/$VERSION_PATCH/g; - s/\$TIER/$TIER/g; - s/\$VERSION_TAG/$VERSION_TAG/g; - s/\$VERSION_WITH_TAG/$VERSION_WITH_TAG/g; - s/\$VERSION_GIT_TAG/$VERSION_GIT_TAG/g; - s/\$VERSION_ENVIRONMENT/$VERSION_ENVIRONMENT/g; + s/\$KEYMAN_VERSION_WIN/$KEYMAN_VERSION_WIN/g; + s/\$KEYMAN_VERSION_RELEASE/$KEYMAN_VERSION_RELEASE/g; + s/\$KEYMAN_VERSION_MAJOR/$KEYMAN_VERSION_MAJOR/g; + s/\$KEYMAN_VERSION_MINOR/$KEYMAN_VERSION_MINOR/g; + s/\$KEYMAN_VERSION_PATCH/$KEYMAN_VERSION_PATCH/g; + s/\$KEYMAN_TIER/$KEYMAN_TIER/g; + s/\$KEYMAN_VERSION_TAG/$KEYMAN_VERSION_TAG/g; + s/\$KEYMAN_VERSION_WITH_TAG/$KEYMAN_VERSION_WITH_TAG/g; + s/\$KEYMAN_VERSION_GIT_TAG/$KEYMAN_VERSION_GIT_TAG/g; + s/\$KEYMAN_VERSION_ENVIRONMENT/$KEYMAN_VERSION_ENVIRONMENT/g; - s/\$VERSION/$VERSION_WIN/g; + s/\$KEYMAN_VERSION/$KEYMAN_VERSION_WIN/g; " "$infile" > "$outfile" } @@ -328,11 +330,22 @@ set_keyman_standard_build_path() { PATH="$KEYMAN_ROOT/node_modules/.bin:$PATH" } -# For CI compatbility of building Keyman for Android 16.0 with OpenJDK 8, -# this overrides JAVA_HOME for the builder script to use OpenJDK 11. +# For CI compatibility of building Keyman for Android 18.0 with OpenJDK 11, +# this overrides JAVA_HOME for the builder script to use OpenJDK 21. set_java_home() { + set_java_home_21 +} + +set_java_home_21() { + if [[ ! -z ${JAVA_HOME_21+x} ]]; then + builder_echo "Setting JAVA_HOME to JAVA_HOME_21 (${JAVA_HOME_21})" + export JAVA_HOME="${JAVA_HOME_21}" + fi +} + +set_java_home_11() { if [[ ! -z ${JAVA_HOME_11+x} ]]; then - builder_echo "Setting JAVA_HOME to JAVA_HOME_11 ($JAVA_HOME_11)" + builder_echo "Setting JAVA_HOME to JAVA_HOME_11 (${JAVA_HOME_11})" export JAVA_HOME="${JAVA_HOME_11}" fi } diff --git a/resources/build/ci/cancel-builds/trigger_definitions.mjs b/resources/build/ci/cancel-builds/trigger_definitions.mjs index 16601efcec..f6acdf91bb 100644 --- a/resources/build/ci/cancel-builds/trigger_definitions.mjs +++ b/resources/build/ci/cancel-builds/trigger_definitions.mjs @@ -5,7 +5,7 @@ export const testBuildConfigurations = { 'linux': ['KeymanLinux_TestPullRequests', 'Keyman_Linux_Test_Integration', 'Keyman_Common_KPAPI_TestPullRequests_Linux', 'deb-pr-packaging_GitHub'], 'mac': ['Keyman_KeymanMac_PullRequests', 'Keyman_Common_KPAPI_TestPullRequests_macOS'], 'windows': ['KeymanDesktop_TestPullRequests', 'KeymanDesktop_TestPrRenderOnScreenKeyboards', 'Keyman_Common_KPAPI_TestPullRequests_Windows'], - 'web': ['Keymanweb_TestPullRequests', 'Keyman_Common_LMLayer_TestPullRequests', 'Keyman_Common_KPAPI_TestPullRequests_WASM'], + 'web': ['Keymanweb_TestPullRequests', 'Keyman_Common_KPAPI_TestPullRequests_WASM'], 'developer': ['Keyman_Developer_Test'], 'common_web': ['Keyman_Test_Common_Web'], diff --git a/resources/build/ci/pull-requests.inc.sh b/resources/build/ci/pull-requests.inc.sh index 09c0903fe5..6959f826d3 100644 --- a/resources/build/ci/pull-requests.inc.sh +++ b/resources/build/ci/pull-requests.inc.sh @@ -73,7 +73,7 @@ function ci_repo_has_cached_changes { # * hub # * uuidgen (or TeamCity `$BUILD_NUMBER` if `uuidgen` not present on system) # Example: -# ci_open_pull_request "$S_KEYMAN_COM" auto/keymanweb/release "auto: KeymanWeb release $VERSION_RELEASE"" +# ci_open_pull_request "$S_KEYMAN_COM" auto/keymanweb/release "auto: KeymanWeb release $KEYMAN_VERSION_RELEASE"" # function ci_open_pull_request { local repo="$1" @@ -117,4 +117,4 @@ function ci_open_pull_request { builder_echo "Pull request successfully created" return 0 -} \ No newline at end of file +} diff --git a/resources/build/help-keyman-com.sh b/resources/build/help-keyman-com.sh index 7901be5475..0bf4067bfa 100755 --- a/resources/build/help-keyman-com.sh +++ b/resources/build/help-keyman-com.sh @@ -74,38 +74,38 @@ function upload { function upload_keyman_help { case $platform in android) - upload android/docs/help products/android/$VERSION_RELEASE - upload android/docs/engine developer/engine/android/$VERSION_RELEASE + upload android/docs/help products/android/$KEYMAN_VERSION_RELEASE + upload android/docs/engine developer/engine/android/$KEYMAN_VERSION_RELEASE ;; ios) - upload ios/docs/help products/iphone-and-ipad/$VERSION_RELEASE - upload ios/docs/engine developer/engine/iphone-and-ipad/$VERSION_RELEASE + upload ios/docs/help products/iphone-and-ipad/$KEYMAN_VERSION_RELEASE + upload ios/docs/engine developer/engine/iphone-and-ipad/$KEYMAN_VERSION_RELEASE ;; linux) pushd "$KEYMAN_ROOT/linux/keyman-config" > /dev/null ./build.sh build popd > /dev/null - upload linux/docs/help products/linux/$VERSION_RELEASE + upload linux/docs/help products/linux/$KEYMAN_VERSION_RELEASE ;; mac) - upload mac/docs/help products/mac/$VERSION_RELEASE + upload mac/docs/help products/mac/$KEYMAN_VERSION_RELEASE ;; web) - upload web/docs/engine developer/engine/web/$VERSION_RELEASE + upload web/docs/engine developer/engine/web/$KEYMAN_VERSION_RELEASE ;; windows) # Note: `/windows/src/desktop/help/build.sh web` must be run first - upload windows/bin/help/md/desktop products/windows/$VERSION_RELEASE - upload windows/docs/engine developer/engine/windows/$VERSION_RELEASE + upload windows/bin/help/md/desktop products/windows/$KEYMAN_VERSION_RELEASE + upload windows/docs/engine developer/engine/windows/$KEYMAN_VERSION_RELEASE ;; developer) # Note: `/developer/build.sh api` must be run first - covers both uploads - upload developer/docs/help developer/$VERSION_RELEASE - upload developer/build/docs developer/$VERSION_RELEASE/reference/api - upload developer/src/kmc/build/messages developer/$VERSION_RELEASE/reference/messages + upload developer/docs/help developer/$KEYMAN_VERSION_RELEASE + upload developer/build/docs developer/$KEYMAN_VERSION_RELEASE/reference/api + upload developer/src/kmc/build/messages developer/$KEYMAN_VERSION_RELEASE/reference/messages # Note: we publish the core help alongside developer because we don't have # a core CI release build - upload core/docs/api developer/core/$VERSION_RELEASE + upload core/docs/api developer/core/$KEYMAN_VERSION_RELEASE ;; *) display_usage @@ -152,6 +152,6 @@ if ! ci_repo_has_cached_changes "$HELP_KEYMAN_COM"; then exit 0 fi -ci_open_pull_request "$HELP_KEYMAN_COM" "auto/$platform-help-$VERSION_WITH_TAG" "auto: Keyman for $platform help deployment" +ci_open_pull_request "$HELP_KEYMAN_COM" "auto/$platform-help-$KEYMAN_VERSION_WITH_TAG" "auto: Keyman for $platform help deployment" -exit 0 \ No newline at end of file +exit 0 diff --git a/resources/build/increment-version.sh b/resources/build/increment-version.sh index 9e9ac699b8..f1d15c6a7d 100755 --- a/resources/build/increment-version.sh +++ b/resources/build/increment-version.sh @@ -111,10 +111,10 @@ fi if [[ $ABORT = 50 ]]; then if [[ $FORCE = 0 ]]; then - echo "Skipping version increment from $VERSION: no recently merged pull requests were found" + echo "Skipping version increment from $KEYMAN_VERSION: no recently merged pull requests were found" if [ ! -z "${TEAMCITY_VERSION-}" ]; then # Send TeamCity a build status - echo "##teamcity[buildStatus status='SUCCESS' text='Skipping version increment from $VERSION: no recently merged pull requests were found']" + echo "##teamcity[buildStatus status='SUCCESS' text='Skipping version increment from $KEYMAN_VERSION: no recently merged pull requests were found']" fi exit 0 else @@ -131,14 +131,14 @@ popd > /dev/null # if [ "$action" == "commit" ]; then - echo "increment-version.sh: committing to repository and tagging release version $VERSION_WITH_TAG" - VERSION_MD="$KEYMAN_ROOT/VERSION.md" - NEWVERSION=`cat $VERSION_MD | tr -d "[:space:]"` + echo "increment-version.sh: committing to repository and tagging release version $KEYMAN_VERSION_WITH_TAG" + KEYMAN_VERSION_MD="$KEYMAN_ROOT/VERSION.md" + NEWVERSION=`cat $KEYMAN_VERSION_MD | tr -d "[:space:]"` pushd "$KEYMAN_ROOT" > /dev/null message="auto: increment $base version to $NEWVERSION" branch="auto/version-$base-$NEWVERSION" - git tag -a "$VERSION_GIT_TAG" -m "Keyman release $VERSION_WITH_TAG" + git tag -a "$KEYMAN_VERSION_GIT_TAG" -m "Keyman release $KEYMAN_VERSION_WITH_TAG" git checkout -b "$branch" git add VERSION.md HISTORY.md @@ -206,7 +206,7 @@ if [ "$action" == "commit" ]; then echo "Version was incremented and pull request was created, and builds were triggered" if [ ! -z "${TEAMCITY_VERSION-}" ]; then # Send TeamCity a build status - echo "##teamcity[buildStatus status='SUCCESS' text='Version was incremented from $VERSION to $NEWVERSION and pull request was created']" + echo "##teamcity[buildStatus status='SUCCESS' text='Version was incremented from $KEYMAN_VERSION to $NEWVERSION and pull request was created']" fi # diff --git a/resources/build/meson-utils.inc.sh b/resources/build/meson-utils.inc.sh index 24375a840a..271e017ea7 100644 --- a/resources/build/meson-utils.inc.sh +++ b/resources/build/meson-utils.inc.sh @@ -2,6 +2,7 @@ # This script contains common meson functions +source "$KEYMAN_ROOT/resources/build/meson/standard_meson_build.inc.sh" # ---------------------------------------------------------------------------- # clean @@ -17,11 +18,8 @@ do_meson_clean() { # ---------------------------------------------------------------------------- do_meson_configure() { - # Import our standard compiler defines; this is copied from - # /resources/build/meson/standard.meson.build by build.sh, because meson doesn't - # allow us to reference a file outside its root - mkdir -p "$THIS_SCRIPT_PATH/resources" - cp "$KEYMAN_ROOT/resources/build/meson/standard.meson.build" "$THIS_SCRIPT_PATH/resources/meson.build" + # Import our standard compiler defines + standard_meson_build pushd "$THIS_SCRIPT_PATH" > /dev/null # Additional arguments are used by Linux build, e.g. -Dprefix=${INSTALLDIR} diff --git a/resources/build/meson/standard.meson.build b/resources/build/meson/standard.meson.build index a08eb7a6ad..0bf48f9e9c 100644 --- a/resources/build/meson/standard.meson.build +++ b/resources/build/meson/standard.meson.build @@ -4,6 +4,11 @@ # This file has the master location /resources/build/meson/standard.meson.build, # and is copied into each project's /resources/meson.build in the # configure step, so that it can be referenced by meson directly. +# +# Use standard_meson_build.inc.sh to make the file copy +# + + # # Where possible, we want to use these flags everywhere # @@ -26,6 +31,17 @@ message('meson.project_version(): ' + meson.project_version()) message('host_machine.system(): ' + host_machine.system()) message('compiler.get_id(): ' + cpp_compiler.get_id()) +# +# Import `VERSION_WITH_TAG` which we can use where appropriate instead of +# meson.project_version() which is only major.minor.patch. +# + +fs = import('fs') +if fs.exists('VERSION_WITH_TAG.md') + VERSION_WITH_TAG = fs.read('VERSION_WITH_TAG.md').strip() + message('VERSION_WITH_TAG: ' + VERSION_WITH_TAG) +endif + # # Standard compiler flags for all platforms # diff --git a/resources/build/meson/standard_meson_build.inc.sh b/resources/build/meson/standard_meson_build.inc.sh new file mode 100644 index 0000000000..f910b8e35d --- /dev/null +++ b/resources/build/meson/standard_meson_build.inc.sh @@ -0,0 +1,35 @@ +# +# Keyman is copyright (C) SIL Global. MIT License. +# +# Created by mcdurdin on 2025-04-30 +# +# This import script helps us setup a consistent meson environment across all +# our build scripts. It will generate two files in the calling scripts +# resources/ folder (which should be git-ignored): +# +# * resources/meson.build - a copy of standard.meson.build from this folder +# * resources/KEYMAN_VERSION_WITH_TAG.md - the value of the env var $KEYMAN_VERSION_WITH_TAG +# +# We do this because meson doesn't allow us to: +# +# * reference a file outside the project root, or +# * read environment variables +# +# Sample of normal usage: +# +# if builder_has_action configure; then +# # Import our standard compiler defines +# source "$KEYMAN_ROOT/resources/build/meson/standard_meson_build.inc.sh" +# standard_meson_build +# fi +# +# ${THIS_SCRIPT_PATH}/meson.build then includes `resources` as a subdir: +# +# subdir('resources') +# + +standard_meson_build() { + mkdir -p "$THIS_SCRIPT_PATH/resources" + cp "$KEYMAN_ROOT/resources/build/meson/standard.meson.build" "$THIS_SCRIPT_PATH/resources/meson.build" + echo "$KEYMAN_VERSION_WITH_TAG" > "$THIS_SCRIPT_PATH/resources/KEYMAN_VERSION_WITH_TAG.md" +} diff --git a/resources/build/minimum-versions.inc.sh b/resources/build/minimum-versions.inc.sh index a8ad69be05..49e546c0fb 100644 --- a/resources/build/minimum-versions.inc.sh +++ b/resources/build/minimum-versions.inc.sh @@ -21,21 +21,23 @@ KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME=53.0 # min version of Chrome for Keyman # earlier versions, YMMV KEYMAN_MIN_TARGET_VERSION_WEB_CHROME=95.0 # Note: 95.0 is final version that runs on Android 5.0 KEYMAN_MIN_TARGET_VERSION_WEB_FIREFOX=79.0 # TBD -KEYMAN_MIN_TARGET_VERSION_WEB_OPERA=47.0 # TBD +KEYMAN_MIN_TARGET_VERSION_WEB_OPERA=58.0 # TBD KEYMAN_MIN_TARGET_VERSION_WEB_SAFARI=13.0 # iOS 13.0, macOS 10.13.6+ -# Dependency versions +# Dependency minimum versions KEYMAN_MIN_VERSION_NODE_MAJOR=20 # node version source of truth is /package.json:/engines/node; use KEYMAN_USE_NVM to automatically update KEYMAN_MIN_VERSION_NPM=10.5.1 # 10.5.0 has bug, discussed in #10350 KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.58 # Use KEYMAN_USE_EMSDK to automatically update to this version KEYMAN_MIN_VERSION_VISUAL_STUDIO=2019 KEYMAN_MIN_VERSION_MESON=1.0.0 -KEYMAN_VERSION_GRADLE=7.6.4 # See /android/KMEA/gradle/wrapper/gradle-wrapper.properties +KEYMAN_VERSION_GRADLE=8.12 # See /android/KMEA/gradle/wrapper/gradle-wrapper.properties + +# Dependencies with fixed versions KEYMAN_VERSION_ICU=73.1 # See /core/subprojects/icu-minimal.wrap # Language and runtime versions -KEYMAN_VERSION_JAVA=11 # We're using Java/OpenJDK 11 +KEYMAN_VERSION_JAVA=21 # We're using Java/OpenJDK 21 KEYMAN_MIN_VERSION_CPP=17 # C++17 KEYMAN_MIN_VERSION_ANDROID_SDK=21 @@ -43,8 +45,8 @@ KEYMAN_MIN_VERSION_ANDROID_SDK=21 KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER=noble # Ubuntu 24.04 Noble # Data versions -- see resources/standards-data/readme.md -KEYMAN_VERSION_CLDR=45 # LDML Keyboards version +KEYMAN_VERSION_CLDR=46 # LDML Keyboards version KEYMAN_VERSION_ISO639_3=2024-05-22 # Date of last import -KEYMAN_VERSION_LANGTAGS=2025-02-18 # Date of last import +KEYMAN_VERSION_LANGTAGS=2025-02-18 # _version value KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY=2025-03-10 # Date from first line of language-subtag-registry KEYMAN_VERSION_UNICODE=16.0.0 # UCD + related data diff --git a/resources/build/minimum-versions.tests.inc.sh b/resources/build/minimum-versions.tests.inc.sh new file mode 100644 index 0000000000..b961124081 --- /dev/null +++ b/resources/build/minimum-versions.tests.inc.sh @@ -0,0 +1,90 @@ +# +# Keyman is copyright (C) SIL Global. MIT License. +# +# Created by mcdurdin on 2025-04-30 +# +# Test that resources in our repository match the expected minimum versions, +# where possible. Uses a variety of mechanisms to extract the version +# + +. "${KEYMAN_ROOT}/resources/build/jq.inc.sh" + +# +# Tests that the referenced filename has the expected version +# found in the input variable. The actual version is extracted +# with a helper function named _minver_test_$variable, which +# parses the file or its metadata, as this may vary considerably +# +# Parameters +# 1: variable the name of the environment variable to test +# 2: filename the file to test the version against +# +_minver_test() { + local variable="$1" + local filename="$2" + + local actual_version="$(_minver_test_${variable} "$filename")" + local -n expected_version="${variable}" + + builder_echo "Testing \$$1, expecting '$expected_version'" + if [[ "$expected_version" != "$actual_version" ]]; then + builder_die "$filename: expected '$expected_version', actual version in repo is '$actual_version'" + fi +} + +_minver_test_KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY() { + # language-subtag-registry format: + # File-Date: 2025-03-10 + head "$1" -n 1 | cut -d" " -f 2 - +} + +_minver_test_KEYMAN_VERSION_UNICODE() { + # Blocks.txt format: + # # Blocks-16.0.0.txt + head "$1" -n 1 | cut -d- -f 2 - | cut -d. -f 1,2,3 - +} + +_minver_test_KEYMAN_VERSION_LANGTAGS() { + # entry with tag '_version' + "$JQ" -r -c '.[] | select(.tag == "_version") .date' "$1" +} + +_minver_test_KEYMAN_VERSION_CLDR() { + # finds the folder with the expected name + ## find "$1" -name '[0-9][0-9]' -type d -printf '%f' + # note macos find does not support -printf, so went with this: + find "$1" -name '[0-9][0-9]' -type d -exec basename {} \; +} + +_minver_test_KEYMAN_VERSION_ISO639_3() { + # git commit log for last commit for the file + git log -1 --format="%cs" -- "$1" +} + +_minver_test_KEYMAN_VERSION_ICU() { + # source_filename = icu4c-73_1-src.tgz + grep "source_filename" "$1" | cut -d- -f 2 - | tr _ . +} + +_minver_test_KEYMAN_MIN_VERSION_NODE_MAJOR() { + # package.json, major.minor.patch, we use only major + "$JQ" -r '.engines.node' "$1" | cut -d. -f 1 - +} + +# +# Test that the minimum version of imported resources matches as expected +# +minver_test_all() { + local STANDARDS_DATA="$KEYMAN_ROOT/resources/standards-data" + + _minver_test KEYMAN_MIN_VERSION_NODE_MAJOR "$KEYMAN_ROOT/package.json" + + _minver_test KEYMAN_VERSION_ICU "$KEYMAN_ROOT/core/subprojects/icu-minimal.wrap" + + _minver_test KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY "$STANDARDS_DATA/language-subtag-registry/language-subtag-registry" + _minver_test KEYMAN_VERSION_UNICODE "$STANDARDS_DATA/unicode-character-database/Blocks.txt" + _minver_test KEYMAN_VERSION_UNICODE "$STANDARDS_DATA/unicode-character-database/WordBreakProperty.txt" + _minver_test KEYMAN_VERSION_LANGTAGS "$STANDARDS_DATA/langtags/langtags.json" + _minver_test KEYMAN_VERSION_CLDR "$STANDARDS_DATA/ldml-keyboards/" + _minver_test KEYMAN_VERSION_ISO639_3 "$STANDARDS_DATA/iso639-3/iso639-3.tab" +} \ No newline at end of file diff --git a/resources/build/publish-minimum-versions.sh b/resources/build/publish-minimum-versions.sh index 8c420acb6c..4a65c3352d 100755 --- a/resources/build/publish-minimum-versions.sh +++ b/resources/build/publish-minimum-versions.sh @@ -12,6 +12,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" ## END STANDARD BUILD SCRIPT INCLUDE . "${KEYMAN_ROOT}/resources/build/minimum-versions.inc.sh" +. "${KEYMAN_ROOT}/resources/build/minimum-versions.tests.inc.sh" builder_describe "Build and test minimum-versions" \ clean build test @@ -30,7 +31,17 @@ generate_table() { # echo "${strarr[@]}" for n in ${!strarr[@]}; do - echo "| ${strarr[n]} | ${!strarr[n]} |" + case "${strarr[n]}" in + KEYMAN_VERSION | KEYMAN_VERSION_MAJOR | KEYMAN_VERSION_MINOR | \ + KEYMAN_VERSION_PATCH | KEYMAN_VERSION_RELEASE | KEYMAN_VERSION_WIN | \ + KEYMAN_VERSION_TAG | KEYMAN_VERSION_WITH_TAG | \ + KEYMAN_VERSION_ENVIRONMENT | KEYMAN_VERSION_GIT_TAG) + # ignore the variables that contain the Keyman version + ;; + *) + echo "| ${strarr[n]} | ${!strarr[n]} |" + ;; + esac done echo "" @@ -53,18 +64,12 @@ do_test() { builder_die "minimum-versions.md is not up to date. Please run 'publish-minimum-versions.sh build'" ) - rm "$new_table" + rm "$new_table" - # TODO: this should have a test for each of the known machine-extractable versions. - - # language-subtag-registry - - local NEW_KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY="$(head ../standards-data/language-subtag-registry/language-subtag-registry -n 1 | cut -d" " -f 2 -)" - if [[ "$NEW_KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY" != "$KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY" ]]; then - builder_die "language-subtag-registry is version '$NEW_KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY' but minimum-version.inc.sh has version '$KEYMAN_VERSION_LANGUAGE_SUBTAG_REGISTRY'" - fi + # From minimum-versions.tests.inc.sh + minver_test_all } builder_run_action clean rm -f "$KEYMAN_ROOT/docs/minimum-versions.md" builder_run_action build do_build -builder_run_action test do_test \ No newline at end of file +builder_run_action test do_test diff --git a/resources/build/run-required-test-builds.sh b/resources/build/run-required-test-builds.sh index eada3e90ca..c0b3ce8680 100755 --- a/resources/build/run-required-test-builds.sh +++ b/resources/build/run-required-test-builds.sh @@ -166,8 +166,10 @@ while IFS= read -r line; do # Which platform are we watching? eval watch='$'watch_$platform # Add common patterns to the watch list - watch="^($platform|(oem/[^/]+/$platform)|resources|$watch)" - if [[ "$line" =~ $watch ]]; then + watch="^(${platform}|(oem/[^/]+/${platform})|resources/((?!teamcity)|teamcity/(${platform}|includes))|${watch})" + # Since bash doesn't support negative look-aheads we use perl to test + # (grep has a --perl-regexp option, but not the version on macOS) + if perl -e 'exit($ARGV[0] =~ /$ARGV[1]/ ? 0 : 1)' "${line}" "${watch}"; then build_platforms+=($platform) fi fi diff --git a/resources/build/sentry-control.inc.sh b/resources/build/sentry-control.inc.sh index e7909e4b99..b01695af19 100644 --- a/resources/build/sentry-control.inc.sh +++ b/resources/build/sentry-control.inc.sh @@ -27,14 +27,14 @@ function makeSentryRelease() { if isSentryConfigured; then if isSentryCliAvailable; then # This version tag matches the repository version tag release@x.y.z - echo "Making a Sentry release for tag $VERSION_GIT_TAG" - sentry-cli releases new -p keyman-android -p keyman-developer -p keyman-ios -p keyman-linux -p keyman-mac -p keyman-web -p keyman-windows $VERSION_GIT_TAG + echo "Making a Sentry release for tag $KEYMAN_VERSION_GIT_TAG" + sentry-cli releases new -p keyman-android -p keyman-developer -p keyman-ios -p keyman-linux -p keyman-mac -p keyman-web -p keyman-windows $KEYMAN_VERSION_GIT_TAG - echo "Setting commits for release tag $VERSION_GIT_TAG" - sentry-cli releases set-commits --auto $VERSION_GIT_TAG + echo "Setting commits for release tag $KEYMAN_VERSION_GIT_TAG" + sentry-cli releases set-commits --auto $KEYMAN_VERSION_GIT_TAG - echo "Finalizing release tag $VERSION_GIT_TAG" - sentry-cli releases finalize "$VERSION_GIT_TAG" + echo "Finalizing release tag $KEYMAN_VERSION_GIT_TAG" + sentry-cli releases finalize "$KEYMAN_VERSION_GIT_TAG" fi fi } diff --git a/resources/build/set-bundle-versions-and-settings-tagged.sh b/resources/build/set-bundle-versions-and-settings-tagged.sh index 9ad0e9758c..6985ae3c3c 100755 --- a/resources/build/set-bundle-versions-and-settings-tagged.sh +++ b/resources/build/set-bundle-versions-and-settings-tagged.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # Calls script in xcode-utils to update the version -# true: applies VERSION_WITH_TAG to custom KeymanVersionWithTag plist member used for in-app display +# true: applies KEYMAN_VERSION_WITH_TAG to custom KeymanVersionWithTag plist member used for in-app display # updates the version string for Settings source "$KEYMAN_ROOT/resources/build/xcode-utils.sh" phaseSetBundleVersions true -setSettingsBundleVersion \ No newline at end of file +setSettingsBundleVersion diff --git a/resources/build/set-bundle-versions-and-settings-untagged.sh b/resources/build/set-bundle-versions-and-settings-untagged.sh index 38256c7a9a..93cd8a7eab 100755 --- a/resources/build/set-bundle-versions-and-settings-untagged.sh +++ b/resources/build/set-bundle-versions-and-settings-untagged.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # Calls script in xcode-utils to update the version -# true: applies VERSION_WITH_TAG to custom KeymanVersionWithTag plist member used for in-app display +# true: applies KEYMAN_VERSION_WITH_TAG to custom KeymanVersionWithTag plist member used for in-app display # updates the version string for Settings source "$KEYMAN_ROOT/resources/build/xcode-utils.sh" phaseSetBundleVersions -setSettingsBundleVersion \ No newline at end of file +setSettingsBundleVersion diff --git a/resources/build/set-bundle-versions-tagged.sh b/resources/build/set-bundle-versions-tagged.sh index 8b4075a037..a941d460fe 100755 --- a/resources/build/set-bundle-versions-tagged.sh +++ b/resources/build/set-bundle-versions-tagged.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Calls script in xcode-utils to update the version -# true: applies VERSION_WITH_TAG to custom KeymanVersionWithTag plist member used for in-app display +# true: applies KEYMAN_VERSION_WITH_TAG to custom KeymanVersionWithTag plist member used for in-app display source "$KEYMAN_ROOT/resources/build/xcode-utils.sh" -phaseSetBundleVersions true \ No newline at end of file +phaseSetBundleVersions true diff --git a/resources/build/test/debug-deps/test.sh b/resources/build/test/debug-deps/test.sh index fe0c29c32f..9ac32d474a 100755 --- a/resources/build/test/debug-deps/test.sh +++ b/resources/build/test/debug-deps/test.sh @@ -17,6 +17,6 @@ function do_build() { echo "PASS: parent: builder_has_option --debug is true" } -builder_parse build:child -d +builder_parse build:child --debug builder_run_action build do_build builder_run_child_actions build diff --git a/resources/build/test/test.sh b/resources/build/test/test.sh index 1cbaa25476..75bffe4637 100755 --- a/resources/build/test/test.sh +++ b/resources/build/test/test.sh @@ -89,6 +89,12 @@ fi builder_parse_test() { local expected="$1" local expected_options="$2" + + # When testing in a local environment, --debug will be added by default (see #11106) + if [[ $KEYMAN_VERSION_ENVIRONMENT == "local" ]]; then + expected_options="${expected_options} --debug" + fi + shift shift local parameters="$@" @@ -97,8 +103,8 @@ builder_parse_test() { if [[ "$expected" != "${_builder_chosen_action_targets[@]}" ]]; then builder_die " Test: builder_parse $parameters action:target != \"$expected\"" fi - if [[ "$expected_options" != "${_builder_chosen_options[@]}" ]]; then - builder_die " Test: builder_parse $parameters, options != \"$expected\"" + if [[ "${expected_options}" != "${_builder_chosen_options[@]}" ]]; then + builder_die " Test: builder_parse $parameters, options != \"${expected_options}\"" fi } @@ -184,10 +190,173 @@ fi echo -e "${COLOR_BLUE}## Testing output of: builder_parse --feature xyzzy --bar abc --baz def test${COLOR_RESET}" parse_output=$(builder_parse --feature xyzzy --bar abc --baz def test) expected="$(builder_echo setmark "test.sh parameters: <--feature xyzzy --bar abc --baz def test>")" + +if [[ $KEYMAN_VERSION_ENVIRONMENT == "local" ]]; then + # When run in a local-dev environment, an extra line appears about the automatic --debug option. + expected="$(builder_echo grey "Local build environment detected: setting --debug")"$'\n'"$(builder_echo setmark "test.sh parameters: <--feature xyzzy --bar abc --baz def test --debug>")" +fi if [[ "${parse_output[*]}" != "${expected}" ]]; then builder_die "FAIL: Wrong output for '--feature xyzzy --bar abc --baz def test':\n Actual : ${parse_output[*]}\n Expected: ${expected}" fi +#---------------------------------------------------------------------- +function builder_echo_tests() { + echo -e "${COLOR_BLUE}## Testing builder_echo (heading)${COLOR_RESET}" + local _old_builder_debug_internal=${_builder_debug_internal} + + # regular build, no internal debug + _builder_debug_internal=false + expected="$(echo -e "${BUILDER_BOLD}${COLOR_BRIGHT_WHITE}[resources/build/test]${COLOR_RESET} ${COLOR_BLUE}description${COLOR_RESET}")" + result=$(builder_echo heading "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo heading description' (no debug):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + + # regular build, with internal debug + _builder_debug_internal=true + expected="$(echo -e "${BUILDER_BOLD}${COLOR_BRIGHT_WHITE}[resources/build/test]${COLOR_RESET} ${COLOR_BLUE}description${COLOR_RESET}")" + result=$(builder_echo heading "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo start foo description' (debug):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + _builder_debug_internal=${_old_builder_debug_internal} +} + +#---------------------------------------------------------------------- +function builder_echo_start_end_tests() { + echo -e "${COLOR_BLUE}## Testing builder_echo (start/end)${COLOR_RESET}" + local _old_builder_debug_internal=${_builder_debug_internal} + local _OLD_TEAMICITY_GIT_PATH=${TEAMCITY_GIT_PATH:-} + local _old_builder_is_child=${_builder_is_child} + + # regular build, no internal debug, no child build + TEAMCITY_GIT_PATH="" + _builder_debug_internal=false + _builder_is_child=1 + expected="$(builder_echo heading "description")" + result=$(builder_echo start "foo" "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo start foo description' (no debug, no child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="$(builder_echo success "description")" + result=$(builder_echo end "foo" success "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo success description' (no debug, no child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="$(builder_echo error "description")" + result=$(builder_echo end "foo" error "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo error description' (no debug, no child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + + # regular build, no internal debug, is child build + TEAMCITY_GIT_PATH="" + _builder_debug_internal=false + _builder_is_child=0 + expected="" + result=$(builder_echo start "foo" "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo start foo description' (no debug, is child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="" + result=$(builder_echo end "foo" success "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo success description' (no debug, is child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="$(builder_echo error "description")" + result=$(builder_echo end "foo" error "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo error description' (no debug, is child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + + # regular build, with internal debug, no child build + _builder_debug_internal=true + _builder_is_child=1 + expected="$(builder_echo heading "description")" + result=$(builder_echo start "foo" "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo start foo description' (debug, no child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="$(builder_echo success "description")" + result=$(builder_echo end "foo" success "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo success description' (debug, no child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="$(builder_echo error "description")" + result=$(builder_echo end "foo" error "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo error description' (debug, no child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + + # regular build, with internal debug, is child build + _builder_debug_internal=true + _builder_is_child=0 + expected="$(builder_echo heading "description")" + result=$(builder_echo start "foo" "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo start foo description' (debug, is child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="$(builder_echo success "description")" + result=$(builder_echo end "foo" success "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo success description' (debug, is child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="$(builder_echo error "description")" + result=$(builder_echo end "foo" error "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo error description' (debug, is child):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + + # simulate running on TeamCity + TEAMCITY_GIT_PATH="foo" + # TC build, no internal debug + _builder_debug_internal=false + expected="##teamcity[blockOpened name='|[resources/build/test|] foo'] +$(builder_echo heading "description")" + result=$(builder_echo start "foo" "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo start foo description' on TC (no debug):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="$(builder_echo success "description") +##teamcity[blockClosed name='|[resources/build/test|] foo']" + result=$(builder_echo end "foo" success "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo success description' on TC (no debug):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="$(builder_echo error "description") +##teamcity[blockClosed name='|[resources/build/test|] foo']" + result=$(builder_echo end "foo" error "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo error description' on TC (no debug):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + + # TC build, with internal debug + _builder_debug_internal=true + expected="##teamcity[blockOpened name='|[resources/build/test|] foo'] +$(builder_echo heading "description")" + result=$(builder_echo start "foo" "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo start foo description' on TC (debug):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="$(builder_echo success "description") +##teamcity[blockClosed name='|[resources/build/test|] foo']" + result=$(builder_echo end "foo" success "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo success description' on TC (debug):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + expected="$(builder_echo error "description") +##teamcity[blockClosed name='|[resources/build/test|] foo']" + result=$(builder_echo end "foo" error "description") + if [[ "${result[*]}" != "${expected}" ]]; then + builder_die "FAIL: Wrong output for 'builder_echo end foo error description' on TC (debug):\n Actual : ${result[*]}\n Expected: ${expected}" + fi + TEAMCITY_GIT_PATH="${_OLD_TEAMICITY_GIT_PATH}" + _builder_debug_internal=${_old_builder_debug_internal} + _builder_is_child=${_old_builder_is_child} +} +builder_echo_tests +builder_echo_start_end_tests + # Run tests based in separate scripts to facilitate their operation #---------------------------------------------------------------------- diff --git a/resources/build/trigger-builds.inc.sh b/resources/build/trigger-builds.inc.sh index 42de09dae1..639c3d240b 100644 --- a/resources/build/trigger-builds.inc.sh +++ b/resources/build/trigger-builds.inc.sh @@ -78,9 +78,9 @@ function triggerGitHubActionsBuild() { if [ "${action:-""}" == "commit" ]; then # This will only be true if we created and pushed a tag - GIT_BUILD_SHA="$(git rev-parse "refs/tags/release@$VERSION_WITH_TAG")" + GIT_BUILD_SHA="$(git rev-parse "refs/tags/release@$KEYMAN_VERSION_WITH_TAG")" GIT_BASE_REF="$(git rev-parse "${GIT_BUILD_SHA}^")" - GIT_EVENT_TYPE="${GITHUB_ACTION}: release@${VERSION_WITH_TAG}" + GIT_EVENT_TYPE="${GITHUB_ACTION}: release@${KEYMAN_VERSION_WITH_TAG}" elif [[ $GIT_BRANCH != stable-* ]] && [[ $GIT_BRANCH =~ [0-9]+ ]]; then local JSON=$(call_curl "${GITHUB_SERVER}/pulls/${GIT_BRANCH}" --header "Authorization: token $GITHUB_TOKEN") GIT_BUILD_SHA="$(echo "$JSON" | $JQ -r '.head.sha')" diff --git a/resources/build/trigger-definitions.inc.sh b/resources/build/trigger-definitions.inc.sh index aa8312f453..e4ed49d90d 100644 --- a/resources/build/trigger-definitions.inc.sh +++ b/resources/build/trigger-definitions.inc.sh @@ -49,7 +49,7 @@ bc_test_ios=(Keyman_iOS_TestPullRequests Keyman_iOS_TestSamplesAndTestProjects) bc_test_linux=(KeymanLinux_TestPullRequests Keyman_Linux_Test_Integration Keyman_Common_KPAPI_TestPullRequests_Linux deb-pr-packaging_GitHub) bc_test_mac=(Keyman_KeymanMac_PullRequests Keyman_Common_KPAPI_TestPullRequests_macOS) bc_test_windows=(KeymanDesktop_TestPullRequests KeymanDesktop_TestPrRenderOnScreenKeyboards Keyman_Common_KPAPI_TestPullRequests_Windows) -bc_test_web=(Keymanweb_TestPullRequests Keyman_Common_LMLayer_TestPullRequests Keyman_Common_KPAPI_TestPullRequests_WASM) +bc_test_web=(Keymanweb_TestPullRequests Keyman_Common_KPAPI_TestPullRequests_WASM) bc_test_developer=(Keyman_Developer_Test Keyman_Test_Developer_Mac Keyman_Test_Developer_Linux) bc_test_common_web=(Keyman_Test_Common_Web) @@ -62,7 +62,7 @@ bc_test_common_linux=(Keyman_Test_Common_Linux) vcs_test=HttpsGithubComKeymanappKeymanPRs -# Master (Alpha) Build Configurations; these diverge when we need new build configurations for new versions +# Master (Alpha) Build Configurations; these may diverge when we need new build configurations for new versions bc_master_android=(KeymanAndroid_Build) bc_master_ios=(Keyman_iOS_Master) @@ -86,57 +86,7 @@ bc_beta_developer=(Keyman_Developer_Release) vcs_beta=HttpsGithubComKeymanappKeyman -# Stable 14.0 Build Configurations - -bc_stable_14_0_android=(KeymanAndroid_Build) -bc_stable_14_0_ios=(Keyman_iOS_Master) -bc_stable_14_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub) -bc_stable_14_0_mac=(KeymanMac_Master) -bc_stable_14_0_windows=(Keyman_Build) -bc_stable_14_0_web=(Keymanweb_Build) - -# 16.0+ Keyman Developer is split from Windows, but TC configuration will not fail -# on older versions, just no-op -bc_stable_14_0_developer=(Keyman_Developer_Release) - -vcs_stable_14_0=HttpsGithubComKeymanappKeyman - -# Stable 15.0 Build Configurations - -bc_stable_15_0_android=(KeymanAndroid_Build) -bc_stable_15_0_ios=(Keyman_iOS_Master) -bc_stable_15_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub) -bc_stable_15_0_mac=(KeymanMac_Master) -bc_stable_15_0_windows=(Keyman_Build) -bc_stable_15_0_web=(Keymanweb_Build) - -vcs_stable_15_0=HttpsGithubComKeymanappKeyman - -# Stable 16.0 Build Configurations - -bc_stable_16_0_android=(KeymanAndroid_Build) -bc_stable_16_0_ios=(Keyman_iOS_Master) -bc_stable_16_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub) -bc_stable_16_0_mac=(KeymanMac_Master) -bc_stable_16_0_windows=(Keyman_Build) -bc_stable_16_0_web=(Keymanweb_Build) -bc_stable_16_0_developer=(Keyman_Developer_Release) - -vcs_stable_16_0=HttpsGithubComKeymanappKeyman - -# Stable 17.0 Build Configurations - -bc_stable_17_0_android=(KeymanAndroid_Build) -bc_stable_17_0_ios=(Keyman_iOS_Master) -bc_stable_17_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub) -bc_stable_17_0_mac=(KeymanMac_Master) -bc_stable_17_0_windows=(Keyman_Build) -bc_stable_17_0_web=(Keymanweb_Build) -bc_stable_17_0_developer=(Keyman_Developer_Release) - -vcs_stable_17_0=HttpsGithubComKeymanappKeyman - -# Stable 17.0 Build Configurations +# Stable 18.0 Build Configurations bc_stable_18_0_android=(KeymanAndroid_Build) bc_stable_18_0_ios=(Keyman_iOS_Master) @@ -147,3 +97,15 @@ bc_stable_18_0_web=(Keymanweb_Build) bc_stable_18_0_developer=(Keyman_Developer_Release) vcs_stable_18_0=HttpsGithubComKeymanappKeyman + +# Stable 19.0 Build Configurations + +bc_stable_19_0_android=(KeymanAndroid_Build) +bc_stable_19_0_ios=(Keyman_iOS_Master) +bc_stable_19_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub) +bc_stable_19_0_mac=(KeymanMac_Master) +bc_stable_19_0_windows=(Keyman_Build) +bc_stable_19_0_web=(Keymanweb_Build) +bc_stable_19_0_developer=(Keyman_Developer_Release) + +vcs_stable_19_0=HttpsGithubComKeymanappKeyman diff --git a/resources/build/version/package.json b/resources/build/version/package.json index ef27eb0fa1..7eaf125cdc 100644 --- a/resources/build/version/package.json +++ b/resources/build/version/package.json @@ -3,7 +3,7 @@ "type": "module", "dependencies": { "@actions/core": "^1.9.1", - "@actions/github": "^2.1.0", + "@actions/github": "^6.0.1", "emoji-regex": "^10.3.0", "typescript": "^5.4.5", "yargs": "^17.7.2" diff --git a/resources/build/version/src/fixupHistory.ts b/resources/build/version/src/fixupHistory.ts index 1d6a290957..4f34c0c13d 100644 --- a/resources/build/version/src/fixupHistory.ts +++ b/resources/build/version/src/fixupHistory.ts @@ -2,8 +2,10 @@ import { warning as logWarning } from '@actions/core'; -import { GitHub } from '@actions/github'; -import { readFileSync, writeFileSync } from 'fs'; +import { getOctokit } from '@actions/github'; +type GitHub = ReturnType; + +import { readFileSync, writeFileSync } from 'node:fs'; import { gt } from 'semver'; import { reportHistory } from './reportHistory.js'; import { spawnChild } from './util/spawnAwait.js'; @@ -39,7 +41,7 @@ const splicePullsIntoHistory = async (pulls: PRInformation[], base?: string): Pr tier = (await spawnChild('git', ['show', base+':TIER.md'])).trim(); } } - //logInfo(`VERSION="${version}"`); + //logInfo(`KEYMAN_VERSION="${version}"`); // // Load and parse history file, looking for our version @@ -176,7 +178,7 @@ export const sendCommentToPullRequestAndRelatedIssues = async ( // Always serially rather than in parallel .then(() => { console.log(`Creating comment on Pull Request #${pull}`); - return octokit.issues.createComment({ + return octokit.rest.issues.createComment({ owner: 'keymanapp', repo: 'keyman', issue_number: pull, diff --git a/resources/build/version/src/incrementVersion.ts b/resources/build/version/src/incrementVersion.ts index 6183b7f9c4..2d7ea67ab6 100644 --- a/resources/build/version/src/incrementVersion.ts +++ b/resources/build/version/src/incrementVersion.ts @@ -1,11 +1,11 @@ -import { readFileSync, writeFileSync } from 'fs'; +import { readFileSync, writeFileSync } from 'node:fs'; // ------------------------------------------------------------------------------------ // incrementVersion // ------------------------------------------------------------------------------------ export const incrementVersion = () => { - // 1. + // 1. const version = readFileSync('./VERSION.md', 'utf8'); const triplet = version.split('.'); const patch = parseInt(triplet[2], 10) + 1; diff --git a/resources/build/version/src/index.ts b/resources/build/version/src/index.ts index ea0b12e7e7..867e8274eb 100644 --- a/resources/build/version/src/index.ts +++ b/resources/build/version/src/index.ts @@ -1,11 +1,12 @@ import { info as logInfo } from '@actions/core'; -import { GitHub } from '@actions/github'; +import { getOctokit } from '@actions/github'; +type GitHub = ReturnType; import { sendCommentToPullRequestAndRelatedIssues, fixupHistory } from './fixupHistory.js'; import { incrementVersion } from './incrementVersion.js'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { readFileSync } from 'fs'; +import { readFileSync } from 'node:fs'; import { reportHistory } from './reportHistory.js'; const argv = await yargs(hideBin(process.argv)) @@ -59,7 +60,7 @@ const argv = await yargs(hideBin(process.argv)) const main = async (): Promise => { - const octokit: GitHub = new GitHub(argv.token); + const octokit: GitHub = getOctokit(argv.token); // Pretend we have a single change. If we chain commands (as in normal usage), // then we use the real history change count to determine if we continue. diff --git a/resources/build/version/src/reportHistory.ts b/resources/build/version/src/reportHistory.ts index e3c670265f..91bfb53d33 100644 --- a/resources/build/version/src/reportHistory.ts +++ b/resources/build/version/src/reportHistory.ts @@ -1,6 +1,7 @@ import { warning as logWarning, info as logInfo } from '@actions/core'; -import { GitHub } from '@actions/github'; +import { getOctokit } from '@actions/github'; +type GitHub = ReturnType; import { findLastHistoryPR, getAssociatedPR} from './graphql/queries.js'; import { spawnChild } from './util/spawnAwait.js'; diff --git a/resources/build/win/environment.inc.sh b/resources/build/win/environment.inc.sh index 71c18f305b..6da5ad039f 100644 --- a/resources/build/win/environment.inc.sh +++ b/resources/build/win/environment.inc.sh @@ -32,7 +32,7 @@ COMMON_ROOT="$KEYMAN_ROOT/common/windows/delphi" OUTLIB="$WINDOWS_ROOT/lib" COMMON_OUTLIB="$KEYMAN_ROOT/common/windows/lib" -if builder_is_debug_build || [[ $VERSION_ENVIRONMENT == local ]] || [[ ! -z ${TEAMCITY_PR_NUMBER+x} ]]; then +if builder_is_debug_build || [[ $KEYMAN_VERSION_ENVIRONMENT == local ]] || [[ ! -z ${TEAMCITY_PR_NUMBER+x} ]]; then # We do a fast build for debug builds, local builds, test PR builds but not for master/beta/stable release builds GO_FAST=1 else @@ -125,8 +125,8 @@ wrap-symstore() { "$ProgramFilesx86/Windows Kits/10/Debuggers/x64/symstore.exe" \ add \ //s "$KEYMAN_SYMSTOREPATH" \ - //v "$VERSION_WIN" \ - //c "Version: $VERSION_WITH_TAG" \ + //v "$KEYMAN_VERSION_WIN" \ + //c "Version: $KEYMAN_VERSION_WITH_TAG" \ //compress //f "$@" } @@ -147,4 +147,4 @@ create-windows-output-folders() { mkdir -p "$WINDOWS_DEBUGPATH_APP" mkdir -p "$WINDOWS_DEBUGPATH_ENGINE" mkdir -p "$WINDOWS_DEBUGPATH_SUPPORT" -} \ No newline at end of file +} diff --git a/resources/build/xcode-utils.sh b/resources/build/xcode-utils.sh index 9476161ae6..b917b5d63f 100755 --- a/resources/build/xcode-utils.sh +++ b/resources/build/xcode-utils.sh @@ -42,10 +42,10 @@ function phaseSetBundleVersions() { TAGGED=false fi - # For command-line builds, VERSION and VERSION_WITH_TAG) are forwarded through xcodebuild. - if [ -z "${VERSION:-}" ]; then + # For command-line builds, KEYMAN_VERSION and KEYMAN_VERSION_WITH_TAG) are forwarded through xcodebuild. + if [ -z "${KEYMAN_VERSION:-}" ]; then # This is likely not a command-line build at the top level; it's been triggered through Xcode. - # The $VERSION-loading code is in build-utils.sh, but we need $THIS_SCRIPT to be set + # The $KEYMAN_VERSION-loading code is in build-utils.sh, but we need $THIS_SCRIPT to be set # (in similar manner to the "standard build header") in order to avoid script errors therein. THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" @@ -53,9 +53,9 @@ function phaseSetBundleVersions() { # local builds triggered through Xcode's UI, which aren't part of our CI processes. . "$KEYMAN_ROOT/resources/build/build-utils.sh" echo "phaseSetBundleVersions: UI build - fetching version from repository:" - echo " Plain: $VERSION" - echo " Tagged: $VERSION_WITH_TAG" - echo " Environment: $VERSION_ENVIRONMENT" + echo " Plain: $KEYMAN_VERSION" + echo " Tagged: $KEYMAN_VERSION_WITH_TAG" + echo " Environment: $KEYMAN_VERSION_ENVIRONMENT" else echo "phaseSetBundleVersions: Command-line build - using provided version parameters" fi @@ -76,48 +76,48 @@ function phaseSetBundleVersions() { APP_PLIST="$TARGET_BUILD_DIR/$INFOPLIST_PATH" echo "phaseSetBundleVersions: Setting CFBundleVersion to $BUILD_NUMBER for $APP_PLIST" /usr/libexec/Plistbuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$APP_PLIST" - echo "phaseSetBundleVersions: Setting CFBundleShortVersionString to $VERSION for $APP_PLIST" - /usr/libexec/Plistbuddy -c "Set :CFBundleShortVersionString $VERSION" "$APP_PLIST" + echo "phaseSetBundleVersions: Setting CFBundleShortVersionString to $KEYMAN_VERSION for $APP_PLIST" + /usr/libexec/Plistbuddy -c "Set :CFBundleShortVersionString $KEYMAN_VERSION" "$APP_PLIST" /usr/libexec/Plistbuddy -c "Print" "$APP_PLIST" # Only attempt to write this when directly specified (otherwise, generates minor warning) if [ $TAGGED == true ]; then - echo "phaseSetBundleVersions: Setting KeymanVersionWithTag to $VERSION_WITH_TAG for tagged version for $APP_PLIST" - /usr/libexec/Plistbuddy -c "Set :KeymanVersionWithTag $VERSION_WITH_TAG" "$APP_PLIST" - echo "phaseSetBundleVersions: Setting KeymanVersionEnvironment to $VERSION_ENVIRONMENT for $APP_PLIST" - /usr/libexec/Plistbuddy -c "Set :KeymanVersionEnvironment $VERSION_ENVIRONMENT" "$APP_PLIST" + echo "phaseSetBundleVersions: Setting KeymanVersionWithTag to $KEYMAN_VERSION_WITH_TAG for tagged version for $APP_PLIST" + /usr/libexec/Plistbuddy -c "Set :KeymanVersionWithTag $KEYMAN_VERSION_WITH_TAG" "$APP_PLIST" + echo "phaseSetBundleVersions: Setting KeymanVersionEnvironment to $KEYMAN_VERSION_ENVIRONMENT for $APP_PLIST" + /usr/libexec/Plistbuddy -c "Set :KeymanVersionEnvironment $KEYMAN_VERSION_ENVIRONMENT" "$APP_PLIST" fi if [ -f "${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}.dSYM/Contents/Info.plist" ]; then DSYM_PLIST="${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}.dSYM/Contents/Info.plist" echo "phaseSetBundleVersions: Setting CFBundleVersion to $BUILD_NUMBER for $DSYM_PLIST" /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$DSYM_PLIST" - echo "phaseSetBundleVersions: Setting CFBundleShortVersionString to $VERSION for $DSYM_PLIST" - /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$DSYM_PLIST" + echo "phaseSetBundleVersions: Setting CFBundleShortVersionString to $KEYMAN_VERSION for $DSYM_PLIST" + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $KEYMAN_VERSION" "$DSYM_PLIST" fi } # Used by Keyman for iOS to update the human-readable string for its Settings screen. function setSettingsBundleVersion() { - # For command-line builds, VERSION and VERSION_WITH_TAG) are forwarded through xcodebuild. - if [ -z "${VERSION:-}" ]; then + # For command-line builds, KEYMAN_VERSION and KEYMAN_VERSION_WITH_TAG) are forwarded through xcodebuild. + if [ -z "${KEYMAN_VERSION:-}" ]; then # We're not a command-line build... so we'll need to retrieve these values ourselves with ./build-utils.sh. # Note that this script's process will not have access to TC environment variables, but that's fine for # local builds triggered through Xcode's UI, which aren't part of our CI processes. . "$KEYMAN_ROOT/resources/build/build-utils.sh" echo "setSettingsBundleVersion: UI build - fetching version from repository:" - echo " Plain: $VERSION" - echo " Tagged: $VERSION_WITH_TAG" + echo " Plain: $KEYMAN_VERSION" + echo " Tagged: $KEYMAN_VERSION_WITH_TAG" echo " Environment: (not setting, assume 'local')" else echo "setSettingsBundleVersion: Command-line build - using provided version parameters" fi SETTINGS_PLIST="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Settings.bundle/Root.plist" - echo "setSettingsBundleVersion: Setting $VERSION_WITH_TAG for $SETTINGS_PLIST" + echo "setSettingsBundleVersion: Setting $KEYMAN_VERSION_WITH_TAG for $SETTINGS_PLIST" # We assume that entry 0 = the version "preference" entry. - /usr/libexec/PlistBuddy -c "Set :PreferenceSpecifiers:0:DefaultValue $VERSION_WITH_TAG" "$SETTINGS_PLIST" + /usr/libexec/PlistBuddy -c "Set :PreferenceSpecifiers:0:DefaultValue $KEYMAN_VERSION_WITH_TAG" "$SETTINGS_PLIST" } # Build Phase: Upload dSYM (debug) files to Sentry diff --git a/resources/build/zip.inc.sh b/resources/build/zip.inc.sh new file mode 100644 index 0000000000..1f80a71edb --- /dev/null +++ b/resources/build/zip.inc.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# +# This script contains zip and unzip (TODO) utilities to function for all environments. +# unzip is available in all environments +# zip is not available by default on Windows, but can be manually installed. +# If zip is not available, then fall back to 7z for zipping. +# 7z requires env SEVENZ_HOME. +# +# TODO: refactor with /resources/build/win/zip.inc.sh + +# Add files to create a zip/7z archive with the following parameters (in order) +# [zip filename] +# [list of flags to pass to zip command] Flags start with a single-dash +# -x@filename for a file containing list of files to exclude from the archive +# -* all other flags +# Flags passed in are treated as zip parameters, and internally converterted to 7z flags as applicable +# [list of files to include in zip] +function add_zip_files() { + + # Parse parameters + + # $1 for zip filename + local ZIP_FILE="$1" + shift + + # Parse rest of parameters + local ZIP_FLAGS=() + local SEVENZ_FLAGS=('a') # 7z requires a command + local INCLUDE=() + while [[ $# -gt 0 ]] ; do + case "$1" in + -r) + # recursive paths - Identical flag to zip and 7z + ZIP_FLAGS+=($1) + SEVENZ_FLAGS+=($1) + shift + ;; + -x@*) + # Filename for a file containing list of files to exclude from the archive - Identical flag to zip and 7z + ZIP_FLAGS+=($1) + SEVENZ_FLAGS+=($1) + shift + ;; + + # Zip flags that have a corresponding 7z flag + -q) + # quiet mode -> disable progress indicator, set output log level 0 + ZIP_FLAGS+=($1) + SEVENZ_FLAGS+=("-bd") + SEVENZ_FLAGS+=("-bb0") + shift + ;; + -[0123456789]) + # Compression level where + # -0 indicates no compression + # -1 indicates low compression (fastest) + # -9 indicates ultra compression (slowest) + ZIP_FLAGS+=($1) + if [[ $1 =~ -([0-9]) ]]; then + SEVENZ_FLAGS+=("-mx${BASH_REMATCH[1]}") + fi + shift; + ;; + + -*) + # Remaining zip flags that don't apply to 7z + ZIP_FLAGS+=($1) + shift + ;; + + *) + # files to include in the archive + INCLUDE+=($1) + shift + ;; + esac + done + + local COMPRESS_CMD=zip + if ! command -v zip 2>&1 > /dev/null; then + # Fallback to 7z + if [[ -z "${SEVENZ+x}" ]]; then + case "${OSTYPE}" in + "cygwin"|"msys") + SEVENZ="${SEVENZ_HOME}"/7z.exe + ;; + *) + SEVENZ=7z + ;; + esac + fi + + # 7z command to add files so clear zip flags + COMPRESS_CMD="${SEVENZ}" + ZIP_FLAGS=() + else + # Using zip so clear 7z flags + SEVENZ_FLAGS=() + fi + + # Create archive + # builder_echo_debug "${COMPRESS_CMD} ${SEVENZ_FLAGS[@]} ${ZIP_FLAGS[@]} ${ZIP_FILE} ${INCLUDE[@]}" + "${COMPRESS_CMD}" ${SEVENZ_FLAGS[@]} ${ZIP_FLAGS[@]} ${ZIP_FILE} ${INCLUDE[@]} + +} diff --git a/resources/builder.inc.sh b/resources/builder.inc.sh index 6e06313935..25c9b5994c 100755 --- a/resources/builder.inc.sh +++ b/resources/builder.inc.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2310 # Note: these two lines can be uncommented for debugging and profiling build # scripts: @@ -142,16 +143,27 @@ function builder_term() { } function builder_die() { - echo + _builder_error_echo if [[ $# -eq 0 ]]; then builder_echo error "Unspecified error, aborting script" else builder_echo error "$*" fi - echo + _builder_error_echo exit 1 } +# Emit message to stderr instead of stdout +function _builder_error_echo() { + # we only need to support -e flag + if [[ $# -gt 0 ]] && [[ $1 == -e ]]; then + shift + 2>&1 echo -e "$*" + else + 2>&1 echo "$*" + fi +} + function builder_warn() { builder_echo warning "$*" } @@ -169,35 +181,77 @@ function builder_heading() { builder_echo() { - local color=white message= mark= - if [[ $# -gt 1 && $1 =~ ^(white|grey|green|success|blue|heading|yellow|warning|red|error|purple|brightwhite|teal|debug|setmark)$ ]]; then - color="$1" - shift + local color=white message= mark= block= action= do_output=true test= + local echo_target=echo + + if [[ $# -gt 1 ]]; then + if [[ $1 =~ ^(white|grey|green|success|blue|heading|yellow|warning|red|error|purple|brightwhite|teal|debug|setmark)$ ]]; then + color="$1" + shift + elif [[ $1 == "start" ]] || [[ $1 == "startTest" ]]; then + # builder_echo start block message + test="$1" + block="$2" + shift 2 + action="start" + color="heading" + if ! builder_is_running_on_teamcity && builder_is_child_build; then + do_output=${_builder_debug_internal:-false} + fi + elif [[ $1 == "end" ]] || [[ $1 == "endTest" ]]; then + # builder_echo end block status message + test="$1" + block="$2" + color="$3" + shift 3 + action="end" + if [[ "${color}" != "error" ]] && ! builder_is_running_on_teamcity && builder_is_child_build; then + do_output=${_builder_debug_internal:-false} + fi + fi fi message="$*" - if [[ ! -z ${COLOR_RED+x} ]]; then - case $color in - white) color="$COLOR_WHITE" ;; - grey) color="$COLOR_GREY" ;; - green|success) color="$COLOR_GREEN" ;; - blue|heading) color="$COLOR_BLUE" ;; - yellow|warning) color="$COLOR_YELLOW" ;; - red|error) color="$COLOR_RED" ;; - purple) color="$COLOR_PURPLE" ;; - brightwhite) color="$COLOR_BRIGHTWHITE" ;; - teal|debug) color="$COLOR_TEAL" ;; - setmark) mark="$HEADING_SETMARK" color="$COLOR_PURPLE" ;; - esac - - if builder_is_dep_build; then - echo -e "$mark$COLOR_GREY[$THIS_SCRIPT_IDENTIFIER]$COLOR_RESET $color$message$COLOR_RESET" + if [[ "${action}" == "start" ]] && builder_is_running_on_teamcity; then + if [[ "${test}" == "startTest" ]]; then + $echo_target -e "##teamcity[testSuiteStarted name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']" else - echo -e "$mark$BUILDER_BOLD$COLOR_BRIGHT_WHITE[$THIS_SCRIPT_IDENTIFIER]$COLOR_RESET $color$message$COLOR_RESET" + $echo_target -e "##teamcity[blockOpened name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']" + fi + fi + + if ${do_output}; then + if [[ ! -z ${COLOR_RED+x} ]]; then + case $color in + white) color="$COLOR_WHITE" ;; + grey) color="$COLOR_GREY" ;; + green|success) color="$COLOR_GREEN" ;; + blue|heading) color="$COLOR_BLUE" ;; + yellow|warning) color="$COLOR_YELLOW" ;; + red|error) color="$COLOR_RED"; echo_target=_builder_error_echo ;; + purple) color="$COLOR_PURPLE" ;; + brightwhite) color="$COLOR_BRIGHTWHITE" ;; + teal|debug) color="$COLOR_TEAL" ;; + setmark) mark="$HEADING_SETMARK" color="$COLOR_PURPLE" ;; + esac + + if builder_is_dep_build; then + $echo_target -e "$mark$COLOR_GREY[$THIS_SCRIPT_IDENTIFIER]$COLOR_RESET $color$message$COLOR_RESET" + else + $echo_target -e "$mark$BUILDER_BOLD$COLOR_BRIGHT_WHITE[$THIS_SCRIPT_IDENTIFIER]$COLOR_RESET $color$message$COLOR_RESET" + fi + else + # Cope with the case of pre-init message and just emit plain text + $echo_target -e "$message" + fi + fi + + if [[ "${action}" == "end" ]] && builder_is_running_on_teamcity; then + if [[ "${test}" == "endTest" ]]; then + $echo_target -e "##teamcity[testSuiteFinished name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']" + else + $echo_target -e "##teamcity[blockClosed name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']" fi - else - # Cope with the case of pre-init message and just emit plain text - echo -e "$message" fi } @@ -393,9 +447,7 @@ _builder_execute_child() { local script="$THIS_SCRIPT_PATH/${_builder_target_paths[$target]}/build.sh" - if $_builder_debug_internal; then - builder_echo heading "## $action$target starting..." - fi + builder_echo start "$action$target" "## $action$target starting..." # Build array of specified inheritable options local child_options=() @@ -424,12 +476,10 @@ _builder_execute_child() { $builder_debug \ $_builder_offline \ && ( - if $_builder_debug_internal; then - builder_echo success "## $action$target completed successfully" - fi + builder_echo end "$action$target" success "## $action$target completed successfully" ) || ( result=$? - builder_echo error "## $action$target failed with exit code $result" + builder_echo end "$action$target" error "## $action$target failed with exit code $result" exit $result ) || exit $? # Required due to above subshell masking exit } @@ -619,19 +669,19 @@ builder_start_action() { # verify whether a target output is present. if builder_is_dep_build && ! builder_is_full_dep_build && - _builder_dep_output_exists $_builder_matched_action; then - builder_echo "skipping $_builder_matched_action_name, up-to-date" + _builder_dep_output_exists "${_builder_matched_action}"; then + builder_echo "skipping ${_builder_matched_action_name}, up-to-date" return 1 fi - builder_echo blue "## $_builder_matched_action_name starting..." - if [ -n "${_builder_current_action}" ]; then + builder_echo start "${_builder_matched_action_name}" "## ${_builder_matched_action_name} starting..." + if [[ -n "${_builder_current_action}" ]]; then _builder_warn_if_incomplete fi - _builder_current_action="$_builder_matched_action" + _builder_current_action="${_builder_matched_action}" # Build dependencies as required - _builder_do_build_deps "$_builder_matched_action" + _builder_do_build_deps "${_builder_matched_action}" return 0 else return 1 @@ -1048,6 +1098,7 @@ _builder_get_default_description() { :module) description="this module" ;; :tools) description="build tools for this project" ;; --debug) description="debug build" ;; + --release) description="release build (prevents --debug in local-env builds)" ;; esac echo "$description" } @@ -1269,6 +1320,7 @@ _builder_parse_expanded_parameters() { _builder_build_deps=--deps builder_verbose= builder_debug= + local is_release=false local _params=($@) _builder_chosen_action_targets=() _builder_chosen_options=() @@ -1385,10 +1437,15 @@ _builder_parse_expanded_parameters() { _builder_chosen_options+=(--verbose) builder_verbose=--verbose ;; - --debug|-d) + --debug) _builder_chosen_options+=(--debug) builder_debug=--debug ;; + --release) + _builder_chosen_options+=(--release) + # As of #13827, this is only checked when detecting if --debug should be auto-applied. + is_release=true + ;; --deps|--no-deps|--force-deps) _builder_build_deps=$key ;; @@ -1472,11 +1529,21 @@ _builder_parse_expanded_parameters() { else # This is a top-level invocation, so we want to track which dependencies # have been built, so they don't get built multiple times. + export _builder_deps_built=`mktemp` + + # Per #11106, local builds use --debug by default. + # Second condition prevents the block (and message) from executing when --debug is already specified explicitly. + if [[ $KEYMAN_VERSION_ENVIRONMENT == "local" ]] && [[ $builder_debug != --debug ]] && ! $is_release; then + builder_echo grey "Local build environment detected: setting --debug" + _params+=(--debug) + _builder_chosen_options+=(--debug) + builder_debug=--debug + fi + builder_echo setmark "$(basename "$0") parameters: <${_params[@]}>" if [[ ${#builder_extra_params[@]} -gt 0 ]]; then builder_echo grey "$(basename "$0") extra parameters: <${builder_extra_params[@]}>" fi - export _builder_deps_built=`mktemp` fi if builder_is_debug_build; then @@ -1660,14 +1727,14 @@ builder_finish_action() { # file or directory exist now? if _builder_dep_output_defined $matched_action && ! _builder_dep_output_exists "$matched_action"; then builder_echo warning "Expected output: '${_builder_dep_path[$matched_action]}'." - builder_echo warning "## $action_name completed successfully, but output does not exist" + builder_echo end "$action_name" warning "## $action_name completed successfully, but output does not exist" else - builder_echo success "## $action_name completed successfully" + builder_echo end "$action_name" success "## $action_name completed successfully" fi elif [[ $result == failure ]]; then - builder_echo error "## $action_name failed" + builder_echo end "$action_name" error "## $action_name failed" else - builder_echo error "## $action_name failed with message: $result" + builder_echo end "$action_name" error "## $action_name failed with message: $result" fi # Remove $action$target from the array; it is no longer a current action @@ -2095,6 +2162,65 @@ builder_is_target_excluded_by_platform() { return 1 } +# Returns 0 if the script is running in a Docker container +builder_is_running_on_docker() { + if [[ -z ${DOCKER_RUNNING:-} ]]; then + return 1 + else + return 0 + fi +} + +# Returns 0 if the script is running in a GitHub Actions environment +builder_is_running_on_gha() { + if [[ -z ${GITHUB_ACTIONS:-} ]]; then + return 1 + else + return 0 + fi +} + +# Returns 0 if the script is running on TeamCity +builder_is_running_on_teamcity() { + if [[ -z "${TEAMCITY_GIT_PATH:-}" ]]; then + return 1 + else + return 0 + fi +} + +# +# Returns 0 if current build is running in CI, as a pull request test, or as a +# mainline branch test, or as a release build +# +builder_is_ci_build() { + if builder_is_ci_release_build || builder_is_ci_test_build; then + return 0 + fi + return 1 +} + +# +# Returns 0 if current build is running as a release build in CI +# +builder_is_ci_release_build() { + if [[ "$KEYMAN_VERSION_ENVIRONMENT" =~ ^alpha|beta|stable$ ]]; then + return 0 + fi + return 1 +} + +# +# Returns 0 if current build is running in CI, as a pull request test, or as a +# mainline branch test +# +builder_is_ci_test_build() { + if [[ "$KEYMAN_VERSION_ENVIRONMENT" == test ]]; then + return 0 + fi + return 1 +} + ################################################################################ # Final initialization ################################################################################ diff --git a/resources/builder_completion.sh b/resources/builder_completion.inc.sh similarity index 100% rename from resources/builder_completion.sh rename to resources/builder_completion.inc.sh diff --git a/resources/devbox/macos/keyman.macos.env.sh b/resources/devbox/macos/keyman.macos.env.sh index 5250856f9c..5ffb36eaaf 100755 --- a/resources/devbox/macos/keyman.macos.env.sh +++ b/resources/devbox/macos/keyman.macos.env.sh @@ -20,8 +20,8 @@ if [ -z "$HOMEBREW_PREFIX" ]; then fi export PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH" -export PATH="$HOMEBREW_PREFIX/opt/java11/bin:$PATH" -export JAVA_HOME="$HOMEBREW_PREFIX/opt/java11" +export PATH="$HOMEBREW_PREFIX/opt/openjdk@21/bin:$PATH" +export JAVA_HOME="$HOMEBREW_PREFIX/opt/openjdk@21" # Python 2.7 eval "$(pyenv init --path)" diff --git a/resources/devbox/macos/macos.sh b/resources/devbox/macos/macos.sh index 5a3914d9a7..68913942e7 100755 --- a/resources/devbox/macos/macos.sh +++ b/resources/devbox/macos/macos.sh @@ -148,7 +148,7 @@ BREW_ALL="bash jq python3 meson ninja coreutils pyenv" BREW_WEB="node emscripten" BREW_IOS="swiftlint carthage" BREW_MACOS="carthage cocoapods" -BREW_ANDROID="openjdk@8 android-sdk android-studio ant gradle maven" +BREW_ANDROID="openjdk@21 android-commandlinetools android-studio ant gradle maven" # Turn on verbosity set -x @@ -169,16 +169,21 @@ pyenv global 2.7.18 source "$THIS_DIR/keyman.macos.env.sh" $REQUIRE_ANDROID && ( - mkdir -p .android && touch ~/.android/repositories.cfg + mkdir -p ~/.android && touch ~/.android/repositories.cfg + SDK_ROOT=~/.android + # installed via android-commandlinetools which sdkmanager || ( echo "Please run Android Studio to install the Android SDK tools." read -p "Press ENTER to continue after installation" PATH="$HOME/Library/Android/sdk/tools/bin:$PATH" ) - sdkmanager "system-images;android-30;google_apis_playstore;x86_64" - sdkmanager --update - sdkmanager --licenses + + # android-commandlinetools default sdk_root location is elsewhere, as + # documented by https://formulae.brew.sh/cask/android-commandlinetools + sdkmanager --sdk_root=$SDK_ROOT "system-images;android-30;google_apis_playstore;x86_64" + sdkmanager --sdk_root=$SDK_ROOT --update + sdkmanager --sdk_root=$SDK_ROOT --licenses ) # For now, we won't run this step automatically diff --git a/resources/docker-images/README.md b/resources/docker-images/README.md index 6f82b84319..c203d09948 100644 --- a/resources/docker-images/README.md +++ b/resources/docker-images/README.md @@ -22,11 +22,11 @@ resources/docker-images/build.sh ``` By default this will create 64-bit images for building -Keyman Core, Keyman for Android, Keyman for Linux and -Keyman for Web. These images are based on the Ubuntu 24.04 -with Node 20 and Emscripten 3.1.58 (for the exact versions, -see [`minimum-versions.inc.sh`](../build/minimum-versions.inc.sh)) -and are named e.g. `keyman-core-ci:default`. +Keyman Core, Keyman for Android, Keyman for Linux, Keyman for Web +and (cross-platform parts of) Keyman Developer. These images are based +on Ubuntu 24.04 with Node 20 and Emscripten 3.1.58 (for the exact versions, +see [`minimum-versions.inc.sh`](../build/minimum-versions.inc.sh)). +The images are named e.g. `keymanapp/keyman-core-ci:default`. The versions can be changed, e.g. @@ -34,7 +34,8 @@ The versions can be changed, e.g. resources/docker-images/build.sh --distro-version jammy --node 20 ``` -This will create an image named e.g. `keyman-core-ci:jammy-node20`. +This will create an image named e.g. +`keymanapp/keyman-core-ci:ubuntu-jammy-java21-node20.16.0-emsdk3.1.58`. Once the image is built, it may be used to build parts of Keyman. @@ -44,7 +45,7 @@ It is possible to build locally with these images: ```shell # build 'Keyman Core' in docker -resources/docker-images/run.sh core -- core/build.sh --debug build +resources/docker-images/run.sh :core -- core/build.sh --debug build ``` Note: For Core and Linux we put the generated binaries in a @@ -61,5 +62,5 @@ To run the tests locally, use the `run.sh` script: ```shell # Run common/web tests -resources/docker-images/run.sh web -- common/web/build.sh --debug test +resources/docker-images/run.sh :web -- common/web/build.sh --debug test ``` diff --git a/resources/docker-images/android/Dockerfile b/resources/docker-images/android/Dockerfile index 2b289ce077..489284b08c 100644 --- a/resources/docker-images/android/Dockerfile +++ b/resources/docker-images/android/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.4 # Keyman is copyright (C) SIL Global. MIT License. ARG BASE_VERSION=default diff --git a/resources/docker-images/base/Dockerfile b/resources/docker-images/base/Dockerfile index 3c254edfad..de5a82fcf0 100644 --- a/resources/docker-images/base/Dockerfile +++ b/resources/docker-images/base/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.4 # Keyman is copyright (C) SIL Global. MIT License. ARG DISTRO=ubuntu diff --git a/resources/docker-images/build.sh b/resources/docker-images/build.sh index a5623adaab..b650b9ce55 100755 --- a/resources/docker-images/build.sh +++ b/resources/docker-images/build.sh @@ -17,10 +17,10 @@ builder_describe \ ":android" \ ":base" \ ":core" \ + ":developer" \ ":linux" \ ":web" \ - "--distro=DISTRO The distribution to use for the base image "\ - " (debian or ubuntu, default: ubuntu)" \ + "--distro=DISTRO The distribution to use for the base image (debian or ubuntu, default: ubuntu)" \ "--distro-version=DISTRO_VERSION The Ubuntu/Debian version (default: ${KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER})" \ "--no-cache Force rebuild of docker images" \ "build Build docker images" \ @@ -28,62 +28,13 @@ builder_describe \ builder_parse "$@" -_add_build_args() { - local var=$1 - local default_var=$2 - local name=$3 - local value - - value="${!var:=${!default_var:-}}" - - build_args+=(--build-arg="${var}=${value}") - - if [[ -n "${build_version:-}" ]]; then - build_version="${build_version}-${name:-}${value}" - else - build_version="${name}${value}" - fi -} - -_convert_parameters_to_build_args() { - build_args=() - build_version= - local required_node_version keyman_default_distro - # shellcheck disable=SC2034 - required_node_version="$(_print_expected_node_version)" - # shellcheck disable=SC2034 - keyman_default_distro="ubuntu" - - _add_build_args DISTRO keyman_default_distro "" - _add_build_args DISTRO_VERSION KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER "" - _add_build_args JAVA_VERSION KEYMAN_VERSION_JAVA java - _add_build_args REQUIRED_NODE_VERSION required_node_version node - _add_build_args REQUIRED_EMSCRIPTEN_VERSION KEYMAN_MIN_VERSION_EMSCRIPTEN emsdk - - if [[ -n "${BASE_VERSION:-}" ]]; then - build_args+=(--build-arg="BASE_VERSION=${BASE_VERSION}") - fi -} - -_check_for_default_values() { - if [[ -z "${DISTRO_VERSION:-}" ]] && [[ -z "${JAVA_VERSION:-}" ]]; then - is_default_values=true - else - is_default_values=false - fi -} - -_is_default_values() { - ${is_default_values} -} - build_action() { local platform=$1 builder_echo debug "Building image for ${platform}" if [[ "${platform}" == "base" ]]; then - # shellcheck disable=SC2154 # set by _convert_parameters_to_build_args + # shellcheck disable=SC2154 # set by convert_parameters_to_args docker pull "${DISTRO}:${DISTRO_VERSION}" elif [[ "${platform}" == "linux" ]]; then cp "${KEYMAN_ROOT}/linux/debian/control" "${platform}" @@ -95,6 +46,7 @@ build_action() { # shellcheck disable=SC2164 cd "${platform}" + export DOCKER_BUILDKIT=1 # shellcheck disable=SC2248,SC2086 docker build ${OPTION_NO_CACHE:-} -t "keymanapp/keyman-${platform}-ci:${build_version}" "${build_args[@]}" . # If the user didn't specify particular versions we will additionaly create an image @@ -113,18 +65,20 @@ test_action() { builder_echo debug "Testing image for ${platform}" ./run.sh --distro "${DISTRO}" --distro-version "${DISTRO_VERSION}" \ - "${platform}" -- ./build.sh configure,build,test:"${platform}" + ":${platform}" -- ./build.sh configure,build,test:"${platform}" } -_check_for_default_values -_convert_parameters_to_build_args +check_buildx_available +check_for_default_values +convert_parameters_to_args if builder_has_action build; then build_action base - builder_run_action build:android build_action android - builder_run_action build:core build_action core - builder_run_action build:linux build_action linux - builder_run_action build:web build_action web + builder_run_action build:android build_action android + builder_run_action build:core build_action core + builder_run_action build:linux build_action linux + builder_run_action build:web build_action web + builder_run_action build:developer build_action developer fi builder_run_action test:core test_action core @@ -132,3 +86,4 @@ builder_run_action test:linux test_action linux builder_run_action test:web test_action web # Android uses artifacts from web, so it has to come after web builder_run_action test:android test_action android +builder_run_action test:developer test_action developer diff --git a/resources/docker-images/core/Dockerfile b/resources/docker-images/core/Dockerfile index a29cac8e04..8b3e2b4c7a 100644 --- a/resources/docker-images/core/Dockerfile +++ b/resources/docker-images/core/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.4 # Keyman is copyright (C) SIL Global. MIT License. # # ARGS used in this file: diff --git a/resources/docker-images/developer/Dockerfile b/resources/docker-images/developer/Dockerfile new file mode 100644 index 0000000000..aaec7b55cf --- /dev/null +++ b/resources/docker-images/developer/Dockerfile @@ -0,0 +1,55 @@ +# syntax=docker/dockerfile:1.4 +# Keyman is copyright (C) SIL Global. MIT License. +# +# ARGS used in this file: +# - ARG BASE_VERSION=default +# - ARG REQUIRED_EMSCRIPTEN_VERSION=unset + +ARG BASE_VERSION=default +FROM keymanapp/keyman-base-ci:${BASE_VERSION} + +LABEL org.opencontainers.image.authors="SIL Global." +LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git" +LABEL org.opencontainers.image.title="Keyman Developer Build Image" + +USER root +RUN apt-get update && \ + apt-get install -qy devscripts jq meson + +# Pre-install emscripten +USER build +ARG REQUIRED_EMSCRIPTEN_VERSION=unset +RUN echo "Installing emscripten version ${REQUIRED_EMSCRIPTEN_VERSION}" && \ + export EMSDK_KEEP_DOWNLOADS=1 && \ + cd /home/build/ && \ + git clone https://github.com/emscripten-core/emsdk.git && \ + cd emsdk && \ + ./emsdk install ${REQUIRED_EMSCRIPTEN_VERSION} && \ + ./emsdk activate ${REQUIRED_EMSCRIPTEN_VERSION} +USER root +RUN echo "export EMSCRIPTEN_BASE=/home/build/emsdk/upstream/emscripten" >> /usr/bin/bashwrapper && \ + echo "export KEYMAN_USE_EMSDK=1" >> /usr/bin/bashwrapper + +# Finish bashwrapper script and adjust permissions +RUN <> /usr/bin/bashwrapper + +if [[ "\$@" =~ test ]] && [ -f /usr/bin/run-tests.sh ]; then + /usr/bin/run-tests.sh "\${@:-bash}" +else + "\${@:-bash}" +fi +EOF + +# now, switch to build user +USER build + +# Pre-install node +RUN export NVM_DIR="/home/build/.nvm" && \ + . /home/build/.nvm/nvm.sh && \ + cd /home/build/emsdk/upstream/emscripten && \ + npm install + +VOLUME /home/build/build +WORKDIR /home/build/build + +ENTRYPOINT [ "/usr/bin/bashwrapper" ] diff --git a/resources/docker-images/docker-build.inc.sh b/resources/docker-images/docker-build.inc.sh index 4bd9275d75..855ba6e641 100644 --- a/resources/docker-images/docker-build.inc.sh +++ b/resources/docker-images/docker-build.inc.sh @@ -59,3 +59,15 @@ check_for_default_values() { is_default_values() { ${is_default_values} } + +# Check that `docker buildx` is available +check_buildx_available() { + if ! docker --help | grep -q buildx; then + if [[ "${OSTYPE:-}" == "linux-gnu" ]]; then + builder_echo error "Docker buildx is not available. Please install Docker buildx to use this script. e.g. 'sudo apt install docker-buildx'" + else + builder_echo error "Docker buildx is not available. Please install Docker buildx to use this script." + fi + exit 1 + fi +} diff --git a/resources/docker-images/linux/Dockerfile b/resources/docker-images/linux/Dockerfile index 061e9f619b..da6bf59906 100644 --- a/resources/docker-images/linux/Dockerfile +++ b/resources/docker-images/linux/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.4 # Keyman is copyright (C) SIL Global. MIT License. ARG BASE_VERSION=default diff --git a/resources/docker-images/run.sh b/resources/docker-images/run.sh index c71047e22f..2c8cd3e51d 100755 --- a/resources/docker-images/run.sh +++ b/resources/docker-images/run.sh @@ -14,10 +14,12 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" builder_describe \ "Run build.sh script inside of a docker image. Pass the build script and parameters after --." \ - "android" \ - "core" \ - "linux" \ - "web" \ + ":android" \ + ":core" \ + ":developer" \ + ":linux" \ + ":web" \ + "run+ Run command in docker image" \ "--distro=DISTRO The distribution (debian or ubuntu, default: ubuntu)" \ "--distro-version=DISTRO_VERSION The Ubuntu/Debian version (default: ${KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER})" @@ -48,6 +50,13 @@ run_core() { "${builder_extra_params[@]}" } +run_developer() { + docker run -it --rm -v "${KEYMAN_ROOT}":/home/build/build \ + -v "${KEYMAN_ROOT}/core/build/docker-core/${build_dir}":/home/build/build/core/build \ + "keymanapp/keyman-developer-ci:${image_version}" \ + "${builder_extra_params[@]}" +} + run_linux() { mkdir -p "${KEYMAN_ROOT}/linux/build/docker-linux/${build_dir}" mkdir -p "${KEYMAN_ROOT}/linux/keyman-system-service/build/docker-linux/${build_dir}" @@ -67,17 +76,10 @@ run_web() { "${builder_extra_params[@]}" } -if [[ -z "${DISTRO_VERSION:-}" ]]; then - image_version=default - build_dir=default -else - image_version="${DISTRO:-}-${DISTRO_VERSION}-java${KEYMAN_VERSION_JAVA}-node$(_print_expected_node_version)-emsdk${KEYMAN_MIN_VERSION_EMSCRIPTEN}" - build_dir="${DISTRO:-}-${DISTRO_VERSION}" -fi - mkdir -p "${KEYMAN_ROOT}/core/build/docker-core/${build_dir}" -builder_run_action android run_android -builder_run_action core run_core -builder_run_action linux run_linux -builder_run_action web run_web +builder_run_action run:android run_android +builder_run_action run:core run_core +builder_run_action run:developer run_developer +builder_run_action run:linux run_linux +builder_run_action run:web run_web diff --git a/resources/docker-images/web/Dockerfile b/resources/docker-images/web/Dockerfile index 1f08daf92c..655a2d8592 100644 --- a/resources/docker-images/web/Dockerfile +++ b/resources/docker-images/web/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.4 # Keyman is copyright (C) SIL Global. MIT License. # # ARGS used in this file: diff --git a/resources/gosh/gosh.sh b/resources/gosh/gosh similarity index 100% rename from resources/gosh/gosh.sh rename to resources/gosh/gosh diff --git a/resources/gosh/package.json b/resources/gosh/package.json index bc70a30cc0..f87cfcf7db 100644 --- a/resources/gosh/package.json +++ b/resources/gosh/package.json @@ -8,7 +8,7 @@ "homepage": "https://github.com/keymanapp/keyman", "license": "MIT", "bin": { - "gosh": "./gosh.js" + "gosh": "gosh.js" }, "private": true, "files": [ @@ -18,6 +18,7 @@ "type": "git", "url": "git+https://github.com/keymanapp/keyman.git" }, + "scripts": {}, "bugs": { "url": "https://github.com/keymanapp/keyman/issues" } diff --git a/resources/locate_emscripten.inc.sh b/resources/locate_emscripten.inc.sh index 57ed0cd559..d390e83bd2 100644 --- a/resources/locate_emscripten.inc.sh +++ b/resources/locate_emscripten.inc.sh @@ -44,7 +44,7 @@ locate_emscripten() { # For developers, define KEYMAN_USE_EMSDK to do this on your # build machine. verify_emscripten_version() { - if [[ "$VERSION_ENVIRONMENT" != local || ! -z "${KEYMAN_USE_EMSDK+x}" ]]; then + if [[ "$KEYMAN_VERSION_ENVIRONMENT" != local || ! -z "${KEYMAN_USE_EMSDK+x}" ]]; then _select_emscripten_version_with_emsdk fi } diff --git a/resources/scopes/commit-types.json b/resources/scopes/commit-types.json index ec1d2bff0a..958b577175 100644 --- a/resources/scopes/commit-types.json +++ b/resources/scopes/commit-types.json @@ -6,8 +6,9 @@ "docs", "feat", "fix", + "maint", "refactor", "style", "test" ] -} \ No newline at end of file +} diff --git a/resources/scopes/scopes.json b/resources/scopes/scopes.json index d547588ecc..1ceab19967 100644 --- a/resources/scopes/scopes.json +++ b/resources/scopes/scopes.json @@ -2,44 +2,30 @@ "scopes": { "android": { "app": null, - "browser": null, "engine": null, - "resources": null, "samples": null }, "common": { - "models": { - "types": null, - "templates": null, - "wordbreakers": null - }, - "resources": null, "web": null }, "core": null, "developer": { "compilers": null, - "ide": null, - "resources": null, - "tools": null + "ide": null }, "ios": { "app": null, - "browser": null, "engine": null, - "resources": null, "samples": null }, "linux": { "config": null, "engine": null, - "resources": null, "samples": null }, "mac": { "config": null, "engine": null, - "resources": null, "samples": null }, "oem": { @@ -49,17 +35,17 @@ "windows": null } }, + "resources": null, "web": { "bookmarklet": null, "engine": null, - "resources": null, "ui": null, - "samples": null + "samples": null, + "predictive-text": null }, "windows": { "config": null, "engine": null, - "resources": null, "samples": null } } diff --git a/resources/shellHelperFunctions.sh b/resources/shellHelperFunctions.sh index ebec70d0b7..802106401b 100755 --- a/resources/shellHelperFunctions.sh +++ b/resources/shellHelperFunctions.sh @@ -137,8 +137,16 @@ write_download_info() { FILE_EXTENSION="${BASE_FILE##*.}" - FILE_SIZE=$(/usr/bin/stat -f"%z" "${BASE_PATH}/${BASE_FILE}") - MD5_HASH=$(md5 -q "${BASE_PATH}/${BASE_FILE}") + # stat flags to get filesize in bytes + if [ "$BUILDER_OS" == "mac" ] && [[ $(which stat) == /usr/bin/stat ]]; then + # /usr/bin/stat on mac is BSD + STAT_FLAGS="-f%z" + else + # GNU (coreutils) + STAT_FLAGS="-c%s" + fi + FILE_SIZE=$(stat ${STAT_FLAGS} "${BASE_PATH}/${BASE_FILE}") + MD5_HASH=$(md5sum "${BASE_PATH}/${BASE_FILE}" | cut -d" " -f 1 -) # hash is the first element returned if [[ -f "$DOWNLOAD_INFO_FILEPATH" ]]; then builder_warn "Overwriting $DOWNLOAD_INFO_FILEPATH" @@ -162,19 +170,19 @@ write_download_info() { set_version ( ) { PRODUCT_PATH=$1 - if [ $VERSION ]; then + if [ $KEYMAN_VERSION ]; then if [ $2 ]; then # $2 = product name. - echo "Setting version numbers in $2 to $VERSION." + echo "Setting version numbers in $2 to $KEYMAN_VERSION." fi - /usr/libexec/Plistbuddy -c "Set CFBundleVersion $VERSION" "$PRODUCT_PATH/Info.plist" - /usr/libexec/Plistbuddy -c "Set CFBundleShortVersionString $VERSION" "$PRODUCT_PATH/Info.plist" + /usr/libexec/Plistbuddy -c "Set CFBundleVersion $KEYMAN_VERSION" "$PRODUCT_PATH/Info.plist" + /usr/libexec/Plistbuddy -c "Set CFBundleShortVersionString $KEYMAN_VERSION" "$PRODUCT_PATH/Info.plist" fi } # Uses npm to set the current package version (package.json). # -# This sets the version according to the current VERSION_WITH_TAG. +# This sets the version according to the current KEYMAN_VERSION_WITH_TAG. # # Usage: # @@ -186,7 +194,7 @@ set_npm_version () { # release of Keyman Developer -- these two versions should be in sync. Because this # is a large repo with multiple projects and build systems, it's better for us that # individual build systems don't take too much ownership of git tagging. :) - npm version --allow-same-version --no-git-tag-version --no-commit-hooks "$VERSION_WITH_TAG" + npm version --allow-same-version --no-git-tag-version --no-commit-hooks "$KEYMAN_VERSION_WITH_TAG" } # @@ -257,7 +265,7 @@ verify_npm_setup() { # If we are on CI environment, automatically select a node version with nvm # Also, a developer can set KEYMAN_USE_NVM variable to get this behaviour # automatically too (see /docs/build/node.md) - if [[ "$VERSION_ENVIRONMENT" != local || ! -z "${KEYMAN_USE_NVM+x}" ]]; then + if [[ "$KEYMAN_VERSION_ENVIRONMENT" != local || ! -z "${KEYMAN_USE_NVM+x}" ]]; then _select_node_version_with_nvm fi @@ -329,7 +337,7 @@ check-markdown() { builder_do_typescript_tests() { local MOCHA_FLAGS= - if [[ "${TEAMCITY_GIT_PATH:-}" != "" ]]; then + if builder_is_running_on_teamcity; then # we're running in TeamCity MOCHA_FLAGS="-reporter mocha-teamcity-reporter" fi diff --git a/resources/teamcity/README.md b/resources/teamcity/README.md new file mode 100644 index 0000000000..c6db3af4d9 --- /dev/null +++ b/resources/teamcity/README.md @@ -0,0 +1,34 @@ +# Teamcity Build Configuration Scripts + +This directory contains TeamCity (TC) build scripts. Each script corresponds +to a build configuration on TC. + +## Running scripts locally + +It's possible to run some of the build locally in a Docker container. +Of course this won't work for the parts, like uploading files, that +require keys or other secrets. + +To run locally start a Docker container: + +```bash +docker run -v $(pwd):/Develop -it ubuntu:24.04 /bin/bash +``` + +(for the Linux integration tests, you'll have to pass `--privileged` as well.) + +Inside of the container, run + +```bash +apt update && apt install -y sudo +export DOCKER_RUNNING=1 +echo "ubuntu ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers +su ubuntu +``` + +and then run the build, e.g. + +```bash +cd /Develop +resources/teamcity/linux/keyman-linux-test.sh configure,build,test +``` diff --git a/resources/teamcity/developer/developer-actions.inc.sh b/resources/teamcity/developer/developer-actions.inc.sh new file mode 100644 index 0000000000..45d3792dbe --- /dev/null +++ b/resources/teamcity/developer/developer-actions.inc.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. + +developer_install_dependencies_on_linux_action() { + if ! is_ubuntu; then + return 0 + fi + + builder_echo start "install dependencies" "Installing dependencies" + + linux_check_and_install_packages devscripts jq meson + install_nvm + install_emscripten + + builder_echo end "install dependencies" success "Finished installing dependencies" +} diff --git a/resources/teamcity/developer/download-symbol-server-index.ps1 b/resources/teamcity/developer/download-symbol-server-index.ps1 new file mode 100644 index 0000000000..ffa0d97ba1 --- /dev/null +++ b/resources/teamcity/developer/download-symbol-server-index.ps1 @@ -0,0 +1,63 @@ +# Keyman is copyright (C) SIL Global. MIT License. +# +# This script should be the identical for nightly/beta/stable for a given platform +# + +$ErrorActionPreference = "Stop" + +$RSYNC_HOME = $env:RSYNC_HOME +$RSYNC_PATH = $env:RSYNC_PATH +$RSYNC_USER = $env:RSYNC_USER +$RSYNC_HOST = $env:RSYNC_HOST +$RSYNC_ROOT = $env:RSYNC_ROOT +$USERPROFILE = $env:USERPROFILE + +New-Item -Force -ItemType Directory symbols\000admin +cd symbols\000admin + +# +# Download the symbol server index files with rsync from downloads.keyman.com +# (rsync requires that we are in the symbols folder to get folders in +# sync correctly; it is possible to resolve this but easier to just cd.) +# + +$rsync_args = @( + '-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions + '--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security + '--stats', # show statistics for log + "--rsync-path='$RSYNC_PATH'", # path on remote server + "--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh + "$RSYNC_USER@$RSYNC_HOST:$RSYNC_ROOT/windows/symbols/000admin/lastid.txt", # target server + path + "." # download the whole symbols 000Admin folder +) + +& $RSYNC_HOME\rsync.exe $rsync_args +if ($LASTEXITCODE -ne 0) { throw "Exit code is $LASTEXITCODE" } + +$rsync_args = @( + '-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions + '--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security + '--stats', # show statistics for log + "--rsync-path='$RSYNC_PATH'", # path on remote server + "--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh + "$RSYNC_USER@$RSYNC_HOST:$RSYNC_ROOT/windows/symbols/000admin/history.txt", # target server + path + "." # download the whole symbols 000Admin folder +) + +& $RSYNC_HOME\rsync.exe $rsync_args +if ($LASTEXITCODE -ne 0) { throw "Exit code is $LASTEXITCODE" } + +$rsync_args = @( + '-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions + '--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security + '--stats', # show statistics for log + "--rsync-path='$RSYNC_PATH'", # path on remote server + "--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh + "$RSYNC_USER@$RSYNC_HOST:$RSYNC_ROOT/windows/symbols/000admin/server.txt", # target server + path + "." # download the whole symbols 000Admin folder +) + +& $RSYNC_HOME\rsync.exe $rsync_args +if ($LASTEXITCODE -ne 0) { throw "Exit code is $LASTEXITCODE" } + +# EOF diff --git a/resources/teamcity/developer/keyman-developer-release.sh b/resources/teamcity/developer/keyman-developer-release.sh new file mode 100755 index 0000000000..5aa8608a23 --- /dev/null +++ b/resources/teamcity/developer/keyman-developer-release.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. +# +# TC build script to build release of Keyman Developer + +# shellcheck disable=SC2164 +# shellcheck disable=SC1091 + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# shellcheck disable=SC2154 +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh" + +################################ Main script ################################ + +builder_describe \ + "Build Keyman Developer on Windows" \ + "all run all actions" \ + "build build Keyman Developer and test keyboards" \ + "publish publish release of Keyman Developer" \ + "--rsync-path=RSYNC_PATH rsync path on remote server" \ + "--rsync-user=RSYNC_USER rsync user on remote server" \ + "--rsync-host=RSYNC_HOST rsync host on remote server" \ + "--rsync-root=RSYNC_ROOT rsync root on remote server" \ + "--help.keyman.com=HELP_KEYMAN_COM path to help.keyman.com repository" + +builder_parse "$@" + +# shellcheck disable=SC2154 +cd "${KEYMAN_ROOT}/developer/src" + +function _build_developer() { + builder_echo start "build developer" "Building Keyman Developer" + + ./build.sh configure build test api publish --npm-publish + + builder_echo end "build developer" success "Finished building Keyman Developer" +} + +function _build_testkeyboards() { + builder_echo start "build testkeyboards" "Building test keyboards" + + "${KEYMAN_ROOT}/common/test/keyboards/build.sh" + + builder_echo end "build testkeyboards" success "Finished building test keyboards" +} + +function _publish_sentry() { + builder_echo start "publish sentry" "Publishing debug information files to Sentry" + + "${KEYMAN_ROOT}/developer/src/tools/sentry-upload-difs.sh" + + builder_echo end "publish sentry" success "Finished publishing debug information files to Sentry" +} + +function _download_symbol_server_index() { + # Download symbol server index from symbol server + builder_echo start "download symbol server index" "Downloading symbol server index" + + cd "${KEYMAN_ROOT}/.." + # shellcheck disable=SC2154 + powershell -NonInteractive -ExecutionPolicy Bypass -File "${THIS_SCRIPT_PATH}/download-symbol-server-index.ps1" + cd "${KEYMAN_ROOT}/developer/src" + + builder_echo end "download symbol server index" success "Finished downloading symbol server index" +} + +function _publish_new_symbols() { + # Publish new symbols to symbol server + builder_echo start "publish new symbols" "Publishing new symbols to symbol server" + + cd "${KEYMAN_ROOT}/../symbols" + # shellcheck disable=SC2154 + powershell -NonInteractive -ExecutionPolicy Bypass -File "${THIS_SCRIPT_PATH}/publish-new-symbols.ps1" + cd "${KEYMAN_ROOT}/developer/src" + + builder_echo end "publish new symbols" success "Finished publishing new symbols to symbol server" +} + +function _publish_to_downloads_keyman_com() { + # Publish to downloads.keyman.com + builder_echo start "publish to downloads.keyman.com" "Publishing release to downloads.keyman.com" + + cd "${KEYMAN_ROOT}/developer" + # shellcheck disable=SC2154 + powershell -NonInteractive -ExecutionPolicy Bypass -File "${THIS_SCRIPT_PATH}/publish-to-downloads-keyman-com.ps1" + cd "${KEYMAN_ROOT}/developer/src" + + builder_echo end "publish to downloads.keyman.com" success "Finished publishing release to downloads.keyman.com" +} + +function _publish_api_documentation() { + # Upload new Keyman Developer API documentation to help.keyman.com + builder_echo start "publish api documentation" "Uploading new Keyman Developer API documentation to help.keyman.com" + + export HELP_KEYMAN_COM="${HELP_KEYMAN_COM:-${KEYMAN_ROOT}/../help.keyman.com}" + "${KEYMAN_ROOT}/resources/build/help-keyman-com.sh" developer + + builder_echo end "publish api documentation" success "Finished uploading new Keyman Developer API documentation to help.keyman.com" +} + +function build_developer_action() { + _build_developer + _build_testkeyboards +} + +function publish_action() { + if ! is_windows; then + # requires Powershell, so currently only supported on Windows + builder_echo error "This script is intended to be run on Windows only." + return 1 + fi + + export RSYNC_PATH + export RSYNC_USER + export RSYNC_HOST + export RSYNC_ROOT + + _publish_sentry + _download_symbol_server_index + _publish_new_symbols + _publish_to_downloads_keyman_com + _publish_api_documentation +} + +if builder_has_action all; then + build_developer_action + publish_action +else + builder_run_action build build_developer_action + builder_run_action publish publish_action +fi diff --git a/resources/teamcity/developer/keyman-developer-test-linux-macos.sh b/resources/teamcity/developer/keyman-developer-test-linux-macos.sh new file mode 100755 index 0000000000..c273873fe0 --- /dev/null +++ b/resources/teamcity/developer/keyman-developer-test-linux-macos.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. +# +# TC build script for Keyman Developer on Linux and macOS + +# shellcheck disable=SC2164 +# shellcheck disable=SC1091 + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# shellcheck disable=SC2154 +. "${KEYMAN_ROOT}/resources/teamcity/developer/developer-actions.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-actions.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh" + +################################ Main script ################################ + +builder_describe \ + "Build Keyman Developer on Linux and macOS" \ + "all run all actions" \ + "configure install dependencies" \ + "build build" + +builder_parse "$@" + +cd "${KEYMAN_ROOT}/developer/src" + +function build_developer_action() { + builder_echo start "build developer" "Building Keyman Developer" + + "${KEYMAN_ROOT}/developer/src/build.sh" configure build test + + builder_echo end "build developer" success "Finished building Keyman Developer" +} + +if is_windows; then + builder_echo error "This script is intended to be run on Linux or macOS only." + exit 1 +fi + +if builder_has_action all; then + developer_install_dependencies_on_linux_action + + set_variables_for_nvm + set_variables_for_emscripten + + build_developer_action +else + builder_run_action configure developer_install_dependencies_on_linux_action + + set_variables_for_nvm + set_variables_for_emscripten + + builder_run_action build build_developer_action +fi diff --git a/resources/teamcity/developer/keyman-developer-test-windows.sh b/resources/teamcity/developer/keyman-developer-test-windows.sh new file mode 100755 index 0000000000..75bb60b4ac --- /dev/null +++ b/resources/teamcity/developer/keyman-developer-test-windows.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. +# +# TC build script for Keyman Developer on Windows + +# shellcheck disable=SC2164 +# shellcheck disable=SC1091 + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# shellcheck disable=SC2154 +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh" + +################################ Main script ################################ + +builder_describe \ + "Build Keyman Developer on Windows" \ + "all run all actions" \ + "build build Keyman Developer and test keyboards" \ + "publish publish debug information files to sentry" + +builder_parse "$@" + +# shellcheck disable=SC2154 +cd "${KEYMAN_ROOT}/developer/src" + +function build_developer_action() { + _build_developer + _build_testkeyboards +} + +function _build_developer() { + builder_echo start "build developer" "Building Keyman Developer" + + ./build.sh configure build test api publish --dry-run + + builder_echo end "build developer" success "Finished building Keyman Developer" +} + +function _build_testkeyboards() { + builder_echo start "build testkeyboards" "Building test keyboards" + + "${KEYMAN_ROOT}/common/test/keyboards/build.sh" --zip-source --index + + builder_echo end "build testkeyboards" success "Finished building test keyboards" +} + +function publish_sentry_action() { + builder_echo start "publish sentry" "Publishing debug information files to Sentry" + + "${KEYMAN_ROOT}/developer/src/tools/sentry-upload-difs.sh" + + builder_echo end "publish sentry" success "Finished publishing debug information files to Sentry" +} + +if ! is_windows; then + builder_echo error "This script is intended to be run on Windows only." + exit 1 +fi + +if builder_has_action all; then + build_developer_action + publish_sentry_action +else + builder_run_action build build_developer_action + builder_run_action publish publish_sentry_action +fi diff --git a/resources/teamcity/developer/publish-new-symbols.ps1 b/resources/teamcity/developer/publish-new-symbols.ps1 new file mode 100644 index 0000000000..40c43df1c8 --- /dev/null +++ b/resources/teamcity/developer/publish-new-symbols.ps1 @@ -0,0 +1,39 @@ +# Keyman is copyright (C) SIL Global. MIT License. +# +# This script should be the identical for nightly/beta/stable for a given platform +# + +$ErrorActionPreference = "Stop" + +$RSYNC_HOME = $env:RSYNC_HOME +$RSYNC_PATH = $env:RSYNC_PATH +$RSYNC_USER = $env:RSYNC_USER +$RSYNC_HOST = $env:RSYNC_HOST +$RSYNC_ROOT = $env:RSYNC_ROOT +$USERPROFILE = $env:USERPROFILE + +# Rename 000Admin to 000admin + +ren 000Admin 000admin_ +ren 000admin_ 000admin + +# +# Upload with rsync to downloads.keyman.com +# (rsync requires that we are in the symbols folder to get folders in +# sync correctly; it is possible to resolve this but easier to just cd.) +# + +$rsync_args = @( + '-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions + '--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security + '--stats', # show statistics for log + "--rsync-path='$RSYNC_PATH'", # path on remote server + "--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh + ".", # upload the whole symbols folder + "$RSYNC_USER@$RSYNC_HOST:$RSYNC_ROOT/windows/symbols/" # target server + path +) + +& $RSYNC_HOME\rsync.exe $rsync_args +if ($LASTEXITCODE -ne 0) { throw "Exit code is $LASTEXITCODE" } + +# EOF diff --git a/resources/teamcity/developer/publish-to-downloads-keyman-com.ps1 b/resources/teamcity/developer/publish-to-downloads-keyman-com.ps1 new file mode 100644 index 0000000000..884e098afa --- /dev/null +++ b/resources/teamcity/developer/publish-to-downloads-keyman-com.ps1 @@ -0,0 +1,223 @@ +# Keyman is copyright (C) SIL Global. MIT License. + +$ErrorActionPreference = "Stop" + +$tier = Get-Content ..\TIER.md +$build_number = Get-Content ..\VERSION.md +$build_counter = $build_number -replace "^\d+\.\d+\.(\d+)$", '$1' +$msi_major_version = $build_number -replace "^(\d+)\.(\d+)\.(\d+)$", '$1$2' + +if ( $tier -eq "alpha" -or $tier -eq "beta") { + $version_tag = $tier + $version_with_tag = $build_number # we may go with $build_number + "-" + $version_tag in the future one day +} else { + $version_tag = "" + $version_with_tag = $build_number +} + +$upload_path = "upload\$build_number" +$keyboards_path = "$upload_path\keyboards" + +# Keyman Developer installers +$kmcomp_zip = "kmcomp-$version_with_tag.zip" +$keyman_developer_exe = "keymandeveloper-$version_with_tag.exe" + +$setup_exe = "setup.exe" + +# Debug files +$debug_zip = "debug-$build_number.zip" + +$7Z_HOME = $env:7Z_HOME +$RSYNC_HOME = $env:RSYNC_HOME +$RSYNC_PATH = $env:RSYNC_PATH +$RSYNC_USER = $env:RSYNC_USER +$RSYNC_HOST = $env:RSYNC_HOST +$RSYNC_ROOT = $env:RSYNC_ROOT +$USERPROFILE = $env:USERPROFILE + +########################################################################################### +## Developer upload +########################################################################################### + +# +# Preparation +# + +if((Test-Path $upload_path) -ne 0) { + Remove-Item -Path $upload_path -Recurse +} + +mkdir $upload_path + +# +# Build Keyman Compiler WINE archive +# + +if((Test-Path ..\release\$kmcomp_zip) -ne 0) { + copy ..\release\$kmcomp_zip ..\$upload_path\$kmcomp_zip +} else { + cd bin + if((Test-Path ..\..\common\schemas\keyboard_info\keyboard_info.source.json) -ne 0) { + # Keyman versions through -16.0 + copy ..\..\common\schemas\keyboard_info\keyboard_info.source.json . + copy ..\..\common\schemas\keyboard_info\keyboard_info.distribution.json . + & "$7Z_HOME\7z.exe" a -bd -bb0 ..\$upload_path\$kmcomp_zip kmcomp.exe kmcmpdll.dll kmcomp.x64.exe kmcmpdll.x64.dll kmconvert.exe keyboard_info.source.json keyboard_info.distribution.json xml\layoutbuilder\*.keyman-touch-layout projects\ + + # Add Keyman Developer Server to the archive (15.0 late alpha - after 171) + if((Test-Path ..\src\server\build) -ne 0) { + copy ..\src\server\build\ server\ -Recurse + & "$7Z_HOME\7z.exe" a -bd -bb0 ..\$upload_path\$kmcomp_zip server\ + } + } else { + # Keyman versions 17.0+; note, use npm install for most modules + copy ..\..\common\schemas\keyboard_info\keyboard_info.schema.json . + & "$7Z_HOME\7z.exe" a -bd -bb0 ..\$upload_path\$kmcomp_zip kmconvert.exe keyboard_info.schema.json xml\layoutbuilder\*.keyman-touch-layout projects\ server\ + } + cd .. +} + +# +# Copy source files +# + +copy release\$build_number\$keyman_developer_exe $upload_path\$keyman_developer_exe + +# +# Construct keyman_developer.download_info +# + +$hash = get-filehash $upload_path\$keyman_developer_exe -Algorithm MD5 + +$keyman_developer_download_info = @" +{ + "name": "Keyman Developer", + "version": "$version_with_tag", + "date": "$([DateTime]::Now.ToString("yyyy-MM-dd"))", + "platform": "win", + "stability": "$tier", + "file": "keymandeveloper-$version_with_tag.exe", + "md5": "$($hash.Hash)", + "type": "exe", + "build": "$build_counter" +} +"@ + +[System.IO.File]::WriteAllLines("$pwd\$upload_path\$keyman_developer_exe.download_info", $keyman_developer_download_info) + +# +# Construct kmcomp.download_info +# + +$hash = get-filehash $upload_path\$kmcomp_zip -Algorithm MD5 + +$kmcomp_download_info = @" +{ + "name": "Keyman Developer Command-Line Compiler", + "version": "$version_with_tag", + "date": "$([DateTime]::Now.ToString("yyyy-MM-dd"))", + "platform": "win", + "stability": "$tier", + "file": "kmcomp-$version_with_tag.zip", + "md5": "$($hash.Hash)", + "type": "zip", + "build": "$build_counter" +} +"@ + +[System.IO.File]::WriteAllLines("$pwd\$upload_path\$kmcomp_zip.download_info", $kmcomp_download_info) + +# +# Copy common/test/keyboards/*/build/*.kmp to keyboards/ +# +if((Test-Path ..\common\test\keyboards) -ne 0) { + mkdir $keyboards_path + Copy-Item ..\common\test\keyboards\*\build\*.kmp $keyboards_path\ +} + +# +# Upload with rsync to downloads.keyman.com +# (rsync requires that we are in the upload folder to get folders in +# sync correctly; it is possible to resolve this but easier to just cd.) +# + +$rsync_args = @( + '-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions + '--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security + '--stats', # show statistics for log + "--rsync-path='$RSYNC_PATH'", # path on remote server + "--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh + "$build_number", # upload the whole build folder + "$RSYNC_USER@$RSYNC_HOST:$RSYNC_ROOT/developer/$tier/" # target server + path +) + +# Write-Output "rsync parameters:" $rsync_args + +cd upload +& $RSYNC_HOME\rsync.exe $rsync_args +if( $LASTEXITCODE -ne 0 ) { + Write-Output "rsync failed." + exit 99 +} +cd .. + +################# +# DEBUG.ZIP +################# + +if((Test-Path release\$build_number\$debug_zip) -ne 0) { + # + # Copy source files + # + + copy release\$build_number\$debug_zip $upload_path\$debug_zip + + # + # Construct debug-.download_info + # + + $hash = get-filehash $upload_path\$debug_zip -Algorithm MD5 + + $debug_zip_download_info = @" +{ + "name": "Keyman Developer debug files", + "version": "$version_with_tag", + "date": "$([DateTime]::Now.ToString("yyyy-MM-dd"))", + "platform": "win", + "stability": "$tier", + "file": "debug-$build_number.zip", + "md5": "$($hash.Hash)", + "type": "zip", + "build": "$build_counter" +} +"@ + + [System.IO.File]::WriteAllLines("$pwd\$upload_path\$debug_zip.download_info", $debug_zip_download_info) +} + +# +# Upload with rsync to downloads.keyman.com +# (rsync requires that we are in the upload folder to get folders in +# sync correctly; it is possible to resolve this but easier to just cd.) +# + +$rsync_args = @( + '-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions + '--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security + '--stats', # show statistics for log + "--rsync-path='$RSYNC_PATH'", # path on remote server + "--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh + "$build_number", # upload the whole build folder + "$RSYNC_USER@$RSYNC_HOST:$RSYNC_ROOT/developer/$tier/" # target server + path +) + +# Write-Output "rsync parameters:" $rsync_args + +cd upload +& $RSYNC_HOME\rsync.exe $rsync_args +if( $LASTEXITCODE -ne 0 ) { + Write-Output "rsync failed." + exit 99 +} +cd .. + +# EOF diff --git a/resources/teamcity/includes/tc-actions.inc.sh b/resources/teamcity/includes/tc-actions.inc.sh new file mode 100644 index 0000000000..8f671e14b2 --- /dev/null +++ b/resources/teamcity/includes/tc-actions.inc.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. + +# Run the clean action in the `linux` directory.` +linux_clean_action() { + builder_heading "Cleaning up" + # shellcheck disable=SC2154 + "${KEYMAN_ROOT}/linux/build.sh" clean +} + +# Install required dependencies for building Keyman on Linux. +linux_install_dependencies_action() { + builder_echo start "install dependencies" "Installing dependencies" + . "${KEYMAN_ROOT}/linux/scripts/package-build.inc.sh" + checkAndInstallRequirements + builder_echo end "install dependencies" success "Finished installing dependencies" +} + +# Install additional dependencies required for determining test coverage. +linux_additional_test_dependencies_action() { + builder_echo start additional_dependencies "Installing additional dependencies" + local TOINSTALL="lcov libdatetime-perl gcovr python3-venv jq" + + if is_os_version_or_higher 24.04; then + TOINSTALL="${TOINSTALL} libgirepository-2.0-dev python3-coverage" + fi + + # shellcheck disable=SC2086 + linux_check_and_install_packages ${TOINSTALL} + + if ! is_os_version_or_higher 24.04; then + builder_heading "Installing python3-coverage from pip" + pip3 install --user coverage + fi + + linux_install_nvm + linux_install_dependencies_for_tests + + builder_echo end additional_dependencies success "Finished installing additional dependencies" +} + +# Build Keyman for Linux. +linux_build_action() { + INSTALLDIR="$(mktemp -d)" + # shellcheck disable=SC2068 + DESTDIR="${INSTALLDIR}" "${KEYMAN_ROOT}/linux/build.sh" clean configure build install $@ +} + +# Run unit tests for Keyman for Linux. +linux_unit_tests_action() { + builder_echo startTest unit_tests "Running unit tests" + + rm -f /tmp/ibus-engine-keyman.log + rm -f /tmp/ibus-daemon.log + # symlink might point to wrong location, so delete it - will be re-created during tests + rm -rf ~/.local/share/keyman/test_kmx + + export NO_AT_BRIDGE=1 + # shellcheck disable=SC2068 + "${KEYMAN_ROOT}/linux/build.sh" test $@ + builder_echo endTest unit_tests success "Finished running unit tests" +} diff --git a/resources/teamcity/includes/tc-helpers.inc.sh b/resources/teamcity/includes/tc-helpers.inc.sh new file mode 100644 index 0000000000..5170f3c25e --- /dev/null +++ b/resources/teamcity/includes/tc-helpers.inc.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. + +# Returns 0 if we're running on Ubuntu. +is_ubuntu() { + if [[ "${OSTYPE:-}" == "linux-gnu" ]]; then + return 0 + else + return 1 + fi +} + +# Returns 0 if we're running on Windows, i.e. if the environment variable +# `OSTYPE` is set to "msys" or "cygwin". +is_windows() { + if [[ "${OSTYPE:-}" == "msys" ]] || [[ "${OSTYPE:-}" == "cygwin" ]]; then + return 0 + else + return 1 + fi +} + +# Returns 0 if we're running on macOS. +is_macos() { + if [[ "${OSTYPE:-}" == "darwin" ]]; then + return 0 + else + return 1 + fi +} + +install_nvm() { + if ! is_ubuntu; then + # on Windows and macOS build agents are configured manually + return 0 + fi + linux_install_nvm +} + +# Set the environment variables required to use node/nvm and set the +# `KEYMAN_USE_NVM` variable so that the build can automatically install +# the required node version. +set_variables_for_nvm() { + if [[ -f "${HOME}/.nvm/nvm.sh" ]] && [[ -d "${HOME}/.keyman/node" ]]; then + # nvm.sh uses some variables that might not be initialized, so we + # disable the "unbound variable" check temporarily + set -u + export NVM_DIR="${HOME}/.nvm" + # shellcheck disable=SC1091 + . "${NVM_DIR}/nvm.sh" + set +u + export KEYMAN_USE_NVM=1 + PATH=${HOME}/.keyman/node:${PATH} + fi +} + +install_emscripten() { + if ! is_ubuntu; then + # on Windows and macOS build agents are configured manually + return 0 + fi + linux_install_emscripten +} + +set_variables_for_emscripten() { + export EMSCRIPTEN_BASE="${EMSCRIPTEN_BASE:-${HOME}/emsdk/upstream/emscripten}" + export KEYMAN_USE_EMSDK=1 +} diff --git a/resources/teamcity/includes/tc-linux.inc.sh b/resources/teamcity/includes/tc-linux.inc.sh new file mode 100644 index 0000000000..cf1964de21 --- /dev/null +++ b/resources/teamcity/includes/tc-linux.inc.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. + +# Returns 0 if the OS version is greater than or equal to the specified version. +# Parameter: +# $1 - OS version to compare against (e.g., "20.04") +is_os_version_or_higher() { + local OS_VERSION=$1 + + # we use `dpkg --compare-versions` to compare the current Ubuntu version + # shellcheck disable=SC2312 + dpkg --compare-versions "$(lsb_release -r -s)" ge "${OS_VERSION}" +} + +# Returns 0 if the specified package is installed. +# Parameter: +# $1 - Package name to check (e.g., "curl") +is_package_installed() { + local PACKAGE=$1 + + dpkg -s "${PACKAGE}" >/dev/null 2>&1 +} + +# Check if the specified packages are installed and install them if not. +# Parameters: +# $* - List of package names to check and install (e.g., "lcov jq") +linux_check_and_install_packages() { + if ! is_ubuntu; then + return 0 + fi + + builder_echo start "check and install packages" "Checking and installing packages" + local PACKAGES=$* + local TOINSTALL="" + + for p in ${PACKAGES} + do + if ! is_package_installed "${p}"; then + TOINSTALL="${TOINSTALL} ${p}" + fi + done + + if [[ -n "${TOINSTALL}" ]]; then + sudo apt-get update + # shellcheck disable=SC2086 + sudo DEBIAN_FRONTEND="noninteractive" apt-get install -qy ${TOINSTALL} + fi + builder_echo end "check and install packages" success "Finished checking and installing packages" +} + +# Install nvm if it is not already installed and install the latest LTS +linux_install_nvm() { + builder_echo start install_nvm "Checking and installing nvm" + if [[ -f "${HOME}/.nvm/nvm.sh" ]]; then + builder_echo "nvm is already installed" + else + # Install nvm + # shellcheck disable=SC2312 + NVM_RELEASE=$(curl -s https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep tag_name | cut -d : -f 2 | cut -d '"' -f 2) + # shellcheck disable=SC2312 + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_RELEASE}/install.sh | bash + set_variables_for_nvm + nvm install --lts --default --save + nvm use --lts + fi + builder_echo end install_nvm success "Finished checking and installing nvm" +} + +linux_install_emscripten() { + builder_echo start "install emscripten" "Checking and installing emscripten" + if { [[ ! -z "${EMSCRIPTEN_BASE:-}" ]] && [[ -f "${EMSCRIPTEN_BASE}/emcc" ]] ; } || + [[ -f "${HOME}/emsdk/upstream/emscripten/emcc" ]]; then + builder_echo "Emscripten is already installed at ${EMSCRIPTEN_BASE:-${HOME}/emsdk/upstream/emscripten}" + else + # shellcheck disable=SC2154 + . "${KEYMAN_ROOT}/resources/build/minimum-versions.inc.sh" + + builder_echo "Installing emscripten version ${KEYMAN_MIN_VERSION_EMSCRIPTEN}" + export EMSDK_KEEP_DOWNLOADS=1 + # shellcheck disable=SC2164 + cd "${HOME}" + git clone https://github.com/emscripten-core/emsdk.git + # shellcheck disable=SC2164 + cd emsdk + ./emsdk install "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" + ./emsdk activate "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" + fi + set_variables_for_emscripten + builder_echo end "install emscripten" success "Finished checking and installing emscripten" +} + +# Install additional dependencies required for running integration tests. +linux_install_dependencies_for_tests() { + builder_echo start install_dependencies_for_tests "Installing dependencies for tests" + + linux_check_and_install_packages xvfb xserver-xephyr metacity mutter dbus-x11 weston xwayland + + builder_echo end install_dependencies_for_tests success "Finished installing dependencies for tests" +} + +linux_start_xvfb() { + # On Linux start Xvfb etc + local PID_FILE=/tmp/keymanweb-pids + builder_echo "Starting Xvfb..." + Xvfb -screen 0 1024x768x24 :33 &> /dev/null & + echo "kill -9 $! || true" > "${PID_FILE}" + sleep 1 + builder_echo "Starting Xephyr..." + DISPLAY=:33 Xephyr :32 -screen 1024x768 &> /dev/null & + echo "kill -9 $! || true" >> "${PID_FILE}" + sleep 1 + builder_echo "Starting metacity" + metacity --display=:32 &> /dev/null & + echo "kill -9 $! || true" >> "${PID_FILE}" + export DISPLAY=:32 +} + +linux_stop_xvfb() { + # On Linux stop Xvfb etc + local PID_FILE=/tmp/keymanweb-pids + if [[ -f "${PID_FILE}" ]]; then + bash "${PID_FILE}" + rm -f "${PID_FILE}" + fi +} diff --git a/resources/teamcity/linux/keyman-linux-release.sh b/resources/teamcity/linux/keyman-linux-release.sh new file mode 100755 index 0000000000..14457a54fb --- /dev/null +++ b/resources/teamcity/linux/keyman-linux-release.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# TC build script for Keyman Linux/Release +# shellcheck disable=SC2164 + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# shellcheck disable=SC2154 +. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-actions.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh" + +################################ Main script ################################ + +# shellcheck disable=SC2016 +builder_describe \ + "Create a daily release of 'master', 'beta', or 'stable-*' branch." \ + "all run all actions" \ + "configure install dependencies" \ + "build make a release build" \ + "test run unit tests" \ + "publish make a source tarball and publish to downloads and launchpad" \ + "--gpgkey=GPGKEYGRIP GPG key for signing" \ + "--gpgpw=GPGKEYPW GPG key passphrase" \ + "--rsync-path=RSYNC_PATH rsync path on remote server" \ + "--rsync-user=RSYNC_USER rsync user on remote server" \ + "--rsync-host=RSYNC_HOST rsync host on remote server" \ + "--rsync-root=RSYNC_ROOT rsync root on remote server" + + +builder_parse "$@" + +cd "${KEYMAN_ROOT}/linux" + +function _install_additional_dependencies() { + builder_echo start additional_dependencies "Installing additional dependencies" + + # Additionally we need quilt to be able to create the source package. + # Since this is not needed to build the binary package, it is not + # (and should not be) included in `build-depends` in `debian/control`. + sudo DEBIAN_FRONTEND=noninteractive apt-get -q -y install quilt + + builder_echo end additional_dependencies success "Finished installing additional dependencies" +} + +function _cleanup_before_creating_source_package() { + # Cleanup before creating the source package. + builder_echo start cleanup "Cleanup before creating the source package" + git clean -dxf + builder_echo end cleanup success "Cleanup before creating the source package" +} + +function _make_release_source_tarball() { + builder_echo start "make source tarball" "Make source tarball" + rm -rf dist + ./scripts/reconf.sh + PKG_CONFIG_PATH="${KEYMAN_ROOT}/core/build/arch/release/meson-private" ./scripts/dist.sh + mkdir -p "upload/${KEYMAN_VERSION}" + cp -a dist/*.tar.gz "upload/${KEYMAN_VERSION}" + ( + cd "upload/${KEYMAN_VERSION}" + sha256sum ./*.tar.gz > SHA256SUMS + builder_echo end "make source tarball" success "Make source tarball" + ) +} + +function _sign_source_tarball() { + ( + builder_echo start "sign source tarball" "Sign source tarball" + cd "upload/${KEYMAN_VERSION}" + + eval "$(gpg-agent -vv --daemon --allow-preset-passphrase --debug-level 9)" + /usr/lib/gnupg/gpg-preset-passphrase --passphrase "${GPGKEYPW}" --preset "${GPGKEYGRIP}" + for f in ./*.tar.gz; do gpg --output "${f}.asc" -a --detach-sig "${f}"; done + /usr/lib/gnupg/gpg-preset-passphrase --forget "${GPGKEYGRIP}" || true + builder_echo end "sign source tarball" success "Sign source tarball" + ) +} + +function _publish_to_downloads() { + builder_echo start "publish to downloads" "Publish to downloads.keyman.com" + + local DATE UPLOAD_BASE UPLOAD_FOLDER UPLOAD_DIR + local ARTIFACTS NAMES + local TAR_GZ HASH SIZE DOWNLOAD_INFO + + DATE=$(date +%F) + + UPLOAD_BASE="upload" + UPLOAD_FOLDER="${KEYMAN_VERSION}" + UPLOAD_DIR="${UPLOAD_BASE}/${UPLOAD_FOLDER}" + + # Set permissions as required on download site + builder_echo "Setting upload file permissions for downloads.keyman.com" + chmod a+rx "${UPLOAD_DIR}" + chmod g+w "${UPLOAD_DIR}" + + chmod g+rw "${UPLOAD_DIR}"/* + chmod a+r "${UPLOAD_DIR}"/* + + ARTIFACTS=("keyman-${KEYMAN_VERSION}.tar.gz") + NAMES=("Keyman for Linux") + + for i in "${!ARTIFACTS[@]}"; do + TAR_GZ=${ARTIFACTS[${i}]} + # Construct .download_info + HASH=$(md5sum "${UPLOAD_DIR}/${TAR_GZ}" | cut -d ' ' -f 1) + SIZE=$(stat --print="%s" "${UPLOAD_DIR}/${TAR_GZ}") + # TODO: Truncate NAME + DOWNLOAD_INFO=$( jq -n \ + --arg NAME "${NAMES[${i}]}" \ + --arg BUILD_NUMBER "${KEYMAN_VERSION}" \ + --arg DATE "${DATE}" \ + --arg KEYMAN_TIER "${KEYMAN_TIER}" \ + --arg FILENAME "${TAR_GZ}" \ + --arg HASH "${HASH}" \ + --arg BUILD_COUNTER "${KEYMAN_VERSION_PATCH}" \ + --arg SIZE "${SIZE}" \ + '{name: $NAME, version: $BUILD_NUMBER, date: $DATE, platform: "linux", stability: $KEYMAN_TIER, file: $FILENAME, md5: $HASH, type: "tar.gz", build: $BUILD_COUNTER, size: $SIZE}' ) + echo "${DOWNLOAD_INFO}" | jq . >> "${UPLOAD_DIR}/${TAR_GZ}.download_info" + done + + # Expanded, documented form of the arguments + # ========================================== + # "-vrzltp " # verbose, recurse, zip, copy symlinks, preserve times, permissions + # "--perms " # perfectly matches existing file permissions on the build agent + # "--stats " # show statistics for log + # "--rsync-path=\"sudo -u vu2009 rsync\" # path on remote server + # "--rsh=ssh " # use ssh + + # The actual rsync call. + # We run into weird quote-based issues if we don't do a monolithic call as seen below, at least at present. + builder_echo "Performing rsync call" + rsync -vrzltp --perms --stats --rsync-path="${RSYNC_PATH}" --rsh=ssh "${UPLOAD_DIR}" "${RSYNC_USER}@${RSYNC_HOST}:${RSYNC_ROOT}/linux/${KEYMAN_TIER}" + + builder_echo end "publish to downloads" success "Publish to downloads.keyman.com" +} + +function _publish_to_launchpad() { + builder_echo start "upload to launchpad" "Upload to launchpad" + + git reset --hard + + /usr/lib/gnupg/gpg-preset-passphrase --passphrase "${GPGKEYPW}" --preset "${GPGKEYGRIP}" + UPLOAD=yes scripts/launchpad.sh + /usr/lib/gnupg/gpg-preset-passphrase --forget "${GPGKEYGRIP}" || true + + builder_echo end "upload to launchpad" success "Upload to launchpad" +} + +function _publish_linux_help() { + builder_echo start "upload linux help" "Upload new Keyman Linux help to help.keyman.com" + + ( + cd "${KEYMAN_ROOT}/../help.keyman.com" || exit 1 + # shellcheck disable=SC2016 + git config credential.helper '!f() { sleep 1; echo "username=${GITHUB_USER}"; echo "password=${GITHUB_TOKEN}"; }; f' + cd "${KEYMAN_ROOT}/resources/build" + ./help-keyman-com.sh linux + ) + + builder_echo end "upload linux help" success "Upload new Keyman Linux help to help.keyman.com" +} + +function publish_action() { + _cleanup_before_creating_source_package + _make_release_source_tarball + _sign_source_tarball + _publish_to_downloads + _publish_to_launchpad + _publish_linux_help +} + +if builder_has_action all; then + linux_install_dependencies_action + _install_additional_dependencies + + set_variables_for_nvm + + linux_build_action + linux_unit_tests_action --no-integration + publish_action +else + builder_run_action configure linux_install_dependencies_action + builder_run_action configure _install_additional_dependencies + + set_variables_for_nvm + + builder_run_action build linux_build_action + builder_run_action test linux_unit_tests_action --no-integration + builder_run_action publish publish_action +fi diff --git a/resources/teamcity/linux/keyman-linux-test-integration.sh b/resources/teamcity/linux/keyman-linux-test-integration.sh new file mode 100755 index 0000000000..0868cbb835 --- /dev/null +++ b/resources/teamcity/linux/keyman-linux-test-integration.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# TC build script for Keyman Linux/Test:Integration +# shellcheck disable=SC2164 +# shellcheck disable=SC1091 + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# shellcheck disable=SC2154 +. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-actions.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh" + +################################ Main script ################################ + +builder_describe \ + "Runs unit plus integration tests." \ + "all run all actions" \ + "clean remove all build artifacts" \ + "configure install dependencies" \ + "build make a release build" \ + "test run unit tests" + +builder_parse "$@" + +cd "${KEYMAN_ROOT}/linux" + +if builder_has_action all; then + linux_clean_action + linux_install_dependencies_action + linux_additional_test_dependencies_action + set_variables_for_nvm + linux_build_action + linux_unit_tests_action +else + builder_run_action clean linux_clean_action + builder_run_action configure linux_install_dependencies_action + builder_run_action configure linux_additional_test_dependencies_action + + set_variables_for_nvm + + builder_run_action build linux_build_action + builder_run_action test linux_unit_tests_action +fi diff --git a/resources/teamcity/linux/keyman-linux-test.sh b/resources/teamcity/linux/keyman-linux-test.sh new file mode 100755 index 0000000000..7c8387d153 --- /dev/null +++ b/resources/teamcity/linux/keyman-linux-test.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# TC build script for Keyman Linux/Test +# shellcheck disable=SC2164 +# shellcheck disable=SC1091 + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# shellcheck disable=SC2154 +. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-actions.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh" + +################################ Main script ################################ + +builder_describe \ + "Tests install build, making a source package and runs unit tests." \ + "all run all actions" \ + "configure install dependencies" \ + "build make a release build" \ + "test run unit tests" \ + "publish make a source tarball" + +builder_parse "$@" + +cd "${KEYMAN_ROOT}/linux" + +function make_source_tarball_action() { + builder_echo start "make source tarball" "Make source tarball" + rm -rf dist + make tmpsources + builder_echo end "make source tarball" success "Finished making source tarball" +} + +if builder_has_action all; then + linux_install_dependencies_action + linux_additional_test_dependencies_action + + set_variables_for_nvm + + linux_build_action --coverage + linux_unit_tests_action --coverage --report --no-integration + make_source_tarball_action +else + builder_run_action configure linux_install_dependencies_action + builder_run_action configure linux_additional_test_dependencies_action + + set_variables_for_nvm + + builder_run_action build linux_build_action --coverage + builder_run_action test linux_unit_tests_action --coverage --report --no-integration + builder_run_action publish make_source_tarball_action +fi diff --git a/resources/teamcity/web/keyman-web-release.sh b/resources/teamcity/web/keyman-web-release.sh new file mode 100755 index 0000000000..d2aa1c378b --- /dev/null +++ b/resources/teamcity/web/keyman-web-release.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. +# +# TC build script to build release of KeymanWeb. + +# shellcheck disable=SC2164 +# shellcheck disable=SC1091 + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# shellcheck disable=SC2154 +. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh" +. "${KEYMAN_ROOT}/resources/teamcity/web/web-actions.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh" + +################################ Main script ################################ + +builder_describe \ + "Run tests for native KeymanWeb" \ + "all run all actions" \ + "configure install dependencies" \ + "build build Web + embedded" \ + "publish publish release" \ + "--rsync-path=RSYNC_PATH rsync path on remote server" \ + "--rsync-user=RSYNC_USER rsync user on remote server" \ + "--rsync-host=RSYNC_HOST rsync host on remote server" \ + "--rsync-root=RSYNC_ROOT rsync root on remote server" \ + "--s.keyman.com=S_KEYMAN_COM path to s.keyman.com repository" \ + "--help.keyman.com=HELP_KEYMAN_COM path to help.keyman.com repository" + +builder_parse "$@" + +cd "${KEYMAN_ROOT}/web" + +function _push_release_to_skeymancom() { + # Push release to s.keyman.com/kmw/engine (do this before updating + # downloads.keyman.com so we can ensure files are available) + builder_echo start publish "Publishing release to s.keyman.com" + + cd "${S_KEYMAN_COM:=${KEYMAN_ROOT}/../s.keyman.com}" + git pull https://github.com/keymanapp/s.keyman.com.git master + cd "${KEYMAN_ROOT}/web" + "${KEYMAN_ROOT}/web/ci.sh" prepare:s.keyman.com --s.keyman.com "${S_KEYMAN_COM}" + + builder_echo end publish success "Finished publishing release to s.keyman.com" +} + +function _zip_and_upload_artifacts() { + builder_echo start "zip and upload artifacts" "Zipping and uploading artifacts" + + # shellcheck disable=SC2154 + powershell -NonInteractive -ExecutionPolicy Bypass -File "${THIS_SCRIPT_PATH}/zip-and-upload-artifacts.ps1" + + builder_echo end "zip and upload artifacts" success "Finished zipping and uploading artifacts" +} + +function _upload_help() { + builder_echo start "upload help" "Uploading new Keyman for Web help to help.keyman.com" + + export HELP_KEYMAN_COM="${HELP_KEYMAN_COM:-${KEYMAN_ROOT}/../help.keyman.com}" + cd "${KEYMAN_ROOT}/resources/build" + "${KEYMAN_ROOT}/resources/build/help-keyman-com.sh" web + cd "${KEYMAN_ROOT}/web" + + builder_echo end "upload help" success "Finished uploading new Keyman for Web help to help.keyman.com" +} + +function publish_web_action() { + builder_echo start publish "Publishing KeymanWeb release" + + # TODO: refactor to allow to run on Linux/macOS as well + if ! is_windows; then + builder_echo end publish error "Publishing KeymanWeb is only supported on Windows" + return 1 + fi + + export RSYNC_PATH + export RSYNC_USER + export RSYNC_HOST + export RSYNC_ROOT + + # Push release to s.keyman.com/kmw/engine (do this before updating + # downloads.keyman.com so we can ensure files are available) + _push_release_to_skeymancom + + _zip_and_upload_artifacts + _upload_help + + builder_echo end publish success "Finished publishing KeymanWeb release" +} + +if builder_has_action all; then + web_install_dependencies_on_linux_action + + set_variables_for_nvm + + web_build_action + publish_web_action +else + builder_run_action configure web_install_dependencies_on_linux_action + + set_variables_for_nvm + + builder_run_action build web_build_action + builder_run_action publish publish_web_action +fi diff --git a/resources/teamcity/web/keyman-web-test.sh b/resources/teamcity/web/keyman-web-test.sh new file mode 100755 index 0000000000..0996ab7ec3 --- /dev/null +++ b/resources/teamcity/web/keyman-web-test.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. +# +# TC build script for Keyman Web/Test + +# shellcheck disable=SC2164 +# shellcheck disable=SC1091 + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# shellcheck disable=SC2154 +. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh" +. "${KEYMAN_ROOT}/resources/teamcity/web/web-actions.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh" +. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh" + +################################ Main script ################################ + +builder_describe \ + "Run tests for native KeymanWeb" \ + "all run all actions" \ + "configure install dependencies" \ + "build build Web + embedded" \ + "test run native KeymanWeb tests and check build size" + +builder_parse "$@" + +cd "${KEYMAN_ROOT}/web" + +function check_build_size_action() { + builder_echo start "check build size" "Check build size" + "${KEYMAN_ROOT}/web/ci.sh" validate-size + builder_echo end "check build size" success "Finished checking build size" +} + +if builder_has_action all; then + web_install_dependencies_on_linux_action + + set_variables_for_nvm + + web_build_action + web_test_action + check_build_size_action +else + builder_run_action configure web_install_dependencies_on_linux_action + + set_variables_for_nvm + + builder_run_action build web_build_action + builder_run_action test web_test_action + builder_run_action test check_build_size_action +fi diff --git a/resources/teamcity/web/web-actions.inc.sh b/resources/teamcity/web/web-actions.inc.sh new file mode 100644 index 0000000000..187eef8bb6 --- /dev/null +++ b/resources/teamcity/web/web-actions.inc.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. + +web_install_dependencies_on_linux_action() { + if ! is_ubuntu; then + return 0 + fi + + builder_echo start "install dependencies" "Install dependencies" + + # shellcheck disable=SC2086 + linux_check_and_install_packages devscripts jq + + linux_install_nvm + _install_playwright_dependencies + + builder_echo end "install dependencies" success "Finished installing dependencies" +} + +_install_playwright_dependencies() { + if ! is_ubuntu || ! is_os_version_or_higher 24.04; then + return 0 + fi + + # shellcheck disable=SC2086 + linux_check_and_install_packages ibevent-2.1-7t64 libxslt1.1 libwoff1 \ + libvpx9 libgstreamer-plugins-bad1.0-0 libwebpdemux2 libharfbuzz-icu0 \ + libenchant-2-2 libsecret-1-0 libhyphen0 libmanette-0.2-0 libflite1 \ + gstreamer1.0-libav libnss3 libnspr4 libatk1.0-0t64 libatk-bridge2.0-0t64 \ + libcups2t64 libatspi2.0-0t64 libxcomposite1 libxdamage1 libxrandr2 \ + libxcursor1 libgtk-3-0t64 libgles2 +} + +web_build_action() { + builder_echo start web_build "Building web" + # shellcheck disable=SC2154 + "${KEYMAN_ROOT}/web/ci.sh" build + builder_echo end web_build success "Finished building web" +} + +web_test_action() { + builder_echo start web_test "Running tests for native KeymanWeb" + if is_ubuntu; then + linux_start_xvfb + trap "linux_stop_xvfb" ERR + fi + + "${KEYMAN_ROOT}/web/ci.sh" test + + if is_ubuntu; then + linux_stop_xvfb + trap ERR + fi + + "${KEYMAN_ROOT}/web/build.sh" coverage + + builder_echo end web_test success "Finished running tests for native KeymanWeb" +} + diff --git a/resources/teamcity/web/zip-and-upload-artifacts.ps1 b/resources/teamcity/web/zip-and-upload-artifacts.ps1 new file mode 100755 index 0000000000..91d1cb9667 --- /dev/null +++ b/resources/teamcity/web/zip-and-upload-artifacts.ps1 @@ -0,0 +1,79 @@ +# Keyman is copyright (C) SIL Global. MIT License. +# +# This script should be the identical for nightly/beta/stable for a given platform +# +# Working directory should be ${KEYMAN_ROOT}/web +# +$ErrorActionPreference = "Stop"; + +$tier = Get-Content ..\TIER.md +$build_number = Get-Content ..\VERSION.md +$build_counter = $build_number -replace "^\d+\.\d+\.(\d+)$", '$1' + +$upload_path = "build\upload\$build_number" +$zip = "$upload_path\keymanweb-$build_number.zip" + +$7Z_HOME = $env:7Z_HOME +$RSYNC_HOME = $env:RSYNC_HOME +$RSYNC_PATH = $env:RSYNC_PATH +$RSYNC_USER = $env:RSYNC_USER +$RSYNC_HOST = $env:RSYNC_HOST +$RSYNC_ROOT = $env:RSYNC_ROOT +$USERPROFILE = $env:USERPROFILE + +# Since shell-scripting doesn't like number-initial variables, we convert it to a friendlier name. +$env:SEVEN_Z_HOME=$7Z_HOME + +# PowerShell fun: `--` parameters will break a PowerShell command when not escaped in some form. +# We can build the command in string form first and then execute the string, fortunately. +$cmd = '"C:\Program Files\Git\bin\bash.exe" --init-file "c:\Program Files\Git\etc\profile" -l ./ci.sh prepare:downloads.keyman.com' +cmd /c $cmd + +$hash = get-filehash $zip -Algorithm MD5 + +# +# Construct .build_info +# + +$download_info = @" +{ + "name": "KeymanWeb", + "version": "$build_number", + "date": "$([DateTime]::Now.ToString("yyyy-MM-dd"))", + "platform": "web", + "stability": "$tier", + "file": "keymanweb-$build_number.zip", + "md5": "$($hash.Hash)", + "type": "zip", + "build": "$build_counter" +} +"@ + +# WriteAllLines is needed to avoid BOM +[System.IO.File]::WriteAllLines("$pwd\$upload_path\keymanweb-$build_number.zip.download_info", $download_info) +# $download_info | Out-File $upload_path\keymanweb-$build_number.zip.download_info -Encoding utf8 + +# +# Upload with rsync to downloads.keyman.com +# (rsync requires that we are in the upload folder to get folders in +# sync correctly; it is possible to resolve this but easier to just cd.) +# + +$rsync_args = @( + '-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions + '--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security + '--stats', # show statistics for log + "--rsync-path='$RSYNC_PATH'", # path on remote server + "--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh + "$build_number", # upload the whole build folder + "$RSYNC_USER@$RSYNC_HOST:$RSYNC_ROOT/web/$tier/" # target server + path +) + +# Write-Output "rsync parameters:" $rsync_args + +cd "$upload_path\.." +& $RSYNC_HOME\rsync.exe $rsync_args +if ($LASTEXITCODE -ne 0) { throw "Exit code is $LASTEXITCODE" } +cd .. + +# EOF diff --git a/resources/tools/check-markdown/build.sh b/resources/tools/check-markdown/build.sh index 607be1dea6..c288b9a6be 100755 --- a/resources/tools/check-markdown/build.sh +++ b/resources/tools/check-markdown/build.sh @@ -15,7 +15,7 @@ builder_describe "Check markdown internal links" \ "build" builder_describe_outputs \ - configure /node_modules \ + configure "${THIS_SCRIPT_PATH}/node_modules" \ build build/index.js builder_parse "$@" diff --git a/resources/tools/check-markdown/package.json b/resources/tools/check-markdown/package.json index 16c85f65a4..a75e6e3568 100644 --- a/resources/tools/check-markdown/package.json +++ b/resources/tools/check-markdown/package.json @@ -4,6 +4,7 @@ "type": "module", "main": "build/index.js", "license": "MIT", + "scripts": {}, "devDependencies": { "marked": "^14.1.2", "chalk": "^2.4.2" diff --git a/web/build.sh b/web/build.sh index 0dd9b5ef53..51301682ca 100755 --- a/web/build.sh +++ b/web/build.sh @@ -9,6 +9,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" ## END STANDARD BUILD SCRIPT INCLUDE . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" # ################################ Main script ################################ @@ -26,6 +27,7 @@ builder_describe "Builds engine modules for Keyman Engine for Web (KMW)." \ ":app/webview A puppetable version of KMW designed for use in a host app's WebView" \ ":app/ui Builds KMW's desktop form-factor keyboard-selection UI modules" \ ":engine/attachment Subset used for detecting valid page contexts for use in text editing " \ + ":engine/common/web-utils Low-level, headless utility methods and classes used across multiple modules" \ ":engine/dom-utils A common subset of function used for DOM calculations, layout, etc" \ ":engine/events Specialized classes utilized to support KMW API events" \ ":engine/element-wrappers Subset used to integrate with website elements" \ @@ -40,8 +42,7 @@ builder_describe "Builds engine modules for Keyman Engine for Web (KMW)." \ ":samples Builds all needed resources for the KMW sample-page set" \ ":tools Builds engine-related development resources" \ ":test-pages=src/test/manual Builds resources needed for the KMW manual testing pages" \ - ":_all (Meta build target used when targets are not specified)" \ - "--ci+ Set to utilize CI-based test configurations & reporting." + ":_all (Meta build target used when targets are not specified)" # Possible TODO? # "upload-symbols Uploads build product to Sentry for error report symbolification. Only defined for $DOC_BUILD_EMBED_WEB" \ @@ -70,6 +71,7 @@ builder_describe_outputs \ build:engine/main "/web/build/engine/main/lib/index.mjs" \ build:engine/osk "/web/build/engine/osk/lib/index.mjs" \ build:engine/predictive-text "/web/src/engine/predictive-text/worker-main/build/lib/web/index.mjs" \ + build:engine/common/web-utils "/web/src/engine/common/web-utils/build/lib/index.mjs" \ build:samples "/web/src/samples/simplest/keymanweb.js" \ build:tools "/web/build/tools/building/sourcemap-root/index.js" \ build:test-pages "/web/build/test-resources/sentry-manager.js" @@ -82,6 +84,8 @@ BUNDLE_CMD="node ${KEYMAN_ROOT}/web/src/tools/es-bundling/build/common-bundle.mj # We can run all clean & configure actions at once without much issue. +builder_run_action clean:_all rm -rf build/ + builder_run_child_actions clean ## Clean actions @@ -131,14 +135,6 @@ build_action() { "${KEYMAN_ROOT}/web/build/test/dom/cases/attachment/" } -test_action() { - TEST_OPTS= - if builder_has_option --ci; then - TEST_OPTS=--ci - fi - ./test.sh "${TEST_OPTS}" -} - coverage_action() { builder_echo "Creating coverage report..." @@ -163,6 +159,7 @@ coverage_action() { rm -rf build/coverage/tmp } +builder_run_child_actions build:engine/common/web-utils builder_run_child_actions build:engine/dom-utils builder_run_child_actions build:engine/keyboard @@ -208,7 +205,7 @@ builder_run_action build:_all build_action # Run tests builder_run_child_actions test -builder_run_action test:_all test_action +builder_run_action test:_all ./test.sh function do_test_help() { check-markdown "$KEYMAN_ROOT/web/docs/engine" diff --git a/web/ci.sh b/web/ci.sh index 70cb24b965..f3f70eef93 100755 --- a/web/ci.sh +++ b/web/ci.sh @@ -37,7 +37,7 @@ builder_parse "$@" #### -TIER=$(cat ../TIER.md) +KEYMAN_TIER=$(cat ../TIER.md) BUILD_NUMBER=$(cat ../VERSION.md) function web_sentry_upload () { @@ -50,7 +50,7 @@ function web_sentry_upload () { # --strip-common-prefix does not take an argument, unlike --strip-prefix. It auto-detects # the most common prefix instead. - sentry-cli releases files "${VERSION_GIT_TAG}" upload-sourcemaps --strip-common-prefix "$1" \ + sentry-cli releases files "${KEYMAN_VERSION_GIT_TAG}" upload-sourcemaps --strip-common-prefix "$1" \ --rewrite --ext js --ext map --ext ts echo "Upload successful." } @@ -63,8 +63,7 @@ function build_action() { # - also useful when validating this script on a local dev machine! # - build: then do the ACTUAL build. # one option: - # - --ci: For app/browser, outputs 'release' config filesize profiling logs - ./build.sh configure clean build --ci + ./build.sh configure clean build # Upload the sentry-configuration engine used by the mobile apps to sentry # Also, clean 'em first. @@ -79,19 +78,7 @@ function build_action() { } function test_action() { - # Testing step: run ALL unit tests, including those of the submodules. - - OPTIONS= - if ! builder_is_debug_build; then - OPTIONS=--ci - fi - - # No --reporter option exists yet for the headless modules. - - "${KEYMAN_ROOT}/web/src/engine/keyboard/build.sh" test ${OPTIONS} - "${KEYMAN_ROOT}/web/src/engine/osk/gesture-processor/build.sh" test ${OPTIONS} - - ./build.sh test ${OPTIONS} + ./build.sh test } function post_test_action() { @@ -132,7 +119,7 @@ function prepare_s_keyman_com_action() { # Second phase: copy the artifacts over # The main build products are expected to reside at the root of this folder. - BASE_PUBLISH_FOLDER="${S_KEYMAN_COM}/kmw/engine/${VERSION}" + BASE_PUBLISH_FOLDER="${S_KEYMAN_COM}/kmw/engine/${KEYMAN_VERSION}" echo "FOLDER: ${BASE_PUBLISH_FOLDER}" mkdir -p "${BASE_PUBLISH_FOLDER}" @@ -143,27 +130,27 @@ function prepare_s_keyman_com_action() { # Third phase: tweak the sourcemaps # We can use an alt-mode of Web's sourcemap-root tool for this. for sourcemap in "${BASE_PUBLISH_FOLDER}/"*.map; do - node "${KEYMAN_ROOT}/web/build/tools/building/sourcemap-root/index.js" null "${sourcemap}" --sourceRoot "https://s.keyman.com/kmw/engine/${VERSION}/src" + node "${KEYMAN_ROOT}/web/build/tools/building/sourcemap-root/index.js" null "${sourcemap}" --sourceRoot "https://s.keyman.com/kmw/engine/${KEYMAN_VERSION}/src" done # Construct the PR - echo "Committing and pushing KeymanWeb release ${VERSION} to s.keyman.com" + echo "Committing and pushing KeymanWeb release ${KEYMAN_VERSION} to s.keyman.com" - ci_add_files "${S_KEYMAN_COM}" "kmw/engine/${VERSION}" + ci_add_files "${S_KEYMAN_COM}" "kmw/engine/${KEYMAN_VERSION}" if ! ci_repo_has_cached_changes "${S_KEYMAN_COM}"; then builder_die "No release was added to s.keyman.com, something went wrong" fi - ci_open_pull_request "${S_KEYMAN_COM}" auto/keymanweb/release "auto: KeymanWeb release ${VERSION}" + ci_open_pull_request "${S_KEYMAN_COM}" auto/keymanweb/release "auto: KeymanWeb release ${KEYMAN_VERSION}" } # Note: for now, this command is used to prepare the artifacts used by the download site, but # NOT to actually UPLOAD them via rsync or to produce related .download_info files. function prepare_downloads_keyman_com_action() { - UPLOAD_PATH="${KEYMAN_ROOT}/web/build/upload/${VERSION}" + UPLOAD_PATH="${KEYMAN_ROOT}/web/build/upload/${KEYMAN_VERSION}" # --- First action artifact - the KMW zip file --- - ZIP="${UPLOAD_PATH}/keymanweb-${VERSION}.zip" + ZIP="${UPLOAD_PATH}/keymanweb-${KEYMAN_VERSION}.zip" mkdir -p "${UPLOAD_PATH}" diff --git a/web/common.inc.sh b/web/common.inc.sh index 5b48464378..4b24730d5d 100644 --- a/web/common.inc.sh +++ b/web/common.inc.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash # +# Needed for builder_is_ci_build check. +. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" + BUNDLE_CMD="node $KEYMAN_ROOT/web/src/tools/es-bundling/build/common-bundle.mjs" # Compiles all build products corresponding to the specified target. @@ -23,6 +26,8 @@ function compile() { builder_die "Scripting error: insufficient argument count!" fi + eslint . + local COMPILE_TARGET="$1" local SRC_DIR=${2:-"${KEYMAN_ROOT}/web/src"} local BUILD_DIR=${3:-"${KEYMAN_ROOT}/web/build"} @@ -99,7 +104,7 @@ function test-headless() { fi TEST_OPTS= - if builder_has_option --ci; then + if builder_is_ci_build; then TEST_OPTS="--reporter mocha-teamcity-reporter" fi if [[ -n "$TEST_EXTENSIONS" ]]; then diff --git a/web/docs/engine/reference/osk/classes.md b/web/docs/engine/reference/osk/classes.md index ea82b0bcf3..b26edd676c 100644 --- a/web/docs/engine/reference/osk/classes.md +++ b/web/docs/engine/reference/osk/classes.md @@ -146,6 +146,12 @@ div.kmw-key.kmw-key-default:not([id^='popup-']) | `kmw-alert-text` | Message box alert text | | `kmw-alert-close` | Message box close icon | +### Class names applied to other elements + +| Class name | Element | +| ----------------------- | ------------------------------- | +| `kmw-page-trailer` | Element appended to page on mobile devices to prevent keyboard overlap at the page's bottom | + The default styles for _KeymanWeb_ can be found online at https://github.com/keymanapp/keyman/blob/master/web/src/resources/osk/kmwosk.css. diff --git a/web/docs/internal/gestures/gesture-input-serialization.md b/web/docs/internal/gestures/gesture-input-serialization.md new file mode 100644 index 0000000000..e54ff808a0 --- /dev/null +++ b/web/docs/internal/gestures/gesture-input-serialization.md @@ -0,0 +1,29 @@ +# Gesture Input Serialization + +The GestureRecognizer engine is designed to handle and process asynchronous, continuous input of one or more touchpoints in parallel. The processing of events takes time, though, and on older devices it is possible for new input events to come in while older events are still being processed. As it is possible for an earlier event to affect how a later event should be processed, it is necessary to ensure that all events are processed in a specific order. We wish to capture the user's original intent for each aspect of input and later match it against the system state that a user would reasonably anticipate for the input based on their previous interactions, even if such a state hasn't yet actually been reached or activated at the true time of input. + +For a keyboard, suppose a user taps the `Shift` and `A` keys with minimal time gap between the two events. Also suppose that external code sets `state = 'shift'` when processing the `Shift` key, with `default` its current value - perhaps reflecting the name of different layers in an on-screen keyboard. For this example, the `Shift` state may not be in place yet once the `A` is tapped - event processing does take _some_ time, after all - especially if the events are considered simultaneous by the browser. + +Note that JS mouse and touch events lock in their target elements at the time of observation, even if that element is swapped out of the DOM by the time the event is actually handled. The serialization scheme used by the engine handles the `A`s event (provisionally selecting the unshifted `A`, per the original `default` state) but delays further processing for it until the `Shift` state has taken effect. If supplied with a properly-designed `itemIdentifier` method as part of the engine's configuration, the serialization processes then uses that method to finalize the associated element once processing resumes, remapping from the unshifted `A` to the Shift-layer `A` per the shift in the event's associated engine `state`. + +Furthermore, the engine leverages `Promise`s internally to help keep the different layers of [gesture processing](./gesture-processing.md) separate and isolated; these need a chance to resolve before continuing processing of the new event and any events that may follow it. In particular, cases like the `Shift` + `A` example invoke `setTimeout` waits that leverage the JS task queue, not just the microtask queue that `Promise`s natively leverage. During times of rapid, heavy input, it is thus possible for newly-incoming touch events to be received before all `Promise`s for prior input gesture processing have fully resolved. It is thus necessary to artificially defer processing of newly-incoming touch events so that their processing does not occur before cases like `Shift` + `A` above fully resolve. + +## Gesture Processing serialization + +The first point that requires special handling is detection of newly-received inputs - new mouse or touch interaction points. As some gesture models may specify autocompletion whenever a new touch comes in, those should resolve before further handling the new input. + +Toward this end, `MatcherSelector`'s `matchGesture` method maintains a `Promise` serving as a lock within the field `pendingMatchSetup`. This is set whenever checking such cases - whether or not existing gestures have special interactions triggered by new touchpoints - and is cleared once these checks have completed. This lock is referenced early within the same method to prevent `matchGesture` from handling any subsequent inputs while waiting on this process to complete. + +The method calling this - `TouchpointCoordinator.onNewTrackedPath` - also includes special handling for this case. It will not emit an event for the newly-incoming touchpoint until after its `matchGesture` call has completed and determined which set of gestures the input may match. Said process relates strongly to the [serialization](#touchpath-sampling-serialization) scheme documented below and is the point where these two aspects interact. The event raised by `onNewTrackedPath` - `'inputstart'` - should only fire once gesture processing may proceed for the input. + +Serialization at this level may be further complicated for cases where a gesture state was being maintained due to having pushed an engine alt-state that had at least one live gesture; should the last sustaining sub-gesture reach completion by the new input, the sustain state must be unwound before continuing. For example, suppose a new menu appears when a key or button is held down, with this action triggering an "alt-mode" using a different base set of gesture models for use of that new menu while it displays. That base gesture may specify that it is to be kept alive to avoid interrupting interactions with the new menu when the "held down" key is released, only dropping the menu (and the "alt-mode") after those interactions resolve. + +## Touchpath sampling serialization + +Delaying new inputs also invokes a need to delay events that occur at or after the same timestamp as the first delayed input. After all, tracking for any new input, including for the first delayed input, needs to be initiated _before_ any future observations for such an input may be properly handled. This is especially critical for touch processing, as it is easily possible for the user to perform touch input with multiple fingers in parallel. + +To address this, the touch-event processing component - `TouchEventEngine` - captures events as they come in and adds them to a processing queue, implemented as `AsyncClosureDispatchQueue`, that ensures the events stay in proper sequential, serialized order by use of `Promise`s. It also creates specialized `Promise`-based locks whenever a new touchpoint is received that are added to the serialization queue; these locks are only released when `TouchpointCoordinator.onNewTrackedPath` has proceeded far enough to publish the touchpoint's tracking `GestureSource` instance - this is the purpose of the `fulfillInputStart` call within `onNewTrackedPath`. + +These locks are important in case external code wishes to receive each incoming input event for the touchpoint via event; path event listeners can't be registered without access to the base touchpath object. + +Use of the `AsyncClosureDispatchQueue` comes with an added benefit; it enforces a macrotask queue wait after gesture-level processing completes for each input event. This gives newly-incoming input events a chance to be recorded in a responsive fashion, which is particularly important for any gestures that require a sense of timing. This way, the true, original time of the input events will stay reasonably accurate, even if the actual gesture-level processing operations are delayed until later. \ No newline at end of file diff --git a/web/docs/internal/gestures/gesture-modeling.md b/web/docs/internal/gestures/gesture-modeling.md new file mode 100644 index 0000000000..63b0459198 --- /dev/null +++ b/web/docs/internal/gestures/gesture-modeling.md @@ -0,0 +1,56 @@ +# GestureRecognizer - gesture modeling + +For definitions to a few commonly-used terms below, see the [Glossary](./glossary.md) document. + +The four layers comprising gesture modeling with `GestureRecognizer` are as follows: + +1. [`GestureModelDefs`](#gesturemodeldefs) corresponds to the top-most object used to specify gesture definitions. It represents a full set of custom gesture-component definitions as well at least one set of gesture-component ids that corresponds to legal initial gesture-components for a newly-beginning gesture sequence. +2. Each [`GestureModel`](#gesturemodel) object directly models a gesture-component. To do so, it owns at least one `ContactModel` and defines the gesture-component's properties and relationships to other gesture-components. Also defines a start-up timer for cases that lack active touchpoints during startup, such as the "wait" for the start of a second tap during multitap gestures. +3. Each [`ContactModel`](#contactmodel) owns a single `PathModel` and is additionally responsible for validating initial state, changes in associated item, and the portion of previously-existing path to consider if continuing an active gesture sequence. +4. [`PathModel`](#pathmodel) specifies which paths do and do not satisfy the requirements for the touchpoint handled by the `ContactModel` owning it. + +## `GestureModelDefs` + +`GestureModelDefs` is used to specify a full set of all `GestureModel` definitions to be used for an instance of `GestureRecognizer`. These may also be listed in `sets`, each identified with a unique `string` and comprised of `GestureModel` ids, for cases where the set of gestures available by default should change in certain conditions. + +`GestureModelDefs` is used internally to define and implicitly specify a finite-state-machine (FSM) for interpreting input as gestures. The currently-active gesture set - `default` by default - specifies the legal states that may be reached from an implicit "start" state. Once one is successfully matched, the properties of its `GestureModel` specify any further legal states that may succeed it. + +There are essentially three implicit states not defined directly: +- "start" - when no gesture is yet matched +- "finalize" - when the last gesture-component possible has been matched. After processing here is completed, state then transitions to "finish". +- "finish" - when all possible gesture-matching for the input has been completed - whether via cancellation, end of input, or reaching the end of supported gesture-components for the sequence of gesture-components already interpreted. + +## `GestureModel` + +Each `GestureModel` encapsulates the specification of an individual gesture-component and its relationships to other gesture-components. + +From the FSM perspective, each `GestureModel` specifies the behaviors of a set of tightly-related FSM states, the requirements of any inputs / inbound edges that will satisfy them, and the legal outbound transitions to other states that each permits. It does this in isolation, with minimal need to consider the specification of any other `GestureModel` "states" - the [`MatcherSelector`](./gesture-processing.md#gesturematcher) tier of gesture processing exists to support this paradigm. + +The only aspect that affects one `GestureModel`'s relationship to others is that of its `resolutionPriority` value. Processing of `GestureModel`s with higher values for this property will always occur the same for those of lower values, allowing them to "win" in the case that more than one `GestureModel` is satisfied by the input at the time that the latest sample input is received. + +The `contacts` (`ContactModel`s) and `sustainTimer` properties encapsulate specification for the states' 'inputs'. `sustainTimer` exists to handle the case where there should be no active 'inputs' for the initial state and should be only used here in that case. `contacts` defines the specification and expectations for what matching mouse or touch input paths should look like. These specifications also indicate how each matching input type should be interpreted in relation to permitted outbound transitions. + +`resolutionAction` and `rejectionActions` encapsulate specification for the states' permitted outbound transitions. Proper 'resolution' is the single required type of outbound transition that must be defined, defining one state for the tightly-related set. When `rejectionActions` has defined alternatives (on the basis of `'path'`, `'item'`, and/or `'timer'`), each variant should be considered its own variant of the tightly-related set of states for the `GestureModel`. Each such state then has its own permitted outbound transition (or possibly set thereof) to other `GestureModel`s (and thereby, states). Note that "resolution" records the state as a permanent component of the observed gesture sequence, while any form of rejection does not do so, instead "replacing" the current model with that of the outbound edge as an option for further processing. + +Finally, `sustainWhenNested` defines a special property for its corresponding state. In the case that gesture processing is "shifted" to have a different initial state by the modeled gesture sequence, when set to `true` this property will defer any outbound transitions until all other gestures started during this alt-state reach completion. When set to `false`, such 'nested' gestures would instead be cancelled. + +## `ContactModel` + +A `ContactModel` and its properties specify the expectations of the model for its touchpoint's path and its relationship to the corresponding path taken at the time the previous state (if it existed) finished processing. The `pathModel` property is delegated responsibility for the path itself, while `pathResolutionAction` determines how to interpret successful resolution of the path. + +From the FSM perspective, before the `pathModel` is even considered, if there is still an active touchpoint associated with the active gesture sequence, `allowsInitialState` is used to verify that the current state of the touchpath is allowed as input. For example, certain gesture types may only be supported when certain 'items' were currently selected by the corresponding state's predecessor. Once satisfied, `baseCoordReplacer` and `pathInheritance` are used to create a customized view of the path and selectively limit the range of path-history preserved as needed to facilitate the use of its `pathModel` for evaluating further updates input for the touchpoint's path. + +Once the initial sample from the touchpoint's path is validated, `timer` is used to set a limited time window in place in relation to the touchpoint and how to interpret the elapsed amount of time once resolution is triggered. If `timer.expectedResult == true`, once the set amount of time has elapsed, that itself will trigger resolution. If `false`, path resolution will only trigger resolution of the model if the set amount of time has _not_ elapsed. + +When either the timer (if specified) reaches its end or the `pathModel` signals resolution of the path, `validateItem` may be specified to perform a final check to ensure that the selected `item` is valid for the gesture being modeled. + +## `PathModel` + +`PathModel` is used within gesture modeling to specify whether the path-based aspects of the user's input for the modeled touchpoint satisfy a gesture-component's requirements. The type is an object containing a single method - `evaluate` - which receives four parameters related to values accessible on the 'path' level and may return one of three values: +- `'reject'`: the path is not legal for this model's gesture component; it should no longer be considered +- `'resolve'`: the path satisfies and completes the path aspect of this model's gesture component; we're ready to transition and either accept or reject it (based on other, higher-level model properties) +- `undefined`: the path is legal but does not _yet_ satisfy the path aspect of this model's gesture component. + +## See also + +Refer to [the page on Keyman's gesture model design](./keyman-keyboard-gesture-specs.md) for documentation on how touch-keyboard gestures are specified and implemented. \ No newline at end of file diff --git a/web/docs/internal/gestures/gesture-processing.md b/web/docs/internal/gestures/gesture-processing.md new file mode 100644 index 0000000000..32fe86fcdd --- /dev/null +++ b/web/docs/internal/gestures/gesture-processing.md @@ -0,0 +1,43 @@ +# GestureRecognizer - gesture processing + +For definitions to a few commonly-used terms below, see the [Glossary](./glossary.md) document. + +There are four separate layers used when processing gesture input to ensure that each gesture and gesture component may be modeled as they would work in isolation. Sequential components are connected via FSM-style state transitions that the engine can resolve to ensure the best gesture-models for the user's input are selected. + +The processing layers for gesture modeling are as follows: +1. [`PathMatcher`](#pathmatcher) - Handles modeling and matching of a single touchpoint's path over time. +2. [`GestureMatcher`](#gesturematcher) - Handles modeling and matching for a single gesture-component's isolated model. +3. [`MatcherSelector`](#matcherselector) - Coordinates groups of gesture-component models for each active input and ensures only one gesture model for each input reaches completion, keeping each individual gesture-component model isolated but ensuring only the user's intended one remains. +4. [`GestureSequence`](#gesturesequence) - Handles coordination of the higher-level state machine connecting the components of a "gesture sequence" - of multiple related gesture-components over time - as a complete, connected gesture. + +Each layer but the last and outermost - `GestureSequence` - leverages `Promise`s that resolve once the layer's role in detecting input of a gesture-component is completed. + +## `PathMatcher` + +The `PathMatcher` class is responsible for matching the all aspects of a single touchpoint to its corresponding part of a potential gesture-component. In particular, it matches both the [`PathModel`](./gesture-modeling.md#pathmodel) and [`ContactModel`](./gesture-modeling.md#contactmodel) layers of a gesture-component model for a single input point. + +## `GestureMatcher` + +The `GestureMatcher` class is responsible for the matching against the higher-level aspects of a single gesture-component model as defined on the [`GestureModel`](./gesture-modeling.md#gesturemodel) layer of gesture-component specification. This includes handling the multi-touchpoint aspects of gesture-component matching (when one gesture-component permits or requires multiple `ContactModel`s). `GestureMatcher` and `PathMatcher` together take a view in which its models operate in full isolation from all others. + +It also handles cases where there may be _no_ touchpoints at the start of the model - this can be the case for gestures such as multitap, where a new component may start after another component finalizes with the release of a lone, active tap location. + +When a `GestureMatcher` finalizes and its `Promise` resolves, the gesture component is considered to have matched and be valid component of the ongoing gesture sequence. It will also indicate the manner in which the gesture component should resolve as well as indicating if further gesture components are permitted for the ongoing gesture sequence. + +In the case that a `GestureModel` cannot be matched for an input due to an invalid initial state, `GestureMatcher` is responsible for handling this case and cancelling attempts to match that input. `reject`ion is invalid for such cases in order to prevent a `reject`->replace loop from occurring, as gesture models support the ability to 'replace' themselves for the matching attempt with another instance of the same model in order to reset gesture properties, and the initial state would not change between matching attempts. + +## `MatcherSelector` + +Both `PathMatcher` and `GestureMatcher` above permit gesture-component modeling to be specified and act as if the modeled gesture is the only one under consideration. This abstraction is permitted thanks to the efforts of `MatcherSelector`, which models the _potential_ gesture-components that _could_ result for each active input, filtering out any that are no longer valid possibilities. When a new touchpoint starts being input, its gesture-component models are spun up here, as are the models permitted to continue an active gestures sequence. + +There is no single, direct FSM used to determine which gesture-component, if any, is matched. Instead, `MatcherSelector` enforces an "implicit" one built by considering all permitted transitions to new states (allowed [`GestureModels`](./gesture-modeling.md#gesturemodel)s) in isolation from each other. Once any component moves from the "potential" state to an "actually matched" state by satisfying the required 'input' properties to reach the state, `MatcherSelector` "selects" that component as the "winner" for its corresponding input(s) and transitions to its state. Specific aspects of the input are used to select _which_ version of the state (each with a different permitted transition to other states) is selected, and the corresponding outbound "edges" are then noted to prepare for processing any follow-up gesture-components. `MatcherSelector` also handles cancellation of any no-longer-valid competitors and signals completion of the winner as a new component of its gesture sequence... or as the base component of a new sequence if applicable. + +`MatcherSelector` will enforce, respect, and utilize "resolution priority" ordering among potential gesture-component processing. In the case more than one model is matched, the correct (highest-priority) one will be matched as the "winner", partly by leveraging the standard properties of `Promise`s; only the first resolution of any `Promise` counts. + +When gestures permit, start, or enforce alternative modeling states that cause the set of permitted gesture types to change, this is enforced by pushing a new `MatcherSelector` onto a stack to model the alternative gesture-modeling state. When said state ends, the `MatcherSelector` will be popped off of the stack. For Keyman keyboards, this is leveraged by modipress gestures in order to prevent nested modipresses. + +## GestureSequence + +Whenever the first gesture-component for a `GestureSequence` is matched, and thus a valid gesture for input has been confirmed, a `GestureSequence` for that input is created. This class will raise the `'stage'` event to signal when new valid gesture-component segments of the sequence have been recognized - including the first - and ensure that all such segments for the same input are properly associated with each other and properly ordered in chronological order. + +Internally, `GestureSequence` works with `MatcherSelector` and handles its outputs, doing the actual work of interpreting newly-resolved models, their states, and related data. `GestureSequence` is responsible for receiving the outbound transitions and passing them back to `MatcherSelector` in order to start interpretation for any potential follow-up gesture-components. \ No newline at end of file diff --git a/web/docs/internal/gestures/glossary.md b/web/docs/internal/gestures/glossary.md new file mode 100644 index 0000000000..d86ba6118b --- /dev/null +++ b/web/docs/internal/gestures/glossary.md @@ -0,0 +1,36 @@ +# GestureRecognizer Glossary + +- [**Finite State Machine (FSM)**](https://en.wikipedia.org/wiki/Finite-state_machine): A formal mathematical model of computation that abstracts a process into distinct states that may transition among each other. [Wikipedia page](https://en.wikipedia.org/wiki/Finite-state_machine) + +- **Gesture**: what the user perceives as a single gesture. This may consist of multiple **gesture components**. Often, though not always, this notion corresponds directly to a full **gesture sequence**. + +- **Gesture component**: one segment of the touchpath and related metadata sufficient to trigger transition of state for gesture interpretation. Each represents a **state cluster** (see below) for the implicit gesture-modeling **FSM**. + +- **Gesture model**: the specification needed to interpret a segment of the touchpath as a gesture component. Each directly defines a **state cluster** on the implicit **FSM** based on whether the input "resolves" the model or is "rejected" due to specific causes. Each such cause may permit a different transition and thus corresponds to a different implicit state on the implicit gesture-modeling FSM. + +- **Gesture processing**: the process of interpreting one or more touchpaths, at run-time, as a gesture sequence. + +- **Gesture sequence**: A full sequence of causally-related and successfully matched **gesture component**s. Represents the path of gesture-components taken by the input through the gesture-modeling **FSM** implicitly specified by the engine's `GestureModelDefs` value. + +- **State cluster**: A set of implicit, highly-related states in the gesture-modeling FSM that arise from a gesture model specification. + +Properties: + 1. All states share the same state ID, which is unique to the state cluster. + 2. After processing, there is no need to identify which specific state was reached - only that one of the variants was reached. + 3. The input paths necessary to reach each state are mutually exclusive and deterministic. + 4. All inbound paths to each state in the cluster have an identical specification that is fully independent from the prior state (or lack thereof). + 5. The permitted outbound transitions may vary for each state (as determined by input), but the non-FSM role in gesture modeling for each is identical. + +- **Touchpath**: the path taken by a touchpoint over time + +- **Touchpoint**: the location of the cursor (during mouse-based input) or of the finger (during touch) being observed. + +## Examples + +Consider the path taken by one finger during a potential touch-based gesture input. Suppose a user holds a button or key down for a period of time to activate a menu, then selecting an item from that menu. Note the phrasing there - there are two separate 'stages', or 'components', to that input: +1. The user holds the button down for a period of time, without moving the input location. + - After a set amount of time passes, the UI's state shifts, displaying a new menu corresponding to the held location and button. +2. Once the menu appears, the user then moves their finger to select an item from the menu. + - Once an item is selected and the finger is lifted, the program should respond and act accordingly to the selected menu item. + +These two items correspond to **gesture components**, corresponding to _segments_ of the overall touch input path. These components are related and happen in sequence as a **gesture sequence**, with the former a prerequisite for the latter. \ No newline at end of file diff --git a/web/docs/internal/gestures/index.md b/web/docs/internal/gestures/index.md new file mode 100644 index 0000000000..69e8bacd4d --- /dev/null +++ b/web/docs/internal/gestures/index.md @@ -0,0 +1,47 @@ +# Gesture Recognizer design - index + +GestureRecognizer is an engine designed to interpret mouse event paths and touch event paths into sequences of externally-specified [gesture components](./glossary.md). This is accomplished via definition of gesture-component "models", which the engine is then able to interpret when "processing" the input. Each gesture-component model may be defined from its own perspective independently of any other, though effort may be required to identify and carefully define components that are shared by two or more gesture types. + +There are a few different aspects to GestureRecognizer, each with its own internal doc. + +- [Gesture modeling](./gesture-modeling.md) refers to the specification of gestures to be interpreted from the user's input. +- [Gesture processing](./gesture-processing.md) refers to the process of interpreting the user's input as from among the specified gesture models. +- [Input serialization](./gesture-input-serialization.md) refers to some of the trickier aspects of handling input to ensure every input event is processed in the correct, user-intended order. +- [Keyman gesture models](./keyman-keyboard-gesture-specs.md) documents how the gestures for Keyman touch keyboards are implemented in regard to the [gesture modeling](./gesture-modeling.md) section above. + +Other resources: +- The [Glossary](./glossary.md) provides definitions for some of the terms seen among the internal documentation pages. + +There is also documentation regarding some of the trickier aspects of [configuring the region that accepts gestures](./recognizer-configuration.md). + +Keyman configures two separate instances of this gesture engine - one for main keyboard body and another for the predictive-text banner. In both cases, gestures (from the user perspective) are broken down into components, each corresponding to different movement patterns that each comprise a gesture type. Once components are recognized, they are added into a sequence that represents the causal relationship among the different components of the gesture and ensures that it may continue if appropriate for that type of gesture. + +---- + +For the main keyboard implementation of Keyman Engine for Web, the full set of Keyman gesture components is defined in `web/src/engine/osk/src/input/gestures/specsForLayout.ts`. + +### Longpresses + +Components: +1. "Hold and wait" + - The subkey menu is displayed once this state is reached. +2. Subkey selection + +### Flicks +1. Flick start - the user has input enough motion to consider this the start of a flick (and not the start of a longpress) +2. Flick mid - the user has input enough motion in a specific direction to "lock" the flick (in order to ensure a smoother animation) +3. Flick reset - the user has moved the touchpoint close enough to the start location to "unlock" the flick and reset the flick's base coordinate. +4. Flick end - the user has input enough motion to confirm the flick _and_ released the touchpoint. + +### Multitap +1. First tap - the user has tapped a key +2. Awaiting the next tap +3. Awaiting release of the current tap +4. Exit case: a key was held too long - no further taps on the key should be considered part of the gesture + +### Modipress +- A "modipress", or "**modi**fier-key long**press**", refers to the act of holding down a modifier key, typing on the resulting layer, and then reverting the layer once the modifier key is released. In essence, using the modifier key like a longpress, with the pushed "layer" acting as its "subkey menu". + +1. The user holds down on a modipress-friendly key +2. Engine alt-state: a different set of initial gesture components is activated and used for newly-starting concurrent gestures +3. Release of the modipressed key \ No newline at end of file diff --git a/web/docs/internal/gestures/keyman-keyboard-gesture-specs.md b/web/docs/internal/gestures/keyman-keyboard-gesture-specs.md new file mode 100644 index 0000000000..8339b8a462 --- /dev/null +++ b/web/docs/internal/gestures/keyman-keyboard-gesture-specs.md @@ -0,0 +1,349 @@ +# Keyman keyboard gesture-model design + +This section continues from the section on [gesture modeling](./gesture-modeling.md), elaborating on how Keyman specifies its set of gestures for use with touch-layout keyboards. + +Speaking in broad strokes, Keyman Engine for Web implements its gestures via four to five gesture-component finite-state machines (FSMs). As mentioned in the [gesture processing](./gesture-processing.md) section, each is defined independently of the others. Whenever a FSM gesture component rejection occurs and an associated transition is triggered, processing continues for all active FSMs as usual, merely swapping out the affected FSM's active component model. The first gesture-component to fully resolve via input match will "lock in" its FSM and disable the other FSMs for that input for all further input processing. + +Furthermore, the set of active gestures will vary depending upon the properties of the active keyboard. Keyboards that do not define flick gestures on their keys will permit "roaming touch" behaviors - these two are mutually exclusive. The longpress and simple tap gestures have different configurations depending on whether "roaming touch" is enabled for the active keyboard. Flick support is handled via its own dedicated FSM. + +## Overview + +Touch-keyboard gestures and their models are grouped together as follows: + +- Special keys + - Backspace has dedicated handling that triggers on key-down, unlike most keys. + - The globe key (K_LOPT) and hide-keyboard key (K_ROPT) execute immediately and do not listen for further input. + - As these are relatively simple, no FSM for them will be elaborated on here. +- Simple-taps (roaming / not roaming) and multitaps +- Longpresses (roaming / not roaming) +- Modipresses + multitap-modipresses + - "modipress" is short for **modi**fier long**press** - using a modifier key like a longpress key, with the alternate layer as its subkey menu +- Flicks + +## Simple taps and Multitaps + +Multitaps function as a continuation of standard simple-tap input, so both are naturally supported in the same set of models. + +Note that there are two parameters to a multitap. These two values do not necessarily have to match. +- the maximum time a key may be held before that key is "locked in" +- the maximum time allowed between consecutive keys + + +```mermaid +--- +title: Simple-tap + Multitap gesture flowchart +--- +%% For rendering, use e.g. https://mermaid.live +%%{init: {"flowchart": {"htmlLabels": false}} }%% +graph TD; + initial-tap['initial-tap': input held but could lead to multitap] + simple-tap['simple-tap': no further multitap support] + multitap-start['multitap-start': await new inputs] + multitap-end['multitap-end': input held and may continue multitap] + complete(**Complete**) + + hold-timer-elapsed-initial{Tap-hold time limit has elapsed?} + valid-multitap-key{Key supports multitap?} + await-new-timer-elapsed{Time limit between taps has elapsed?} + hold-timer-elapsed-subsequent{Tap-hold time limit has elapsed?} + initial-moved-keys{Roaming: input moved to different key?} + late-user-action{Roaming: User action} + + subgraph Stage1[" "] + initial-tap --> initial-moved-keys + initial-moved-keys -- No --> hold-timer-elapsed-initial + initial-moved-keys -- Yes (reset) --> initial-tap + valid-multitap-key + end + + hold-timer-elapsed-initial -- No + key is released --> valid-multitap-key + hold-timer-elapsed-initial -- Yes --> simple-tap + + valid-multitap-key -- Yes --> multitap-start + valid-multitap-key -- No --> complete + + subgraph Stage2[" "] + simple-tap --> late-user-action + end + + late-user-action -- Key is released --> complete + late-user-action -- Moved to different key (reset) --> simple-tap + + subgraph Stage3[" "] + complete + end + + subgraph Stage4[" "] + multitap-start --> await-new-timer-elapsed + await-new-timer-elapsed -- Yes --> complete + end + + await-new-timer-elapsed -- No --> multitap-end + + subgraph Stage5[" "] + multitap-end --> hold-timer-elapsed-subsequent + end + + hold-timer-elapsed-subsequent -- Yes (replace) --> simple-tap + hold-timer-elapsed-subsequent -- No + key is released --> multitap-start +``` + +When roaming-touch is disabled, the 'initial-tap' and 'simple-tap' components are replaced with alternate versions that: +- lack the outbound transition associated with change of key ('item') as defined within the `rejectionActions` property of its model. +- do not reject when the key underneath the touchpoint changes, instead remaining locked on the original key. + +## Longpresses + +Longpresses typically take hold once the initial key is held down a sufficient amount of time, before the key is released. While multitaps also invoke a similar timer, that one rejects its gesture-component - an act that resets its FSM in a different state and that does not cancel the longpress FSM. Longpresses can also trigger early via an up-flick shortcut for keys that do not also support defined flicks. + +```mermaid +--- +title: Longpress gesture flowchart +--- +%% For rendering, use e.g. https://mermaid.live +%%{init: {"flowchart": {"htmlLabels": false}} }%% +graph TD; + longpress['longpress': key held down] + subkey-select['subkey-select': subkeys visible + interactive] + longpress-roam['longpress-roam': 'longpress' with up-flick shortcut disabled] + longpress-roam-restore['longpress-roam-restore': reset timer on new key selection] + reject(**Reject**) + complete(**Complete**) + + didShortcut{Up-flick shortcut supported for the key and used?} + didWait{Longpress timer elapsed?} + didWait2{Longpress timer elapsed?} + allowedRoam{Roaming: roaming-touch supported and used?} + supportsSubkeys{Does the key support subkeys?} + supportsSubkeys2{Does the key support subkeys?} + didRoam{Roaming touch used?} + + subgraph Stage1[" "] + longpress-->didShortcut + didShortcut -- Yes --> supportsSubkeys + didShortcut -- No --> didWait + didWait -- Yes --> supportsSubkeys + didWait -- No --> allowedRoam + end + + supportsSubkeys -- Yes --> subkey-select + supportsSubkeys -- No --> reject + + subgraph Stage2[" "] + subkey-select --> complete + end + + allowedRoam -- Yes --> longpress-roam + allowedRoam -- No + input finishes --> reject + + subgraph RoamingSupport["Longpresses+roaming"] + subgraph Stage3[" "] + longpress-roam --> didWait2 + didWait2 -- Yes --> supportsSubkeys2 + end + + didWait2 -- No --> didRoam + didRoam -- Yes --> longpress-roam + supportsSubkeys2 -- No --> longpress-roam-restore + + subgraph Stage4[" "] + longpress-roam-restore + end + end + + longpress-roam-restore --> didRoam + + supportsSubkeys2 -- Yes --> subkey-select + didRoam -- No + input finishes --> reject +``` + +When roaming-touch is disabled, the 'longpress' component is replaced with a variant similar to the one described above for 'initial-tap' and 'simple-tap' that: +- removes outbound transitions needed to support roaming touch +- prevents any change in key under the touchpoint from affecting the gesture + +## Modipresses + +On their own, modipresses are not particularly difficult to model. When seeking to support multitaps and modipresses on the same key... that's when things get trickier. It's a relatively common paradigm for Latin-script touch-keyboards to support double-tap on Shift to reach a Caps-lock state, so it's necessary to include some form of multitap handling with the modipress FSM. Also note that modipressible keys trigger immediately on key-down, rather than on key-up like standard taps - meaning the modipress FSM immediately disables the standard simple-tap/multitap FSM and must be defined independently from it. + +In Keyman Engine for Web, we actually found a way to model the two together to yield an extra benefit - the ability to multitap and use the final "key" of the multitap as a modipress key. This is particularly useful on keyboards like `sil_euro_latin` that support multiple layers of numbers + symbols. + +```mermaid +--- +title: Modipress / modipress-multitap gesture flowchart +--- +%% For rendering, use e.g. https://mermaid.live +%%{init: {"flowchart": {"htmlLabels": false}} }%% +graph TD; + start(**Start**) + modipress-start['modipress-start': key-down detection + filtering] + modipress-hold['modipress-hold': modipress state active] + modipress-end['modipress-end': modipress state still active] + modipress-end-multitap-transition['modipress-end-multitap-transition': transition to multitap mode] + modipress-multitap-start['modipress-multitap-start': await new tap on base key] + modipress-multitap-end['modipress-multitap-end': modipress state active] + modipress-multitap-lock-transition['modipress-multitap-lock-transition': no further multitapping allowed] + complete(**Complete**) + reject(**Reject**) + + valid-multitap-key{Key supports multitap?} + valid-modipress-check{Is the key valid for modipresses?} + early-new-gesture{New gesture started during modipress hold?} + hold-timer-elapsed-initial{Tap-hold time limit has elapsed?} + await-new-timer-elapsed{Time limit between taps has elapsed?} + late-new-gesture{New gesture started during modipress hold?} + hold-timer-elapsed-subsequent{Tap-hold time limit has elapsed?} + + subgraph Stage1[" "] + start --> valid-modipress-check + end + + valid-modipress-check -- Yes --> modipress-start + valid-modipress-check -- No --> reject + + subgraph Stage2[" "] + reject + end + + subgraph Stage3[" "] + modipress-start + end + + modipress-start -- (emits key) --> modipress-hold + + subgraph Stage4[" "] + modipress-hold --> early-new-gesture + early-new-gesture -- No --> hold-timer-elapsed-initial + end + + early-new-gesture -- Yes --> modipress-end + hold-timer-elapsed-initial -- No + input finishes --> modipress-end-multitap-transition + hold-timer-elapsed-initial -- Yes --> modipress-end + + subgraph Stage5[" "] + modipress-end + end + + modipress-end --> complete + + subgraph Stage6[" "] + complete + end + + subgraph MultitapSupport["Multitap support block"] + subgraph Stage7[" "] + modipress-end-multitap-transition + end + + modipress-end-multitap-transition --> valid-multitap-key + valid-multitap-key -- Yes --> modipress-multitap-start + valid-multitap-key -- No --> complete + + modipress-multitap-start + + subgraph Stage8[" "] + modipress-multitap-start --> await-new-timer-elapsed + end + + await-new-timer-elapsed -- No --> modipress-multitap-end + + subgraph Stage9[" "] + modipress-multitap-end --> late-new-gesture + late-new-gesture -- No --> hold-timer-elapsed-subsequent + end + + late-new-gesture -- Yes --> modipress-multitap-lock-transition + hold-timer-elapsed-subsequent -- Yes --> modipress-multitap-lock-transition + hold-timer-elapsed-subsequent -- No + input finishes --> modipress-multitap-start + + subgraph Stage10[" "] + modipress-multitap-lock-transition + end + + end + + await-new-timer-elapsed -- Yes --> complete + + modipress-multitap-lock-transition --> modipress-end +``` + +Note that both `modipress-end-multitap-transition` and `modipress-multitap-lock-transition` are transitory (but real!) states that immediately transition to their successors. They exist to abstractify and clarify the mechanisms at play. + +## Flicks + +The flick gesture models have their own devoted FSM in order to support their complexities. Of particular note is that flicks and longpresses compete in a similar space, so the 'flick-start' -> 'flick-mid' transition exists to "lock in" after a small-but-significant amount of motion as a flick, rather than a longpress. If a key is held too long without enough movement on a key supporting both flicks and longpresses, the longpress FSM will transition first and cancel any further attempt to match a flick for that input. + +One less-obvious aspect of this FSM is that of the "reset" + "recentering" mechanism. If a user attempts to "reset" the flick by returning to where they started, it is very rare that they will actually succeed in doing so. They'll usually be off somewhat and/or base their judgment based upon the center of the key, which may not be the actual coordinate where input began. The "recentering" mechanism seeks to address this aspect of user behavior, predicting a "center" that matches a user's intuitions better and that provides more "wiggle room" before re-activating direction locking. + +```mermaid +--- +title: Flick gesture flowchart +--- +%% For rendering, use e.g. https://mermaid.live +%%{init: {"flowchart": {"htmlLabels": false}} }%% +graph TD; + flick-start['flick-start': detect early directional key movement] + flick-restart['flick-restart': lock in the new 'base' coord, emulate 'flick-start' after reset] + flick-mid['flick-mid': determine the flick's direction] + flick-end['flick-end': finalize the flick] + flick-reset['flick-reset': unlock the flick, immediately transition] + flick-reset-centering['flick-reset-centering': determine perceived 'base' coord] + flick-reset-end['flick-reset-end': revert to a basic tap] + complete(**Complete**) + reject(**Reject**) + + start-base-condition{Key supports flick?} + start-move-condition{Net movement: >= 30% row-height?} + lock-condition{Touchpoint moves a total of 35% row-height from start?} + unlock-condition{Touchpoint moves < 34% of row-height from start?} + recenter-condition{Touchpoint starts moving further from start again?} + finalize-condition{Input finishes >= 75% row-height from start in locked direction?} + restart-move-condition{Net movement: >= 30% row-height?} + + subgraph Stage1[" "] + flick-start --> start-base-condition + start-base-condition -- Yes --> start-move-condition + end + + start-base-condition -- No --> reject + start-move-condition -- Yes --> flick-mid + start-move-condition -- No + input finishes --> reject + + subgraph Stage2[" "] + flick-mid --> lock-condition + end + + lock-condition -- Yes --> flick-end + lock-condition -- No + input finishes --> flick-reset-end + + subgraph StageFail[" "] + flick-reset-end + end + + flick-reset-end --> complete + + subgraph Stage3[" "] + flick-end --> finalize-condition + finalize-condition -- No --> unlock-condition + end + + finalize-condition -- Yes --> complete + unlock-condition -- Yes --> flick-reset + unlock-condition -- No + input finishes --> flick-reset-end + + subgraph Stage4[" "] + flick-reset --> flick-reset-centering + flick-reset-centering --> recenter-condition + end + + recenter-condition -- Yes --> flick-restart + recenter-condition -- No + input finishes --> reject + + subgraph Stage6[" "] + flick-restart --> restart-move-condition + end + + restart-move-condition -- Yes --> flick-mid + restart-move-condition -- No + input finishes --> reject +``` + +Note that the early cases from 'flick-start' that lead to 'reject' are all valid inputs for gesture types handled by previously-defined FSMs. \ No newline at end of file diff --git a/web/docs/internal/keystroke-lifecycle.md b/web/docs/internal/keystroke-lifecycle.md new file mode 100644 index 0000000000..c2e401b008 --- /dev/null +++ b/web/docs/internal/keystroke-lifecycle.md @@ -0,0 +1,301 @@ +# The Keyman Engine for Web keystroke lifecycle + +The phases of Keyman Engine for Web keystroke processings are as follows: + +1. Keystroke pre-processing: event interception + interpretation (pre-processing) +2. Keystroke processing: keystroke main-group rule evaluation +3. Keystroke post-processing: keystroke post-keystroke evaluation + side-effects +4. Keystroke resolution: UI state updating + +Keyman Engine for Web currently has two different builds, each used in specific +contexts: + +* app/browser - for direct use on a page or site within a web browser +* app/webview - for integration with a host app, such as Keyman itself + +The overall phases for the keystroke lifecycle are the same for both; the +implementation is merely a bit different for each within specific phases. These +differences are documented after the common structure and patterns for each +phase. + +Also note that Keyman Engine for Web supports keystroke processing for different +keyboard specification formats: +- JS +- KMX (via web-core) +- KMX+ / LDML (via web-core) + +The `KeyboardProcessor` classes each handle keystroke processing for one +specification format while adhering to a common interface exposed and accessible +to the other layers of the keystroke lifecycle. + +```mermaid +--- +title: Keystroke lifecycle flowchart +--- +%% For rendering, use e.g. https://mermaid.live +%%{init: {"flowchart": {"htmlLabels": false}} }%% +sequenceDiagram; + +actor User as User + UI +participant Keystroke interpretation functionality + +Note over User, KeymanEngine: Start: Keystroke pre-processing +User ->> Keystroke interpreter: Types a key +Keystroke interpreter ->> Keystroke interpreter: Preprocess keystroke events +Keystroke interpreter ->> KeymanEngine: Raise preprocessed event +Note over User, KeymanEngine: End: Keystroke pre-processing +KeymanEngine ->>+ InputProcessor: Forward keystroke data + +Note over InputProcessor, KeyboardProcessor: Start: Keystroke processing +InputProcessor ->> InputProcessor: Validate context, keyboard +InputProcessor ->> InputProcessor: Snapshot context state + +InputProcessor ->>+ KeyboardProcessor: Process keystroke +KeyboardProcessor ->> KeyboardProcessor: Determine effects +KeyboardProcessor -->> User: Update raw context +KeyboardProcessor ->>- InputProcessor: Return effects + +opt Predictive Text + loop For each nearby key + InputProcessor ->>+ KeyboardProcessor: Fat-finger (alternate) keystrokes + KeyboardProcessor ->> KeyboardProcessor: Determine effects + KeyboardProcessor ->>- InputProcessor: Return effects + end +end + +Note over InputProcessor, KeyboardProcessor: End: Keystroke processing +Note over InputProcessor, KeyboardProcessor: Start: Keystroke post-processing + +InputProcessor ->> InputProcessor: Apply keystroke effects +InputProcessor ->> InputProcessor: Cache context state, keystroke effects +InputProcessor -->> User: Update OSK layer +InputProcessor ->>+ KeyboardProcessor: Process post-keystroke effects +KeyboardProcessor ->>- InputProcessor: Return effects +InputProcessor -->> User: Update OSK layer + +opt Predictive Text + InputProcessor --)+ Predictive-text engine: Request corrections, predictions + InputProcessor -->> User: Invalidate outdated predictions +end + +Note over InputProcessor, KeyboardProcessor: End: Keystroke post-processing + +opt app/browser + InputProcessor ->> InputProcessor: Trigger DOM `input` event +end + +Note over Keystroke interpreter, InputProcessor: Start: Keystroke resolution +InputProcessor ->>- KeymanEngine: Return effects + +KeymanEngine ->> Keystroke interpreter: Store state token for multitaps +KeymanEngine ->> Keystroke interpreter: Determine TAB, ENTER, etc effects +Note over Keystroke interpreter, InputProcessor: End: Keystroke resolution + +opt Predictive Text + Predictive-text engine ->> Predictive-text engine: Determine corrections,
    predictions (async) + Note over User, Predictive-text engine: After async delay + Predictive-text engine --)- User: Return new predictions, update banner +end +``` + +## Keystroke pre-processing + +Both the `app/browser` and `app/webview` products are capable of handling +keystrokes from the on-screen keyboard as well as hardware keystrokes. For +hardware keystrokes, this varies between the two, while the majority of +on-screen keyboard keystrokes are handled identically. + +### `app/browser` - hardware keystrokes + +The `app/browser` version of the engine is designed to integrate with the active +page of the user's browser. As a result of this, contexts for text manipulation +are directly implemented within Web elements that the engine has direct access +to. It can thus attach DOM event handlers (for +[`keydown`](https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event), +[`keyup`](https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event), +and +[`keypress`](https://developer.mozilla.org/en-US/docs/Web/API/Element/keypress_event)) +in order to process hardware keystrokes. Its `HardwareEventKeyboard` class is +the primary module for handling hardware keystrokes - it establishes the needed +engine event hooks and methods used to fully handle and preprocess their +keystroke data for further use later in the keystroke lifecycle. + +The bulk of DOM key events trigger keystroke processing on key-down, though modifier keys also receive key-up handling. Key-press events are handled selectively for held backspaces and possibly certain keystrokes for mnemonic keyboards. In all cases, DOM keyboard event objects are preprocessed by the centralized `preprocessKeyboardEvent` method, which converts the DOM event [`KeyboardEvent`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent) format into the Web engine's `KeyEvent` format, which is the form required by the keyboard rules within compiled JS keyboards. This includes mnemonic keystroke preprocessing and remapping in addition to AltGr emulation. + +### `app/webview` - hardware keystrokes + +The `app/webview` version of the engine, which is designed to be used while embedded in a platform-specific host app, does not have direct access to standard hardware keystroke events, as those are handled by the host app's OS and by native code handlers run outside the host app's WebView containing the app/webview Web engine. It is the responsibility of the host app to handle hardware keystroke events and preprocess them on behalf of the Web engine, then forward them to the Web engine via JS call into the WebView. The `PassthroughKeyboard` class within `app/webview` space provides the method `raiseKeyEvent` as an internal API for this purpose, which converts the mobile-app format for hardware keystroke into the internal `KeyEvent` format. `raiseKeyEvent` also handles mnemonic keystroke processing and remapping. + +Also note that this variant does not model the user's text context with Web elements - it is entirely managed through the `Mock` type. + +### On-screen keyboard + +For both `app/browser` and `app/webview`, the on-screen keyboard delegates mouse +and touch processing to [our internal gesture engine](./gestures/index.md) for +most keyboards. (Keyboards that instead display "help pages", such as +`sil_euro_latin` on desktop, have specialized handling.) The gesture engine is +responsible for interpreting all DOM mouse and touch events for OSKs based on +the touch-layout format, which are managed by the `VisualKeyboard` class. The +gesture engine will raise events about the gesture lifecycle that, when +appropriate, will emit `KeyElement` instances - elements of the constructed OSK +corresponding to elements of the OSK directly representing keyboard keys. + +OSK keystroke preprocessing operates a little differently from hardware +keystrokes. The `ActiveLayout` object for the current form-factor of the +`Keyboard` maintains a "base" `KeyEvent` object, per key, that is +lazy-constructed (by the `.baseKeyEvent` property) when first needed - the meat +of which is performed by the `ActiveKeyBase.constructBaseKeyEvent` method. This +is only evaluated up to once per key per activation of the keyboard. As +keystrokes are received during OSK use, `VisualKeyboard.keyEventFromSpec` calls +the `Keyboard.constructKeyEvent` method to clone that base `KeyEvent` object for +the incoming keystroke and then adjust it to match current modifier and +state-key settings. + +### Passing `KeyEvent` on for processing + +Regardless of the path taken to generate the `KeyEvent` object, _all_ keystroke +handling modules of Keyman Engine for Web then raise that `KeyEvent` object via +their `keyevent` event as specified by the `KeyEventSourceInterface` type. This +event also permits a callback that is used by the final phase of the lifecycle - +keystroke resolution - as needed. The main `KeymanEngine` base class listens +for these events and forwards them for next phase of the lifecycle - keystroke +processing - to the `InputProcessor` module, which passes control to whichever +`KeystrokeProcessor` type is active for the keyboard when appropriate. + +## Keystroke processing + +Once the `KeyEvent` object for a keystroke has been passed into the +`InputProcessor` module and its `processKeyEvent` method, the +keystroke-processing stage has been reached. There are a number of subprocesses +that trigger here in sequence - keystrokes don't go _straight_ to evaluation +against keyboard rules. + +### Pre-keystroke state management + +First, the engine checks the keyboard associated with the `KeyEvent`, ensuring +that it is still in place. (See [#7543](https://github.com/keymanapp/keyman/issues/7543) for more details.) With that assertion +in place, it then performs a check needed to support multitaps - it checks to +see if the key event has an associated "base context" and reverts the current +context to match it if so. Once those checks are fulfilled, the engine then +checks to see if the key event corresponds to a pure layer-switching or +modifier/state-key interaction. If those are detected, rule-evaluation is +bypassed. + +On a similar note, backspace and spacebar keystrokes receive special handling +next in associated with predictive-text functionality. When autocorrect is +enabled, the spacebar in particular can be used to accept the current suggestion +instead of acting as whitespace; if this is triggered, its `KeyEvent` will be +swallowed - no rule processing will occur for the it. The backspace may trigger +display of a "reversion" suggestion when applicable but will not be swallowed. + +Once this phase is reached, the engine will then snapshot the current state of +the context, then trigger standard rule processing against the original context +by calling the current `KeyboardProcessor`'s `processKeystroke` method. Any +operations against the context will be persisted and take full effect; DOM input +elements (for `app/browser`) are updated during rule evaluation. + +### Determining direct keystroke effects + +The `KeyboardProcessor` classes, which each embody how one keyboard +specification pattern interprets keystrokes, are responsible for determining the +effects that should arise when evaluating the incoming keystroke with the +current context state. While the exact mechanisms of this may vary depending +upon the type of keyboard - be it JS, KMX, or KMX+ (LDML), the role is the same +- interpreting the keystroke in context. + +### JS keyboards + +Before proceeding further, `KeyboardProcessor` does a context check to see +whether or not the context contains anything considered deletable - in the case +of a BKSP, we need to ensure that the keystroke passes through to the engine's +host cleanly, as it may have special implications for the site or host app. + +With that out of the way, the JS keyboard's main group is then provided with the +`KeyEvent` object in order to evaluate the keyboard's rules against the +keystroke. This involves constructing a `RuleBehavior` object to track any and +all needed side-effects for the keystroke. Such changes are generally not +applied to the engine at large at this time in order to prevent unwanted side +effects that may arise to and from predictive text operations that follow +shortly afterward. + +The `KeyboardInterface` class defines the internal API used by keyboards for +their evaluation of keyboard rules. + +Once the keyboard's script returns control to the main engine, the engine then +determines the net effects to the context that resulted from applying the +keyboard rule and preserves that as a `Transcription`, part of includes a +`Transform` that most directly represents the changes to text surrounding the +text insertion point. + +For JS keyboards, if no keyboard rule is defined for a non-frame key on the +`default` or `shift` layer, there is an additional set of implied "default" +keyboard rules that is applied. (A keyboard's source must _explicitly_ specify +to not emit anything for such keys if such an outcome is desired.) This occurs +regardless of the keystroke's source, as such effects are necessary to +incorporate for predictive-text operations. If no such rule exists, the +processor will add a note to the `RuleBehavior` object that default processing +could not be provided and is thus needed from the engine's host. + +### KMX keyboards & KMX+ (LDML) keyboards + +For KMX and KMX+ keyboards, keystrokes are evaluated by using the embedded WASM +version of Keyman Core. + +TODO: add link to specific mechanisms here once available. + +### "Alternate" processing (predictive-text) + +Once the incoming `KeyEvent` has been processed, if predictive-text is active +and the `KeyEvent` includes fat-finger oriented data, the engine will then build +a set of "alternates" - of "phantom" keystrokes - and process _them_ against +headless clones of the original context in the same manner as the original +keystroke. + +Once all relevant "alternates" have been evaluated, the engine will then signal +the predictive-text engine with all relevant data, including the original, +snapshotted state of the context. It does not actually trigger predictive-text +at this time; processing of alternates is done before keystroke post-processing +in order to avoid cross-effects from JS-keyboard variable store manipulations +and similar mechanisms. Fat-finger "alternate" `KeyEvent`s are evaluated as if +they occurred under the exact same conditions as the keystroke that was actually +detected. + +### Keystroke post-processing + +At this stage, the side-effects desired for the original keystroke, as saved to +its `RuleBehavior`, are then applied. Note that this includes changes to the +`&layer` store - this update will also trigger an event that updates the state +of the OSK to match the desired layer. + +Once that is done, the engine will then save data relevant to the original +context state, the `KeyEvent`, and the `RuleBehavior` to the "context cache" +supporting multitap context reversion. These effects include layer-change +effects that result from JS keyboard rules, which modify the KMN-style keyboard +stores `&oldlayer` and `&newlayer` in preparation for `begin PostKeystroke` +group processing, which is then executed (and which can trigger another `&layer` +& OSK-layer update). + +Once post-keystroke effects have been determined, the engine then signals +predictive-text to generate new predictions associated with the keystroke. For +`app/browser` variants of the engine, changes to the context will then also +trigger the DOM `input` event on the context's element in case third parties +wish to leverage such events for their website's pages. + +### Keystroke resolution + +At this stage, `InputProcessor` passes control back to the main `KeymanEngine` +module, which then triggers the "on rule finalization" setting if configured. +This setting is mostly used for page-integration within `app/browser`, +supporting DOM element `change` event emulation whenever focus shifts away from +the active context element. + +For OSK keystrokes, an identifier associated with the `KeyEvent`'s context +snapshot will be made accessible through the `keyevent` event's callback +parameter - a feature utilized by multitap gesture support. + +For hardware-keyboard keystrokes, the `keyevent` callback is used to determine +whether or not native handling for the keystroke should be triggered - such as +for TAB and ENTER keys, which may have semantic command-like functionality +depending on the state of the Web engine's host - be that a web page or an +Android/iOS app utilizing the keyboard. ENTER keys are often used to submit +forms, and this helps ensure we trigger such functionality when appropriate. \ No newline at end of file diff --git a/web/docs/internal/keystroke-processing.md b/web/docs/internal/keystroke-processing.md index 4406b1dcab..8606d18cba 100644 --- a/web/docs/internal/keystroke-processing.md +++ b/web/docs/internal/keystroke-processing.md @@ -1,6 +1,6 @@ # Keystroke Processing -In addition to handling keystroke events produced from hardware keyboards, Keyman Engine for Web manages an on-screen keyboard that can also be used to generate such events. This document will enumerate the various components of data management and control flow used to handle and process keystrokes. +In addition to handling keystroke events produced from hardware keyboards, Keyman Engine for Web manages an on-screen keyboard that can also be used to generate such events. This document will enumerate the various components of data management and control flow used to handle and process keystrokes. For a more thorough examination of the keystroke lifecycle, see the [keystroke-lifecycle](./keystroke-lifecycle.md) page. ## Input: KeyEvent - the pre-processed keystroke diff --git a/web/package.json b/web/package.json index f30a0b4218..3d73175202 100644 --- a/web/package.json +++ b/web/package.json @@ -109,7 +109,6 @@ "@zip.js/zip.js": "^2.7.32", "c8": "^7.12.0", "jsdom": "^23.0.1", - "mocha": "^10.0.0", "tsx": "^4.19.0" }, "scripts": { diff --git a/web/src/app/browser/build.sh b/web/src/app/browser/build.sh index d4c1023314..85c696ba4a 100755 --- a/web/src/app/browser/build.sh +++ b/web/src/app/browser/build.sh @@ -19,8 +19,7 @@ builder_describe "Builds the Keyman Engine for Web's website-integrating version "clean" \ "configure" \ "build" \ - "test" \ - "--ci+ Set to utilize CI-based test configurations & reporting." + "test" # Possible TODO?s # "upload-symbols Uploads build product to Sentry for error report symbolification. Only defined for $DOC_BUILD_EMBED_WEB" \ diff --git a/web/src/app/browser/src/beepHandler.ts b/web/src/app/browser/src/beepHandler.ts index eb6d6583ee..6e23c09464 100644 --- a/web/src/app/browser/src/beepHandler.ts +++ b/web/src/app/browser/src/beepHandler.ts @@ -38,7 +38,7 @@ export class BeepHandler { } // All code after this point is DOM-based, triggered by the beep. - var Pelem: HTMLElement = outputTarget.getElement(); + let Pelem: HTMLElement = outputTarget.getElement(); if(outputTarget instanceof DesignIFrame) { Pelem = outputTarget.docRoot; // I1446 - beep sometimes fails to flash when using OSK and rich control } @@ -51,7 +51,7 @@ export class BeepHandler { return; } - for(var Lbo=0; Lbo { this.keyboardInterface.resetContextCache(); - var Lbo; + let Lbo; this._BeepTimeout = 0; for(Lbo=0;Lbo { - let target = this.currentTarget || this.mostRecentTarget; + const target = this.currentTarget || this.mostRecentTarget; if(this.isTargetKeyboardIndependent(target)) { return target; @@ -378,7 +378,7 @@ export default class ContextManager extends ContextManagerBase): boolean { - let attachmentInfo = target?.getElement()._kmwAttachment; + const attachmentInfo = target?.getElement()._kmwAttachment; // If null or undefined, we're in 'global' mode. return !!(attachmentInfo?.keyboard || attachmentInfo?.keyboard === ''); @@ -386,7 +386,7 @@ export default class ContextManager extends ContextManagerBase) { - let attachment = target?.getElement()._kmwAttachment; + const attachment = target?.getElement()._kmwAttachment; if(!attachment) { // if not set with an "independent keyboard", changes the global. @@ -425,7 +425,7 @@ export default class ContextManager extends ContextManagerBase): boolean { const focusAssistant = this.focusAssistant; - let activeKeyboard = this.activeKeyboard?.keyboard; + const activeKeyboard = this.activeKeyboard?.keyboard; if(!focusAssistant.restoringFocus) { outputTarget?.deadkeys().clear(); activeKeyboard?.notify(0, outputTarget, 1); // I2187 @@ -684,7 +684,7 @@ export default class ContextManager extends ContextManagerBase) { if(target.changed) { - let event = new Event('change', {"bubbles": true, "cancelable": false}); + const event = new Event('change', {"bubbles": true, "cancelable": false}); target.getElement().dispatchEvent(event); } @@ -756,7 +756,7 @@ export default class ContextManager extends ContextManagerBase('KeymanWeb_Keyboard'); - var v = cookie.load(decodeURIComponent); + const v = cookie.load(decodeURIComponent); if(typeof(v.current) != 'string') { return null; @@ -776,7 +776,7 @@ export default class ContextManager extends ContextManagerBase { const contextManager = this.contextManager; diff --git a/web/src/app/browser/src/hardwareEventKeyboard.ts b/web/src/app/browser/src/hardwareEventKeyboard.ts index 5e8b30ce4f..ae5fa680a3 100644 --- a/web/src/app/browser/src/hardwareEventKeyboard.ts +++ b/web/src/app/browser/src/hardwareEventKeyboard.ts @@ -2,7 +2,7 @@ import { Codes, DeviceSpec, KeyEvent, KeyMapping, Keyboard } from 'keyman/engine import { KeyboardProcessor } from 'keyman/engine/js-processor'; import { ModifierKeyConstants } from '@keymanapp/common-types'; -import { HardKeyboard, processForMnemonicsAndLegacy } from 'keyman/engine/main'; +import { HardKeyboardBase, processForMnemonicsAndLegacy } from 'keyman/engine/main'; import { DomEventTracker } from 'keyman/engine/events'; import { DesignIFrame, nestedInstanceOf } from 'keyman/engine/element-wrappers'; import { eventOutputTarget, outputTargetForElement } from 'keyman/engine/attachment'; @@ -59,10 +59,11 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar } // Stage 1 - track the true state of the keyboard's modifiers. - var prevModState = keyboardState.modStateFlags, curModState = 0x0000; - var ctrlEvent = false, altEvent = false; + const prevModState = keyboardState.modStateFlags; + let curModState = 0x0000; + let ctrlEvent = false, altEvent = false; - let keyCodes = Codes.keyCodes; + const keyCodes = Codes.keyCodes; switch(Lcode) { case keyCodes['K_CTRL']: // The 3 shorter "K_*CTRL" entries exist in some legacy keyboards. case keyCodes['K_LCTRL']: @@ -129,7 +130,7 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar keyboardState.modStateFlags = curModState; // For European keyboards, not all browsers properly send both key-up events for the AltGr combo. - let altGrMask = ModifierKeyConstants.RALTFLAG | ModifierKeyConstants.LCTRLFLAG; + const altGrMask = ModifierKeyConstants.RALTFLAG | ModifierKeyConstants.LCTRLFLAG; if((prevModState & altGrMask) == altGrMask && (curModState & altGrMask) != altGrMask) { // We just released AltGr - make sure it's all released. curModState &= ~ altGrMask; @@ -139,7 +140,7 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar curModState &= ~ModifierKeyConstants.LCTRLFLAG; } - let modifierBitmasks = Codes.modifierBitmasks; + const modifierBitmasks = Codes.modifierBitmasks; // Stage 4 - map the modifier set to the appropriate keystroke's modifiers. const activeKeyboard = keyboardState.activeKeyboard; let Lmodifiers: number; @@ -192,17 +193,17 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar }); // The 0x6F used to be 0x60 - this adjustment now includes the chiral alt and ctrl modifiers in that check. - let LisVirtualKeyCode = (typeof e.charCode != 'undefined' && e.charCode != null && (e.charCode == 0 || (Lmodifiers & 0x6F) != 0)); + const LisVirtualKeyCode = (typeof e.charCode != 'undefined' && e.charCode != null && (e.charCode == 0 || (Lmodifiers & 0x6F) != 0)); s.LisVirtualKey = LisVirtualKeyCode || e.type != 'keypress'; s = processForMnemonicsAndLegacy(s, activeKeyboard, keyboardState.baseLayout); - let processedEvent = new KeyEvent(s); + const processedEvent = new KeyEvent(s); processedEvent.source = e; return processedEvent; } -export default class HardwareEventKeyboard extends HardKeyboard { +export default class HardwareEventKeyboard extends HardKeyboardBase { private readonly hardDevice: DeviceSpec; // Needed properties & methods: @@ -304,17 +305,17 @@ export default class HardwareEventKeyboard extends HardKeyboard { */ _KeyUp: (e: KeyboardEvent) => boolean = (e) => { const target = eventOutputTarget(e); - var Levent = preprocessKeyboardEvent(e, this.processor, this.hardDevice); + const Levent = preprocessKeyboardEvent(e, this.processor, this.hardDevice); if(Levent == null || target == null) { return true; } - var inputEle = target.getElement(); + const inputEle = target.getElement(); // Since this part concerns DOM element + browser interaction management, we preprocess it for // browser form commands before passing control to the Processor module. if(Levent.Lcode == 13) { - var ignore = false; + let ignore = false; if(nestedInstanceOf(inputEle, "HTMLTextAreaElement")) { ignore = true; } @@ -349,12 +350,12 @@ export default class HardwareEventKeyboard extends HardKeyboard { this.swallowKeypress = false; // Get event properties - var Levent = preprocessKeyboardEvent(e, this.processor, this.hardDevice); + const Levent = preprocessKeyboardEvent(e, this.processor, this.hardDevice); if(Levent == null) { return true; } - let resultCapture: { LeventMatched: boolean } = { + const resultCapture: { LeventMatched: boolean } = { LeventMatched: false } @@ -385,17 +386,17 @@ export default class HardwareEventKeyboard extends HardKeyboard { // 1) To detect browser form submissions (handled in kmwdomevents.ts) // 2) To detect modifier state changes. private keyUp(e: KeyboardEvent): boolean { - var Levent = preprocessKeyboardEvent(e, this.processor, this.hardDevice); + const Levent = preprocessKeyboardEvent(e, this.processor, this.hardDevice); if(Levent == null) { return true; } - let outputTarget = eventOutputTarget(e); + const outputTarget = eventOutputTarget(e); return this.processor.doModifierPress(Levent, outputTarget, false); } private keyPress(e: KeyboardEvent): boolean { - var Levent = preprocessKeyboardEvent(e, this.processor, this.hardDevice); + const Levent = preprocessKeyboardEvent(e, this.processor, this.hardDevice); if(Levent == null || Levent.LisVirtualKey) { return true; } @@ -417,7 +418,7 @@ export default class HardwareEventKeyboard extends HardKeyboard { // Only reached if it's a mnemonic keyboard. - let resultCapture: { preventDefaultKeystroke?: boolean } = {}; + const resultCapture: { preventDefaultKeystroke?: boolean } = {}; // Should only be run if `preventDefaultKeystroke` is required by the following conditional // block. If it isn't - that is, swallowKeypress == true, we want to swallow that keypress diff --git a/web/src/app/browser/src/hotkeyManager.ts b/web/src/app/browser/src/hotkeyManager.ts index 11da2aea3b..f0dbe8b3bb 100644 --- a/web/src/app/browser/src/hotkeyManager.ts +++ b/web/src/app/browser/src/hotkeyManager.ts @@ -29,7 +29,7 @@ export class HotkeyManager { */ addHotKey(keyCode: number, shiftState: number, handler: () => void) { // Test if existing handler for this code and replace it if so - for(var i=0; i { constructor( diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index dd53693ce5..59987035cc 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -1,5 +1,5 @@ import { KeymanWebKeyboard } from '@keymanapp/common-types'; -import { KeymanEngine as KeymanEngineBase, DeviceDetector } from 'keyman/engine/main'; +import { KeymanEngineBase, DeviceDetector } from 'keyman/engine/main'; import { getAbsoluteY } from 'keyman/engine/dom-utils'; import { OutputTarget } from 'keyman/engine/element-wrappers'; import { @@ -29,7 +29,7 @@ import { BeepHandler } from './beepHandler.js'; import KeyboardInterface from './keyboardInterface.js'; import { WorkerFactory } from '@keymanapp/lexical-model-layer/web'; -export default class KeymanEngine extends KeymanEngineBase { +export class KeymanEngine extends KeymanEngineBase { touchLanguageMenu?: LanguageMenu; private pageIntegration: PageIntegrationHandlers; @@ -158,8 +158,8 @@ export default class KeymanEngine extends KeymanEngineBase) { - let deviceDetector = new DeviceDetector(); - let device = deviceDetector.detect(); + const deviceDetector = new DeviceDetector(); + const device = deviceDetector.detect(); const totalOptions = {...BrowserInitOptionDefaults, ...options}; @@ -397,7 +397,7 @@ export default class KeymanEngine extends KeymanEngineBase /* [b/c Toolbar UI]*/) { let kbd: Keyboard; if(k0) { - let kbdDetail = k0 as ReturnType; + const kbdDetail = k0 as ReturnType; if(kbdDetail.KeyboardID){ kbd = this.keyboardRequisitioner.cache.getKeyboard(kbdDetail.KeyboardID); } else { @@ -682,7 +682,7 @@ export default class KeymanEngine extends KeymanEngineBase { @@ -55,8 +55,8 @@ export class LanguageMenu { // Display build only if touching menu, space *and* one other point on screen (build 369) if(e.touches.length > 2) { - var sX=e.touches[1].pageX,sY=e.touches[1].pageY; - let spaceBar = osk.vkbd.spaceBar; + const sX=e.touches[1].pageX,sY=e.touches[1].pageY; + const spaceBar = osk.vkbd.spaceBar; if(sX > spaceBar.offsetLeft && sX < spaceBar.offsetLeft+spaceBar.offsetWidth && sY > spaceBar.offsetTop && sY < spaceBar.offsetTop+spaceBar.offsetHeight ) { @@ -73,26 +73,26 @@ export class LanguageMenu { **/ show() { const device = this.keyman.config.hostDevice; - let kbdList=this.keyman.keyboardRequisitioner.cache.getStubList(); - let nKbds=kbdList.length; + const kbdList=this.keyman.keyboardRequisitioner.cache.getStubList(); + const nKbds=kbdList.length; if(nKbds < 1) { return; } // Create the menu list container element - var menu=this.lgList=util._CreateElement('div'); + const menu=this.lgList=util._CreateElement('div'); this.lgList.id='kmw-language-menu'; // Insert a transparent overlay to prevent anything else happening during keyboard selection, // but allow the menu to be closed if anywhere else on screen is touched - let languageMenu = this; + const languageMenu = this; document.body.appendChild(this.shim); // Add two nested DIVs to properly support iOS scrolling with momentum // c.f. https://github.com/joelambert/ScrollFix/issues/2 - var m2=util._CreateElement('div'),s2=m2.style, + const m2=util._CreateElement('div'),s2=m2.style, m3=util._CreateElement('div'); m2.id='kmw-menu-scroll-container'; m3.id='kmw-menu-scroller'; @@ -105,7 +105,8 @@ export class LanguageMenu { menu.appendChild(m2); // Add menu index strip - let x,mx=util._CreateElement('div'); + let x: HTMLParagraphElement; + const mx=util._CreateElement('div'); mx.id='kmw-menu-index'; for(let i=1; i<=26; i++) { x=util._CreateElement('p'); @@ -154,7 +155,7 @@ export class LanguageMenu { // Adjust width for pixel scaling on Android tablets if(device.OS == 'android' && device.formFactor == 'tablet' && 'devicePixelRatio' in window) { - var w=parseInt(util.getStyleValue(menu,'width'),10), + let w=parseInt(util.getStyleValue(menu,'width'),10), ms=menu.style; if(!isNaN(w)) { ms.width=ms.maxWidth=(2*w/window.devicePixelRatio)+'px'; @@ -175,17 +176,17 @@ export class LanguageMenu { this.adjust(0); // Adjust the index font size and line height - var dy=(mx.childNodes[1]).offsetTop-(mx.childNodes[0]).offsetTop, - lineHeight=Math.floor(menu.offsetHeight/26.0), - scale=Math.round(100.0*lineHeight/dy)/100.0, - factor=(scale > 0.6 ? 1 : 2); + const dy=(mx.childNodes[1]).offsetTop-(mx.childNodes[0]).offsetTop; + const lineHeight=Math.floor(menu.offsetHeight/26.0); + let scale=Math.round(100.0*lineHeight/dy)/100.0; + const factor=(scale > 0.6 ? 1 : 2); if(scale > 1.25) { scale=1.25; } for(let i=0;i<26;i++) { - var qs=(mx.childNodes[i]).style; + const qs=(mx.childNodes[i]).style; if(factor == 2 && (i%2) == 1) { qs.display='none'; } else { @@ -195,7 +196,7 @@ export class LanguageMenu { } // Increase width of outer menu DIV by index, else hide index - var menuWidth=m2.offsetWidth; + let menuWidth=m2.offsetWidth; if(m2.scrollHeight > m2.offsetHeight+3) { menuWidth = menuWidth+mx.offsetWidth; } else { @@ -216,15 +217,16 @@ export class LanguageMenu { * @param {number} nKbds number of displayed keyboards to add to number of languages **/ adjust(nKbds: number) { - let osk = this.keyman.osk; - let device = this.keyman.config.hostDevice; + const osk = this.keyman.osk; + const device = this.keyman.config.hostDevice; - var menu=this.lgList, m2=menu.firstChild, m3=m2.firstChild, - barWidth=0,s=menu.style,mx=menu.childNodes[1], - maxHeight=window.innerHeight-osk.vkbd.lgKey.offsetHeight-16, + const menu=this.lgList, m2=menu.firstChild, m3=m2.firstChild; + let barWidth=0; + let maxHeight=window.innerHeight-osk.vkbd.lgKey.offsetHeight-16; + const s=menu.style,mx=menu.childNodes[1], nItems=m3.childNodes.length+nKbds-1, // Number of (visible) keyboard selectors - itemHeight=(m3.firstChild.firstChild).offsetHeight, - menuHeight=nItems*itemHeight; + itemHeight=(m3.firstChild.firstChild).offsetHeight; + let menuHeight=nItems*itemHeight; // Correct maxheight for viewport scaling (iPhone/iPod only) and internal position corrections if(device.OS == 'ios') { @@ -263,17 +265,18 @@ export class LanguageMenu { e.stopPropagation(); e.preventDefault(); - let target = e.touches[0].target; + const target = e.touches[0].target; // Will return 'P', not 'p'. if(target.nodeName != 'P') { return; } - var i,t,initial=target.innerHTML.charCodeAt(0),nn=menu.childNodes; + let i: number; + const initial=target.innerHTML.charCodeAt(0), nn=menu.childNodes; try { for(i=0; inn[i].firstChild).innerHTML.toUpperCase().charCodeAt(0); + const t=(nn[i].firstChild).innerHTML.toUpperCase().charCodeAt(0); if(t >= initial) { break; } @@ -316,11 +319,11 @@ export class LanguageMenu { * @return {number} index of currently active language **/ addLanguages(menu: HTMLDivElement, kbdList: KeyboardStub[]): number { - var nStubs=kbdList.length; - let device = this.keyman.config.hostDevice; + const nStubs=kbdList.length; + const device = this.keyman.config.hostDevice; // Create and sort a list of languages - let langs: string[] = []; + const langs: string[] = []; for(let n=0; nlanguageMenu.lgList.childNodes[0], - yMax=scroller.scrollHeight-scroller.offsetHeight, - y, dy; + const scroller=languageMenu.lgList.childNodes[0]; + const yMax=scroller.scrollHeight-scroller.offsetHeight; + let y: number; // TS does not have a standard definition for TouchEvent.pageY. //@ts-ignore @@ -513,7 +516,7 @@ export class LanguageMenu { return false; } - dy=y-languageMenu.y0; + const dy=y-languageMenu.y0; // Scroll up (show later listed languages) if(dy < 0) { @@ -577,7 +580,7 @@ export class LanguageMenu { * Remove the language menu again **/ hide() { - let osk = this.keyman.osk; + const osk = this.keyman.osk; if(this.lgList) { osk.vkbd.highlightKey(osk.vkbd.lgKey,false); diff --git a/web/src/app/browser/src/oskConfiguration.ts b/web/src/app/browser/src/oskConfiguration.ts index 154b969865..d9f5764671 100644 --- a/web/src/app/browser/src/oskConfiguration.ts +++ b/web/src/app/browser/src/oskConfiguration.ts @@ -1,7 +1,7 @@ import { OSKView } from "keyman/engine/osk"; import { KEYMAN_VERSION } from "@keymanapp/keyman-version"; import ContextManager from "./contextManager.js"; -import KeymanEngine from "./keymanEngine.js"; +import { KeymanEngine } from "./keymanEngine.js"; import { LanguageMenu } from "./languageMenu.js"; export function setupOskListeners(engine: KeymanEngine, osk: OSKView, contextManager: ContextManager) { diff --git a/web/src/app/browser/src/release-main.ts b/web/src/app/browser/src/release-main.ts index 01b318c714..faf96e9097 100644 --- a/web/src/app/browser/src/release-main.ts +++ b/web/src/app/browser/src/release-main.ts @@ -1,4 +1,4 @@ -import KeymanEngine from './keymanEngine.js' +import { KeymanEngine } from './keymanEngine.js' import { Worker } from '@keymanapp/lexical-model-layer/web' /** @@ -8,9 +8,9 @@ import { Worker } from '@keymanapp/lexical-model-layer/web' * This can only be done during load when the active script will be the * last script loaded. Otherwise the script must be identified by name. */ - var scripts = document.getElementsByTagName('script'); - var ss = scripts[scripts.length-1].src; - var sPath = ss.substr(0,ss.lastIndexOf('/')+1); + const scripts = document.getElementsByTagName('script'); + const ss = scripts[scripts.length-1].src; + const sPath = ss.substr(0,ss.lastIndexOf('/')+1); // @ts-ignore window['keyman'] = new KeymanEngine(Worker, sPath); \ No newline at end of file diff --git a/web/src/app/browser/src/test-index.ts b/web/src/app/browser/src/test-index.ts index 47d3591311..408b47cba6 100644 --- a/web/src/app/browser/src/test-index.ts +++ b/web/src/app/browser/src/test-index.ts @@ -2,6 +2,6 @@ export { BrowserConfiguration, BrowserInitOptionSpec } from './configuration.js' export { default as ContextManager, KeyboardCookie } from "./contextManager.js"; export { preprocessKeyboardEvent, default as HardwareEventKeyboard } from './hardwareEventKeyboard.js'; -export { default as KeymanEngine } from './keymanEngine.js'; +export { KeymanEngine } from './keymanEngine.js'; export { default as KeyboardInterface } from './keyboardInterface.js'; export { UIModule } from './uiModuleInterface.js'; diff --git a/web/src/app/browser/src/utils/rotationProcessor.ts b/web/src/app/browser/src/utils/rotationProcessor.ts index e8141d1ab1..07e1c1b202 100644 --- a/web/src/app/browser/src/utils/rotationProcessor.ts +++ b/web/src/app/browser/src/utils/rotationProcessor.ts @@ -1,4 +1,4 @@ -import KeymanEngine from "../keymanEngine.js"; +import { KeymanEngine } from "../keymanEngine.js"; class RotationState { innerWidth: number; @@ -40,7 +40,7 @@ export class RotationProcessor { } resolve() { - var osk = this.keyman.osk; + const osk = this.keyman.osk; // `keyman: KeymanEngine` (modularized app/browser) this.keyman.touchLanguageMenu?.hide(); @@ -81,8 +81,8 @@ export class RotationProcessor { */ init() { // Note: we use wrapper functions instead of `.bind(this)` in this method to facilitate stubbing for our rotation test page. - var os = this.keyman.config.hostDevice.OS; - var util = this.keyman.util; + const os = this.keyman.config.hostDevice.OS; + const util = this.keyman.util; if(os == 'ios') { /* iOS is rather inconsistent about these events, with changes to important window state information - @@ -137,7 +137,7 @@ export class RotationProcessor { } iOSEventUpdate() { - var newState = new RotationState(); + const newState = new RotationState(); if(this.rotState.equals(newState)) { if(++this.idlePermutationCounter == RotationProcessor.IDLE_PERMUTATION_CAP) { diff --git a/web/src/app/browser/src/viewsAnchorpoint.ts b/web/src/app/browser/src/viewsAnchorpoint.ts index 3df908c402..0c1015505b 100644 --- a/web/src/app/browser/src/viewsAnchorpoint.ts +++ b/web/src/app/browser/src/viewsAnchorpoint.ts @@ -5,7 +5,7 @@ import { FloatingOSKViewConfiguration, InlinedOSKView } from "keyman/engine/osk"; -import KeymanEngine from "./keymanEngine.js"; +import { KeymanEngine } from "./keymanEngine.js"; function buildBaseOskConfiguration(engine: KeymanEngine) { return { @@ -18,7 +18,7 @@ function buildBaseOskConfiguration(engine: KeymanEngine) { class PublishedAnchoredOSKView extends AnchoredOSKView { constructor(engine: KeymanEngine, config?: ViewConfiguration) { - let finalConfig = { + const finalConfig = { ...buildBaseOskConfiguration(engine), ...(config || {}) }; @@ -29,7 +29,7 @@ class PublishedAnchoredOSKView extends AnchoredOSKView { class PublishedFloatingOSKView extends FloatingOSKView { constructor(engine: KeymanEngine, config?: FloatingOSKViewConfiguration) { - let finalConfig: FloatingOSKViewConfiguration = { + const finalConfig: FloatingOSKViewConfiguration = { ...buildBaseOskConfiguration(engine), ...(config || {}) }; @@ -40,7 +40,7 @@ class PublishedFloatingOSKView extends FloatingOSKView { class PublishedInlineOSKView extends InlinedOSKView { constructor(engine: KeymanEngine, config?: ViewConfiguration) { - let finalConfig: ViewConfiguration = { + const finalConfig: ViewConfiguration = { ...buildBaseOskConfiguration(engine), ...(config || {}) }; diff --git a/web/src/app/ui/build.sh b/web/src/app/ui/build.sh index a0ac5c4007..3a431f9a0a 100755 --- a/web/src/app/ui/build.sh +++ b/web/src/app/ui/build.sh @@ -18,8 +18,7 @@ builder_describe "Builds the Keyman Engine for Web's desktop form-factor keyboar "clean" \ "configure" \ "build" \ - "test" \ - "--ci+ Set to utilize CI-based test configurations & reporting." + "test" # Possible TODO?s # "upload-symbols Uploads build product to Sentry for error report symbolification. Only defined for $DOC_BUILD_EMBED_WEB" \ diff --git a/web/src/app/ui/kmwuibutton.ts b/web/src/app/ui/kmwuibutton.ts index d3b55d7036..16377d4f3d 100644 --- a/web/src/app/ui/kmwuibutton.ts +++ b/web/src/app/ui/kmwuibutton.ts @@ -28,7 +28,7 @@ if(!keymanweb) { /** * Do not enclose in an anonymous function, as the compiler may create - * global scope variables to replace true, false, null, whcih can then collide + * global scope variables to replace true, false, null, which can then collide * with other variables. * Instead, use the --output-wrapper command during optimization, which will * add the anonymous function to enclose all code, including those optimized @@ -78,10 +78,10 @@ if(!keymanweb) { * Highlight the currently active keyboard in the list of keyboards **/ private _ShowSelected() { - let kbd=keymanweb.getActiveKeyboard(); - let lgc=keymanweb.getActiveLanguage(); - let kList = this._KeymanWeb_KbdList.childNodes; - let _r = /^KMWSel_(.*)\$(.*)$/; + const kbd=keymanweb.getActiveKeyboard(); + const lgc=keymanweb.getActiveLanguage(); + const kList = this._KeymanWeb_KbdList.childNodes; + const _r = /^KMWSel_(.*)\$(.*)$/; for(let i=1; i { + private readonly _SelectKeyboard = async (_id: Event): Promise => { let id: string = ''; if(typeof(_id) == 'object') { let t: HTMLElement = null; @@ -121,26 +121,27 @@ if(!keymanweb) { } } - let _r=/^KMWSel_(.*)\$(.*)$/; - let _rv=_r.exec(id),_lgc='',_name=''; + const _r=/^KMWSel_(.*)\$(.*)$/; + const _rv=_r.exec(id); + let _lgc='',_name=''; if(_rv !== null) { _name = _rv[1].split('$')[0]; //new code _lgc = id.split('$')[1]; if(this._KMWSel != null) { this._KMWSel.className = ''; } - let _k = document.getElementById(id); + const _k = document.getElementById(id); if(_k) { _k.className='selected'; } this._KMWSel = _k; - keymanweb.setActiveKeyboard(_name,_lgc); + await keymanweb.setActiveKeyboard(_name,_lgc); } else { _name=null; } keymanweb.focusLastActiveElement(); - let osk = keymanweb.osk; + const osk = keymanweb.osk; if(osk && osk.isEnabled()) { osk.show(true); } @@ -251,7 +252,7 @@ if(!keymanweb) { * * @param {Event} e event */ - readonly _SelectorMouseOut = (e: MouseEvent) => { + private readonly _SelectorMouseOut = (e: MouseEvent) => { if(keymanweb.activatingUI) { keymanweb.activatingUI(0); } @@ -265,7 +266,7 @@ if(!keymanweb) { */ private _ShowKeyboardButton(_name?: string) { let kbdName = keymanweb.getActiveKeyboard(); - let kbdId = document.getElementById("KMW_Keyboard"); + const kbdId = document.getElementById("KMW_Keyboard"); if(arguments.length > 0) { kbdName = _name; } @@ -274,14 +275,14 @@ if(!keymanweb) { if((kbdName == '') || keymanweb.isCJK()) { kbdId.className='kmw_disabled'; } else { - let osk = keymanweb.osk; + const osk = keymanweb.osk; kbdId.className = osk && osk.isEnabled() ? 'kmw_show' : 'kmw_hide'; } } } registerEvents() { - let osk = keymanweb.osk; + const osk = keymanweb.osk; if(!osk) { return; } @@ -304,7 +305,7 @@ if(!keymanweb) { /* TODO: why is this still needed??? Does it actually do anything?? */ osk.addEventListener('hide', function(obj) { if((arguments.length > 0) && obj.HiddenByUser) { - let _a = document.getElementById('KMW_Keyboard'); + const _a = document.getElementById('KMW_Keyboard'); if(_a) { _a.className = 'kmw_hide'; } @@ -320,7 +321,7 @@ if(!keymanweb) { **/ readonly _ShowKeymanWebKeyboard = () => { const kbdId=document.getElementById("KMW_Keyboard"); - let osk = keymanweb.osk; + const osk = keymanweb.osk; if((kbdId.className!='kmw_disabled') && osk && osk.show) { if(osk.isEnabled()) { @@ -384,7 +385,7 @@ if(!keymanweb) { } - let imgPath=util.getOption('resources')+'ui/button/'; + const imgPath=util.getOption('resources')+'ui/button/'; if(_elem) { // Append another DIV to follow the main control with clear:both to prevent selection over entire width of window const dx=document.createElement('DIV') @@ -417,9 +418,9 @@ if(!keymanweb) { // Thus, in essence: if(true) { /* ... */ } // @ts-ignore if(!keymanweb['iOS']) { - var _li = util.createElement('li'); - var _a = util.createElement('a'); - var _img = util.createElement('img'); + const _li = util.createElement('li'); + const _a = util.createElement('a'); + const _img = util.createElement('img'); _img.src = imgPath+'kbdicon.gif'; _a.appendChild(_img); @@ -430,7 +431,7 @@ if(!keymanweb) { _sp1.appendChild(_txt1); _a.appendChild(_sp1); - var _sp2 = util.createElement('span'); + const _sp2 = util.createElement('span'); _sp2.id = 'KMW_KbdHiddenMsg'; _sp2.appendChild(_txt2); _a.appendChild(_sp2); @@ -443,9 +444,9 @@ if(!keymanweb) { this._KeymanWeb_KbdList.appendChild(_li); } - var _li1 = util.createElement('li'); + const _li1 = util.createElement('li'); _li1.id = 'KMW_ButtonUI_KbdList'; - var _a1 = util.createElement('a'); + const _a1 = util.createElement('a'); _a1.appendChild(document.createTextNode('(System keyboard)')); _a1.onclick = this._SelectKeyboard; @@ -460,7 +461,7 @@ if(!keymanweb) { document.getElementById('kmwico_li').appendChild(this._KeymanWeb_KbdList); - var _sfEl = document.getElementById("kmwico_li"); + const _sfEl = document.getElementById("kmwico_li"); util.attachDOMEvent(_sfEl,'mousedown',this._SelectorMouseDown); util.attachDOMEvent(_sfEl,'mouseover',this._SelectorMouseOver); util.attachDOMEvent(_sfEl,'mouseout',this._SelectorMouseOut); @@ -494,7 +495,7 @@ if(!keymanweb) { const kbds=keymanweb.getKeyboards(); if(kbds.length > 0) { - for(var i:number=0; i { keymanweb.activatingUI(true); - let osk = keymanweb.osk; + const osk = keymanweb.osk; if(osk && osk.show) { if(osk.isEnabled()) { osk.hide(); @@ -133,7 +133,7 @@ if(!keymanweb) { this.outerDiv.innerHTML = "" + "KeymanWeb"; /* I2081 */ - var s=this.outerDiv.style; + let s=this.outerDiv.style; s.backgroundColor='white'; s.border='solid 1px black'; s.position='absolute'; s.height='18px'; s.font='bold 8pt sans-serif'; s.display='none'; s.textAlign='left';s.overflow='hidden'; @@ -150,7 +150,7 @@ if(!keymanweb) { // Set initial OSK button style (off by default) this.oskButtonState(false); - var Lhdiv = util.createElement('div'); + const Lhdiv = util.createElement('div'); this.oskButton = Lhdiv; Lhdiv.onclick = this.toggleOSK; Lhdiv.appendChild(this.kbdIcon); @@ -177,7 +177,8 @@ if(!keymanweb) { this.innerDiv.appendChild(this.KeyboardSelector); //this may need to be moved up.... // Check required interface alignment and default keyboard - var opt=util.getOption('ui'), dfltKeyboard='(System keyboard)'; + const opt=util.getOption('ui'); + let dfltKeyboard='(System keyboard)'; if(opt && typeof(opt) == 'object') { if(typeof(opt['position']) == 'string' && opt['position'] == 'right') { this.floatRight = true; @@ -186,7 +187,7 @@ if(!keymanweb) { } } - var Lopt = util.createElement('option'); + let Lopt = util.createElement('option'); Lopt.value = '-'; Lopt.innerHTML = dfltKeyboard; this.KeyboardSelector.appendChild(Lopt); @@ -209,7 +210,7 @@ if(!keymanweb) { * UI removal - resource cleanup */ shutdown() { - var root = this.outerDiv; + const root = this.outerDiv; if(root) { root.parentNode.removeChild(root); } @@ -238,14 +239,14 @@ if(!keymanweb) { } // Loop over installed keyboards and add to selection list - var Lkbds=keymanweb.getKeyboards(); + const Lkbds=keymanweb.getKeyboards(); for(let Ln=0; Ln { - var i=0; + let i=0; // This can be called during startup before fully initialized - ignore if so if(!this.initialized) { return; } - var match = kbd; + let match = kbd; if(lg != '') { match += ':' + lg; } @@ -321,7 +322,7 @@ if(!keymanweb) { * Description Update kbd icon border style to indicate whether OSK is enabled for display or not **/ readonly oskButtonState = (oskEnabled: boolean) => { - var s = this.kbdIcon.style; + const s = this.kbdIcon.style; s.width='24px'; s.height='13px'; s.top='1px'; @@ -383,7 +384,7 @@ if(!keymanweb) { this.addButtonOSK(); }); - let osk = keymanweb.osk; + const osk = keymanweb.osk; if(!osk) { return; } @@ -449,15 +450,15 @@ if(!keymanweb) { * @param {Object} e event * Description Change active keyboard in response to user selection event */ - readonly SelectKeyboardChange = (e: Event) => { + private readonly SelectKeyboardChange = async (e: Event) => { keymanweb.activatingUI(true); if(this.KeyboardSelector.value != '-') { - var i=this.KeyboardSelector.selectedIndex; - var t=this.KeyboardSelector.options[i].value.split(':'); - keymanweb.setActiveKeyboard(t[0],t[1]); + const i=this.KeyboardSelector.selectedIndex; + const t=this.KeyboardSelector.options[i].value.split(':'); + await keymanweb.setActiveKeyboard(t[0],t[1]); } else { - keymanweb.setActiveKeyboard(''); + await keymanweb.setActiveKeyboard(''); } //if(osk['show']) osk['show'](osk['isEnabled']()); handled by keyboard change event??? @@ -489,7 +490,7 @@ if(!keymanweb) { readonly ShowInterface = (Px?: number, Py?: number) => { if(!this.initialized) return; - var Ls = this.outerDiv.style; + const Ls = this.outerDiv.style; if(Px && Py) { Ls.left = Px + 'px'; @@ -531,7 +532,7 @@ if(!keymanweb) { this.outerDiv.style.width = this.KeyboardSelector.offsetWidth+30+'px'; } else { this.oskButton.style.display = 'block'; - let osk = keymanweb.osk; + const osk = keymanweb.osk; if(osk) { this.oskButtonState(osk.isEnabled()); } else { @@ -551,7 +552,7 @@ if(!keymanweb) { */ readonly _Resize = (e: Event) => { if(this.outerDiv.style.display =='block') { - var elem = keymanweb.getLastActiveElement(); + const elem = keymanweb.getLastActiveElement(); if(this.floatRight) { // I1296 this.ShowInterface(util.getAbsoluteX(elem) + elem.offsetWidth + 1, util.getAbsoluteY(elem) + 1); } else { diff --git a/web/src/app/ui/kmwuitoggle.ts b/web/src/app/ui/kmwuitoggle.ts index f5a234471a..1aa4561465 100644 --- a/web/src/app/ui/kmwuitoggle.ts +++ b/web/src/app/ui/kmwuitoggle.ts @@ -118,7 +118,7 @@ if(!keymanweb) { let x = p.x; let y = p.y; - var ownerDoc = someElement.ownerDocument; + const ownerDoc = someElement.ownerDocument; if(ownerDoc.designMode == 'on' && ownerDoc.defaultView && ownerDoc.defaultView.frameElement) { w = ownerDoc.defaultView.frameElement.clientWidth; h = ownerDoc.defaultView.frameElement.clientHeight; @@ -145,7 +145,7 @@ if(!keymanweb) { } registerEvents() { - let osk = keymanweb.osk; + const osk = keymanweb.osk; if(!osk) { return; @@ -187,7 +187,7 @@ if(!keymanweb) { /** * Toggle a single keyboard on or off - KMW button control event **/ - readonly switchSingleKbd = () => { + readonly switchSingleKbd = async () => { const _v = keymanweb.getActiveKeyboard() == ''; let nLastKbd=0, kbdName='', lgCode=''; @@ -202,10 +202,10 @@ if(!keymanweb) { kbdName = this.keyboards[nLastKbd]._InternalName; lgCode = this.keyboards[nLastKbd]._LanguageCode; - keymanweb.setActiveKeyboard(kbdName,lgCode); + await keymanweb.setActiveKeyboard(kbdName,lgCode); this.lastActiveKeyboard = nLastKbd; } else { - keymanweb.setActiveKeyboard(''); + await keymanweb.setActiveKeyboard(''); } if(this.kbdButton) { @@ -216,7 +216,7 @@ if(!keymanweb) { /** * Switch to the next keyboard in the list - KMW button control event **/ - readonly switchNextKbd = () => { + readonly switchNextKbd = async () => { let _v = (keymanweb.getActiveKeyboard() == ''); let kbdName='', lgCode=''; @@ -227,16 +227,16 @@ if(!keymanweb) { kbdName = this.keyboards[0]._InternalName; lgCode = this.keyboards[0]._LanguageCode; - keymanweb.setActiveKeyboard(kbdName,lgCode); + await keymanweb.setActiveKeyboard(kbdName,lgCode); this.lastActiveKeyboard = 0; } else { if(this.lastActiveKeyboard == this.keyboards.length-1) { - keymanweb.setActiveKeyboard(''); + await keymanweb.setActiveKeyboard(''); _v = false; } else { kbdName = this.keyboards[++this.lastActiveKeyboard]._InternalName; lgCode = this.keyboards[this.lastActiveKeyboard]._LanguageCode; - keymanweb.setActiveKeyboard(kbdName,lgCode); + await keymanweb.setActiveKeyboard(kbdName,lgCode); _v = true; } } @@ -288,7 +288,7 @@ if(!keymanweb) { }; private __updatestyle() { - var ss=this._elem.style; + const ss=this._elem.style; if(this._over) { ss.margin = '0px'; if(this._selected) { @@ -370,8 +370,8 @@ if(!keymanweb) { constructor(/* TODO: put wrapping method's params HERE instead upon class-extraction */) { this._selected = _selected; - let imgPath=util.getOption('resources') + 'ui/toggle/'; - let _elemImg = util.createElement('img') as HTMLImageElement & Owned