Merge branch 'feat/developer/kmc-convert' into feat/developer/kmc-convert-useArtifacts

This commit is contained in:
SabineSIL 2025-06-27 12:07:08 +02:00 • committed by GitHub
commit b50d8e9c09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
673 changed files with 10287 additions and 8159 deletions

View file

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

9
.gitattributes vendored Normal file
View file

@ -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 <<EOF cat > foo
# echo "Hello world"
# EOF
# ```
Dockerfile text eol=lf

View file

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

54
.github/labeler.yml vendored
View file

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

View file

@ -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.*\):'

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,277 @@
# 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)
@ -113,6 +385,49 @@
* 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)

View file

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

View file

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

View file

@ -1 +1 @@
19.0.26
19.0.64

3
android/.gitignore vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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();

View file

@ -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() {

View file

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

View file

@ -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') {

View file

@ -9,7 +9,6 @@
<title>Keyman</title>
<!-- DOMRect only exists as of Chrome 61... thus coming later than ES6. -->
<script src="other-polyfills.js"></script>
<script src="sentry.min.js"></script>
<script src="keyman-sentry.js"></script>
<script src="keymanweb-webview.js"></script>
<script src="android-host.js"></script>

View file

@ -362,8 +362,7 @@ public final class KMManager {
// 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";
@ -982,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);

View file

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

View file

@ -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<String, String> 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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,6 +14,8 @@ 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 ################################
@ -36,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" \
@ -69,4 +72,51 @@ function do_test_help() {
builder_run_action test:help do_test_help
builder_run_child_actions publish
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

View file

@ -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
//println "version.gradle: KEYMAN_VERSION_TAG: " + KEYMAN_VERSION_TAG

1
android/zip-excludes Normal file
View file

@ -0,0 +1 @@
Samples/*/build.sh

View file

@ -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() {
<html>
<head>
<meta charset='utf-8'>
<title>Test Keyboards - $VERSION_WITH_TAG</title>
<title>Test Keyboards - $KEYMAN_VERSION_WITH_TAG</title>
</head>
<body>
<h1>Test Keyboards - $VERSION_WITH_TAG</h1>
<h1>Test Keyboards - $KEYMAN_VERSION_WITH_TAG</h1>
<ul>
EOF
@ -90,4 +92,4 @@ function build() {
fi
}
builder_run_action build build
builder_run_action build build

View file

@ -0,0 +1,3 @@
build/
*.kpj.user
*_source.zip

View file

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

View file

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

View file

@ -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"
},

View file

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

View file

@ -11,6 +11,7 @@
"/build/"
],
"license": "MIT",
"scripts": {},
"type": "module",
"devDependencies": {
"typescript": "^5.4.5"

View file

@ -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
},
]
};

View file

@ -82,7 +82,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

View file

@ -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": {

View file

@ -54,7 +54,7 @@ export const
export function keymanTargetsFromString(targets: string, options?: {expandTargets?: boolean}): KeymanTarget[] {
let result = new Set<KeymanTarget>(<KeymanTarget[]>targets.split(/ +/));
const result = new Set<KeymanTarget>(<KeymanTarget[]>targets.split(/ +/));
if(options?.expandTargets) {
if(result.has(KeymanTarget.any)) {
return AllKeymanTargets;

View file

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

View file

@ -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<ElemElement> {
* @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<ElemElement> {
}
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<ElemElement> {
// 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) {

View file

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

View file

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

View file

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

View file

@ -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)}}`));
}
}

View file

@ -39,7 +39,7 @@ export class ListItem extends Array<ListIndex> 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;

View file

@ -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;
}
/**

View file

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

View file

@ -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("𐌰𐌱𐌲"),

View file

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

View file

@ -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');

View file

@ -69,7 +69,8 @@ Libraries will be built in 'build/<target>/<configuration>/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

View file

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

View file

@ -1,3 +0,0 @@
@echo off
REM Get the version number from VERSION.md
more ..\VERSION.md

View file

@ -1,3 +0,0 @@
#!/usr/bin/env bash
# Get the version number from VERSION.md
cat ../VERSION.md

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<KeymanDeveloperProject>
<Options>
<BuildPath>$PROJECTPATH</BuildPath>
<CompilerWarningsAsErrors>False</CompilerWarningsAsErrors>
<WarnDeprecatedCode>False</WarnDeprecatedCode>
<CheckFilenameConventions>False</CheckFilenameConventions>
<ProjectType>keyboard</ProjectType>
</Options>
<Files>
@keyboards_kpj@
</Files>
</KeymanDeveloperProject>

View file

@ -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 += '''
<File>
<ID>keyboard_'''+kbd+'''</ID>
<Filename>'''+kbd+'''.kmn</Filename>
<Filepath>'''+kbd+'''.kmn</Filepath>
</File>
<File>
<ID>package_'''+kbd+'''</ID>
<Filename>'''+kbd+'''.kps</Filename>
<Filepath>'''+kbd+'''.kps</Filepath>
</File>
'''
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

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<KeymanDeveloperProject>
<Options>
<BuildPath>$PROJECTPATH</BuildPath>
<CompilerWarningsAsErrors>False</CompilerWarningsAsErrors>
<WarnDeprecatedCode>False</WarnDeprecatedCode>
<CheckFilenameConventions>False</CheckFilenameConventions>
<ProjectType>keyboard</ProjectType>
</Options>
<Files>
@keyboards_kpj@
</Files>
</KeymanDeveloperProject>

View file

@ -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 += '''
<File>
<ID>keyboard_'''+kbd+'''</ID>
<Filename>'''+kbd+'''.xml</Filename>
<Filepath>'''+kbd+'''.xml</Filepath>
</File>
'''
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@']
)

View file

@ -2,5 +2,6 @@
"name": "@keymanapp/api-header-extractor",
"private": true,
"type": "module",
"scripts": {},
"main": "build/src/index.js"
}

View file

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

View file

@ -0,0 +1,140 @@
---
author: "Steven R. Loomis <srl295@gmail.com>"
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

View file

@ -11,7 +11,7 @@
"dependencies": {
"@keymanapp/common-types": "*",
"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",
@ -25,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": {

View file

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

View file

@ -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 */
@ -417,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 ?? `<param>`);
export const CompilerMessageSpecWithException = (code: number, message: string, exceptionVar: any, detail?: string) : CompilerEvent => ({

View file

@ -40,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,8 +67,9 @@ export { UrlSubpathCompilerCallback } from './utils/UrlSubpathCompilerCallback.j
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';
@ -72,4 +77,4 @@ export { getFontFamily, getFontFamilySync } from './font-family.js';
export * as ValidIds from './valid-ids.js';
export * as ProjectLoader from './project-loader.js';
export * as ProjectLoader from './project-loader.js';

View file

@ -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<string, LineFinder> = 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;
}
}

View file

@ -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<T>(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<T>(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;
}
}

View file

@ -5,12 +5,14 @@
*/
import { SchemaValidators, util } from '@keymanapp/common-types';
import { DeveloperUtilsMessages } from '../../developer-utils-messages.js';
import { CompilerCallbacks } from "../../compiler-callbacks.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
}
/**
@ -223,6 +241,7 @@ export class LDMLKeyboardXMLSourceFileReader {
return false;
}
let importData: Uint8Array;
let importPath: string;
if (base === constants.cldr_import_base) {
// CLDR import
@ -235,10 +254,10 @@ export class LDMLKeyboardXMLSourceFileReader {
/** 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(DeveloperUtilsMessages.Error_ImportReadFail({base, path, subtag}));
@ -246,6 +265,7 @@ export class LDMLKeyboardXMLSourceFileReader {
}
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. <keys/>
this.eventResolver.addFile(importPath, new TextDecoder().decode(importData)); // TODO: double decode
// importXml will have one property: the root element.
if (!importRootNode) {
@ -263,7 +283,11 @@ export class LDMLKeyboardXMLSourceFileReader {
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);

View file

@ -6,7 +6,13 @@
* 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>
@ -48,6 +54,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': {
@ -108,11 +115,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') {
@ -131,9 +181,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;
}
@ -169,7 +219,7 @@ export class KeymanXMLReader {
}
}
});
return Object.fromEntries(e);
return SymbolUtils.copySymbols(Object.fromEntries(e), data);
} else {
return data;
}
@ -249,12 +299,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);
}
}

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