├── .clang-format ├── .clang-tidy ├── .clangd ├── .githooks └── pre-commit ├── .github └── workflows │ ├── build-ci.yml │ ├── commit-ci.yml │ ├── linux-build.yml │ ├── macos-build.yml │ ├── release-ci.yml │ └── windows-build.yml ├── .gitignore ├── .gitmodules ├── .prettierrc ├── .vscode └── settings.json ├── CMakeLists.txt ├── LICENSE ├── action-install.bat ├── action-install.sh ├── benchmark ├── CMakeLists.txt └── dict │ ├── dictionary_benchmark.cc │ ├── map.hpp │ └── trie_ext.hpp ├── contrib └── rime.d.ts ├── doc ├── build-linux.md ├── build-macos.md ├── build-windows.md ├── plugin-dev-with-jsc.md ├── plugin-dev.cn.md ├── plugin-dev.en.md └── readme.en.md ├── readme.md ├── src ├── dicts │ ├── dictionary.cc │ ├── dictionary.h │ ├── leveldb.cc │ ├── leveldb.h │ ├── trie.cc │ └── trie.h ├── engines │ ├── common.h │ ├── for_each_macros.h │ ├── javascriptcore │ │ ├── javascriptcore_engine.h │ │ ├── jsc_code_loader.cc │ │ ├── jsc_code_loader.h │ │ ├── jsc_engine_impl.cc │ │ ├── jsc_engine_impl.h │ │ ├── jsc_macros.h │ │ └── jsc_string_raii.hpp │ ├── js_exception.h │ ├── js_macros.h │ ├── js_traits.h │ ├── jscode_utils.hpp │ └── quickjs │ │ ├── quickjs_code_loader.cc │ │ ├── quickjs_code_loader.h │ │ ├── quickjs_engine.h │ │ ├── quickjs_engine_impl.cc │ │ └── quickjs_engine_impl.h ├── gears │ ├── qjs_filter.hpp │ ├── qjs_module.h │ ├── qjs_processor.h │ ├── qjs_translation.h │ └── qjs_translator.h ├── misc │ ├── process_memory.hpp │ └── system_info.hpp ├── module.cc ├── patch │ └── quickjs │ │ ├── node_module_loader.c │ │ ├── node_module_loader.h │ │ └── node_module_logger.cc ├── qjs_component.hpp └── types │ ├── environment.cc │ ├── environment.h │ ├── js_wrapper.h │ ├── qjs_candidate.h │ ├── qjs_candidate_iterator.h │ ├── qjs_commit_history.h │ ├── qjs_commit_record.h │ ├── qjs_config.h │ ├── qjs_config_item.h │ ├── qjs_config_list.h │ ├── qjs_config_map.h │ ├── qjs_config_value.h │ ├── qjs_context.h │ ├── qjs_engine.h │ ├── qjs_environment.h │ ├── qjs_key_event.h │ ├── qjs_leveldb.h │ ├── qjs_notifier.h │ ├── qjs_notifier_connection.h │ ├── qjs_os_info.h │ ├── qjs_preedit.h │ ├── qjs_schema.h │ ├── qjs_segment.h │ ├── qjs_trie.h │ └── qjs_types.h ├── tests ├── CMakeLists.txt ├── component.test.cpp ├── dict_data_helper.hpp ├── dictionary.test.cpp ├── fake_translation.hpp ├── filter.test.cpp ├── js │ ├── .prettierrc │ ├── bundle.js │ ├── dist │ │ ├── fast_filter.iife.js │ │ ├── fast_filter.iterator.iife.js │ │ ├── filter_is_applicable.iife.js │ │ ├── filter_test.esm.js │ │ ├── filter_test.iife.js │ │ ├── help_menu.esm.js │ │ ├── help_menu.iife.js │ │ ├── lib.esm.js │ │ ├── lib.iife.js │ │ ├── main.esm.js │ │ ├── node-modules.test.esm.js │ │ ├── node-modules.test.iife.js │ │ ├── processor_test.esm.js │ │ ├── processor_test.iife.js │ │ ├── runtime-error.esm.js │ │ ├── search.filter.esm.js │ │ ├── search.filter.iife.js │ │ ├── sort_by_pinyin.esm.js │ │ ├── sort_by_pinyin.iife.js │ │ ├── testutils.esm.js │ │ ├── translator_no_return.esm.js │ │ ├── translator_no_return.iife.js │ │ ├── translator_test.esm.js │ │ ├── translator_test.iife.js │ │ ├── types_test.esm.js │ │ └── types_test.iife.js │ ├── fast_filter.iterator.js │ ├── fast_filter.js │ ├── filter_is_applicable.js │ ├── filter_test.js │ ├── help_menu.js │ ├── lib.js │ ├── lib │ │ ├── string.js │ │ └── trie.js │ ├── main.js │ ├── node-modules.test.js │ ├── package-lock.json │ ├── package.json │ ├── processor_test.js │ ├── runtime-error.js │ ├── search.filter.js │ ├── sort_by_pinyin.js │ ├── testutils.js │ ├── translator_no_return.js │ ├── translator_test.js │ └── types_test.js ├── jsc │ ├── expose-class-to-js.test.cpp │ └── load-bundled-plugin.test.cpp ├── notifier.test.cpp ├── processor.test.cpp ├── qjs │ ├── expose-cpp-class.test.cpp │ ├── import-js-module.test.cpp │ ├── qjs_iterator.hpp │ ├── report-js-error.test.cpp │ └── vector-iterator.test.cpp ├── rime-qjs-test-main.test.cpp ├── test_helper.hpp ├── test_switch.h ├── translation.test.cpp ├── translator.test.cpp └── types.test.cpp └── tools ├── check-api-coverage.js ├── clang-format.sh ├── clang-tidy.sh ├── expand-macro.sh ├── package.json ├── update-version.bat └── update-version.sh /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: '-*, 3 | bugprone-*, 4 | clang-analyzer-*, 5 | cppcoreguidelines-*, 6 | modernize-*, 7 | performance-*, 8 | portability-*, 9 | readability-*, 10 | readability-magic-numbers, 11 | -fuchsia-*, 12 | -google-*, 13 | -zircon-*, 14 | -abseil-*, 15 | -llvm-*, 16 | -modernize-use-trailing-return-type, 17 | -modernize-avoid-c-arrays, 18 | -readability-identifier-length, 19 | -cppcoreguidelines-pro-bounds-pointer-arithmetic, 20 | -cppcoreguidelines-pro-type-vararg, 21 | -cppcoreguidelines-pro-type-reinterpret-cast, 22 | -cppcoreguidelines-owning-memory, 23 | -cppcoreguidelines-avoid-c-arrays, 24 | -bugprone-easily-swappable-parameters, 25 | -bugprone-exception-escape' 26 | 27 | WarningsAsErrors: '' 28 | 29 | FormatStyle: file 30 | 31 | # Ignore system headers completely 32 | SystemHeaders: true 33 | 34 | # Add this line to ignore external headers 35 | InheritParentConfig: false 36 | 37 | CheckOptions: 38 | readability-identifier-naming.ClassCase: CamelCase 39 | readability-identifier-naming.ConstexprVariableCase: UPPER_CASE 40 | readability-identifier-naming.EnumCase: CamelCase 41 | readability-identifier-naming.FunctionCase: camelBack 42 | readability-identifier-naming.GlobalConstantCase: UPPER_CASE 43 | readability-identifier-naming.NamespaceCase: lower_case 44 | readability-identifier-naming.StructCase: CamelCase 45 | readability-identifier-naming.VariableCase: camelBack 46 | readability-identifier-naming.ParameterCase: camelBack 47 | readability-identifier-naming.PrivateMemberCase: camelBack 48 | readability-identifier-naming.PrivateMemberSuffix: '_' 49 | readability-identifier-naming.ProtectedMemberCase: camelBack 50 | readability-identifier-naming.ProtectedMemberSuffix: '_' 51 | cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor: true 52 | modernize-use-nullptr.NullMacros: 'NULL,nullptr' 53 | readability-magic-numbers.IgnoredIntegerValues: '0,1,2,3,4,5,6' 54 | readability-magic-numbers.IgnoredFloatingPointValues: '0.0,1.0,100.0' 55 | readability-magic-numbers.IgnorePowersOf2IntegerValues: true 56 | -------------------------------------------------------------------------------- /.clangd: -------------------------------------------------------------------------------- 1 | # documentation: https://clangd.llvm.org/config 2 | # Installation: `sudo port install clang-20` 3 | # VSCode Extension: https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.vscode-clangd 4 | 5 | CompileFlags: 6 | Add: 7 | # Add compiler flags 8 | - "-Wall" 9 | - "-Wextra" 10 | - "-Werror" 11 | # Add macro definitions 12 | - "-DGLOG_EXPORT=__attribute__((visibility(\"default\")))" 13 | - "-DGLOG_NO_EXPORT=__attribute__((visibility(\"default\")))" 14 | - "-DGLOG_DEPRECATED=__attribute__((deprecated))" 15 | # Add include paths 16 | - "-Isrc/**" 17 | - "-Itests/**" 18 | - "-Ithirdparty/quickjs/**" 19 | - "-Ithirdparty/yas/include" 20 | - "-I../../src" 21 | - "-I../../src/rime" 22 | - "-I../../build/src" 23 | - "-I../../include" 24 | - "-I../../include/**" 25 | - "-I../../include/glog" 26 | - "-I../../deps/boost-1.84.0" 27 | - "-I../../build/_deps/googlebenchmark-src/include" 28 | - "-I/usr/local/include" 29 | 30 | Diagnostics: 31 | # Suppress diagnostics from headers 32 | Suppress: 33 | - header-included-from-* 34 | - unused-parameter # unused parameter when overriding a virtual function from libRime 35 | # - unknown-type-name 36 | # - unknown-pragma 37 | # - missing-includes 38 | # - unused-includes 39 | 40 | # Disable specific diagnostic categories 41 | # UnusedIncludes: None # Disable unused include warnings 42 | # MissingIncludes: None # Disable missing include errors 43 | 44 | Index: 45 | Background: Build # Enable background indexing, Build or Skip 46 | StandardLibrary: false # Don't index standard library headers 47 | 48 | InlayHints: 49 | BlockEnd: Yes 50 | Designators: No 51 | Enabled: Yes 52 | ParameterNames: No 53 | DeducedTypes: Yes 54 | # DefaultArguments: Yes # aviable in Clangd-20 55 | TypeNameLimit: 0 56 | 57 | Hover: 58 | ShowAKA: Yes 59 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### configure the project to use this custom hooks directory 4 | # git config core.hooksPath .githooks 5 | 6 | # Get list of staged files 7 | files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(cpp|cc|h|hpp)$') 8 | 9 | if [ -n "$files" ]; then 10 | # Format all staged files 11 | echo "Formatting staged files..." 12 | for file in $files; do 13 | clang-format -i "$file" 14 | git add "$file" 15 | done 16 | fi 17 | 18 | exit 0 19 | -------------------------------------------------------------------------------- /.github/workflows/build-ci.yml: -------------------------------------------------------------------------------- 1 | name: Build CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | macos: 7 | uses: ./.github/workflows/macos-build.yml 8 | with: 9 | repository: HuangJian/librime 10 | rime_plugins: ${{ github.repository }}@${{ github.ref_name }} 11 | 12 | windows: 13 | uses: ./.github/workflows/windows-build.yml 14 | with: 15 | repository: HuangJian/librime 16 | ref: ci-build-librime-qjs # The branch, tag or SHA to checkout 17 | rime_plugins: ${{ github.repository }}@${{ github.ref_name }} 18 | 19 | linux: 20 | uses: ./.github/workflows/linux-build.yml 21 | with: 22 | repository: HuangJian/librime 23 | ref: ci-build-librime-qjs # The branch, tag or SHA to checkout 24 | rime_plugins: ${{ github.repository }}@${{ github.ref_name }} 25 | -------------------------------------------------------------------------------- /.github/workflows/commit-ci.yml: -------------------------------------------------------------------------------- 1 | name: Development CI (linters) 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' # This will match all branches 7 | - '!main' # Then exclude main 8 | tags: 9 | - '![0-9]+.*' 10 | pull_request: 11 | 12 | 13 | jobs: 14 | lint: 15 | runs-on: macos-14 16 | steps: 17 | 18 | - name: Checkout last commit 19 | uses: actions/checkout@v4 20 | 21 | - name: Checkout submodules/dependencies 22 | run: bash ./action-install.sh 23 | 24 | - name: Configure build environment 25 | run: brew update && brew install cmake llvm clang-format nodejs 26 | 27 | - name: Code format lint 28 | run: bash ./tools/clang-format.sh lint 29 | 30 | - name: Code style lint on modified files 31 | run: bash ./tools/clang-tidy.sh modified 32 | 33 | - name: Check rime.d.ts 34 | run: (cd ./tools; node ./check-api-coverage.js) 35 | -------------------------------------------------------------------------------- /.github/workflows/linux-build.yml: -------------------------------------------------------------------------------- 1 | name: Linux build 2 | on: 3 | workflow_call: 4 | inputs: 5 | repository: 6 | default: ${{ github.repository }} 7 | required: false 8 | type: string 9 | ref: 10 | default: ${{ github.ref_name }} 11 | required: false 12 | type: string 13 | rime_plugins: 14 | required: false 15 | type: string 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-24.04 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | compiler: [gcc, clang-19] 24 | include: 25 | - compiler: gcc 26 | cxx_compiler: g++ 27 | - compiler: clang-19 28 | cxx_compiler: clang++-19 29 | env: 30 | CC: ${{ matrix.compiler }} 31 | CXX: ${{ matrix.cxx_compiler }} 32 | RIME_PLUGINS: ${{ inputs.rime_plugins }} 33 | steps: 34 | # - name: Configure tmate scrolling 35 | # # in copy mode (press Ctrl-b [ then use arrow keys or Page Up/Page Down) 36 | # run: | 37 | # mkdir -p ~/.tmate 38 | # echo "set -g history-limit 50000" > ~/.tmate.conf 39 | # echo "set -g terminal-overrides 'xterm*:smcup@:rmcup@'" >> ~/.tmate.conf 40 | # - name: Setup tmate session 41 | # if: matrix.compiler == 'clang-19' 42 | # uses: mxschmitt/action-tmate@v3 43 | # with: 44 | # limit-access-to-actor: true 45 | # timeout-minutes: 360 46 | # detached: true 47 | 48 | - name: Checkout last commit 49 | uses: actions/checkout@v4 50 | with: 51 | # Repository to check out (format: owner/repo) 52 | repository: ${{ inputs.repository }} # optional, defaults to current repository 53 | # The branch, tag or SHA to checkout 54 | ref: ${{ inputs.ref }} # optional, defaults to the branch triggering the workflow 55 | 56 | - name: Install dependency 57 | run: ./action-install-linux.sh 58 | 59 | - name: Install Rime plugins 60 | run: ./action-install-plugins-linux.sh 61 | 62 | - name: Install clang-19 63 | if: matrix.compiler == 'clang-19' 64 | run: | 65 | sudo apt-get update 66 | sudo apt-get install -y clang-19 67 | 68 | - name: Build and test 69 | run: make test 70 | env: 71 | CMAKE_GENERATOR: Ninja 72 | 73 | - name: Detect Memory Leak 74 | if: matrix.compiler == 'clang-19' 75 | run: | 76 | make clean 77 | make test-debug CFLAGS="-g -fsanitize=address" LDFLAGS="-fsanitize=address" 78 | ASAN_OPTIONS=detect_leaks=1 ./plugins/qjs/build/librime-qjs-tests 79 | env: 80 | CMAKE_GENERATOR: Ninja 81 | -------------------------------------------------------------------------------- /.github/workflows/macos-build.yml: -------------------------------------------------------------------------------- 1 | name: macOS build 2 | on: 3 | workflow_call: 4 | inputs: 5 | repository: 6 | default: HuangJian/librime 7 | required: false 8 | type: string 9 | rime_plugins: 10 | default: ${{ github.repository }}@${{ github.ref_name }} 11 | required: false 12 | type: string 13 | build_type: 14 | default: Nightly 15 | required: false 16 | type: string 17 | jobs: 18 | build: 19 | runs-on: ${{ matrix.runs-on }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | runs-on: [macos-13, macos-14] 24 | include: 25 | - runs-on: macos-13 26 | create-distributable: false 27 | - runs-on: macos-14 28 | create-distributable: true 29 | build_variant: universal 30 | env: 31 | boost_version: 1.84.0 32 | BOOST_ROOT: ${{ github.workspace }}/deps/boost-1.84.0 33 | RIME_PLUGINS: ${{ inputs.rime_plugins }} 34 | steps: 35 | - name: Checkout last commit 36 | uses: actions/checkout@v4 37 | with: 38 | repository: ${{ inputs.repository }} 39 | submodules: recursive 40 | 41 | - name: Configure build environment 42 | run: | 43 | brew install llvm ninja 44 | echo "CMAKE_GENERATOR=Ninja" >> $GITHUB_ENV 45 | echo "/usr/local/opt/llvm/bin" >> $GITHUB_PATH 46 | echo git_ref_name="$(git describe --always)" >> $GITHUB_ENV 47 | 48 | - name: Configure build variant 49 | if: matrix.build_variant == 'universal' 50 | run: | 51 | echo BUILD_UNIVERSAL=1 >> $GITHUB_ENV 52 | 53 | - name: Install Boost 54 | run: ./install-boost.sh 55 | 56 | - name: Check submodules 57 | run: git submodule > submodule-status 58 | 59 | - name: Cache dependencies 60 | id: cache-deps 61 | uses: actions/cache@v4 62 | with: 63 | path: | 64 | bin 65 | include 66 | lib 67 | share 68 | key: ${{ runner.os }}-${{ matrix.build_variant || runner.arch }}-deps-${{ hashFiles('submodule-status') }} 69 | 70 | - name: Build dependencies 71 | if: steps.cache-deps.outputs.cache-hit != 'true' 72 | run: make deps 73 | 74 | - name: Install Rime plugins 75 | run: ./action-install-plugins-macos.sh 76 | 77 | - name: Get librime-qjs commit hash 78 | run: | 79 | cd ./plugins/qjs 80 | echo qjs_git_ref_name="$(git describe --always)" >> $GITHUB_ENV 81 | 82 | - name: Build and test 83 | run: make test 84 | 85 | - name: Create distributable 86 | if: matrix.create-distributable 87 | run: | 88 | make install 89 | mkdir assets 90 | cp -r ./version-info.txt ./assets 91 | cp -r ./dist/lib/** ./assets 92 | cp -r ./plugins/qjs/build/qjs ./assets 93 | cd ./plugins/qjs 94 | bash ./tools/update-version.sh ${{ inputs.build_type }} 95 | cd ../.. 96 | cp -r ./plugins/qjs/contrib/rime.d.ts ./assets 97 | tar -cjvf librime-qjs-${{ env.qjs_git_ref_name }}-${{ runner.os }}-${{ matrix.build_variant || runner.arch }}.tar.bz2 -C ./assets . 98 | 99 | - name: Upload artifacts 100 | if: matrix.create-distributable 101 | uses: actions/upload-artifact@v4 102 | with: 103 | name: artifact-${{ runner.os }}-${{ matrix.build_variant || runner.arch }} 104 | path: | 105 | librime-qjs-${{ env.qjs_git_ref_name }}-${{ runner.os }}-${{ matrix.build_variant || runner.arch }}.tar.bz2 106 | -------------------------------------------------------------------------------- /.github/workflows/release-ci.yml: -------------------------------------------------------------------------------- 1 | name: Release CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '[0-9]+.*' 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: write # Allow workflow to create and manage releases 13 | 14 | jobs: 15 | macos: 16 | uses: ./.github/workflows/macos-build.yml 17 | with: 18 | repository: HuangJian/librime 19 | rime_plugins: ${{ github.repository }}@${{ github.ref_name }} 20 | build_type: "${{ github.ref == 'refs/heads/main' && 'Nightly' || 'Release' }}" 21 | 22 | windows: 23 | uses: ./.github/workflows/windows-build.yml 24 | with: 25 | repository: HuangJian/librime 26 | ref: ci-build-librime-qjs 27 | rime_plugins: ${{ github.repository }}@${{ github.ref_name }} hchunhui/librime-lua lotem/librime-octagram rime/librime-predict 28 | build_type: "${{ github.ref == 'refs/heads/main' && 'Nightly' || 'Release' }}" 29 | 30 | linux: 31 | uses: HuangJian/librime/.github/workflows/linux-build.yml@ci-build-librime-qjs 32 | with: 33 | repository: HuangJian/librime 34 | ref: ci-build-librime-qjs 35 | rime_plugins: ${{ github.repository }}@${{ github.ref_name }} 36 | 37 | release: 38 | needs: [macos, windows] 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Download artifacts 42 | uses: actions/download-artifact@v4 43 | with: 44 | path: artifact 45 | merge-multiple: true 46 | 47 | - name: Create Nightly release 48 | if: ${{ github.repository == 'HuangJian/librime-qjs' && github.ref == 'refs/heads/main' }} 49 | uses: 'marvinpinto/action-automatic-releases@latest' 50 | with: 51 | repo_token: ${{ secrets.GITHUB_TOKEN }} 52 | automatic_release_tag: latest 53 | prerelease: true 54 | title: "librime-qjs Nightly Build" 55 | files: | 56 | artifact/* 57 | 58 | - name: Create Stable release 59 | if: ${{ github.ref != 'refs/heads/main' }} 60 | uses: 'marvinpinto/action-automatic-releases@latest' 61 | with: 62 | repo_token: ${{ secrets.GITHUB_TOKEN }} 63 | draft: true 64 | prerelease: ${{ contains(github.ref_name, '-') }} 65 | title: librime-qjs ${{ github.ref_name }} 66 | files: | 67 | artifact/* 68 | -------------------------------------------------------------------------------- /.github/workflows/windows-build.yml: -------------------------------------------------------------------------------- 1 | name: Windows build 2 | on: 3 | workflow_call: 4 | inputs: 5 | repository: 6 | default: HuangJian/librime 7 | required: false 8 | type: string 9 | ref: 10 | default: master 11 | required: false 12 | type: string 13 | rime_plugins: 14 | default: ${{ github.repository }}@${{ github.ref_name }} 15 | required: false 16 | type: string 17 | build_type: 18 | default: Nightly 19 | required: false 20 | type: string 21 | 22 | jobs: 23 | build: 24 | runs-on: windows-latest 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | compiler: [clang] 29 | arch: [x64] 30 | include: 31 | - { compiler: clang, cc: clang, cxx: clang++ } 32 | env: 33 | boost_version: 1.84.0 34 | BOOST_ROOT: ${{ github.workspace }}\deps\boost-1.84.0 35 | RIME_PLUGINS: ${{ inputs.rime_plugins }} 36 | 37 | steps: 38 | # - name: Setup tmate session 39 | # uses: mxschmitt/action-tmate@v3 40 | # with: 41 | # limit-access-to-actor: true 42 | # timeout-minutes: 360 43 | # detached: true 44 | 45 | - name: Checkout last commit 46 | uses: actions/checkout@v4 47 | with: 48 | # Repository to check out (format: owner/repo) 49 | repository: ${{ inputs.repository }} # optional, defaults to current repository 50 | # The branch, tag or SHA to checkout 51 | ref: ${{ inputs.ref }} # optional, defaults to the master branch 52 | submodules: recursive 53 | - name: Create env.bat 54 | run: | 55 | $envfile = ".\env.bat" 56 | $envcontent = @" 57 | set RIME_ROOT=%CD% 58 | set CXX=${{ matrix.cxx }} 59 | set CC=${{ matrix.cc }} 60 | set CMAKE_GENERATOR=Ninja 61 | "@ 62 | Set-Content -Path $envfile -Value $envcontent 63 | cat $envfile 64 | 65 | - name: Configure MSVC 66 | uses: ilammy/msvc-dev-cmd@v1 67 | with: 68 | arch: ${{ matrix.cross_arch || matrix.arch }} 69 | 70 | - name: Configure Ninja 71 | run: pip install ninja 72 | 73 | - name: Configure clang 74 | run: choco upgrade -y llvm 75 | 76 | - name: Configure build environment 77 | run: | 78 | $git_ref_name = git describe --always 79 | echo "git_ref_name=$git_ref_name" >> $env:GITHUB_ENV 80 | git submodule > submodule-status 81 | 82 | - name: Install boost 83 | run: .\install-boost.bat 84 | 85 | - name: Cache dependencies 86 | id: cache-deps 87 | uses: actions/cache@v4 88 | with: 89 | path: | 90 | bin 91 | include 92 | lib 93 | share 94 | key: ${{ runner.os }}-${{ matrix.compiler }}-${{ matrix.arch }}-${{ hashFiles('submodule-status') }} 95 | 96 | - name: Build dependencies 97 | if: steps.cache-deps.outputs.cache-hit != 'true' 98 | run: .\build.bat deps 99 | 100 | - name: Install Rime plugins 101 | run: .\action-install-plugins-windows.bat 102 | 103 | - name: Get librime-qjs commit hash 104 | run: | 105 | cd .\plugins\qjs 106 | $qjs_git_ref_name = git describe --always 107 | echo "qjs_git_ref_name=$qjs_git_ref_name" >> $env:GITHUB_ENV 108 | 109 | - name: Build and test 110 | run: | 111 | .\build.bat 112 | .\build.bat test 113 | cp .\dist\lib\rime.dll .\plugins\qjs\build\rime.dll 114 | .\plugins\qjs\build\librime-qjs-tests.exe 115 | 116 | - name: Create distributable 117 | run: | 118 | mkdir assets 119 | xcopy /Y /I .\version-info.txt .\assets\ 120 | xcopy /Y /I /E .\dist\lib\rime.dll .\assets\ 121 | xcopy /Y /I /E .\plugins\qjs\build\qjs.exe .\assets\ 122 | cd .\plugins\qjs 123 | .\tools\update-version.bat ${{ inputs.build_type}} 124 | cd ..\.. 125 | xcopy /Y /I .\plugins\qjs\contrib\rime.d.ts .\assets\ 126 | 7z a librime-qjs-${{ env.qjs_git_ref_name }}-${{ runner.os }}-${{ matrix.compiler }}-${{ matrix.arch }}.7z .\assets\* 127 | 128 | - name: Upload artifacts 129 | uses: actions/upload-artifact@v4 130 | with: 131 | name: artifact-${{ runner.os }}-${{ matrix.compiler }}-${{ matrix.arch }} 132 | path: | 133 | librime-qjs-${{ env.qjs_git_ref_name }}-${{ runner.os }}-${{ matrix.compiler }}-${{ matrix.arch }}.7z 134 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | *.cmake 3 | CMakeCache.txt 4 | CMakeFiles/ 5 | build/ 6 | .cache 7 | 8 | _deps 9 | Testing 10 | 11 | tests/js/qjs 12 | tests/**/*.i 13 | tests/**/*.bin 14 | tests/**/*.trie 15 | tests/**/*.yas 16 | tests/**/node_modules 17 | 18 | 19 | benchmark/**/*.bin 20 | benchmark/**/*.trie 21 | benchmark/**/*.yas 22 | 23 | thirdparty 24 | src/types/others 25 | 26 | compile_commands.json 27 | 28 | .vscode 29 | .editorconfig 30 | .cache 31 | 32 | requirement.md 33 | 34 | # visual studio files 35 | *.vcxproj 36 | *.vcxproj.filters 37 | *.sln 38 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "thirdparty/quickjs"] 2 | path = thirdparty/quickjs 3 | url = https://github.com/quickjs-ng/quickjs.git 4 | fetchRecurseSubmodules = false 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "bracketSpacing": true, 8 | "jsxBracketSameLine": false, 9 | "arrowParens": "always", 10 | "endOfLine": "lf", 11 | "printWidth": 110 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.configureSettings": { 3 | "CMAKE_C_COMPILER": "/usr/local/opt/llvm/bin/clang", 4 | "CMAKE_CXX_COMPILER": "/usr/local/opt/llvm/bin/clang++", 5 | "CMAKE_EXPORT_COMPILE_COMMANDS": true 6 | }, 7 | "cmake.configureOnOpen": true, 8 | "clangd.path": "/opt/local/libexec/llvm-20/bin/clangd", 9 | "clangd.arguments": [ 10 | "--compile-commands-dir=${workspaceFolder}/build", 11 | "--background-index=1", 12 | "--clang-tidy=1", 13 | "--completion-style=detailed", 14 | "--header-insertion=iwyu", 15 | "--all-scopes-completion=1", 16 | "--background-index-priority=low", 17 | "--completion-parse=auto", 18 | "--debug-origin=0", 19 | "--fallback-style=LLVM", 20 | "--function-arg-placeholders=1", 21 | "--header-insertion-decorators=1", 22 | "--import-insertions=0", 23 | "--include-ineligible-results=0", 24 | "--limit-references=1000", 25 | "--limit-results=100", 26 | "--ranking-model=decision_forest", 27 | "--rename-file-limit=50", 28 | "--parse-forwarding-functions=0", 29 | "--pch-storage=disk", 30 | "--use-dirty-headers=0" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, HuangJian 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /action-install.bat: -------------------------------------------------------------------------------- 1 | rem This script installs the dependencies to build the project within libRime 2 | 3 | git submodule update --init --recursive 4 | 5 | choco install nodejs 6 | 7 | cd tests/js 8 | npm install 9 | -------------------------------------------------------------------------------- /action-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script installs the dependencies to build the project within libRime 4 | git submodule update --init --recursive 5 | 6 | if [[ "$OSTYPE" == "darwin"* ]]; then 7 | brew install nodejs 8 | elif [[ "$OSTYPE" == "linux-gnu"* ]] && [ -f /etc/lsb-release ]; then 9 | sudo apt-get update 10 | sudo apt-get install -y nodejs 11 | else 12 | echo "Error: NodeJS is required to run the unit tests." 13 | echo "Please install NodeJS for your operating system in 'action-install.sh'." 14 | exit 1 15 | fi 16 | 17 | 18 | cd tests/js 19 | npm install 20 | -------------------------------------------------------------------------------- /benchmark/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(FETCHCONTENT_QUIET OFF) 2 | include(FetchContent) 3 | 4 | set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../build) 5 | 6 | # Google Benchmark 7 | # https://github.com/ashvardanian/less_slow.cpp/blob/main/CMakeLists.txt 8 | FetchContent_Declare( 9 | GoogleBenchmark 10 | GIT_REPOSITORY https://github.com/google/benchmark.git 11 | GIT_TAG v1.9.1 12 | ) 13 | # Suppress building tests/docs/etc. for faster builds: 14 | set(BENCHMARK_ENABLE_TESTING 15 | OFF 16 | CACHE BOOL "" FORCE 17 | ) 18 | set(BENCHMARK_ENABLE_INSTALL 19 | OFF 20 | CACHE BOOL "" FORCE 21 | ) 22 | set(BENCHMARK_ENABLE_DOXYGEN 23 | OFF 24 | CACHE BOOL "" FORCE 25 | ) 26 | set(BENCHMARK_INSTALL_DOCS 27 | OFF 28 | CACHE BOOL "" FORCE 29 | ) 30 | set(BENCHMARK_DOWNLOAD_DEPENDENCIES 31 | ON 32 | CACHE BOOL "" FORCE 33 | ) 34 | set(BENCHMARK_ENABLE_GTEST_TESTS 35 | OFF 36 | CACHE BOOL "" FORCE 37 | ) 38 | set(BENCHMARK_USE_BUNDLED_GTEST 39 | ON 40 | CACHE BOOL "" FORCE 41 | ) 42 | 43 | if (CMAKE_SYSTEM_NAME STREQUAL "Linux") 44 | set(BENCHMARK_ENABLE_LIBPFM 45 | OFF 46 | CACHE BOOL "" FORCE 47 | ) 48 | endif () 49 | 50 | FetchContent_MakeAvailable(GoogleBenchmark) 51 | include_directories(${GoogleBenchmark}/Headers) 52 | 53 | # Remove Google Benchmark's built-in debug warning in Release mode: 54 | if (CMAKE_BUILD_TYPE STREQUAL "Release") 55 | target_compile_definitions(benchmark PRIVATE NDEBUG) 56 | endif () 57 | 58 | # aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/dict SRC_DICT) 59 | set(SRC_DICT "dict/dictionary_benchmark.cc") 60 | 61 | add_executable(qjs-benchmark ${SRC_DICT}) 62 | target_link_libraries(qjs-benchmark 63 | librime-qjs-objs 64 | ${rime_library} 65 | ${rime_dict_library} 66 | ${rime_gears_library} 67 | benchmark 68 | ) 69 | -------------------------------------------------------------------------------- /benchmark/dict/trie_ext.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | // mmap headers 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "dicts/trie.h" 17 | 18 | class TrieWithStringExt : public rime::Trie { 19 | public: 20 | void saveToFiles(const std::string& dataPath) { 21 | const auto triePath = dataPath + ".trie"; 22 | getTrie().save(triePath.c_str()); 23 | 24 | // Save associated string data 25 | std::ofstream dataFile(dataPath, std::ios::binary); 26 | if (!dataFile) { 27 | throw std::runtime_error("Failed to open data file for writing"); 28 | } 29 | 30 | size_t size = getData().size(); 31 | dataFile.write(reinterpret_cast(&size), sizeof(size)); 32 | 33 | // Write each string with its length 34 | for (const auto& str : getData()) { 35 | size_t strLen = str.length(); 36 | dataFile.write(reinterpret_cast(&strLen), sizeof(strLen)); 37 | dataFile.write(str.data(), static_cast(strLen)); 38 | } 39 | } 40 | 41 | void loadFromFiles(const std::string& dataPath) { 42 | // Load trie data 43 | std::string triePath = dataPath + ".trie"; 44 | getTrie().load(triePath.c_str()); 45 | 46 | auto& data = getData(); 47 | 48 | // Load associated string data 49 | std::ifstream dataFile(dataPath, std::ios::binary); 50 | if (!dataFile) { 51 | throw std::runtime_error("Failed to open data file for reading"); 52 | } 53 | 54 | size_t size = 0; 55 | dataFile.read(reinterpret_cast(&size), sizeof(size)); 56 | data.resize(size); 57 | 58 | // Read each string 59 | for (size_t i = 0; i < size; ++i) { 60 | size_t strLen = 0; 61 | dataFile.read(reinterpret_cast(&strLen), sizeof(strLen)); 62 | 63 | std::string str(strLen, '\0'); 64 | dataFile.read(str.data(), static_cast(strLen)); 65 | data[i] = std::move(str); 66 | } 67 | 68 | if (dataFile.fail()) { 69 | throw std::runtime_error("Failed to read data from file"); 70 | } 71 | } 72 | 73 | void loadFromSingleFile(const std::string& filePath) { 74 | std::ifstream file(filePath, std::ios::binary); 75 | if (!file) { 76 | throw std::runtime_error("Failed to open file for reading"); 77 | } 78 | 79 | IOUtil::readVectorData(file, getData()); 80 | 81 | // Read and load trie 82 | size_t trieSize = 0; 83 | file.read(reinterpret_cast(&trieSize), sizeof(trieSize)); 84 | 85 | ScopedTempFile tempFile{filePath}; 86 | { 87 | std::ofstream trieFile(tempFile.path(), std::ios::binary); 88 | std::vector buffer(trieSize); 89 | file.read(buffer.data(), static_cast(trieSize)); 90 | trieFile.write(buffer.data(), static_cast(trieSize)); 91 | } 92 | 93 | getTrie().load(tempFile.path().c_str()); 94 | 95 | if (file.fail()) { 96 | throw std::runtime_error("Failed to read data from file"); 97 | } 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /doc/build-linux.md: -------------------------------------------------------------------------------- 1 | # Building librime-qjs on Linux 2 | 3 | ## Prerequisites 4 | 5 | ### System Requirements 6 | 7 | - Arch Linux 8 | - Other Linux distributions may work with appropriate package adaptations 9 | 10 | ## Environment Setup 11 | 12 | ### Setting up the build environment 13 | 14 | ```shell 15 | sudo pacman -S base-devel boost-libs capnproto gcc-libs glibc google-glog leveldb librime-data lua marisa opencc yaml-cpp boost cmake git gtest ninja nodejs npm 16 | paru -S quickjs-ng 17 | ``` 18 | 19 | ## Build Steps 20 | 21 | ### Getting the Source Code 22 | 23 | - Clone the [librime](https://github.com/rime/librime) repository: 24 | 25 | ```shell 26 | cd 27 | git clone https://github.com/rime/librime.git 28 | ``` 29 | 30 | - Clone the [librime-qjs](https://github.com/HuangJian/librime-qjs) repository: 31 | 32 | ```shell 33 | cd librime/plugins 34 | ## quickjs-ng is cloned as a submodule to be patched to load node modules. 35 | git clone --recursive https://github.com/HuangJian/librime-qjs.git qjs 36 | ``` 37 | 38 | ### Building the Project 39 | 40 | - Build librime and librime-qjs: `make` or `make debug` 41 | 42 | ### Running Tests 43 | 44 | - Install the node modules: `(cd plugins/qjs/tests/js; npm install)` 45 | - Run all unit tests: `make test` or `make test-debug` 46 | - Run only the librime-qjs tests: `(cd plugins/qjs; ctest)` 47 | 48 | ## Installation 49 | 50 | ### Installing the Plugin 51 | 52 | ```shell 53 | sudo mkdir -p /usr/local/lib/rime-plugins/ 54 | sudo ln -s "$(pwd)/build/lib/rime-plugins/librime-qjs.so" /usr/local/lib/rime-plugins/ 55 | ``` 56 | -------------------------------------------------------------------------------- /doc/readme.en.md: -------------------------------------------------------------------------------- 1 | English | [中文](../readme.md) | [Plugin Development Guide](./plugin-dev.en.md) | [插件开发指南](./plugin-dev.cn.md) 2 | 3 | # librime-qjs 4 | 5 | Experience a vast JavaScript plugin ecosystem for the Rime Input Method Engine, delivering lightning-fast speed and feather-light performance for a revolutionary input experience! 6 | 7 | ## Features 8 | 9 | - 🔌 Powerful JavaScript plugin ecosystem for the [Rime Input Method Engine](https://github.com/rime/librime). 10 | - 🎮 Unleash the full potential of JavaScript with all essential Rime engine features at your fingertips. 11 | - ✨ Tired of writing code and debugging? The NPM repository has everything you need, all in one place. 12 | - 👀 See our capabilities in action! All Lua plugins from [Rime Frost](https://github.com/gaboolic/rime-frost) have been perfectly rewritten in [JavaScript](https://github.com/HuangJian/rime-frost/tree/hj/js). 13 | - 📝 Smooth plugin development with comprehensive [JavaScript type definitions](./contrib/rime.d.ts). 14 | - 🔄 Simple and flexible [type binding templates](./src/helpers/qjs_macros.h) for seamless JavaScript and Rime engine integration. 15 | - 🚀 Lightweight JavaScript engine powered by [QuickJS-NG](https://github.com/quickjs-ng/quickjs). 16 | - 💪 Enjoy the latest ECMAScript features: regular expressions, Unicode, ESM, big numbers, and more! 17 | - 🚄 Blazing-fast performance: all plugins respond within milliseconds. 18 | - 🪶 Incredibly small memory footprint: <20MB! 19 | - 🍎 Exponential speed improvements delivered by JavaScriptCore on macOS/iOS! 20 | - 📚 Custom-built Trie structure for large dictionaries. 21 | - 💥 Lightning-fast dictionary loading: 110,000-entry [Chinese-English dictionary](https://www.mdbg.net/chinese/dictionary?page=cc-cedict) loads in just 20ms after binary conversion. 22 | - 🎯 Swift exact lookups: finding English definitions for 200 Chinese words in under 5ms. 23 | - 🌪️ Rapid prefix search: searching English words with Chinese translations in a 60,000-entry [English-Chinese dictionary](https://github.com/skywind3000/ECDICT) takes only 1-3ms. 24 | - 🗡️ Share JavaScript plugins across all Rime sessions for seamless transitions. 25 | - 🎉 No more lag when switching input methods with large plugins - solved once and for all! 26 | - 🚀 Ready for immersive writing across different applications! 27 | - 🛡️ Comprehensive testing with both C++ and JavaScript. 28 | - ✅ Every Rime API thoroughly tested with [C++ tests](./tests/). 29 | - 🧪 JavaScript plugins? Test freely with qjs/nodejs/bun/deno using our [test suite](https://github.com/HuangJian/rime-frost/tree/hj/js/tests). 30 | 31 | ## Documentation 32 | - [JavaScript Plugin Development Guide@quickjs](./doc/plugin-dev.en.md) 33 | - [JavaScript Plugin Development Guide@javascriptcore](./doc/plugin-dev-with-jsc.md) 34 | - [libRime-qjs Development Guide@macOS](./doc/build-macos.md) 35 | - [libRime-qjs Development Guide@Windows](./doc/build-windows.md) 36 | - [libRime-qjs Development Guide@Linux](./doc/build-linux.md) 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [English](./doc/readme.en.md) | 中文 | [Plugin Development Guide](./doc/plugin-dev.en.md) | [插件开发指南](./doc/plugin-dev.cn.md) 2 | 3 | # librime-qjs 4 | 5 | 为 Rime 输入法引擎带来浩瀚的 JavaScript 插件生态,以闪电般的速度和羽毛般的轻盈,让输入体验焕然一新! 6 | 7 | ## 功能特性 8 | 9 | - 🔌 为 [Rime 输入法引擎](https://github.com/rime/librime) 带来强大的 JavaScript 插件生态。 10 | - 🎮 让 JavaScript 尽情发挥,所有 Rime 引擎的精华功能都已为您精心备妥。 11 | - ✨ 还在为编写代码调试程序绞尽脑汁? NPM 仓库群星荟萃,应有尽有。 12 | - 👀 看看我们的实力![白霜拼音](https://github.com/gaboolic/rime-frost) 的所有 Lua 插件都已完美重写为 [JavaScript 版本](https://github.com/HuangJian/rime-frost/tree/hj/js)。 13 | - 📝 贴心提供 [JavaScript 类型定义](./contrib/rime.d.ts),让插件开发体验如丝般顺滑。 14 | - 🔄 简单灵活的[类型绑定模板](./src/engines/js_macros.h),让 JavaScript 和 Rime 引擎完美配合。 15 | - 🚀 基于 [QuickJS-NG](https://github.com/quickjs-ng/quickjs) 打造的轻量级 JavaScript 引擎。 16 | - 💪 畅享最新 ECMAScript 特性:正则表达式、Unicode、ESM、big number,应有尽有! 17 | - 🚄 运行速度快得惊人:所有插件均毫秒级响应。 18 | - 🪶 内存占用小得不可思议:<20MB! 19 | - 🍎 macOS/iOS 尊享极致性能:脚本引擎升级为 JavaScriptCore,速度翻番再翻番! 20 | - 📚 为大型词典量身打造的 LevelDB / Trie 结构。 21 | - 💥 词典加载快如闪电:11 万词条的[汉译英词典](https://www.mdbg.net/chinese/dictionary?page=cc-cedict),转为二进制格式后,加载完成仅需 1ms。 22 | - 🎯 精确查找速如箭矢:11 万词条的汉译英词典,精确查找 200 个汉语词语的英文释义不到 5ms。 23 | - 🌪️ 前缀搜索迅如疾风:6 万词条的[英译汉词典](https://github.com/skywind3000/ECDICT),搜索前缀匹配的英文单词及其汉语翻译仅需 1~3ms。 24 | - 🗡️ 所有 JavaScript 插件一次加载到处可用,让输入法会话切换轻松洒脱。 25 | - 🎉 切换输入法时加载大型插件卡顿严重?现在我们一劳永逸! 26 | - 🚀 在不同应用间沉浸式写作?插件早已准备就绪! 27 | - 🛡️ 双剑合璧:C++ 和 JavaScript 的单元测试。 28 | - ✅ 每个 Rime API 都经过严格的 [C++ 测试](./tests/)。 29 | - 🧪 JavaScript 插件?随心所欲地用 qjs/nodejs/bun/deno [执行测试](https://github.com/HuangJian/rime-frost/tree/hj/js/tests)。 30 | 31 | ## 文档 32 | - [JavaScript 插件开发指南@quickjs](./doc/plugin-dev.cn.md) 33 | - [JavaScript 插件开发指南@javascriptcore](./doc/plugin-dev-with-jsc.md) 34 | - [libRime-qjs 开发说明@macOS](./doc/build-macos.md) 35 | - [libRime-qjs 开发说明@Windows](./doc/build-windows.md) 36 | - [libRime-qjs 开发说明@Linux](./doc/build-linux.md) 37 | -------------------------------------------------------------------------------- /src/dicts/dictionary.cc: -------------------------------------------------------------------------------- 1 | #include "dictionary.h" 2 | 3 | #include 4 | #include 5 | 6 | std::unordered_map Dictionary::parseTextFile( 7 | const std::string& path, 8 | const ParseTextFileOptions& options) { 9 | std::unordered_map ret(options.lines); 10 | std::ifstream infile(path); 11 | std::string line; 12 | while (std::getline(infile, line)) { 13 | if (!line.empty() && line.find(options.comment) == 0) { 14 | continue; 15 | } 16 | 17 | if (!options.charsToRemove.empty()) { 18 | removeChars(line, options.charsToRemove); 19 | } 20 | 21 | size_t tabPos = line.find(options.delimiter); 22 | if (tabPos == std::string::npos) { 23 | continue; 24 | } 25 | 26 | std::string key = line.substr(0, tabPos); 27 | std::string value = line.substr(tabPos + 1); 28 | 29 | if (options.isReversed) { 30 | std::swap(key, value); 31 | } 32 | 33 | if (options.onDuplicatedKey != OnDuplicatedKey::Overwrite) { 34 | auto it = ret.find(key); 35 | if (it != ret.end()) { 36 | if (options.onDuplicatedKey == OnDuplicatedKey::Skip) { 37 | continue; 38 | } 39 | if (options.onDuplicatedKey == OnDuplicatedKey::Concat) { 40 | value = ret[key].append(options.concatSeparator).append(value); 41 | } 42 | } 43 | } 44 | 45 | ret[key] = value; 46 | } 47 | return ret; 48 | } 49 | 50 | std::vector Dictionary::split(const std::string& str, const std::string& delimiters) { 51 | std::vector tokens; 52 | size_t start = 0; 53 | size_t end = 0; 54 | 55 | while ((end = str.find_first_of(delimiters, start)) != std::string::npos) { 56 | if (end != start) { // Avoid empty tokens 57 | tokens.push_back(str.substr(start, end - start)); 58 | } 59 | start = end + 1; 60 | } 61 | 62 | if (start < str.length()) { 63 | tokens.push_back(str.substr(start)); 64 | } 65 | 66 | return tokens; 67 | } 68 | 69 | void Dictionary::removeChars(std::string& str, const std::string& charsToRemove) { 70 | str.erase(std::remove_if( 71 | str.begin(), str.end(), 72 | [&charsToRemove](char c) { return charsToRemove.find(c) != std::string::npos; }), 73 | str.end()); 74 | } 75 | -------------------------------------------------------------------------------- /src/dicts/dictionary.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | constexpr const char* MAGIC_KEY_TO_STORE_CONCAT_SEPARATOR = "MAGIC_KEY_TO_STORE_CONCAT_SEPARATOR"; 10 | 11 | enum class OnDuplicatedKey : std::uint8_t { 12 | Overwrite, 13 | Skip, 14 | Concat, 15 | }; 16 | 17 | struct ParseTextFileOptions { 18 | std::string delimiter = "\t"; 19 | std::string comment = "#"; 20 | size_t lines = 0; 21 | bool isReversed = false; 22 | std::string charsToRemove = "\r"; 23 | OnDuplicatedKey onDuplicatedKey = OnDuplicatedKey::Overwrite; 24 | std::string concatSeparator = "$|$"; 25 | }; 26 | 27 | class Dictionary { 28 | public: 29 | virtual ~Dictionary() = default; 30 | 31 | virtual void loadTextFile(const std::string& txtPath, const ParseTextFileOptions& options) = 0; 32 | virtual void loadBinaryFile(const std::string& filePath) = 0; 33 | virtual void saveToBinaryFile(const std::string& filePath) = 0; 34 | [[nodiscard]] virtual std::optional find(const std::string& key) const = 0; 35 | [[nodiscard]] virtual std::vector> prefixSearch( 36 | const std::string& prefix) const = 0; 37 | 38 | static std::unordered_map parseTextFile( 39 | const std::string& path, 40 | const ParseTextFileOptions& options); 41 | 42 | protected: 43 | static std::vector split(const std::string& str, const std::string& delimiters); 44 | 45 | private: 46 | static void removeChars(std::string& str, const std::string& charsToRemove); 47 | }; 48 | -------------------------------------------------------------------------------- /src/dicts/leveldb.cc: -------------------------------------------------------------------------------- 1 | #include "dicts/leveldb.h" 2 | 3 | #include 4 | #include 5 | 6 | LevelDb::~LevelDb() { 7 | if (ptr_ != nullptr) { 8 | delete ptr_; 9 | ptr_ = nullptr; 10 | } 11 | } 12 | 13 | void LevelDb::close() { 14 | if (ptr_ != nullptr) { 15 | delete ptr_; 16 | ptr_ = nullptr; 17 | } 18 | } 19 | 20 | void LevelDb::loadTextFile(const std::string& txtPath, const ParseTextFileOptions& options) { 21 | txtPath_ = txtPath; 22 | textFileOptions_ = options; 23 | } 24 | 25 | void LevelDb::loadBinaryFile(const std::string& filePath) { 26 | leveldb::Options options; 27 | options.create_if_missing = false; 28 | options.paranoid_checks = false; // Disable expensive checks 29 | options.reuse_logs = true; // Reuse existing log files 30 | leveldb::DB::Open(options, filePath, &ptr_); 31 | 32 | auto optSeparator = find(MAGIC_KEY_TO_STORE_CONCAT_SEPARATOR); 33 | concatSeparator_ = optSeparator.has_value() ? optSeparator.value() : ""; 34 | } 35 | 36 | void LevelDb::saveToBinaryFile(const std::string& filePath) { 37 | if (txtPath_.empty()) { 38 | throw std::runtime_error("No text file loaded."); 39 | } 40 | if (ptr_ != nullptr) { 41 | throw std::runtime_error("LevelDb already loaded."); 42 | } 43 | 44 | std::unordered_map map = parseTextFile(txtPath_, textFileOptions_); 45 | 46 | if (textFileOptions_.onDuplicatedKey == OnDuplicatedKey::Concat) { 47 | map[MAGIC_KEY_TO_STORE_CONCAT_SEPARATOR] = textFileOptions_.concatSeparator; 48 | } 49 | 50 | leveldb::WriteBatch batch; 51 | for (const auto& entry : map) { 52 | batch.Put(entry.first, entry.second); 53 | } 54 | 55 | leveldb::Options options; 56 | options.create_if_missing = true; 57 | options.error_if_exists = false; 58 | leveldb::DB::Open(options, filePath, &ptr_); 59 | ptr_->Write(leveldb::WriteOptions(), &batch); 60 | batch.Clear(); 61 | } 62 | 63 | std::optional LevelDb::find(const std::string& key) const { 64 | if (ptr_ == nullptr) { 65 | throw std::runtime_error("LevelDb not loaded."); 66 | } 67 | std::string value; 68 | auto status = ptr_->Get(leveldb::ReadOptions(), key, &value); 69 | return status.ok() ? std::make_optional(value) : std::nullopt; 70 | } 71 | 72 | std::vector> LevelDb::prefixSearch( 73 | const std::string& prefix) const { 74 | if (ptr_ == nullptr) { 75 | throw std::runtime_error("LevelDb not loaded."); 76 | } 77 | 78 | std::vector> results; 79 | leveldb::Iterator* it = ptr_->NewIterator(leveldb::ReadOptions()); 80 | for (it->Seek(prefix); it->Valid(); it->Next()) { 81 | std::string key = it->key().ToString(); 82 | if (key.find(prefix) != 0) { 83 | break; 84 | } 85 | std::string value = it->value().ToString(); 86 | 87 | if (!concatSeparator_.empty()) { 88 | auto arr = split(value, concatSeparator_); 89 | for (auto& item : arr) { 90 | results.emplace_back(key, item); 91 | } 92 | } else { 93 | results.emplace_back(key, value); 94 | } 95 | } 96 | delete it; 97 | return results; 98 | } 99 | -------------------------------------------------------------------------------- /src/dicts/leveldb.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "dicts/dictionary.h" 9 | 10 | class LevelDb : public Dictionary { 11 | public: 12 | LevelDb() = default; 13 | LevelDb(const LevelDb&) = delete; 14 | LevelDb(LevelDb&&) = delete; 15 | LevelDb& operator=(const LevelDb&) = delete; 16 | LevelDb& operator=(LevelDb&&) = delete; 17 | ~LevelDb() override; 18 | 19 | void close(); 20 | void loadTextFile(const std::string& txtPath, const ParseTextFileOptions& options) override; 21 | void loadBinaryFile(const std::string& filePath) override; 22 | void saveToBinaryFile(const std::string& filePath) override; 23 | [[nodiscard]] std::optional find(const std::string& key) const override; 24 | [[nodiscard]] std::vector> prefixSearch( 25 | const std::string& prefix) const override; 26 | 27 | private: 28 | leveldb::DB* ptr_ = nullptr; 29 | std::string txtPath_; 30 | ParseTextFileOptions textFileOptions_; 31 | 32 | std::string concatSeparator_; 33 | }; 34 | -------------------------------------------------------------------------------- /src/dicts/trie.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "dicts/dictionary.h" 15 | 16 | namespace rime { 17 | 18 | class Trie : public Dictionary { 19 | private: 20 | marisa::Trie trie_; 21 | std::vector data_; 22 | std::string concatSeparator_; 23 | 24 | protected: 25 | marisa::Trie& getTrie() { return trie_; } 26 | std::vector& getData() { return data_; } 27 | 28 | // Enhanced I/O utilities 29 | struct IOUtil { 30 | static void writeSizeAndData(std::ostream& out, const std::string& data) { 31 | const size_t len = data.length(); 32 | out.write(reinterpret_cast(&len), sizeof(len)); 33 | out.write(data.data(), static_cast(len)); 34 | } 35 | 36 | static std::string readSizedString(std::istream& in) { 37 | size_t len = 0; 38 | in.read(reinterpret_cast(&len), sizeof(len)); 39 | std::string result(len, '\0'); 40 | in.read(result.data(), static_cast(len)); 41 | return result; 42 | } 43 | 44 | static void writeVectorData(std::ostream& out, const std::vector& data) { 45 | const size_t size = data.size(); 46 | out.write(reinterpret_cast(&size), sizeof(size)); 47 | for (const auto& str : data) { 48 | writeSizeAndData(out, str); 49 | } 50 | } 51 | 52 | static void readVectorData(std::istream& in, std::vector& data) { 53 | size_t size = 0; 54 | in.read(reinterpret_cast(&size), sizeof(size)); 55 | data.resize(size); 56 | for (auto& str : data) { 57 | str = readSizedString(in); 58 | } 59 | } 60 | }; 61 | 62 | class ScopedTempFile { 63 | std::filesystem::path path_; 64 | 65 | public: 66 | explicit ScopedTempFile(const std::filesystem::path& base) : path_(base.string() + ".temp") {} 67 | ~ScopedTempFile() { std::filesystem::remove(path_); } 68 | 69 | ScopedTempFile(const ScopedTempFile&) = delete; 70 | ScopedTempFile& operator=(const ScopedTempFile&) = delete; 71 | ScopedTempFile(ScopedTempFile&&) = delete; 72 | ScopedTempFile& operator=(ScopedTempFile&&) = delete; 73 | 74 | [[nodiscard]] const std::filesystem::path& path() const { return path_; } 75 | }; 76 | 77 | public: 78 | void loadBinaryFile(const std::string& filePath) override; 79 | void loadTextFile(const std::string& txtPath, const ParseTextFileOptions& options) override; 80 | void saveToBinaryFile(const std::string& filePath) override; 81 | [[nodiscard]] std::optional find(const std::string& key) const override; 82 | [[nodiscard]] std::vector> prefixSearch( 83 | const std::string& prefix) const override; 84 | 85 | void add(const std::string& key, const std::string& value); 86 | void build(const std::unordered_map& map); 87 | [[nodiscard]] bool contains(std::string_view key) const; 88 | }; 89 | 90 | } // namespace rime 91 | -------------------------------------------------------------------------------- /src/engines/common.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "engines/js_exception.h" 4 | 5 | #include "engines/js_macros.h" 6 | 7 | #include "engines/quickjs/quickjs_engine.h" 8 | 9 | #ifdef _ENABLE_JAVASCRIPTCORE 10 | #include "engines/javascriptcore/javascriptcore_engine.h" 11 | #endif 12 | -------------------------------------------------------------------------------- /src/engines/javascriptcore/jsc_code_loader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class JscCodeLoader { 10 | public: 11 | static JSObjectRef createInstanceOfIifeBundledModule(JSContextRef ctx, 12 | const std::string& baseFolderPath, 13 | const std::string& moduleName, 14 | const std::vector& args, 15 | JSValueRef* exception); 16 | 17 | static JSValueRef loadEsmBundledModuleToGlobalThis(JSContextRef ctx, 18 | const std::string& baseFolderPath, 19 | const std::string& moduleName, 20 | JSValueRef* exception); 21 | 22 | static JSValueRef getExportedClassHavingMethodNameInModule(JSContextRef ctx, 23 | JSValueRef moduleObj, 24 | const char* methodName); 25 | static JSValueRef getExportedClassByNameInModule(JSContextRef ctx, 26 | JSValueRef moduleObj, 27 | const char* className); 28 | static JSValueRef getMethodByNameInClass(JSContextRef ctx, 29 | JSValueRef classObj, 30 | const char* methodName); 31 | 32 | private: 33 | static std::pair loadModuleSource( 34 | JSContextRef ctx, 35 | const std::string& baseFolderPath, 36 | const std::string& moduleName); 37 | }; 38 | -------------------------------------------------------------------------------- /src/engines/javascriptcore/jsc_engine_impl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // NEVER USE TEMPLATE IN THIS HEADER FILE 11 | class JscEngineImpl { 12 | public: 13 | JscEngineImpl(); 14 | ~JscEngineImpl(); 15 | 16 | JscEngineImpl(const JscEngineImpl& other) = default; 17 | JscEngineImpl(JscEngineImpl&&) = delete; 18 | JscEngineImpl& operator=(const JscEngineImpl&) = delete; 19 | JscEngineImpl& operator=(JscEngineImpl&&) = delete; 20 | 21 | [[nodiscard]] JSGlobalContextRef getContext() const { return ctx_; } 22 | 23 | [[nodiscard]] std::string toStdString(const JSValueRef& value) const; 24 | 25 | void setBaseFolderPath(const char* absolutePath); 26 | JSObjectRef createInstanceOfModule(const char* moduleName, const std::vector& args); 27 | JSValueRef loadJsFile(const char* fileName); 28 | JSValueRef eval(const char* code, const char* filename = ""); 29 | JSObjectRef getGlobalObject(); 30 | 31 | [[nodiscard]] size_t getArrayLength(const JSValueRef& array) const; 32 | void insertItemToArray(JSValueRef array, size_t index, const JSValueRef& value) const; 33 | [[nodiscard]] JSValueRef getArrayItem(const JSValueRef& array, size_t index) const; 34 | 35 | JSValueRef getObjectProperty(const JSObjectRef& obj, const char* propertyName) const; 36 | int setObjectProperty(const JSObjectRef& obj, const char* propertyName, const JSValueRef& value); 37 | int setObjectFunction(JSObjectRef obj, 38 | const char* functionName, 39 | JSObjectCallAsFunctionCallback cppFunction, 40 | int expectingArgc); 41 | 42 | JSValueRef callFunction(const JSObjectRef& func, 43 | const JSObjectRef& thisArg, 44 | int argc, 45 | JSValueRef* argv); 46 | JSObjectRef newClassInstance(const JSObjectRef& clazz, int argc, JSValueRef* argv); 47 | 48 | JSValueRef getJsClassHavingMethod(const JSValueRef& module, const char* methodName) const; 49 | JSObjectRef getMethodOfClassOrInstance(JSObjectRef jsClass, 50 | JSObjectRef instance, 51 | const char* methodName); 52 | 53 | void logErrorStackTrace(const JSValueRef& exception, 54 | const char* file = __FILE_NAME__, 55 | int line = __LINE__); 56 | 57 | void registerType(const char* typeName, 58 | JSClassRef& jsClass, 59 | JSObjectCallAsConstructorCallback constructor, 60 | void (*finalizer)(JSObjectRef), 61 | JSStaticFunction* functions, 62 | int numFunctions, 63 | JSStaticValue* properties, 64 | int numProperties, 65 | JSStaticValue* getters, 66 | int numGetters); 67 | 68 | [[nodiscard]] bool isTypeRegistered(const std::string& typeName) const; 69 | 70 | [[nodiscard]] const JSClassRef& getRegisteredClass(const std::string& typeName) const; 71 | 72 | static void exposeLogToJsConsole(JSContextRef ctx); 73 | 74 | private: 75 | static JSValueRef jsLog(JSContextRef ctx, 76 | JSObjectRef function, 77 | JSObjectRef thisObject, 78 | size_t argumentCount, 79 | const JSValueRef arguments[], 80 | JSValueRef* exception); 81 | 82 | static JSValueRef jsError(JSContextRef ctx, 83 | JSObjectRef function, 84 | JSObjectRef thisObject, 85 | size_t argumentCount, 86 | const JSValueRef arguments[], 87 | JSValueRef* exception); 88 | JSGlobalContextRef ctx_{nullptr}; 89 | std::string baseFolderPath_; 90 | std::unordered_map clazzes_; 91 | }; 92 | -------------------------------------------------------------------------------- /src/engines/javascriptcore/jsc_string_raii.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class JscStringRAII { 6 | public: 7 | JscStringRAII(const char* str) : str_(JSStringCreateWithUTF8CString(str)) {} 8 | JscStringRAII(JSStringRef str) : str_(str) {} 9 | 10 | JscStringRAII(const JscStringRAII&) = delete; 11 | JscStringRAII(JscStringRAII&&) = delete; 12 | JscStringRAII& operator=(const JscStringRAII&) = delete; 13 | JscStringRAII& operator=(JscStringRAII&&) = delete; 14 | 15 | ~JscStringRAII() { JSStringRelease(str_); } 16 | 17 | operator JSStringRef() const { return str_; } 18 | 19 | private: 20 | JSStringRef str_; 21 | }; 22 | -------------------------------------------------------------------------------- /src/engines/js_exception.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | enum class JsErrorType : std::uint8_t { 8 | SYNTAX, 9 | RANGE, 10 | REFERENCE, 11 | TYPE, 12 | EVAL, 13 | GENERIC, 14 | INTERNAL, 15 | UNKNOWN, 16 | }; 17 | 18 | class JsException : public std::exception { 19 | private: 20 | std::string message_; 21 | JsErrorType type_; 22 | 23 | public: 24 | // Constructor that takes an error message 25 | explicit JsException(JsErrorType type, std::string message) 26 | : message_(std::move(message)), type_(type) {} 27 | 28 | // Override what() method from std::exception 29 | [[nodiscard]] const char* what() const noexcept override { return message_.c_str(); } 30 | [[nodiscard]] JsErrorType getType() const noexcept { return type_; } 31 | }; 32 | -------------------------------------------------------------------------------- /src/engines/js_traits.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | // NOLINTBEGIN(readability-identifier-naming) 7 | template 8 | struct raw_ptr_type { 9 | using type = T*; 10 | }; 11 | 12 | template 13 | struct is_shared_ptr_helper : std::false_type {}; 14 | 15 | template 16 | struct is_shared_ptr_helper> : std::true_type {}; 17 | 18 | template 19 | struct is_shared_ptr : is_shared_ptr_helper> {}; 20 | 21 | // C++14/17 versions 22 | template 23 | constexpr bool is_shared_ptr_v = is_shared_ptr::value; 24 | 25 | template 26 | struct shared_ptr_inner { 27 | private: 28 | // Remove cv-qualifiers first 29 | using raw_type = std::remove_cv_t; 30 | 31 | // Check if it's a shared_ptr 32 | template 33 | static std::false_type test(...); 34 | 35 | template 36 | static auto test(U*) 37 | -> std::enable_if_t>, 38 | std::true_type>; 39 | 40 | public: 41 | using type = std:: 42 | conditional_t(nullptr))::value, typename raw_type::element_type, T>; 43 | }; 44 | 45 | template 46 | using shared_ptr_inner_t = typename shared_ptr_inner::type; 47 | 48 | // NOLINTEND(readability-identifier-naming) 49 | -------------------------------------------------------------------------------- /src/engines/jscode_utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // replace the last `new SortCandidatesByPinyinFilter()` with `new SortCandidatesByPinyinFilter_N(this.arg0, this.arg1,...)` 10 | // to load the iife-format bundled file in both JavaScriptCore and QuickJS 11 | static void replaceNewClassInstanceStatementInPlace(std::string& source, 12 | const std::string& instanceName, 13 | const std::vector& argumentNames) { 14 | // find the last statment in this format: `globalThis.sort_by_pinyin_js = new SortCandidatesByPinyinFilter()` 15 | std::regex pattern(R"(globalThis\.\w+\s*=\s*new\s*(\w+)\s*\(\))"); 16 | std::sregex_iterator it(source.begin(), source.end(), pattern); 17 | std::sregex_iterator lastMatch = it; 18 | for (; it != std::sregex_iterator(); ++it) { 19 | lastMatch = it; 20 | } 21 | if (lastMatch == std::sregex_iterator()) { 22 | LOG(ERROR) << "[jsc] replaceNewClassInstanceStatementInPlace: no match found"; 23 | return; 24 | } 25 | DLOG(INFO) << "[jsc] found class: " << lastMatch->str(1); 26 | 27 | std::string className = lastMatch->str(1); 28 | static int classIndex = 0; 29 | ++classIndex; 30 | std::string indexedClassName = className + "_" + std::to_string(classIndex); 31 | 32 | // replace `var SortCandidatesByPinyinFilter = class {` with: `var SortCandidatesByPinyinFilter_N = class {` 33 | source = std::regex_replace(source, std::regex("var\\s*" + className + R"(\s*=\s*class\s*\{)"), 34 | "var " + indexedClassName + " = class {"); 35 | 36 | // replace `globaThis.sort_by_pinyin_js = new SortCandidatesByPinyinFilter()` with: 37 | // `globaThis.${instanceName} = new SortCandidatesByPinyinFilter_N(globaThis.arg0, globaThis.arg1, ...)\n})()` 38 | std::stringstream ss; 39 | ss << "globalThis." << instanceName << " = new " << indexedClassName << "("; 40 | for (const std::string& argumentName : argumentNames) { 41 | ss << "globalThis." << argumentName << ","; 42 | } 43 | std::string newStatementString = ss.str(); 44 | newStatementString.pop_back(); 45 | source = std::regex_replace(source, pattern, newStatementString + ")"); 46 | } 47 | 48 | // remove the statements like: `export { SortCandidatesByPinyinFilter }` 49 | // to load the esm-format bundled file in JavaScriptCore 50 | static void removeExportStatementsInPlace(std::string& source) { 51 | size_t pos = 0; 52 | while ((pos = source.find("export", pos)) != std::string::npos) { 53 | size_t rightBracket = source.find('}', pos); 54 | if (rightBracket == std::string::npos) { 55 | rightBracket = source.length(); 56 | } else { 57 | rightBracket++; 58 | } 59 | source.replace(pos, rightBracket - pos, ""); 60 | pos = rightBracket; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/engines/quickjs/quickjs_code_loader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class QuickJSCodeLoader { 8 | public: 9 | static JSValue loadJsModuleToNamespace(JSContext* ctx, const char* moduleName); 10 | static JSValue loadJsModuleToGlobalThis(JSContext* ctx, const char* moduleName); 11 | static JSValue createInstanceOfEsmBundledModule(JSContext* ctx, 12 | const std::string& moduleName, 13 | std::vector& args, 14 | const std::string& mainFuncName); 15 | static JSValue createInstanceOfIifeBundledModule(JSContext* ctx, 16 | const std::string& baseFolderPath, 17 | const std::string& moduleName, 18 | const std::vector& args); 19 | static JSValue getExportedClassHavingMethodNameInModule(JSContext* ctx, 20 | JSValue moduleObj, 21 | const char* methodName); 22 | static JSValue getExportedClassByNameInModule(JSContext* ctx, 23 | JSValue moduleObj, 24 | const char* className); 25 | static JSValue getMethodByNameInClass(JSContext* ctx, JSValue classObj, const char* methodName); 26 | static void logJsError(JSContext* ctx, const char* prefix, const char* file, int line); 27 | }; 28 | -------------------------------------------------------------------------------- /src/engines/quickjs/quickjs_engine_impl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "engines/js_exception.h" 11 | #include "patch/quickjs/node_module_loader.h" 12 | 13 | // NEVER USE TEMPLATE IN THIS HEADER FILE 14 | class QuickJsEngineImpl { 15 | public: 16 | QuickJsEngineImpl(); 17 | ~QuickJsEngineImpl(); 18 | 19 | QuickJsEngineImpl(const QuickJsEngineImpl&) = delete; 20 | QuickJsEngineImpl(QuickJsEngineImpl&&) = delete; 21 | QuickJsEngineImpl& operator=(const QuickJsEngineImpl&) = delete; 22 | QuickJsEngineImpl& operator=(QuickJsEngineImpl&&) = delete; 23 | 24 | [[nodiscard]] JSContext* getContext() const { return context_; } 25 | 26 | [[nodiscard]] int64_t getMemoryUsage() const; 27 | [[nodiscard]] size_t getArrayLength(const JSValue& array) const; 28 | void insertItemToArray(JSValue array, size_t index, const JSValue& value) const; 29 | [[nodiscard]] JSValue getArrayItem(const JSValue& array, size_t index) const; 30 | [[nodiscard]] JSValue getObjectProperty(const JSValue& obj, const char* propertyName) const; 31 | int setObjectProperty(JSValue obj, const char* propertyName, const JSValue& value) const; 32 | int setObjectFunction(JSValue obj, 33 | const char* functionName, 34 | JSCFunction* cppFunction, 35 | int expectingArgc) const; 36 | [[nodiscard]] JSValue callFunction(const JSValue& func, 37 | const JSValue& thisArg, 38 | int argc, 39 | JSValue* argv) const; 40 | [[nodiscard]] JSValue newClassInstance(const JSValue& clazz, int argc, JSValue* argv) const; 41 | [[nodiscard]] JSValue getJsClassHavingMethod(const JSValue& module, const char* methodName) const; 42 | [[nodiscard]] JSValue getMethodOfClassOrInstance(JSValue jsClass, 43 | JSValue instance, 44 | const char* methodName) const; 45 | void logErrorStackTrace(const JSValue& exception, const char* file, int line) const; 46 | 47 | [[nodiscard]] JSValue createInstanceOfModule(const char* moduleName, 48 | std::vector& args, 49 | const std::string& mainFuncName) const; 50 | [[nodiscard]] JSValue loadJsFile(const char* fileName) const; 51 | [[nodiscard]] JSValue eval(const char* code, const char* filename = "") const; 52 | [[nodiscard]] JSValue getGlobalObject() const; 53 | [[nodiscard]] JSValue throwError(JsErrorType errorType, const std::string& message) const; 54 | 55 | // Type conversion utilities 56 | [[nodiscard]] JSValue toJsString(const char* str) const { return JS_NewString(context_, str); } 57 | [[nodiscard]] JSValue toJsString(const std::string& str) const { 58 | return JS_NewString(context_, str.c_str()); 59 | } 60 | [[nodiscard]] std::string toStdString(const JSValue& value) const; 61 | [[nodiscard]] JSValue toJsNumber(double value) const { return JS_NewFloat64(context_, value); } 62 | [[nodiscard]] JSValue toJsNumber(int64_t value) const { return JS_NewInt64(context_, value); } 63 | [[nodiscard]] double toDouble(const JSValue& value) const; 64 | [[nodiscard]] size_t toInt(const JSValue& value) const; 65 | 66 | void registerType(const char* typeName, 67 | JSClassID& classId, 68 | JSClassDef& classDef, 69 | JSCFunction* constructor, 70 | int constructorArgc, 71 | JSClassFinalizer* finalizer, 72 | const JSCFunctionListEntry* properties, 73 | int propertyCount, 74 | const JSCFunctionListEntry* getters, 75 | int getterCount, 76 | const JSCFunctionListEntry* functions, 77 | int functionCount); 78 | 79 | [[nodiscard]] JSValue wrap(const char* typeName, void* ptr, const char* pointerType) const; 80 | 81 | void setBaseFolderPath(const char* absolutePath) { 82 | this->baseFolderPath_ = absolutePath; 83 | setQjsBaseFolder(absolutePath); 84 | } 85 | static void exposeLogToJsConsole(JSContext* ctx); 86 | 87 | private: 88 | static JSValue jsLog(JSContext* ctx, JSValueConst thisVal, int argc, JSValueConst* argv); 89 | static JSValue jsError(JSContext* ctx, JSValueConst thisVal, int argc, JSValueConst* argv); 90 | 91 | JSRuntime* runtime_; 92 | JSContext* context_; 93 | std::unordered_map registeredTypes_; 94 | std::string baseFolderPath_; // absolute path to the base folder of the js files 95 | }; 96 | -------------------------------------------------------------------------------- /src/gears/qjs_module.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "types/qjs_types.h" 8 | 9 | template 10 | class QjsModule { 11 | protected: 12 | QjsModule(const std::string& nameSpace, Environment* environment, const char* mainFuncName) 13 | : namespace_(nameSpace) { 14 | // the js engine is lazy loaded, so we need to register the types first 15 | registerTypesToJsEngine(); 16 | std::filesystem::path path(rime_get_api()->get_user_data_dir()); 17 | path.append("js"); 18 | auto& jsEngine = JsEngine::instance(); 19 | jsEngine.setBaseFolderPath(path.generic_string().c_str()); 20 | 21 | auto jsEnvironment = jsEngine.wrap(environment); 22 | std::vector args = {jsEnvironment}; 23 | instance_ = jsEngine.createInstanceOfModule(namespace_.c_str(), args, mainFuncName); 24 | jsEngine.freeValue(jsEnvironment); 25 | 26 | if (!jsEngine.isObject(instance_)) { 27 | jsEngine.freeValue(instance_); 28 | LOG(ERROR) << "[qjs] Error creating an instance of the exported class in " << nameSpace; 29 | return; 30 | } 31 | 32 | mainFunc_ = jsEngine.toObject(jsEngine.getObjectProperty(instance_, mainFuncName)); 33 | finalizer_ = jsEngine.toObject(jsEngine.getObjectProperty(instance_, "finalizer")); 34 | 35 | jsEngine.protectFromGC(instance_, mainFunc_, finalizer_); 36 | 37 | isLoaded_ = true; 38 | LOG(INFO) << "[qjs] created an instance of the exported class in " << nameSpace; 39 | } 40 | 41 | ~QjsModule() { 42 | auto& jsEngine = JsEngine::instance(); 43 | if (jsEngine.isUndefined(finalizer_)) { 44 | DLOG(INFO) << "[qjs] ~" << namespace_ << " no `finalizer` function exported."; 45 | } else if (isLoaded_) { 46 | DLOG(INFO) << "[qjs] running the finalizer function of " << namespace_; 47 | T_JS_VALUE finalizerResult = jsEngine.callFunction(finalizer_, instance_, 0, nullptr); 48 | if (jsEngine.isException(finalizerResult)) { 49 | LOG(ERROR) << "[qjs] ~" << namespace_ << " Error running the finalizer function."; 50 | } 51 | jsEngine.freeValue(finalizerResult); 52 | } 53 | 54 | if (isLoaded_) { 55 | jsEngine.unprotectFromGC(instance_, mainFunc_, finalizer_); 56 | } 57 | jsEngine.freeValue(instance_, mainFunc_, finalizer_); 58 | } 59 | 60 | [[nodiscard]] bool isLoaded() const { return isLoaded_; } 61 | [[nodiscard]] typename JsEngine::T_JS_OBJECT getInstance() const { return instance_; } 62 | [[nodiscard]] typename JsEngine::T_JS_OBJECT getMainFunc() const { return mainFunc_; } 63 | [[nodiscard]] std::string getNamespace() const { return namespace_; } 64 | 65 | private: 66 | const std::string namespace_; 67 | 68 | bool isLoaded_ = false; 69 | 70 | typename JsEngine::T_JS_OBJECT instance_; 71 | typename JsEngine::T_JS_OBJECT mainFunc_; 72 | typename JsEngine::T_JS_OBJECT finalizer_; 73 | 74 | public: 75 | QjsModule(const QjsModule&) = delete; 76 | QjsModule(QjsModule&&) = delete; 77 | QjsModule& operator=(const QjsModule&) = delete; 78 | QjsModule& operator=(QjsModule&&) = delete; 79 | }; 80 | -------------------------------------------------------------------------------- /src/gears/qjs_processor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include "qjs_component.hpp" 8 | #include "qjs_module.h" 9 | 10 | template 11 | class QuickJSProcessor : public QjsModule { 12 | public: 13 | explicit QuickJSProcessor(const rime::Ticket& ticket, Environment* environment) 14 | : QjsModule(ticket.name_space, environment, "process") {} 15 | 16 | rime::ProcessResult processKeyEvent(const rime::KeyEvent& keyEvent, Environment* environment) { 17 | if (!this->isLoaded()) { 18 | return rime::kNoop; 19 | } 20 | 21 | auto& engine = JsEngine::instance(); 22 | // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) 23 | T_JS_VALUE jsKeyEvt = engine.wrap(const_cast(&keyEvent)); 24 | auto jsEnvironment = engine.wrap(environment); 25 | T_JS_VALUE args[] = {jsKeyEvt, jsEnvironment}; 26 | T_JS_VALUE jsResult = engine.callFunction(this->getMainFunc(), this->getInstance(), 2, args); 27 | engine.freeValue(jsKeyEvt, jsEnvironment); 28 | 29 | if (engine.isException(jsResult)) { 30 | LOG(ERROR) << "[qjs] " << this->getNamespace() 31 | << " failed to process keyEvent = " << keyEvent.repr(); 32 | return rime::kNoop; 33 | } 34 | 35 | std::string result = engine.toStdString(jsResult); 36 | engine.freeValue(jsResult); 37 | 38 | if (result == "kNoop") { 39 | return rime::kNoop; 40 | } 41 | if (result == "kAccepted") { 42 | return rime::kAccepted; 43 | } 44 | if (result == "kRejected") { 45 | return rime::kRejected; 46 | } 47 | 48 | LOG(ERROR) << "[qjs] " << this->getNamespace() 49 | << "::ProcessKeyEvent unknown result: " << result; 50 | return rime::kNoop; 51 | } 52 | }; 53 | 54 | // Specialization for Processor 55 | template 56 | class rime::ComponentWrapper 57 | : public ComponentWrapperBase { 58 | public: 59 | explicit ComponentWrapper(const rime::Ticket& ticket) 60 | : ComponentWrapperBase(ticket) {} 61 | 62 | // NOLINTNEXTLINE(readability-identifier-naming) 63 | rime::ProcessResult ProcessKeyEvent(const rime::KeyEvent& keyEvent) override { 64 | return this->actual()->processKeyEvent(keyEvent, this->environment()); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/gears/qjs_translator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "engines/common.h" 11 | #include "qjs_component.hpp" 12 | #include "qjs_module.h" 13 | 14 | using namespace rime; 15 | 16 | template 17 | class QuickJSTranslator : public QjsModule { 18 | public: 19 | explicit QuickJSTranslator(const rime::Ticket& ticket, Environment* environment) 20 | : QjsModule(ticket.name_space, environment, "translate") {} 21 | 22 | rime::an query(const std::string& input, 23 | const rime::Segment& segment, 24 | Environment* environment) { 25 | auto translation = New(); 26 | if (!this->isLoaded()) { 27 | return translation; 28 | } 29 | 30 | auto& engine = JsEngine::instance(); 31 | T_JS_VALUE jsInput = engine.wrap(input); 32 | // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) 33 | T_JS_VALUE jsSegment = engine.wrap(const_cast(&segment)); 34 | auto jsEnvironment = engine.wrap(environment); 35 | T_JS_VALUE args[] = {jsInput, jsSegment, jsEnvironment}; 36 | T_JS_VALUE resultArray = 37 | engine.callFunction(this->getMainFunc(), this->getInstance(), countof(args), args); 38 | engine.freeValue(jsInput, jsSegment, jsEnvironment); 39 | if (!engine.isArray(resultArray)) { 40 | LOG(ERROR) << "[qjs] A candidate array should be returned by `translate` of the plugin: " 41 | << this->getNamespace(); 42 | return translation; 43 | } 44 | 45 | size_t length = engine.getArrayLength(resultArray); 46 | for (uint32_t i = 0; i < length; i++) { 47 | T_JS_VALUE item = engine.getArrayItem(resultArray, i); 48 | if (an candidate = engine.template unwrap(item)) { 49 | translation->Append(candidate); 50 | } else { 51 | LOG(ERROR) << "[qjs] Failed to unwrap candidate at index " << i; 52 | } 53 | engine.freeValue(item); 54 | } 55 | 56 | engine.freeValue(resultArray); 57 | return translation; 58 | } 59 | }; 60 | 61 | // Specialization for Translator 62 | template 63 | class rime::ComponentWrapper 64 | : public ComponentWrapperBase { 65 | public: 66 | explicit ComponentWrapper(const rime::Ticket& ticket) 67 | : ComponentWrapperBase(ticket) {} 68 | 69 | // NOLINTNEXTLINE(readability-identifier-naming) 70 | virtual rime::an Query(const std::string& input, 71 | const rime::Segment& segment) { 72 | return this->actual()->query(input, segment, this->environment()); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/misc/process_memory.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifdef __linux__ 6 | #include 7 | 8 | #include 9 | 10 | void getMemoryUsage(size_t& vm_usage, size_t& resident_set) { 11 | vm_usage = 0; 12 | resident_set = 0; 13 | 14 | std::ifstream statm("/proc/self/statm"); 15 | if (statm) { 16 | size_t size, resident, share, text, lib, data, dt; 17 | statm >> size >> resident >> share >> text >> lib >> data >> dt; 18 | 19 | long page_size = sysconf(_SC_PAGESIZE); // in bytes 20 | vm_usage = size * page_size; 21 | resident_set = resident * page_size; 22 | } else { 23 | std::cerr << "Failed to open /proc/self/statm" << std::endl; 24 | } 25 | } 26 | 27 | #elif _WIN32 28 | // do not sort the following includes, otherwise it will cause the compilation error in windows. 29 | // clang-format off 30 | #include 31 | #include 32 | // clang-format on 33 | 34 | void getMemoryUsage(size_t& vm_usage, size_t& resident_set) { 35 | PROCESS_MEMORY_COUNTERS pmc; 36 | if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) { 37 | vm_usage = pmc.WorkingSetSize; 38 | resident_set = pmc.PagefileUsage; 39 | } else { 40 | std::cerr << "Failed to get process memory info" << std::endl; 41 | } 42 | } 43 | 44 | #elif __APPLE__ 45 | #include 46 | #include // For getpid() 47 | 48 | inline void getMemoryUsage(size_t& vmUsage, size_t& residentSet) { 49 | vmUsage = 0; 50 | residentSet = 0; 51 | 52 | task_t task = MACH_PORT_NULL; 53 | struct task_basic_info tInfo{}; 54 | mach_msg_type_number_t tInfoCount = TASK_BASIC_INFO_COUNT; 55 | 56 | if (task_for_pid(current_task(), ::getpid(), &task) != KERN_SUCCESS) { 57 | std::cerr << "Failed to get task for process\n"; 58 | return; 59 | } 60 | 61 | if (task_info(task, TASK_BASIC_INFO, reinterpret_cast(&tInfo), &tInfoCount) != 62 | KERN_SUCCESS) { 63 | std::cerr << "Failed to get task info\n"; 64 | return; 65 | } 66 | 67 | vmUsage = tInfo.virtual_size; // Virtual memory size 68 | residentSet = tInfo.resident_size; // Resident set size 69 | } 70 | 71 | #else 72 | #error "Platform not supported" 73 | #endif 74 | -------------------------------------------------------------------------------- /src/module.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "engines/common.h" 9 | #include "qjs_component.hpp" 10 | #include "qjs_filter.hpp" 11 | #include "qjs_processor.h" 12 | #include "qjs_translator.h" 13 | 14 | using namespace rime; 15 | 16 | template 17 | static void setupJsEngine(Registry& r, const std::string& prefix) { 18 | r.Register(prefix + "_processor", new QuickJSComponent, Processor, T>()); 19 | r.Register(prefix + "_filter", new QuickJSComponent, Filter, T>()); 20 | r.Register(prefix + "_translator", new QuickJSComponent, Translator, T>()); 21 | 22 | JsEngine::setup(); 23 | } 24 | 25 | // NOLINTBEGIN(readability-identifier-naming) 26 | static void rime_qjs_initialize() { 27 | LOG(INFO) << "[qjs] registering components from module 'qjs'."; 28 | Registry& r = Registry::instance(); 29 | 30 | setupJsEngine(r, "qjs"); 31 | 32 | #ifdef _ENABLE_JAVASCRIPTCORE 33 | setupJsEngine(r, "jsc"); 34 | #else 35 | // fallback to the quickjs implementation, to share the same Rime schemas across platforms 36 | setupJsEngine(r, "jsc"); 37 | #endif 38 | } 39 | 40 | static void rime_qjs_finalize() { 41 | JsEngine::shutdown(); 42 | 43 | #ifdef _ENABLE_JAVASCRIPTCORE 44 | JsEngine::shutdown(); 45 | #endif 46 | } 47 | 48 | void rime_require_module_qjs() {} 49 | 50 | #ifdef _WIN32 51 | 52 | static void __cdecl rime_register_module_qjs(void); 53 | __declspec(allocate(".CRT$XCU")) void(__cdecl* rime_register_module_qjs_)(void) = 54 | rime_register_module_qjs; 55 | static void __cdecl rime_register_module_qjs(void) { 56 | static RimeModule module = {0}; 57 | if (!module.data_size) { 58 | module.data_size = sizeof(RimeModule) - sizeof((module).data_size); 59 | module.module_name = "qjs"; 60 | module.initialize = rime_qjs_initialize; 61 | module.finalize = rime_qjs_finalize; 62 | } 63 | RimeRegisterModule(&module); 64 | } 65 | 66 | #else 67 | 68 | static void rime_register_module_qjs() __attribute__((constructor)); 69 | 70 | static void rime_register_module_qjs() { 71 | static RimeModule module = { 72 | .data_size = sizeof(RimeModule) - sizeof((module).data_size), 73 | .module_name = "qjs", 74 | .initialize = rime_qjs_initialize, 75 | .finalize = rime_qjs_finalize, 76 | .get_api = nullptr, 77 | }; 78 | RimeRegisterModule(&module); 79 | } 80 | #endif 81 | 82 | // NOLINTEND(readability-identifier-naming) 83 | -------------------------------------------------------------------------------- /src/patch/quickjs/node_module_loader.h: -------------------------------------------------------------------------------- 1 | #include "quickjs.h" 2 | 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | void setQjsBaseFolder(const char* path); 8 | 9 | // NOLINTNEXTLINE(readability-identifier-naming) 10 | JSModuleDef* js_module_loader(JSContext* ctx, const char* moduleName, void* opaque); 11 | 12 | JSValue loadJsModule(JSContext* ctx, const char* moduleName); 13 | 14 | char* readJsCode(JSContext* ctx, const char* moduleName); 15 | 16 | char* loadFile(const char* absolutePath); 17 | 18 | #ifdef __cplusplus 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /src/patch/quickjs/node_module_logger.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | extern "C" { 4 | void logInfoImpl(const char* message) { 5 | LOG(INFO) << message << "\n"; 6 | } 7 | 8 | void logErrorImpl(const char* message) { 9 | LOG(ERROR) << message << "\n"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/qjs_component.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include "types/environment.h" 14 | 15 | namespace rime { 16 | 17 | // Primary template declaration 18 | template 19 | class ComponentWrapper; 20 | 21 | // Base class for all ComponentWrapper specializations 22 | template 23 | class ComponentWrapperBase : public T_BASE { 24 | public: 25 | std::shared_ptr actual() { return actual_; } 26 | void setActual(const std::shared_ptr actual) { actual_ = actual; } 27 | 28 | [[nodiscard]] Environment* environment() const { return environment_.get(); } 29 | 30 | ComponentWrapperBase(const ComponentWrapperBase&) = delete; 31 | ComponentWrapperBase& operator=(const ComponentWrapperBase&) = delete; 32 | ComponentWrapperBase(ComponentWrapperBase&&) = delete; 33 | ComponentWrapperBase& operator=(ComponentWrapperBase&&) = delete; 34 | 35 | protected: 36 | explicit ComponentWrapperBase(const rime::Ticket& ticket) 37 | : T_BASE(ticket), 38 | environment_(std::make_unique(ticket.engine, ticket.name_space)) { 39 | DLOG(INFO) << "[qjs] " << typeid(T_ACTUAL).name() 40 | << " ComponentWrapper created with ticket: " << ticket.name_space; 41 | } 42 | 43 | virtual ~ComponentWrapperBase() { 44 | DLOG(INFO) << "[qjs] " << typeid(T_ACTUAL).name() << " ComponentWrapper destroyed"; 45 | } 46 | 47 | private: 48 | std::unique_ptr environment_; 49 | std::shared_ptr actual_{nullptr}; 50 | }; 51 | 52 | template 53 | class QuickJSComponent : public T_BASE::Component { 54 | using KeyType = std::pair; 55 | // using T_JS_OBJECT = typename TypeMap::ObjectType; 56 | 57 | public: 58 | // NOLINTNEXTLINE(readability-identifier-naming) 59 | ComponentWrapper* Create(const rime::Ticket& ticket) { 60 | // The same plugin could have difference configurations for different schemas, and then behave differently. 61 | // So we need to create a new component for each schema. 62 | const std::string schemaId = ticket.engine->schema()->schema_id(); 63 | KeyType key = std::make_pair(schemaId, ticket.name_space); 64 | 65 | auto component = new ComponentWrapper(ticket); 66 | std::shared_ptr actual = nullptr; 67 | if (components_.count(key)) { 68 | actual = components_[key]; 69 | } else { 70 | LOG(INFO) << "[qjs] creating component '" << ticket.name_space << "' for schema " << schemaId; 71 | actual = std::make_shared(ticket, component->environment()); 72 | components_[key] = actual; 73 | } 74 | 75 | component->setActual(actual); 76 | return component; 77 | } 78 | 79 | private: 80 | std::map> components_; 81 | }; 82 | 83 | } // namespace rime 84 | -------------------------------------------------------------------------------- /src/types/environment.cc: -------------------------------------------------------------------------------- 1 | #include "environment.h" 2 | 3 | #include 4 | #include 5 | #include "process_memory.hpp" 6 | 7 | #ifdef _WIN32 8 | static FILE* popenx(const std::string& command) { 9 | return _popen(command.c_str(), "r"); 10 | } 11 | 12 | static int pclosex(FILE* pipe) { 13 | return _pclose(pipe); 14 | } 15 | #else 16 | static FILE* popenx(const std::string& command) { 17 | return ::popen(command.c_str(), "r"); 18 | } 19 | 20 | static int pclosex(FILE* pipe) { 21 | return ::pclose(pipe); 22 | } 23 | #endif 24 | 25 | std::string Environment::formatMemoryUsage(size_t usage) { 26 | constexpr size_t KILOBYTE = 1024; 27 | return usage > KILOBYTE * KILOBYTE ? std::to_string(usage / KILOBYTE / KILOBYTE) + "M" 28 | : std::to_string(usage / KILOBYTE) + "K"; 29 | } 30 | 31 | std::string Environment::loadFile(const std::string& path) { 32 | if (path.empty()) { 33 | return ""; 34 | } 35 | 36 | FILE* file = fopen(path.c_str(), "rb"); 37 | if (file == nullptr) { 38 | return ""; 39 | } 40 | 41 | fseek(file, 0, SEEK_END); 42 | long size = ftell(file); 43 | fseek(file, 0, SEEK_SET); 44 | 45 | std::string content; 46 | content.resize(size); 47 | fread(content.data(), 1, size, file); 48 | fclose(file); 49 | 50 | return content; 51 | } 52 | 53 | bool Environment::fileExists(const std::string& path) { 54 | return std::filesystem::exists(path); 55 | } 56 | 57 | std::string Environment::getRimeInfo() { 58 | size_t vmUsage = 0; 59 | size_t residentSet = 0; // memory usage in bytes 60 | getMemoryUsage(vmUsage, residentSet); 61 | 62 | std::stringstream ss{}; 63 | ss << "libRime v" << rime_get_api()->get_version() << " | " 64 | << "libRime-qjs v" << RIME_QJS_VERSION << " | " 65 | << "Process RSS Mem: " << formatMemoryUsage(residentSet); 66 | 67 | return ss.str(); 68 | } 69 | 70 | std::string Environment::popen(const std::string& command) { 71 | if (command.empty()) { 72 | throw std::runtime_error("Command is empty"); 73 | } 74 | 75 | FILE* pipe = popenx(command); 76 | if (pipe == nullptr) { 77 | throw std::runtime_error("Failed to run command: " + command); 78 | } 79 | 80 | // Read the output 81 | constexpr size_t READ_BUFFER_SIZE = 128; 82 | char buffer[READ_BUFFER_SIZE]; 83 | char* ptrBuffer = static_cast(buffer); 84 | std::string result; 85 | while (fgets(ptrBuffer, sizeof(buffer), pipe) != nullptr) { 86 | result += ptrBuffer; 87 | } 88 | 89 | int status = pclosex(pipe); 90 | if (status != 0) { 91 | throw std::runtime_error("Command failed with status: " + std::to_string(status)); 92 | } 93 | 94 | return result; 95 | } 96 | -------------------------------------------------------------------------------- /src/types/environment.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include "misc/system_info.hpp" 16 | 17 | class Environment { 18 | public: 19 | explicit Environment(rime::Engine* engine, std::string nameSpace) 20 | : id_(generateUuid()), engine_(engine), nameSpace_(std::move(nameSpace)) {} 21 | 22 | [[nodiscard]] std::string getId() const { return id_; } 23 | [[nodiscard]] rime::Engine* getEngine() const { return engine_; } 24 | [[nodiscard]] const std::string& getNameSpace() const { return nameSpace_; } 25 | [[nodiscard]] SystemInfo* getSystemInfo() { return &systemInfo_; } 26 | static std::string getUserDataDir() { return rime_get_api()->get_user_data_dir(); } 27 | static std::string getSharedDataDir() { return rime_get_api()->get_shared_data_dir(); } 28 | 29 | static std::string loadFile(const std::string& path); 30 | static bool fileExists(const std::string& path); 31 | static std::string getRimeInfo(); 32 | static std::string popen(const std::string& command); 33 | 34 | static std::string formatMemoryUsage(size_t usage); 35 | 36 | private: 37 | std::string id_; 38 | rime::Engine* engine_; 39 | std::string nameSpace_; 40 | SystemInfo systemInfo_{}; 41 | 42 | static std::string generateUuid() { 43 | boost::uuids::random_generator gen; 44 | boost::uuids::uuid uuid = gen(); 45 | return boost::uuids::to_string(uuid); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/types/js_wrapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | template 7 | class JsWrapper { 8 | public: 9 | // to satisfy clang-tidy -_-! 10 | using T_UNWRAP_TYPE = std::shared_ptr; 11 | inline static const char* typeName = "Unknown"; 12 | inline static JSClassID jsClassId = 0; 13 | }; 14 | -------------------------------------------------------------------------------- /src/types/qjs_candidate.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "engines/js_macros.h" 7 | #include "js_wrapper.h" 8 | 9 | using namespace rime; 10 | 11 | constexpr int MIN_ARGC_NEW_CANDIDATE = 5; 12 | 13 | template <> 14 | class JsWrapper { 15 | DEFINE_GETTER(Candidate, text, obj->text()) 16 | DEFINE_GETTER(Candidate, comment, obj->comment()) 17 | DEFINE_GETTER(Candidate, type, obj->type()) 18 | DEFINE_GETTER(Candidate, start, obj->start()) 19 | DEFINE_GETTER(Candidate, end, obj->end()) 20 | DEFINE_GETTER(Candidate, quality, obj->quality()) 21 | DEFINE_GETTER(Candidate, preedit, obj->preedit()) 22 | 23 | DEFINE_STRING_SETTER(Candidate, text, { 24 | if (auto simpleCandidate = dynamic_cast(obj.get())) { 25 | simpleCandidate->set_text(str); 26 | } 27 | }) 28 | 29 | DEFINE_STRING_SETTER(Candidate, comment, { 30 | if (auto simpleCandidate = dynamic_cast(obj.get())) { 31 | simpleCandidate->set_comment(str); 32 | } else if (auto phrase = dynamic_cast(obj.get())) { 33 | phrase->set_comment(str); 34 | } 35 | }) 36 | 37 | DEFINE_STRING_SETTER(Candidate, type, obj->set_type(str);) 38 | 39 | DEFINE_SETTER(Candidate, start, engine.toInt, obj->set_start(value)) 40 | DEFINE_SETTER(Candidate, quality, engine.toDouble, obj->set_quality(value)) 41 | DEFINE_SETTER(Candidate, end, engine.toInt, obj->set_end(value)) 42 | 43 | DEFINE_STRING_SETTER(Candidate, preedit, { 44 | if (auto simpleCandidate = dynamic_cast(obj.get())) { 45 | simpleCandidate->set_preedit(str); 46 | } else if (auto phrase = dynamic_cast(obj.get())) { 47 | phrase->set_preedit(str); 48 | } 49 | }) 50 | 51 | DEFINE_CFUNCTION_ARGC(makeCandidate, MIN_ARGC_NEW_CANDIDATE, { 52 | auto obj = std::make_shared(); 53 | obj->set_type(engine.toStdString(argv[0])); 54 | obj->set_start(engine.toInt(argv[1])); 55 | obj->set_end(engine.toInt(argv[2])); 56 | obj->set_text(engine.toStdString(argv[3])); 57 | obj->set_comment(engine.toStdString(argv[4])); 58 | if (argc > MIN_ARGC_NEW_CANDIDATE) { 59 | obj->set_quality(engine.toDouble(argv[5])); 60 | } 61 | return engine.wrap>(obj); 62 | }); 63 | 64 | public: 65 | EXPORT_CLASS_WITH_SHARED_POINTER( 66 | Candidate, 67 | WITH_CONSTRUCTOR(makeCandidate, MIN_ARGC_NEW_CANDIDATE), 68 | WITH_PROPERTIES(text, comment, type, start, end, quality, preedit), 69 | WITHOUT_GETTERS, 70 | WITHOUT_FUNCTIONS); 71 | }; 72 | -------------------------------------------------------------------------------- /src/types/qjs_candidate_iterator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "engines/js_macros.h" 7 | #include "js_wrapper.h" 8 | 9 | using namespace rime; 10 | 11 | using CandidateIterator = rime::Translation; 12 | 13 | template <> 14 | class JsWrapper { 15 | DEFINE_CFUNCTION(next, { 16 | auto obj = engine.unwrap(thisVal); 17 | if (obj->exhausted()) { 18 | return engine.null(); 19 | } 20 | auto candidate = obj->Peek(); 21 | obj->Next(); 22 | return engine.wrap(candidate); 23 | }) 24 | 25 | public: 26 | EXPORT_CLASS_WITH_SHARED_POINTER(CandidateIterator, 27 | WITHOUT_CONSTRUCTOR, 28 | WITHOUT_PROPERTIES, 29 | WITHOUT_GETTERS, 30 | WITH_FUNCTIONS(next, 0)); 31 | }; 32 | -------------------------------------------------------------------------------- /src/types/qjs_commit_history.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "engines/js_macros.h" 6 | #include "js_wrapper.h" 7 | 8 | using namespace rime; 9 | 10 | template <> 11 | class JsWrapper { 12 | DEFINE_CFUNCTION_ARGC(push, 2, { 13 | auto type = engine.toStdString(argv[0]); 14 | auto text = engine.toStdString(argv[1]); 15 | auto* obj = engine.unwrap(thisVal); 16 | obj->Push(CommitRecord(type, text)); 17 | return engine.undefined(); 18 | }) 19 | 20 | DEFINE_GETTER(CommitHistory, last, obj->empty() ? nullptr : &obj->back()); 21 | 22 | DEFINE_GETTER(CommitHistory, repr, obj->repr()); 23 | 24 | public: 25 | EXPORT_CLASS_WITH_RAW_POINTER(CommitHistory, 26 | WITHOUT_CONSTRUCTOR, 27 | WITHOUT_PROPERTIES, 28 | WITH_GETTERS(last, repr), 29 | WITH_FUNCTIONS(push, 2)); 30 | }; 31 | -------------------------------------------------------------------------------- /src/types/qjs_commit_record.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "engines/js_macros.h" 6 | #include "js_wrapper.h" 7 | 8 | using namespace rime; 9 | 10 | template <> 11 | class JsWrapper { 12 | DEFINE_GETTER(CommitRecord, type, obj->type) 13 | DEFINE_GETTER(CommitRecord, text, obj->text) 14 | 15 | DEFINE_STRING_SETTER(CommitRecord, text, obj->text = str) 16 | DEFINE_STRING_SETTER(CommitRecord, type, obj->type = str) 17 | 18 | public: 19 | EXPORT_CLASS_WITH_RAW_POINTER(CommitRecord, 20 | WITHOUT_CONSTRUCTOR, 21 | WITH_PROPERTIES(text, type), 22 | WITHOUT_GETTERS, 23 | WITHOUT_FUNCTIONS); 24 | }; 25 | -------------------------------------------------------------------------------- /src/types/qjs_config_item.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "engines/js_macros.h" 5 | #include "js_wrapper.h" 6 | 7 | using namespace rime; 8 | 9 | template <> 10 | class JsWrapper { 11 | DEFINE_CFUNCTION(getType, { 12 | auto obj = engine.unwrap(thisVal); 13 | const char* strType; 14 | switch (obj->type()) { 15 | case rime::ConfigItem::kNull: 16 | strType = "null"; 17 | break; 18 | case rime::ConfigItem::kScalar: 19 | strType = "scalar"; 20 | break; 21 | case rime::ConfigItem::kList: 22 | strType = "list"; 23 | break; 24 | case rime::ConfigItem::kMap: 25 | strType = "map"; 26 | break; 27 | default: 28 | strType = "unknown"; 29 | } 30 | return engine.wrap(strType); 31 | }) 32 | 33 | public: 34 | EXPORT_CLASS_WITH_SHARED_POINTER(ConfigItem, 35 | WITHOUT_CONSTRUCTOR, 36 | WITHOUT_PROPERTIES, 37 | WITHOUT_GETTERS, 38 | WITH_FUNCTIONS(getType, 0)); 39 | }; 40 | -------------------------------------------------------------------------------- /src/types/qjs_config_list.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "engines/js_macros.h" 6 | #include "js_wrapper.h" 7 | 8 | using namespace rime; 9 | 10 | template <> 11 | class JsWrapper { 12 | DEFINE_CFUNCTION(getType, { return engine.wrap("list"); }) 13 | 14 | DEFINE_CFUNCTION(getSize, { 15 | auto obj = engine.unwrap(thisVal); 16 | return engine.wrap(obj->size()); 17 | }) 18 | 19 | DEFINE_CFUNCTION_ARGC(getItemAt, 1, { 20 | int index = engine.toInt(argv[0]); 21 | auto obj = engine.unwrap(thisVal); 22 | 23 | if (index < 0 || size_t(index) >= obj->size()) { 24 | return engine.null(); 25 | } 26 | 27 | auto item = obj->GetAt(index); 28 | if (!item) { 29 | return engine.null(); 30 | } 31 | return engine.wrap(item); 32 | }) 33 | 34 | DEFINE_CFUNCTION_ARGC(getValueAt, 1, { 35 | int index = engine.toInt(argv[0]); 36 | auto obj = engine.unwrap(thisVal); 37 | 38 | if (index < 0 || size_t(index) >= obj->size()) { 39 | return engine.null(); 40 | } 41 | 42 | auto value = obj->GetValueAt(index); 43 | if (!value) { 44 | return engine.null(); 45 | } 46 | return engine.wrap(value); 47 | }) 48 | 49 | DEFINE_CFUNCTION_ARGC(pushBack, 1, { 50 | if (auto item = engine.unwrap(argv[0])) { 51 | auto obj = engine.unwrap(thisVal); 52 | obj->Append(item); 53 | } 54 | return engine.undefined(); 55 | }) 56 | 57 | DEFINE_CFUNCTION(clear, { 58 | auto obj = engine.unwrap(thisVal); 59 | obj->Clear(); 60 | return engine.undefined(); 61 | }) 62 | 63 | public: 64 | EXPORT_CLASS_WITH_SHARED_POINTER( 65 | ConfigList, 66 | WITHOUT_CONSTRUCTOR, 67 | WITHOUT_PROPERTIES, 68 | WITHOUT_GETTERS, 69 | WITH_FUNCTIONS(getType, 0, getSize, 0, getItemAt, 1, getValueAt, 1, pushBack, 1, clear, 0)); 70 | }; 71 | -------------------------------------------------------------------------------- /src/types/qjs_config_map.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "engines/js_macros.h" 6 | #include "js_wrapper.h" 7 | 8 | using namespace rime; 9 | 10 | template <> 11 | class JsWrapper { 12 | DEFINE_CFUNCTION(getType, { return engine.wrap("map"); }) 13 | 14 | DEFINE_CFUNCTION_ARGC(hasKey, 1, { 15 | auto key = engine.toStdString(argv[0]); 16 | auto obj = engine.unwrap(thisVal); 17 | return engine.wrap(obj->HasKey(key)); 18 | }) 19 | 20 | DEFINE_CFUNCTION_ARGC(getItem, 1, { 21 | auto key = engine.toStdString(argv[0]); 22 | auto obj = engine.unwrap(thisVal); 23 | auto value = obj->Get(key); 24 | if (!value) { 25 | return engine.null(); 26 | } 27 | return engine.wrap(value); 28 | }) 29 | 30 | DEFINE_CFUNCTION_ARGC(getValue, 1, { 31 | auto key = engine.toStdString(argv[0]); 32 | auto obj = engine.unwrap(thisVal); 33 | auto value = obj->GetValue(key); 34 | if (!value) { 35 | return engine.null(); 36 | } 37 | return engine.wrap(value); 38 | }) 39 | 40 | DEFINE_CFUNCTION_ARGC(setItem, 2, { 41 | auto key = engine.toStdString(argv[0]); 42 | auto obj = engine.unwrap(thisVal); 43 | if (auto item = engine.unwrap(argv[1])) { 44 | obj->Set(key, item); 45 | } 46 | return engine.undefined(); 47 | }) 48 | 49 | public: 50 | EXPORT_CLASS_WITH_SHARED_POINTER( 51 | ConfigMap, 52 | WITHOUT_CONSTRUCTOR, 53 | WITHOUT_PROPERTIES, 54 | WITHOUT_GETTERS, 55 | WITH_FUNCTIONS(getType, 0, hasKey, 1, getItem, 1, getValue, 1, setItem, 2)); 56 | }; 57 | -------------------------------------------------------------------------------- /src/types/qjs_config_value.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "engines/js_macros.h" 6 | #include "js_wrapper.h" 7 | 8 | using namespace rime; 9 | 10 | template <> 11 | class JsWrapper { 12 | DEFINE_CFUNCTION(getType, { return engine.wrap("scalar"); }) 13 | 14 | DEFINE_CFUNCTION(getBool, { 15 | auto obj = engine.unwrap(thisVal); 16 | bool value = false; 17 | bool success = obj->GetBool(&value); 18 | return success ? engine.wrap(value) : engine.null(); 19 | }) 20 | 21 | DEFINE_CFUNCTION(getInt, { 22 | auto obj = engine.unwrap(thisVal); 23 | int value = 0; 24 | bool success = obj->GetInt(&value); 25 | return success ? engine.wrap(value) : engine.null(); 26 | }) 27 | 28 | DEFINE_CFUNCTION(getDouble, { 29 | auto obj = engine.unwrap(thisVal); 30 | double value = 0; 31 | bool success = obj->GetDouble(&value); 32 | return success ? engine.wrap(value) : engine.null(); 33 | }) 34 | 35 | DEFINE_CFUNCTION(getString, { 36 | auto obj = engine.unwrap(thisVal); 37 | std::string value; 38 | bool success = obj->GetString(&value); 39 | return success ? engine.wrap(value.c_str()) : engine.null(); 40 | }) 41 | 42 | public: 43 | EXPORT_CLASS_WITH_SHARED_POINTER( 44 | ConfigValue, 45 | WITHOUT_CONSTRUCTOR, 46 | WITHOUT_PROPERTIES, 47 | WITHOUT_GETTERS, 48 | WITH_FUNCTIONS(getType, 0, getBool, 0, getInt, 0, getDouble, 0, getString, 0)); 49 | }; 50 | -------------------------------------------------------------------------------- /src/types/qjs_context.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "engines/js_macros.h" 6 | #include "js_wrapper.h" 7 | 8 | using namespace rime; 9 | 10 | template <> 11 | class JsWrapper { 12 | DEFINE_GETTER(Context, input, obj->input()) 13 | DEFINE_GETTER(Context, caretPos, obj->caret_pos()) 14 | 15 | DEFINE_STRING_SETTER(Context, input, obj->set_input(str);) 16 | DEFINE_SETTER(Context, caretPos, engine.toInt, obj->set_caret_pos(value)) 17 | 18 | DEFINE_GETTER(Context, preedit, std::make_shared(obj->GetPreedit())) 19 | 20 | DEFINE_GETTER(Context, 21 | lastSegment, 22 | obj->composition().empty() ? nullptr : &obj->composition().back()); 23 | 24 | DEFINE_GETTER(Context, commitNotifier, &obj->commit_notifier()) 25 | DEFINE_GETTER(Context, selectNotifier, &obj->select_notifier()) 26 | DEFINE_GETTER(Context, updateNotifier, &obj->update_notifier()) 27 | DEFINE_GETTER(Context, deleteNotifier, &obj->delete_notifier()) 28 | 29 | DEFINE_GETTER(Context, commitHistory, &obj->commit_history()) 30 | 31 | DEFINE_CFUNCTION(commit, { 32 | auto obj = engine.unwrap(thisVal); 33 | obj->Commit(); 34 | return engine.undefined(); 35 | }) 36 | 37 | DEFINE_CFUNCTION(getCommitText, { 38 | auto obj = engine.unwrap(thisVal); 39 | return engine.wrap(obj->GetCommitText()); 40 | }) 41 | 42 | DEFINE_CFUNCTION(clear, { 43 | auto obj = engine.unwrap(thisVal); 44 | obj->Clear(); 45 | return engine.undefined(); 46 | }) 47 | 48 | DEFINE_CFUNCTION(hasMenu, { 49 | auto obj = engine.unwrap(thisVal); 50 | return engine.wrap(obj->HasMenu()); 51 | }) 52 | 53 | public: 54 | EXPORT_CLASS_WITH_RAW_POINTER(Context, 55 | WITHOUT_CONSTRUCTOR, 56 | WITH_PROPERTIES(input, caretPos), 57 | WITH_GETTERS(preedit, 58 | lastSegment, 59 | commitNotifier, 60 | selectNotifier, 61 | updateNotifier, 62 | deleteNotifier, 63 | commitHistory), 64 | WITH_FUNCTIONS(commit, 0, getCommitText, 0, clear, 0, hasMenu, 0)); 65 | }; 66 | -------------------------------------------------------------------------------- /src/types/qjs_engine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "engines/js_macros.h" 7 | #include "js_wrapper.h" 8 | #include "types/qjs_schema.h" 9 | 10 | using namespace rime; 11 | 12 | template <> 13 | class JsWrapper { 14 | DEFINE_GETTER(Engine, schema, obj->schema()) 15 | DEFINE_GETTER(Engine, context, obj->context()) 16 | DEFINE_GETTER(Engine, activeEngine, obj->active_engine()) 17 | 18 | DEFINE_CFUNCTION_ARGC(commitText, 1, { 19 | std::string text = engine.toStdString(argv[0]); 20 | auto* obj = engine.unwrap(thisVal); 21 | obj->CommitText(text); 22 | return engine.undefined(); 23 | }) 24 | 25 | DEFINE_CFUNCTION_ARGC(applySchema, 1, { 26 | auto schema = engine.unwrap(argv[0]); 27 | if (!schema) { 28 | return engine.jsFalse(); 29 | } 30 | auto* obj = engine.unwrap(thisVal); 31 | obj->ApplySchema(schema); 32 | return engine.jsTrue(); 33 | }) 34 | 35 | DEFINE_CFUNCTION_ARGC(processKey, 1, { 36 | std::string keyRepr = engine.toStdString(argv[0]); 37 | auto* obj = engine.unwrap(thisVal); 38 | return engine.wrap(obj->ProcessKey(KeyEvent(keyRepr))); 39 | }) 40 | 41 | public: 42 | EXPORT_CLASS_WITH_RAW_POINTER(Engine, 43 | WITHOUT_CONSTRUCTOR, 44 | WITHOUT_PROPERTIES, 45 | WITH_GETTERS(schema, context, activeEngine), 46 | WITH_FUNCTIONS(processKey, 1, commitText, 1, applySchema, 1)); 47 | }; 48 | -------------------------------------------------------------------------------- /src/types/qjs_environment.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "engines/js_exception.h" 8 | #include "engines/js_macros.h" 9 | #include "environment.h" 10 | #include "js_wrapper.h" 11 | 12 | using namespace rime; 13 | 14 | template <> 15 | class JsWrapper { 16 | DEFINE_GETTER(Environment, id, obj->getId()) 17 | DEFINE_GETTER(Environment, engine, obj->getEngine()) 18 | DEFINE_GETTER(Environment, namespace, obj->getNameSpace()) 19 | DEFINE_GETTER(Environment, os, obj->getSystemInfo()) 20 | DEFINE_GETTER(Environment, userDataDir, obj->getUserDataDir()) 21 | DEFINE_GETTER(Environment, sharedDataDir, obj->getSharedDataDir()) 22 | 23 | DEFINE_CFUNCTION_ARGC(loadFile, 1, { 24 | std::string path = engine.toStdString(argv[0]); 25 | if (path.empty()) { 26 | throw JsException(JsErrorType::SYNTAX, "The absolutePath argument should be a string"); 27 | } 28 | return engine.wrap(Environment::loadFile(path)); 29 | }) 30 | 31 | DEFINE_CFUNCTION_ARGC(fileExists, 1, { 32 | std::string path = engine.toStdString(argv[0]); 33 | if (path.empty()) { 34 | throw JsException(JsErrorType::SYNTAX, "The absolutePath argument should be a string"); 35 | } 36 | return engine.wrap(Environment::fileExists(path)); 37 | }) 38 | 39 | DEFINE_CFUNCTION(getRimeInfo, { 40 | auto info = Environment::getRimeInfo(); 41 | int64_t bytes = engine.getMemoryUsage(); 42 | if (bytes >= 0) { 43 | info = info + " | " + engine.engineName + " Mem: " + Environment::formatMemoryUsage(bytes); 44 | } 45 | return engine.wrap(info); 46 | }) 47 | 48 | DEFINE_CFUNCTION_ARGC(popen, 1, { 49 | std::string command = engine.toStdString(argv[0]); 50 | if (command.empty()) { 51 | return engine.throwError(JsErrorType::SYNTAX, "The command argument should be a string"); 52 | } 53 | try { 54 | std::string result = Environment::popen(command); 55 | return engine.wrap(result); 56 | } catch (const std::exception& e) { 57 | return engine.throwError(JsErrorType::GENERIC, e.what()); 58 | } 59 | }) 60 | 61 | public: 62 | EXPORT_CLASS_WITH_RAW_POINTER( 63 | Environment, 64 | WITHOUT_CONSTRUCTOR, 65 | WITHOUT_PROPERTIES, 66 | WITH_GETTERS(id, engine, namespace, userDataDir, sharedDataDir, os), 67 | WITH_FUNCTIONS(loadFile, 1, fileExists, 1, getRimeInfo, 0, popen, 1)); 68 | }; 69 | -------------------------------------------------------------------------------- /src/types/qjs_key_event.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "engines/js_macros.h" 5 | #include "js_wrapper.h" 6 | 7 | using namespace rime; 8 | 9 | template <> 10 | class JsWrapper { 11 | DEFINE_GETTER(KeyEvent, shift, obj->shift()) 12 | DEFINE_GETTER(KeyEvent, ctrl, obj->ctrl()) 13 | DEFINE_GETTER(KeyEvent, alt, obj->alt()) 14 | DEFINE_GETTER(KeyEvent, release, obj->release()) 15 | DEFINE_GETTER(KeyEvent, repr, obj->repr()) 16 | 17 | public: 18 | EXPORT_CLASS_WITH_RAW_POINTER(KeyEvent, 19 | WITHOUT_CONSTRUCTOR, 20 | WITHOUT_PROPERTIES, 21 | WITH_GETTERS(shift, ctrl, alt, release, repr), 22 | WITHOUT_FUNCTIONS); 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/qjs_notifier.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "engines/js_macros.h" 6 | #include "js_exception.h" 7 | #include "js_wrapper.h" 8 | #include "qjs_notifier_connection.h" 9 | 10 | using Notifier = rime::signal; 11 | using NotifierConnection = rime::connection; 12 | 13 | template <> 14 | class JsWrapper { 15 | template // <-- make it a template function to satisfy the clang compiler 16 | static void handleNotification(JsEngine& engine, const T& jsFunc, rime::Context* rimeContext) { 17 | auto undefined = engine.toObject(engine.undefined()); 18 | T arg = engine.wrap(rimeContext); 19 | auto result = engine.callFunction(engine.toObject(jsFunc), undefined, 1, &arg); 20 | if (engine.isException(result)) { 21 | LOG(ERROR) << "Error in notifying the js connection"; 22 | } 23 | engine.freeValue(result, arg); 24 | } 25 | 26 | DEFINE_CFUNCTION_ARGC(connect, 1, { 27 | auto jsListenerFunc = argv[0]; 28 | if (!engine.isFunction(jsListenerFunc)) { 29 | const char* msg = "The argument of notifier.connect(arg) should be a function"; 30 | throw new JsException(JsErrorType::TYPE, msg); 31 | } 32 | 33 | // IMPORTANT: jsListenerFunc should be duplicated before passing to JS_Call, 34 | // otherwise it will be released by the quickjs engine and the function will not be called 35 | auto duplicatedFunc = engine.duplicateValue(jsListenerFunc); 36 | 37 | auto obj = engine.unwrap(thisVal); 38 | auto connection = std::make_shared( 39 | // NOTE: duplicatedFunc should be passed by value but not by reference "&duplicatedFunc", 40 | // otherwise it could crash the program when running with the JavaScriptCore engine. 41 | // I guess it could have been released in jsc's garbage collection. 42 | obj->connect([&engine, duplicatedFunc](rime::Context* rimeContext) { 43 | handleNotification(engine, duplicatedFunc, rimeContext); 44 | })); 45 | 46 | auto jsConnection = engine.wrap(connection); 47 | // attach it to the connection to free it by the js engine when disconnecting 48 | engine.setObjectProperty(jsConnection, JS_LISTENER_PROPERTY_NAME, duplicatedFunc); 49 | return jsConnection; 50 | }) 51 | 52 | public: 53 | EXPORT_CLASS_WITH_RAW_POINTER(Notifier, 54 | WITHOUT_CONSTRUCTOR, 55 | WITHOUT_PROPERTIES, 56 | WITHOUT_GETTERS, 57 | WITH_FUNCTIONS(connect, 1)); 58 | }; 59 | -------------------------------------------------------------------------------- /src/types/qjs_notifier_connection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "engines/js_macros.h" 6 | #include "js_wrapper.h" 7 | 8 | using NotifierConnection = rime::connection; 9 | 10 | constexpr const char* JS_LISTENER_PROPERTY_NAME = "jsListenerFunc"; 11 | 12 | template <> 13 | class JsWrapper { 14 | DEFINE_CFUNCTION(disconnect, { 15 | auto obj = engine.unwrap(thisVal); 16 | obj->disconnect(); 17 | auto jsListenerFunc = engine.getObjectProperty(thisVal, JS_LISTENER_PROPERTY_NAME); 18 | engine.freeValue(jsListenerFunc); 19 | return engine.undefined(); 20 | }) 21 | 22 | DEFINE_GETTER(NotifierConnection, isConnected, obj->connected()) 23 | 24 | public: 25 | EXPORT_CLASS_WITH_SHARED_POINTER(NotifierConnection, 26 | WITHOUT_CONSTRUCTOR, 27 | WITHOUT_PROPERTIES, 28 | WITH_GETTERS(isConnected), 29 | WITH_FUNCTIONS(disconnect, 0)); 30 | }; 31 | -------------------------------------------------------------------------------- /src/types/qjs_os_info.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "engines/js_macros.h" 4 | #include "js_wrapper.h" 5 | #include "misc/system_info.hpp" 6 | 7 | template <> 8 | class JsWrapper { 9 | DEFINE_GETTER(SystemInfo, name, obj->getOSName()); 10 | DEFINE_GETTER(SystemInfo, version, obj->getOSVersion()); 11 | DEFINE_GETTER(SystemInfo, architecture, obj->getArchitecture()); 12 | 13 | public: 14 | EXPORT_CLASS_WITH_RAW_POINTER(SystemInfo, 15 | WITHOUT_CONSTRUCTOR, 16 | WITHOUT_PROPERTIES, 17 | WITH_GETTERS(name, version, architecture), 18 | WITHOUT_FUNCTIONS); 19 | }; 20 | -------------------------------------------------------------------------------- /src/types/qjs_preedit.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "engines/js_macros.h" 5 | #include "js_wrapper.h" 6 | 7 | using namespace rime; 8 | 9 | template <> 10 | class JsWrapper { 11 | DEFINE_GETTER(Preedit, text, obj->text) 12 | DEFINE_GETTER(Preedit, caretPos, obj->caret_pos) 13 | DEFINE_GETTER(Preedit, selectStart, obj->sel_start) 14 | DEFINE_GETTER(Preedit, selectEnd, obj->sel_end) 15 | 16 | DEFINE_STRING_SETTER(Preedit, text, obj->text = str) 17 | DEFINE_SETTER(Preedit, caretPos, engine.toInt, obj->caret_pos = value) 18 | DEFINE_SETTER(Preedit, selectStart, engine.toInt, obj->sel_start = value) 19 | DEFINE_SETTER(Preedit, selectEnd, engine.toInt, obj->sel_end = value) 20 | 21 | public: 22 | EXPORT_CLASS_WITH_SHARED_POINTER(Preedit, 23 | WITHOUT_CONSTRUCTOR, 24 | WITH_PROPERTIES(text, caretPos, selectStart, selectEnd), 25 | WITHOUT_GETTERS, 26 | WITHOUT_FUNCTIONS); 27 | }; 28 | -------------------------------------------------------------------------------- /src/types/qjs_schema.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "engines/js_macros.h" 5 | #include "js_wrapper.h" 6 | 7 | using namespace rime; 8 | 9 | template <> 10 | class JsWrapper { 11 | DEFINE_GETTER(Schema, id, obj->schema_id()) 12 | DEFINE_GETTER(Schema, name, obj->schema_name()) 13 | DEFINE_GETTER(Schema, config, obj->config()) 14 | DEFINE_GETTER(Schema, pageSize, obj->page_size()) 15 | DEFINE_GETTER(Schema, selectKeys, obj->select_keys()) 16 | 17 | public: 18 | EXPORT_CLASS_WITH_RAW_POINTER(Schema, 19 | WITHOUT_CONSTRUCTOR, 20 | WITHOUT_PROPERTIES, 21 | WITH_GETTERS(id, name, config, pageSize, selectKeys), 22 | WITHOUT_FUNCTIONS); 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/qjs_segment.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "engines/js_macros.h" 6 | #include "js_wrapper.h" 7 | #include "types/qjs_candidate.h" 8 | 9 | using namespace rime; 10 | 11 | template <> 12 | class JsWrapper { 13 | DEFINE_GETTER(Segment, selectedIndex, obj->selected_index) 14 | DEFINE_SETTER(Segment, selectedIndex, engine.toInt, obj->selected_index = value) 15 | 16 | DEFINE_GETTER(Segment, prompt, obj->prompt) 17 | DEFINE_STRING_SETTER(Segment, prompt, obj->prompt = str) 18 | 19 | DEFINE_GETTER(Segment, start, obj->start) 20 | DEFINE_GETTER(Segment, end, obj->end) 21 | DEFINE_GETTER(Segment, selectedCandidate, obj->GetSelectedCandidate()) 22 | DEFINE_GETTER(Segment, candidateSize, obj->menu->candidate_count()) 23 | 24 | DEFINE_CFUNCTION_ARGC(getCandidateAt, 1, { 25 | auto obj = engine.unwrap(thisVal); 26 | int32_t index = engine.toInt(argv[0]); 27 | if (index < 0 || size_t(index) >= obj->menu->candidate_count()) { 28 | return engine.null(); 29 | } 30 | return engine.wrap(obj->menu->GetCandidateAt(index)); 31 | }) 32 | 33 | DEFINE_CFUNCTION_ARGC(hasTag, 1, { 34 | auto obj = engine.unwrap(thisVal); 35 | std::string tag = engine.toStdString(argv[0]); 36 | return engine.wrap(obj->HasTag(tag)); 37 | }) 38 | 39 | public: 40 | EXPORT_CLASS_WITH_RAW_POINTER(Segment, 41 | WITHOUT_CONSTRUCTOR, 42 | WITH_PROPERTIES(selectedIndex, prompt), 43 | WITH_GETTERS(start, end, selectedCandidate, candidateSize), 44 | WITH_FUNCTIONS(getCandidateAt, 1, hasTag, 1)); 45 | }; 46 | -------------------------------------------------------------------------------- /src/types/qjs_trie.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "dicts/trie.h" 5 | #include "engines/js_macros.h" 6 | #include "js_wrapper.h" 7 | #include "types/qjs_leveldb.h" 8 | 9 | using namespace rime; 10 | 11 | template <> 12 | class JsWrapper { 13 | DEFINE_CFUNCTION_ARGC(loadTextFile, 1, { 14 | std::string absolutePath = engine.toStdString(argv[0]); 15 | ParseTextFileOptions options; 16 | if (argc > 1) { 17 | options = parseTextFileOptions(engine, argv[1]); 18 | } 19 | 20 | auto obj = engine.unwrap(thisVal); 21 | try { 22 | obj->loadTextFile(absolutePath, options); 23 | } catch (const std::exception& e) { 24 | LOG(ERROR) << "loadTextFile of " << absolutePath << " failed: " << e.what(); 25 | return engine.throwError(JsErrorType::GENERIC, e.what()); 26 | } 27 | 28 | return engine.undefined(); 29 | }) 30 | 31 | DEFINE_CFUNCTION_ARGC(loadBinaryFile, 1, { 32 | std::string absolutePath = engine.toStdString(argv[0]); 33 | auto obj = engine.unwrap(thisVal); 34 | try { 35 | obj->loadBinaryFile(absolutePath); 36 | } catch (const std::exception& e) { 37 | LOG(ERROR) << "loadBinaryFileMmap of " << absolutePath << " failed: " << e.what(); 38 | return engine.throwError(JsErrorType::GENERIC, e.what()); 39 | } 40 | return engine.undefined(); 41 | }) 42 | 43 | DEFINE_CFUNCTION_ARGC(saveToBinaryFile, 1, { 44 | std::string absolutePath = engine.toStdString(argv[0]); 45 | auto obj = engine.unwrap(thisVal); 46 | try { 47 | obj->saveToBinaryFile(absolutePath); 48 | } catch (const std::exception& e) { 49 | LOG(ERROR) << "saveToBinaryFile of " << absolutePath << " failed: " << e.what(); 50 | return engine.throwError(JsErrorType::GENERIC, e.what()); 51 | } 52 | return engine.undefined(); 53 | }) 54 | 55 | DEFINE_CFUNCTION_ARGC(find, 1, { 56 | std::string key = engine.toStdString(argv[0]); 57 | auto obj = engine.unwrap(thisVal); 58 | auto result = obj->find(key); 59 | return result.has_value() ? engine.wrap(result.value()) : engine.null(); 60 | }) 61 | 62 | DEFINE_CFUNCTION_ARGC(prefixSearch, 1, { 63 | std::string prefix = engine.toStdString(argv[0]); 64 | auto obj = engine.unwrap(thisVal); 65 | auto matches = obj->prefixSearch(prefix); 66 | 67 | auto jsArray = engine.newArray(); 68 | for (size_t i = 0; i < matches.size(); ++i) { 69 | auto jsObject = engine.newObject(); 70 | engine.setObjectProperty(jsObject, "text", engine.wrap(matches[i].first)); 71 | engine.setObjectProperty(jsObject, "info", engine.wrap(matches[i].second)); 72 | engine.insertItemToArray(jsArray, i, jsObject); 73 | } 74 | return jsArray; 75 | }) 76 | DEFINE_CFUNCTION(makeTrie, { return engine.wrap(std::make_shared()); }) 77 | 78 | public: 79 | EXPORT_CLASS_WITH_SHARED_POINTER(Trie, 80 | WITH_CONSTRUCTOR(makeTrie, 0), 81 | WITHOUT_PROPERTIES, 82 | WITHOUT_GETTERS, 83 | WITH_FUNCTIONS(loadTextFile, 84 | 1, 85 | loadBinaryFile, 86 | 1, 87 | saveToBinaryFile, 88 | 1, 89 | find, 90 | 1, 91 | prefixSearch, 92 | 1)); 93 | }; 94 | -------------------------------------------------------------------------------- /src/types/qjs_types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "engines/common.h" 4 | #include "qjs_candidate.h" 5 | #include "qjs_candidate_iterator.h" 6 | #include "qjs_commit_history.h" 7 | #include "qjs_commit_record.h" 8 | #include "qjs_config.h" 9 | #include "qjs_config_item.h" 10 | #include "qjs_config_list.h" 11 | #include "qjs_config_map.h" 12 | #include "qjs_config_value.h" 13 | #include "qjs_context.h" 14 | #include "qjs_engine.h" 15 | #include "qjs_environment.h" 16 | #include "qjs_key_event.h" 17 | #include "qjs_leveldb.h" 18 | #include "qjs_notifier.h" 19 | #include "qjs_notifier_connection.h" 20 | #include "qjs_os_info.h" 21 | #include "qjs_preedit.h" 22 | #include "qjs_schema.h" 23 | #include "qjs_segment.h" 24 | #include "qjs_trie.h" 25 | 26 | template 27 | void registerTypesToJsEngine() { 28 | JsEngine& engine = JsEngine::instance(); 29 | DLOG(INFO) << "[qjs] registering rime types to the " << engine.engineName << " engine..."; 30 | 31 | // expose all types 32 | engine.template registerType(); 33 | engine.template registerType(); 34 | engine.template registerType(); 35 | engine.template registerType(); 36 | engine.template registerType(); 37 | engine.template registerType(); 38 | engine.template registerType(); 39 | engine.template registerType(); 40 | engine.template registerType(); 41 | engine.template registerType(); 42 | engine.template registerType(); 43 | engine.template registerType(); 44 | engine.template registerType(); 45 | engine.template registerType(); 46 | engine.template registerType(); 47 | engine.template registerType(); 48 | engine.template registerType(); 49 | engine.template registerType(); 50 | engine.template registerType(); 51 | engine.template registerType(); 52 | engine.template registerType(); 53 | } 54 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | enable_testing() 2 | 3 | set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../build) 4 | 5 | set(JSC_TEST_FILES "") 6 | if(ENABLE_JAVASCRIPTCORE) 7 | file(GLOB JSC_TEST_FILES "jsc/*.test.cpp") 8 | endif() 9 | 10 | file(GLOB QJS_TEST_FILES "qjs/*.test.cpp") 11 | file(GLOB QJS_GEAR_TEST_FILES "*.test.cpp") 12 | add_executable(librime-qjs-tests ${QJS_TEST_FILES} ${QJS_GEAR_TEST_FILES} ${JSC_TEST_FILES}) 13 | 14 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}) 15 | 16 | if(WIN32) 17 | target_compile_definitions(librime-qjs-tests PRIVATE RIME_IMPORTS) 18 | endif() 19 | 20 | find_package(GTest REQUIRED) 21 | 22 | target_link_libraries(librime-qjs-tests PUBLIC 23 | ${rime_library} 24 | qjs 25 | librime-qjs-objs 26 | GTest::gtest 27 | ${Marisa_LIBRARY} 28 | ) 29 | 30 | if(ENABLE_JAVASCRIPTCORE) 31 | find_library(JAVASCRIPTCORE JavaScriptCore REQUIRED) 32 | include_directories(${JAVASCRIPTCORE}/Headers) 33 | target_link_libraries(librime-qjs-tests PRIVATE ${JAVASCRIPTCORE}) 34 | endif() 35 | 36 | if(WIN32) 37 | file(GLOB rime_dll ${CMAKE_SOURCE_DIR}/dist/lib/*.dll) 38 | file(COPY ${rime_dll} DESTINATION ${EXECUTABLE_OUTPUT_PATH}) 39 | endif() 40 | 41 | add_test(NAME librime-qjs-tests 42 | COMMAND librime-qjs-tests 43 | WORKING_DIRECTORY ${EXECUTABLE_OUTPUT_PATH}) 44 | 45 | 46 | # set(benchmark_files "benchmark/dict/load_map_benchmark.cc") 47 | # add_executable(load-dict-benchmark ${benchmark_files}) 48 | # target_link_libraries(load-dict-benchmark 49 | # librime-qjs-objs 50 | # ${rime_library} 51 | # ${rime_dict_library} 52 | # ${rime_gears_library} 53 | # ${GTEST_LIBRARIES} 54 | # ) 55 | -------------------------------------------------------------------------------- /tests/component.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "environment.h" 7 | #include "qjs_component.hpp" 8 | #include "qjs_filter.hpp" 9 | #include "test_switch.h" 10 | 11 | using namespace rime; 12 | 13 | template 14 | class MockFilter { 15 | public: 16 | MockFilter(const MockFilter&) = delete; 17 | MockFilter(MockFilter&&) = delete; 18 | MockFilter& operator=(const MockFilter&) = delete; 19 | MockFilter& operator=(MockFilter&&) = delete; 20 | 21 | explicit MockFilter(const Ticket& ticket, Environment* environment) { 22 | LOG(INFO) << "MockFilter created with ticket: " << ticket.name_space; 23 | }; 24 | ~MockFilter() { LOG(INFO) << "MockFilter destroyed"; } 25 | 26 | // NOLINTNEXTLINE(readability-convert-member-functions-to-static) 27 | an apply(an translation, Environment* environment) { 28 | return translation; 29 | } 30 | }; 31 | 32 | template 33 | class QuickJSComponentTest : public ::testing::Test {}; 34 | 35 | SETUP_JS_ENGINES(QuickJSComponentTest); 36 | 37 | // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 38 | TYPED_TEST(QuickJSComponentTest, ShareComponentAcrossRimeSessions) { 39 | QuickJSComponent, Filter, TypeParam> component; 40 | 41 | the engine1(Engine::Create()); 42 | Ticket ticket(engine1.get(), "test_namespace", "test"); 43 | 44 | auto* instance1 = component.Create(ticket); 45 | auto actualInstance1 = instance1->actual(); 46 | delete instance1; // Rime session 1 ends 47 | 48 | auto* instance2 = component.Create(ticket); 49 | ASSERT_EQ(actualInstance1, instance2->actual()) 50 | << "delete instance1 should not destroy the actual filter instance"; 51 | delete instance2; // Rime session 2 ends 52 | 53 | the engine2(Engine::Create()); 54 | Ticket ticket2(engine2.get(), "test_namespace", "test"); 55 | auto* instance3 = component.Create(ticket2); 56 | ASSERT_EQ(actualInstance1, instance3->actual()) 57 | << "delete instance1 should not destroy the actual filter instance"; 58 | delete instance3; // Rime session 3 ends 59 | } 60 | 61 | // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 62 | TYPED_TEST(QuickJSComponentTest, CreateComponent) { 63 | QuickJSComponent, Filter, TypeParam> component; 64 | 65 | the engine1(Engine::Create()); 66 | Ticket ticket(engine1.get(), "test_namespace", "test"); 67 | auto* instance1 = component.Create(ticket); 68 | ASSERT_NE(nullptr, instance1); 69 | 70 | auto* instance2 = component.Create(ticket); 71 | ASSERT_EQ(instance1->actual(), instance2->actual()) 72 | << "should return the same actual filter with the same ticket"; 73 | 74 | the engine2(Engine::Create()); 75 | Ticket ticket2(engine2.get(), "test_namespace", "test"); 76 | auto* instance3 = component.Create(ticket2); 77 | ASSERT_EQ(instance1->actual(), instance3->actual()) 78 | << "should return the same actual filter with the same ticket namespace"; 79 | 80 | the engine3(Engine::Create()); 81 | Ticket ticket3(engine3.get(), "test_namespace2", "test"); 82 | auto* instance4 = component.Create(ticket3); 83 | ASSERT_TRUE(instance4 != nullptr); 84 | ASSERT_NE(instance1->actual(), instance4->actual()) 85 | << "should create a new instance with a different ticket namespace"; 86 | 87 | delete instance4; 88 | delete instance3; 89 | delete instance2; 90 | delete instance1; 91 | } 92 | -------------------------------------------------------------------------------- /tests/dict_data_helper.hpp: -------------------------------------------------------------------------------- 1 | #ifndef RIME_TRIEDATAHELPER_H_ 2 | #define RIME_TRIEDATAHELPER_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "dicts/dictionary.h" 12 | 13 | class DictionaryDataHelper { 14 | public: 15 | DictionaryDataHelper(const char* folder, const char* dummyFileName) { 16 | if (dummyFileName == nullptr) { 17 | return; 18 | } 19 | 20 | txtPath_ = std::string(folder) + "/" + dummyFileName; 21 | binaryPath_ = std::string(folder) + "/" + "dummy.bin"; 22 | mergedBinaryPath_ = std::string(folder) + "/" + "merged-dummy.bin"; 23 | levelDbFolderPath_ = std::string(folder) + "/" + "leveldb"; 24 | } 25 | 26 | static void testSearchItems(const Dictionary& dict) { 27 | testExistingWords(dict); 28 | testNonExistingWords(dict); 29 | testPrefixSearch(dict); 30 | } 31 | 32 | static void testExistingWords(const Dictionary& dict) { 33 | auto result1 = dict.find("accord"); 34 | ASSERT_TRUE(result1.has_value()); 35 | if (result1.has_value()) { 36 | EXPECT_EQ(result1.value(), "[ә'kɒ:d]; n. 一致, 调和, 协定\\n vt. 给与, 使一致\\n vi. 相符合"); 37 | } 38 | 39 | auto result2 = dict.find("accordion"); 40 | ASSERT_TRUE(result2.has_value()); 41 | if (result2.has_value()) { 42 | EXPECT_EQ(result2.value(), "[ә'kɒ:djәn]; n. 手风琴\\n a. 可折叠的"); 43 | } 44 | } 45 | 46 | static void testNonExistingWords(const Dictionary& dict) { 47 | auto result = dict.find("nonexistent-word"); 48 | ASSERT_FALSE(result.has_value()); 49 | } 50 | 51 | static void testPrefixSearch(const Dictionary& dict) { 52 | auto prefixResults = dict.prefixSearch("accord"); 53 | ASSERT_FALSE(prefixResults.empty()); 54 | EXPECT_EQ(prefixResults.size(), 6); 55 | } 56 | 57 | std::string txtPath_; 58 | std::string binaryPath_; 59 | std::string mergedBinaryPath_; 60 | std::string levelDbFolderPath_; 61 | 62 | static constexpr size_t ENTRY_SIZE = 6; 63 | size_t entrySize_ = ENTRY_SIZE; 64 | 65 | void createDummyTextFile() const { 66 | std::cout << "Creating a dummy text file: " << txtPath_ << '\n'; 67 | 68 | std::ofstream testDict(txtPath_); 69 | testDict << "accord [ә'kɒ:d]; n. 一致, 调和, 协定\\n vt. 给与, " 70 | "使一致\\n vi. 相符合\n"; 71 | testDict << "accordance [ә'kɒ:dәns]; n. 一致, 和谐\n"; 72 | testDict << "according [ә'kɒ:diŋ]; a. 相符的, 根据...而定的\\n adv. 相应地\n"; 73 | testDict << "accordingly [ә'kɒ:diŋli]; adv. 相应地, 因此, 于是\n"; 74 | testDict << "accordion [ә'kɒ:djәn]; n. 手风琴\\n a. 可折叠的\n"; 75 | testDict << "accordionist [ә'kɒ:djәnist]; n. 手风琴师\n"; 76 | testDict.close(); 77 | } 78 | 79 | void cleanupDummyFiles() const { 80 | std::cout << "Removing the dummy files\n"; 81 | 82 | std::remove(txtPath_.c_str()); 83 | std::remove(binaryPath_.c_str()); 84 | std::remove((binaryPath_ + ".trie").c_str()); 85 | std::remove(mergedBinaryPath_.c_str()); 86 | std::filesystem::remove_all(levelDbFolderPath_); 87 | } 88 | }; 89 | 90 | #endif // RIME_TRIEDATAHELPER_H_ 91 | -------------------------------------------------------------------------------- /tests/dictionary.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "dict_data_helper.hpp" 5 | #include "dicts/leveldb.h" 6 | #include "dicts/trie.h" 7 | 8 | #include "test_helper.hpp" 9 | 10 | class DictionaryTest : public ::testing::Test { 11 | private: 12 | DictionaryDataHelper dictHelper_ = 13 | DictionaryDataHelper(getFolderPath(__FILE__).c_str(), "dummy_dict.txt"); 14 | 15 | protected: 16 | DictionaryDataHelper getDictHelper() { return dictHelper_; } 17 | void SetUp() override { dictHelper_.createDummyTextFile(); } 18 | 19 | void TearDown() override { dictHelper_.cleanupDummyFiles(); } 20 | }; 21 | 22 | TEST_F(DictionaryTest, LoadTextFileAndLookupWithTrie) { 23 | rime::Trie trie; 24 | auto helper = getDictHelper(); 25 | ParseTextFileOptions options; 26 | options.lines = helper.entrySize_; 27 | trie.loadTextFile(helper.txtPath_, options); 28 | DictionaryDataHelper::testSearchItems(trie); 29 | 30 | // save to file and load it back 31 | trie.saveToBinaryFile(helper.mergedBinaryPath_); 32 | rime::Trie trie2; 33 | trie2.loadBinaryFile(helper.mergedBinaryPath_); 34 | DictionaryDataHelper::testSearchItems(trie2); 35 | } 36 | 37 | TEST_F(DictionaryTest, LoadTextFileAndLookupWithLevelDb) { 38 | auto helper = getDictHelper(); 39 | 40 | LevelDb dict; 41 | ParseTextFileOptions options; 42 | options.lines = helper.entrySize_; 43 | dict.loadTextFile(helper.txtPath_, options); 44 | dict.saveToBinaryFile(helper.levelDbFolderPath_); 45 | DictionaryDataHelper::testSearchItems(dict); 46 | dict.close(); // a level db could be loaded only once 47 | 48 | LevelDb dict2; 49 | dict2.loadBinaryFile(helper.levelDbFolderPath_); 50 | DictionaryDataHelper::testSearchItems(dict2); 51 | } 52 | -------------------------------------------------------------------------------- /tests/fake_translation.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FAKE_TRANSLATION_H_ 2 | #define FAKE_TRANSLATION_H_ 3 | 4 | #include 5 | #include 6 | 7 | using namespace rime; 8 | 9 | class FakeTranslation : public Translation { 10 | public: 11 | FakeTranslation() { set_exhausted(true); } 12 | bool Next() override { 13 | if (exhausted()) { 14 | return false; 15 | } 16 | set_exhausted(++iter_ >= candidates_.size()); 17 | return true; 18 | } 19 | 20 | an Peek() override { return candidates_[iter_]; } 21 | 22 | void append(const an& candidate) { 23 | candidates_.push_back(candidate); 24 | set_exhausted(iter_ >= candidates_.size()); 25 | } 26 | 27 | private: 28 | std::vector> candidates_; 29 | size_t iter_ = 0; 30 | }; 31 | 32 | #endif // FAKE_TRANSLATION_H_ 33 | -------------------------------------------------------------------------------- /tests/filter.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "fake_translation.hpp" 10 | #include "qjs_filter.hpp" 11 | #include "test_switch.h" 12 | 13 | using namespace rime; 14 | 15 | template 16 | class QuickJSFilterTest : public ::testing::Test { 17 | public: 18 | static an createMockTranslation() { 19 | auto translation = New(); 20 | translation->append(New("mock", 0, 1, "text1", "comment1")); 21 | translation->append(New("mock", 0, 1, "text2", "comment2")); 22 | translation->append(New("mock", 0, 1, "text3", "comment3")); 23 | return translation; 24 | } 25 | }; 26 | 27 | SETUP_JS_ENGINES(QuickJSFilterTest); 28 | 29 | template 30 | // NOLINTNEXTLINE(readability-function-cognitive-complexity) 31 | static std::shared_ptr doFilterInJs(const std::string& nameSpace) { 32 | the engine(Engine::Create()); 33 | 34 | auto* config = engine->schema()->config(); 35 | config->SetString("greet", "hello from c++"); 36 | config->SetString("expectingText", "text2"); 37 | 38 | Ticket ticket(engine.get(), "filter", std::string("qjs_filter@") + nameSpace); 39 | auto env = std::make_unique(engine.get(), nameSpace); 40 | auto filter = New>(ticket, env.get()); 41 | auto translation = QuickJSFilterTest::createMockTranslation(); 42 | return filter->apply(translation, env.get()); 43 | } 44 | 45 | static void checkFilteredValues(const std::shared_ptr& filtered) { 46 | ASSERT_TRUE(filtered != nullptr); 47 | 48 | auto candidate = filtered->Peek(); 49 | ASSERT_TRUE(candidate != nullptr); 50 | ASSERT_EQ(candidate->text(), "text2"); 51 | 52 | filtered->Next(); 53 | EXPECT_TRUE(filtered->exhausted()); 54 | candidate = filtered->Peek(); 55 | ASSERT_TRUE(candidate == nullptr); 56 | ASSERT_FALSE(filtered->Next()); 57 | } 58 | 59 | TYPED_TEST(QuickJSFilterTest, ApplyFilter) { 60 | auto filtered = doFilterInJs("filter_test"); 61 | checkFilteredValues(filtered); 62 | } 63 | 64 | TYPED_TEST(QuickJSFilterTest, TestRestartEngine) { 65 | JsEngine::shutdown(); 66 | 67 | std::filesystem::path path(rime_get_api()->get_user_data_dir()); 68 | path.append("js"); 69 | 70 | auto& engine = JsEngine::instance(); 71 | registerTypesToJsEngine(); 72 | engine.setBaseFolderPath(path.generic_string().c_str()); 73 | 74 | auto filtered = doFilterInJs("filter_test"); 75 | checkFilteredValues(filtered); 76 | } 77 | 78 | TYPED_TEST(QuickJSFilterTest, CheckAppblicable) { 79 | auto source = QuickJSFilterTest::createMockTranslation(); 80 | auto filtered = doFilterInJs("filter_is_applicable"); 81 | 82 | while (!source->exhausted()) { 83 | ASSERT_FALSE(filtered->exhausted()); 84 | ASSERT_STREQ(filtered->Peek()->text().c_str(), source->Peek()->text().c_str()); 85 | source->Next(); 86 | filtered->Next(); 87 | } 88 | ASSERT_TRUE(filtered->exhausted()); 89 | } 90 | 91 | TYPED_TEST(QuickJSFilterTest, TestFastFilter) { 92 | auto filtered = doFilterInJs("fast_filter"); 93 | 94 | ASSERT_FALSE(filtered->exhausted()); 95 | ASSERT_STREQ(filtered->Peek()->text().c_str(), "text1"); 96 | ASSERT_TRUE(filtered->Next()); 97 | ASSERT_STREQ(filtered->Peek()->text().c_str(), "text3"); 98 | ASSERT_FALSE(filtered->Next()); 99 | ASSERT_TRUE(filtered->exhausted()); 100 | ASSERT_TRUE(filtered->Peek() == nullptr); 101 | } 102 | 103 | TYPED_TEST(QuickJSFilterTest, TestFastFilterReturnIterator) { 104 | auto filtered = doFilterInJs("fast_filter.iterator"); 105 | auto expected = QuickJSFilterTest::createMockTranslation(); 106 | 107 | while (!expected->exhausted()) { 108 | ASSERT_FALSE(filtered->exhausted()); 109 | ASSERT_STREQ(filtered->Peek()->text().c_str(), expected->Peek()->text().c_str()); 110 | expected->Next(); 111 | filtered->Next(); 112 | } 113 | ASSERT_TRUE(filtered->exhausted()); 114 | } 115 | -------------------------------------------------------------------------------- /tests/js/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "endOfLine": "lf", 10 | "printWidth": 110 11 | } 12 | -------------------------------------------------------------------------------- /tests/js/bundle.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // usage: `node bundle.js [target]` 4 | // target: all (default), jsc, qjs 5 | 6 | import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs' 7 | import { join, dirname, basename } from 'path' 8 | import { fileURLToPath } from 'url' 9 | import { execSync } from 'child_process' 10 | 11 | const cwd = dirname(fileURLToPath(import.meta.url)) 12 | const args = process.argv.slice(2) 13 | const target = args[0] || 'all' 14 | 15 | // ========== main logic starts ========== 16 | createDistDirIfNotExists() 17 | 18 | const files = readdirSync(cwd).filter((f) => /\.(js|ts|cjs|mjs)$/.test(f) && f !== 'bundle.js') 19 | if (target !== 'jsc') { 20 | // QuickJS leaks memory with the IIFE formatted code, bundle it with the ESM format 21 | // ESM format `class MyFilter {}; export { MyFilter };` 22 | // https://esbuild.github.io/api/#format-esm 23 | bundleToESM(files) 24 | } 25 | if (target !== 'qjs') { 26 | // JavaScriptCore does not support ESM, bundle it with the IIFE format 27 | // IIFE format`;(()=>{ var MyFilter = class {};...; this.instance = MyFilter();}()` 28 | // https://esbuild.github.io/api/#format-iife 29 | bundleToIIFE(files) 30 | } 31 | 32 | // Format all bundled files 33 | execSync('prettier --config .prettierrc --write "dist/**/*.js"', { stdio: 'inherit', cwd }) 34 | 35 | // ========== main logic ends ========== 36 | 37 | function createDistDirIfNotExists() { 38 | const distDir = join(cwd, 'dist') 39 | if (!existsSync(distDir)) { 40 | mkdirSync(distDir) 41 | } 42 | } 43 | 44 | function getBundleOptions(format) { 45 | const esbuildOptions = [ 46 | '--bundle', 47 | '--platform=browser', 48 | '--allow-overwrite=true', // overwrite the output file 49 | '--tree-shaking=true', // remove unused code 50 | '--minify-whitespace', // remove comments and line breaks 51 | '--target=es2022', 52 | ] 53 | if (format === 'iife') { 54 | esbuildOptions.push('--format=iife') 55 | } else if (format === 'esm') { 56 | esbuildOptions.push('--format=esm') 57 | } 58 | return esbuildOptions 59 | } 60 | 61 | function bundleToESM(files) { 62 | console.log('Bundling the plugins to ESM format to run in QuickJS...') 63 | const options = getBundleOptions('esm').join(' ') 64 | files.forEach((f) => { 65 | execSync(`esbuild ${f} --outfile="./dist/${f.replace(/\.(js|ts|mjs|cjs)$/, '.esm.js')}" ${options}`, { 66 | stdio: 'inherit', 67 | cwd, 68 | }) 69 | }) 70 | } 71 | 72 | function bundleToIIFE(files) { 73 | console.log('Bundling the plugins to IIFE format to run in JavaScriptCore...') 74 | const options = getBundleOptions('iife').join(' ') 75 | files.forEach((file) => { 76 | const className = extractExportingPluginName(file) 77 | if (!className) { 78 | console.log(`No class exported in ${file}, skipping...`) 79 | return 80 | } 81 | 82 | const distFile = join(cwd, 'dist', file.replace(/\.(js|ts|mjs|cjs)$/, '.iife.js')) 83 | execSync(`esbuild ${file} --outfile=${distFile} ${options}`, { 84 | stdio: 'inherit', 85 | cwd, 86 | }) 87 | injectPluginInitialization(distFile, className) 88 | }) 89 | } 90 | 91 | // find the exporting class name: `export class TestProcessor {` 92 | function extractExportingPluginName(file) { 93 | const fileContent = readFileSync(join(cwd, file), 'utf8') 94 | const classMatch = fileContent.match(/export\s*class\s*(\w*)\s*{/) || [] 95 | return classMatch[1] 96 | } 97 | 98 | // append `globalThis.${instanceName}= new ${className}()\n}` to the end of the file, before `})();` 99 | function injectPluginInitialization(file, className) { 100 | const filenName = basename(file) 101 | const instanceName = 'iife_instance_' + filenName.replace(/[.-]/g, '_') 102 | const fileContent = readFileSync(file, 'utf8') 103 | const newContent = fileContent.replace( 104 | /\}\)\(\);?\s*$/m, 105 | `globalThis.${instanceName} = new ${className}()\n})()`, 106 | ) 107 | writeFileSync(file, newContent) 108 | } 109 | -------------------------------------------------------------------------------- /tests/js/dist/fast_filter.iife.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | var FastFilterWithGenerator = class { 3 | *filter(iter, env) { 4 | for (let idx = 0, candidate; (candidate = iter.next()); ++idx) { 5 | console.log(`checking candidate at index = ${idx}`) 6 | if (idx % 2 === 0) { 7 | yield candidate 8 | } 9 | } 10 | } 11 | } 12 | globalThis.iife_instance_fast_filter_iife_js = new FastFilterWithGenerator() 13 | })() 14 | -------------------------------------------------------------------------------- /tests/js/dist/fast_filter.iterator.iife.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | var FastFilterReturnIterator = class { 3 | *filter(iter, env) { 4 | return iter 5 | } 6 | } 7 | globalThis.iife_instance_fast_filter_iterator_iife_js = new FastFilterReturnIterator() 8 | })() 9 | -------------------------------------------------------------------------------- /tests/js/dist/filter_is_applicable.iife.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | var FilterWithIsApplicable = class { 3 | isApplicable(env) { 4 | console.log('filter_test isApplicable') 5 | return false 6 | } 7 | filter(candidates, env) { 8 | throw new Error('should not be called as isApplicable returns false') 9 | } 10 | } 11 | globalThis.iife_instance_filter_is_applicable_iife_js = new FilterWithIsApplicable() 12 | })() 13 | -------------------------------------------------------------------------------- /tests/js/dist/filter_test.esm.js: -------------------------------------------------------------------------------- 1 | var totalTests = 0 2 | var passedTests = 0 3 | function assert(condition, message = '') { 4 | totalTests++ 5 | if (condition) { 6 | passedTests++ 7 | console.log('\u2713 ' + message) 8 | } else { 9 | console.log('\u2717 ' + message) 10 | console.log(' Expected true, but got false') 11 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 12 | } 13 | } 14 | var TestFilter = class { 15 | constructor(env) { 16 | console.log('filter_test init') 17 | assert(env.namespace === 'filter_test') 18 | assert(env.userDataDir.endsWith('qjs/tests/')) 19 | console.log(`env = ${env}`) 20 | console.log(`env.engine.schema = ${env.engine.schema}`) 21 | console.log(`env.engine.schema.config = ${env.engine.schema.config}`) 22 | const config = env.engine.schema.config 23 | assert(config.getString('greet') === 'hello from c++') 24 | } 25 | finalizer() { 26 | console.log('filter_test finit') 27 | } 28 | filter(candidates, env) { 29 | console.log('filter_test filter', candidates.length) 30 | assert(env.namespace === 'filter_test') 31 | const config = env.engine.schema.config 32 | assert(config.getString('greet') === 'hello from c++') 33 | const expectingText = config.getString('expectingText') 34 | assert(expectingText === 'text2') 35 | return candidates.filter((it) => it.text === expectingText) 36 | } 37 | } 38 | export { TestFilter } 39 | -------------------------------------------------------------------------------- /tests/js/dist/filter_test.iife.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | var totalTests = 0 3 | var passedTests = 0 4 | function assert(condition, message = '') { 5 | totalTests++ 6 | if (condition) { 7 | passedTests++ 8 | console.log('\u2713 ' + message) 9 | } else { 10 | console.log('\u2717 ' + message) 11 | console.log(' Expected true, but got false') 12 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 13 | } 14 | } 15 | var TestFilter = class { 16 | constructor(env) { 17 | console.log('filter_test init') 18 | assert(env.namespace === 'filter_test') 19 | assert(env.userDataDir.endsWith('qjs/tests/')) 20 | console.log(`env = ${env}`) 21 | console.log(`env.engine.schema = ${env.engine.schema}`) 22 | console.log(`env.engine.schema.config = ${env.engine.schema.config}`) 23 | const config = env.engine.schema.config 24 | assert(config.getString('greet') === 'hello from c++') 25 | } 26 | finalizer() { 27 | console.log('filter_test finit') 28 | } 29 | filter(candidates, env) { 30 | console.log('filter_test filter', candidates.length) 31 | assert(env.namespace === 'filter_test') 32 | const config = env.engine.schema.config 33 | assert(config.getString('greet') === 'hello from c++') 34 | const expectingText = config.getString('expectingText') 35 | assert(expectingText === 'text2') 36 | return candidates.filter((it) => it.text === expectingText) 37 | } 38 | } 39 | globalThis.iife_instance_filter_test_iife_js = new TestFilter() 40 | })() 41 | -------------------------------------------------------------------------------- /tests/js/dist/help_menu.esm.js: -------------------------------------------------------------------------------- 1 | var menus = [ 2 | ['\u5E2E\u52A9\u83DC\u5355', '\u2192 /help'], 3 | ['\u5FEB\u6377\u6307\u4EE4', '\u2192 /deploy /screenshot'], 4 | ['\u65B9\u6848\u9009\u5355', '\u2192 F4'], 5 | [ 6 | '\u5FEB\u901F\u8BA1\u7B97', 7 | '\u2192 /calc \u6216 /js \u7EC4\u5408\u952E\uFF0C\u5982`/calcsin(pi/2)`\u5019\u9009`1.0`', 8 | ], 9 | [ 10 | '\u62C6\u5B57\u53CD\u67E5', 11 | '\u2192 uU \u7EC4\u5408\u952E\uFF0C\u5982`uUguili`\u53CD\u67E5\u51FA`\u9B51\u3018ch\u012B\u3019`', 12 | ], 13 | [ 14 | '\u6C49\u8BD1\u82F1\u4E0A\u5C4F', 15 | '\u2192 /e* \u7EC4\u5408\u952E\uFF0C\u5982`shuxue/en`\u4E0A\u5C4F`mathematics`', 16 | ], 17 | [ 18 | '\u62FC\u97F3\u4E0A\u5C4F', 19 | '\u2192 /p* \u7EC4\u5408\u952E\uFF0C\u5982`pinyin/py1`\u4E0A\u5C4F`p\u012Bn y\u012Bn`', 20 | ], 21 | [ 22 | '\u5FEB\u6377\u6309\u952E', 23 | "\u2192 \u4E8C\u4E09\u5019\u9009 ;' \xA7 \u4E0A\u4E0B\u7FFB\u9875 ,. \xA7 \u4EE5\u8BCD\u5B9A\u5B57 []", 24 | ], 25 | ['\u5355\u8BCD\u5927\u5199', '\u2192 AZ \u5927\u5199\u5B57\u6BCD\u89E6\u53D1'], 26 | ['\u65E5\u671F\u65F6\u95F4', '\u2192 rq | sj | xq | dt | ts | nl'], 27 | [ 28 | '\u4E2D\u6587\u6570\u5B57', 29 | '\u2192 R\u5FEB\u6377\u952E\uFF0C\u5982`R666`\u5019\u9009`\u516D\u767E\u516D\u5341\u516D\u5143\u6574`', 30 | ], 31 | ] 32 | var HelpMenuTranslator = class { 33 | constructor(env) { 34 | console.log('help_menu.js init') 35 | if (env.os.name === 'macOS') { 36 | menus.splice(2, 0, ['\u8BCD\u5178\u6E05\u9664', '\u2192 Fn + \u21E7 + \u232B \u7EC4\u5408\u952E']) 37 | } else { 38 | const idx = menus.findIndex(([text, comment]) => text === '\u5FEB\u6377\u6307\u4EE4') 39 | menus.splice(idx, 1) 40 | } 41 | } 42 | finalizer() { 43 | console.log('help_menu.js finit') 44 | } 45 | translate(input, segment, env) { 46 | if (input.length < 3 || !'/help'.startsWith(input)) return [] 47 | segment.prompt = '\u3014\u5E2E\u52A9\u83DC\u5355\u3015' 48 | const ret = menus.map( 49 | ([text, comment]) => new Candidate('help', segment.start, segment.end, text, comment), 50 | ) 51 | ret.unshift( 52 | new Candidate('help', segment.start, segment.end, '\u7248\u672C\u72B6\u6001', env.getRimeInfo()), 53 | ) 54 | return ret 55 | } 56 | } 57 | export { HelpMenuTranslator } 58 | -------------------------------------------------------------------------------- /tests/js/dist/help_menu.iife.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | var menus = [ 3 | ['\u5E2E\u52A9\u83DC\u5355', '\u2192 /help'], 4 | ['\u5FEB\u6377\u6307\u4EE4', '\u2192 /deploy /screenshot'], 5 | ['\u65B9\u6848\u9009\u5355', '\u2192 F4'], 6 | [ 7 | '\u5FEB\u901F\u8BA1\u7B97', 8 | '\u2192 /calc \u6216 /js \u7EC4\u5408\u952E\uFF0C\u5982`/calcsin(pi/2)`\u5019\u9009`1.0`', 9 | ], 10 | [ 11 | '\u62C6\u5B57\u53CD\u67E5', 12 | '\u2192 uU \u7EC4\u5408\u952E\uFF0C\u5982`uUguili`\u53CD\u67E5\u51FA`\u9B51\u3018ch\u012B\u3019`', 13 | ], 14 | [ 15 | '\u6C49\u8BD1\u82F1\u4E0A\u5C4F', 16 | '\u2192 /e* \u7EC4\u5408\u952E\uFF0C\u5982`shuxue/en`\u4E0A\u5C4F`mathematics`', 17 | ], 18 | [ 19 | '\u62FC\u97F3\u4E0A\u5C4F', 20 | '\u2192 /p* \u7EC4\u5408\u952E\uFF0C\u5982`pinyin/py1`\u4E0A\u5C4F`p\u012Bn y\u012Bn`', 21 | ], 22 | [ 23 | '\u5FEB\u6377\u6309\u952E', 24 | "\u2192 \u4E8C\u4E09\u5019\u9009 ;' \xA7 \u4E0A\u4E0B\u7FFB\u9875 ,. \xA7 \u4EE5\u8BCD\u5B9A\u5B57 []", 25 | ], 26 | ['\u5355\u8BCD\u5927\u5199', '\u2192 AZ \u5927\u5199\u5B57\u6BCD\u89E6\u53D1'], 27 | ['\u65E5\u671F\u65F6\u95F4', '\u2192 rq | sj | xq | dt | ts | nl'], 28 | [ 29 | '\u4E2D\u6587\u6570\u5B57', 30 | '\u2192 R\u5FEB\u6377\u952E\uFF0C\u5982`R666`\u5019\u9009`\u516D\u767E\u516D\u5341\u516D\u5143\u6574`', 31 | ], 32 | ] 33 | var HelpMenuTranslator = class { 34 | constructor(env) { 35 | console.log('help_menu.js init') 36 | if (env.os.name === 'macOS') { 37 | menus.splice(2, 0, ['\u8BCD\u5178\u6E05\u9664', '\u2192 Fn + \u21E7 + \u232B \u7EC4\u5408\u952E']) 38 | } else { 39 | const idx = menus.findIndex(([text, comment]) => text === '\u5FEB\u6377\u6307\u4EE4') 40 | menus.splice(idx, 1) 41 | } 42 | } 43 | finalizer() { 44 | console.log('help_menu.js finit') 45 | } 46 | translate(input, segment, env) { 47 | if (input.length < 3 || !'/help'.startsWith(input)) return [] 48 | segment.prompt = '\u3014\u5E2E\u52A9\u83DC\u5355\u3015' 49 | const ret = menus.map( 50 | ([text, comment]) => new Candidate('help', segment.start, segment.end, text, comment), 51 | ) 52 | ret.unshift( 53 | new Candidate('help', segment.start, segment.end, '\u7248\u672C\u72B6\u6001', env.getRimeInfo()), 54 | ) 55 | return ret 56 | } 57 | } 58 | globalThis.iife_instance_help_menu_iife_js = new HelpMenuTranslator() 59 | })() 60 | -------------------------------------------------------------------------------- /tests/js/dist/lib.esm.js: -------------------------------------------------------------------------------- 1 | function greet(name) { 2 | return `Hello ${name}!` 3 | } 4 | var MyClass = class { 5 | constructor(value) { 6 | this.value = value 7 | } 8 | myMethod() { 9 | return this.value + 1 10 | } 11 | greet(name) { 12 | return greet(name) 13 | } 14 | } 15 | export { MyClass, greet } 16 | -------------------------------------------------------------------------------- /tests/js/dist/lib.iife.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | function greet(name) { 3 | return `Hello ${name}!` 4 | } 5 | var MyClass = class { 6 | constructor(value) { 7 | this.value = value 8 | } 9 | myMethod() { 10 | return this.value + 1 11 | } 12 | greet(name) { 13 | return greet(name) 14 | } 15 | } 16 | globalThis.iife_instance_lib_iife_js = new MyClass() 17 | })() 18 | -------------------------------------------------------------------------------- /tests/js/dist/main.esm.js: -------------------------------------------------------------------------------- 1 | function greet(name) { 2 | return `Hello ${name}!` 3 | } 4 | var MyClass = class { 5 | constructor(value) { 6 | this.value = value 7 | } 8 | myMethod() { 9 | return this.value + 1 10 | } 11 | greet(name) { 12 | return greet(name) 13 | } 14 | } 15 | globalThis.MyClass = MyClass 16 | var obj = new MyClass(10) 17 | console.log(obj.greet('QuickJS')) 18 | console.log(obj.myMethod()) 19 | console.log(obj.greet?.name.includes('greet')) 20 | console.log(obj.hello?.name.includes('greet')) 21 | -------------------------------------------------------------------------------- /tests/js/dist/processor_test.esm.js: -------------------------------------------------------------------------------- 1 | var totalTests = 0 2 | var passedTests = 0 3 | function assertEquals(actual, expected, message = '') { 4 | totalTests++ 5 | const actualStr = JSON.stringify(actual) 6 | const expectedStr = JSON.stringify(expected) 7 | if (actualStr === expectedStr) { 8 | passedTests++ 9 | console.log('\u2713 ' + message) 10 | } else { 11 | console.log('\u2717 ' + message) 12 | console.log(' Expected: ' + expectedStr) 13 | console.log(' Actual: ' + actualStr) 14 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 15 | } 16 | } 17 | var TestProcessor = class { 18 | constructor(env) { 19 | console.log('[processor_test] init') 20 | const config = env.engine.schema.config 21 | const initTestValue = config.getString('init_test') 22 | if (initTestValue) { 23 | console.log(`[processor_test] init_test value: ${initTestValue}`) 24 | } 25 | } 26 | finalizer() { 27 | console.log('[processor_test] finit') 28 | } 29 | process(keyEvent, env) { 30 | assertEquals(env.engine.context.lastSegment?.prompt, 'prompt', 'should have lastSegment with prompt') 31 | console.log(`[processor_test] process: ${keyEvent.repr}`) 32 | const repr = keyEvent.repr 33 | if (repr === 'space') { 34 | return 'kAccepted' 35 | } else if (repr === 'Return') { 36 | return 'kRejected' 37 | } 38 | return 'kNoop' 39 | } 40 | } 41 | export { TestProcessor } 42 | -------------------------------------------------------------------------------- /tests/js/dist/processor_test.iife.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | var totalTests = 0 3 | var passedTests = 0 4 | function assertEquals(actual, expected, message = '') { 5 | totalTests++ 6 | const actualStr = JSON.stringify(actual) 7 | const expectedStr = JSON.stringify(expected) 8 | if (actualStr === expectedStr) { 9 | passedTests++ 10 | console.log('\u2713 ' + message) 11 | } else { 12 | console.log('\u2717 ' + message) 13 | console.log(' Expected: ' + expectedStr) 14 | console.log(' Actual: ' + actualStr) 15 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 16 | } 17 | } 18 | var TestProcessor = class { 19 | constructor(env) { 20 | console.log('[processor_test] init') 21 | const config = env.engine.schema.config 22 | const initTestValue = config.getString('init_test') 23 | if (initTestValue) { 24 | console.log(`[processor_test] init_test value: ${initTestValue}`) 25 | } 26 | } 27 | finalizer() { 28 | console.log('[processor_test] finit') 29 | } 30 | process(keyEvent, env) { 31 | assertEquals(env.engine.context.lastSegment?.prompt, 'prompt', 'should have lastSegment with prompt') 32 | console.log(`[processor_test] process: ${keyEvent.repr}`) 33 | const repr = keyEvent.repr 34 | if (repr === 'space') { 35 | return 'kAccepted' 36 | } else if (repr === 'Return') { 37 | return 'kRejected' 38 | } 39 | return 'kNoop' 40 | } 41 | } 42 | globalThis.iife_instance_processor_test_iife_js = new TestProcessor() 43 | })() 44 | -------------------------------------------------------------------------------- /tests/js/dist/runtime-error.esm.js: -------------------------------------------------------------------------------- 1 | function greet(name) { 2 | return `Hello ${name}!` 3 | } 4 | var MyClass = class { 5 | constructor(value) { 6 | this.value = value 7 | } 8 | myMethod() { 9 | return this.value + 1 10 | } 11 | greet(name) { 12 | return greet(name) 13 | } 14 | } 15 | globalThis.funcWithRuntimeError = function () { 16 | const obj = new MyClass(abcdefg) 17 | obj.hi() 18 | } 19 | funcWithRuntimeError() 20 | -------------------------------------------------------------------------------- /tests/js/dist/sort_by_pinyin.esm.js: -------------------------------------------------------------------------------- 1 | var accents = 2 | '\u0101\xE1\u01CE\xE0\u0113\xE9\u011B\xE8\u012B\xED\u01D0\xEC\u014D\xF3\u01D2\xF2\u016B\xFA\u01D4\xF9\u01D6\u01D8\u01DA\u01DC\xFC' 3 | var without = 'aaaaeeeeiiiioooouuuuvvvvv' 4 | var dict = {} 5 | accents.split('').forEach((char, idx) => (dict[char] = without[idx])) 6 | function unaccent(str) { 7 | return str 8 | .split('') 9 | .map((char) => { 10 | return dict[char] || char 11 | }) 12 | .join('') 13 | } 14 | var SortCandidatesByPinyinFilter = class { 15 | constructor(env) { 16 | console.log('sort_by_pinyin.js init') 17 | } 18 | finalizer() { 19 | console.log('sort_by_pinyin.js finit') 20 | } 21 | #topN = 100 22 | filter(candidates, env) { 23 | const userPhrases = [] 24 | const userPhrasesIndices = [] 25 | const candidatesWithPinyin = [] 26 | const candidatesWithPinyinIndices = [] 27 | const input = env.engine.context.input.replace(/\/.*$/, '') 28 | const size = candidates.length > this.#topN ? this.#topN : candidates.length 29 | candidates.slice(0, size).forEach((candidate, idx) => { 30 | const pinyin = this.extractPinyin(candidate.comment)?.replaceAll(' ', '') 31 | if (candidate.type === 'user_phrase') { 32 | const weight = this.getWeightByPinyin(pinyin, input, true) + size - idx 33 | userPhrasesIndices.push(idx) 34 | userPhrases.push({ candidate, weight }) 35 | } else if (pinyin) { 36 | const weight = this.getWeightByPinyin(pinyin, input, false) + size - idx 37 | candidatesWithPinyinIndices.push(idx) 38 | candidatesWithPinyin.push({ candidate, weight }) 39 | } 40 | }) 41 | userPhrases.sort((a, b) => b.weight - a.weight) 42 | userPhrasesIndices.forEach((originalIndex, idx) => { 43 | candidates[originalIndex] = userPhrases[idx].candidate 44 | }) 45 | candidatesWithPinyin.sort((a, b) => b.weight - a.weight) 46 | candidatesWithPinyinIndices.forEach((originalIndex, idx) => { 47 | candidates[originalIndex] = candidatesWithPinyin[idx].candidate 48 | }) 49 | return candidates 50 | } 51 | getWeightByPinyin(pinyin, input, isInUserPhrase) { 52 | if (pinyin === input) { 53 | return 1e4 54 | } 55 | if (isInUserPhrase && !pinyin) { 56 | return 1e4 57 | } 58 | if (pinyin?.startsWith(input)) { 59 | return 5e3 60 | } 61 | if (pinyin?.includes(input)) { 62 | return 1e3 + pinyin.length 63 | } 64 | return 0 65 | } 66 | extractPinyin(comment) { 67 | const match = comment.match(/〖(.+?)〗/) 68 | if (match) { 69 | return unaccent(match[1]) 70 | } 71 | const match2 = comment.match(/[(.*?)]/) || [] 72 | return match2[1] 73 | } 74 | } 75 | export { SortCandidatesByPinyinFilter } 76 | -------------------------------------------------------------------------------- /tests/js/dist/sort_by_pinyin.iife.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | var accents = 3 | '\u0101\xE1\u01CE\xE0\u0113\xE9\u011B\xE8\u012B\xED\u01D0\xEC\u014D\xF3\u01D2\xF2\u016B\xFA\u01D4\xF9\u01D6\u01D8\u01DA\u01DC\xFC' 4 | var without = 'aaaaeeeeiiiioooouuuuvvvvv' 5 | var dict = {} 6 | accents.split('').forEach((char, idx) => (dict[char] = without[idx])) 7 | function unaccent(str) { 8 | return str 9 | .split('') 10 | .map((char) => { 11 | return dict[char] || char 12 | }) 13 | .join('') 14 | } 15 | var SortCandidatesByPinyinFilter = class { 16 | constructor(env) { 17 | console.log('sort_by_pinyin.js init') 18 | } 19 | finalizer() { 20 | console.log('sort_by_pinyin.js finit') 21 | } 22 | #topN = 100 23 | filter(candidates, env) { 24 | const userPhrases = [] 25 | const userPhrasesIndices = [] 26 | const candidatesWithPinyin = [] 27 | const candidatesWithPinyinIndices = [] 28 | const input = env.engine.context.input.replace(/\/.*$/, '') 29 | const size = candidates.length > this.#topN ? this.#topN : candidates.length 30 | candidates.slice(0, size).forEach((candidate, idx) => { 31 | const pinyin = this.extractPinyin(candidate.comment)?.replaceAll(' ', '') 32 | if (candidate.type === 'user_phrase') { 33 | const weight = this.getWeightByPinyin(pinyin, input, true) + size - idx 34 | userPhrasesIndices.push(idx) 35 | userPhrases.push({ candidate, weight }) 36 | } else if (pinyin) { 37 | const weight = this.getWeightByPinyin(pinyin, input, false) + size - idx 38 | candidatesWithPinyinIndices.push(idx) 39 | candidatesWithPinyin.push({ candidate, weight }) 40 | } 41 | }) 42 | userPhrases.sort((a, b) => b.weight - a.weight) 43 | userPhrasesIndices.forEach((originalIndex, idx) => { 44 | candidates[originalIndex] = userPhrases[idx].candidate 45 | }) 46 | candidatesWithPinyin.sort((a, b) => b.weight - a.weight) 47 | candidatesWithPinyinIndices.forEach((originalIndex, idx) => { 48 | candidates[originalIndex] = candidatesWithPinyin[idx].candidate 49 | }) 50 | return candidates 51 | } 52 | getWeightByPinyin(pinyin, input, isInUserPhrase) { 53 | if (pinyin === input) { 54 | return 1e4 55 | } 56 | if (isInUserPhrase && !pinyin) { 57 | return 1e4 58 | } 59 | if (pinyin?.startsWith(input)) { 60 | return 5e3 61 | } 62 | if (pinyin?.includes(input)) { 63 | return 1e3 + pinyin.length 64 | } 65 | return 0 66 | } 67 | extractPinyin(comment) { 68 | const match = comment.match(/〖(.+?)〗/) 69 | if (match) { 70 | return unaccent(match[1]) 71 | } 72 | const match2 = comment.match(/[(.*?)]/) || [] 73 | return match2[1] 74 | } 75 | } 76 | globalThis.iife_instance_sort_by_pinyin_iife_js = new SortCandidatesByPinyinFilter() 77 | })() 78 | -------------------------------------------------------------------------------- /tests/js/dist/testutils.esm.js: -------------------------------------------------------------------------------- 1 | var totalTests = 0 2 | var passedTests = 0 3 | function assert(condition, message = '') { 4 | totalTests++ 5 | if (condition) { 6 | passedTests++ 7 | console.log('\u2713 ' + message) 8 | } else { 9 | console.log('\u2717 ' + message) 10 | console.log(' Expected true, but got false') 11 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 12 | } 13 | } 14 | function assertEquals(actual, expected, message = '') { 15 | totalTests++ 16 | const actualStr = JSON.stringify(actual) 17 | const expectedStr = JSON.stringify(expected) 18 | if (actualStr === expectedStr) { 19 | passedTests++ 20 | console.log('\u2713 ' + message) 21 | } else { 22 | console.log('\u2717 ' + message) 23 | console.log(' Expected: ' + expectedStr) 24 | console.log(' Actual: ' + actualStr) 25 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 26 | } 27 | } 28 | export { assert, assertEquals, passedTests, totalTests } 29 | -------------------------------------------------------------------------------- /tests/js/dist/translator_no_return.esm.js: -------------------------------------------------------------------------------- 1 | var BadTranslator = class { 2 | translate() { 3 | console.log('no return') 4 | } 5 | } 6 | export { BadTranslator } 7 | -------------------------------------------------------------------------------- /tests/js/dist/translator_no_return.iife.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | var BadTranslator = class { 3 | translate() { 4 | console.log('no return') 5 | } 6 | } 7 | globalThis.iife_instance_translator_no_return_iife_js = new BadTranslator() 8 | })() 9 | -------------------------------------------------------------------------------- /tests/js/dist/translator_test.esm.js: -------------------------------------------------------------------------------- 1 | var totalTests = 0 2 | var passedTests = 0 3 | function assert(condition, message = '') { 4 | totalTests++ 5 | if (condition) { 6 | passedTests++ 7 | console.log('\u2713 ' + message) 8 | } else { 9 | console.log('\u2717 ' + message) 10 | console.log(' Expected true, but got false') 11 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 12 | } 13 | } 14 | function assertEquals(actual, expected, message = '') { 15 | totalTests++ 16 | const actualStr = JSON.stringify(actual) 17 | const expectedStr = JSON.stringify(expected) 18 | if (actualStr === expectedStr) { 19 | passedTests++ 20 | console.log('\u2713 ' + message) 21 | } else { 22 | console.log('\u2717 ' + message) 23 | console.log(' Expected: ' + expectedStr) 24 | console.log(' Actual: ' + actualStr) 25 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 26 | } 27 | } 28 | var TestTranslator = class { 29 | constructor(env) { 30 | console.log('translator_test init') 31 | assertEquals(env.namespace, 'translator_test') 32 | assert(env.userDataDir.endsWith('qjs/tests/')) 33 | assertEquals(env.sharedDataDir, '.') 34 | const config = env.engine.schema.config 35 | assertEquals(config.getString('greet'), 'hello from c++') 36 | console.log('translator_test init done') 37 | } 38 | finalizer() { 39 | console.log('translator_test finit') 40 | } 41 | translate(input, segment, env) { 42 | console.log('translator_test translate', input) 43 | assertEquals(env.namespace, 'translator_test') 44 | const config = env.engine.schema.config 45 | assertEquals(config.getString('greet'), 'hello from c++') 46 | const expectedInput = config.getString('expectedInput') 47 | assertEquals(expectedInput, input) 48 | if (input === 'test_input') { 49 | return [ 50 | new Candidate('test', segment.start, segment.end, 'candidate1', 'comment1'), 51 | new Candidate('test', segment.start, segment.end, 'candidate2', 'comment2'), 52 | new Candidate('test', segment.start, segment.end, 'candidate3', 'comment3'), 53 | ] 54 | } 55 | return [] 56 | } 57 | } 58 | export { TestTranslator } 59 | -------------------------------------------------------------------------------- /tests/js/dist/translator_test.iife.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | var totalTests = 0 3 | var passedTests = 0 4 | function assert(condition, message = '') { 5 | totalTests++ 6 | if (condition) { 7 | passedTests++ 8 | console.log('\u2713 ' + message) 9 | } else { 10 | console.log('\u2717 ' + message) 11 | console.log(' Expected true, but got false') 12 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 13 | } 14 | } 15 | function assertEquals(actual, expected, message = '') { 16 | totalTests++ 17 | const actualStr = JSON.stringify(actual) 18 | const expectedStr = JSON.stringify(expected) 19 | if (actualStr === expectedStr) { 20 | passedTests++ 21 | console.log('\u2713 ' + message) 22 | } else { 23 | console.log('\u2717 ' + message) 24 | console.log(' Expected: ' + expectedStr) 25 | console.log(' Actual: ' + actualStr) 26 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 27 | } 28 | } 29 | var TestTranslator = class { 30 | constructor(env) { 31 | console.log('translator_test init') 32 | assertEquals(env.namespace, 'translator_test') 33 | assert(env.userDataDir.endsWith('qjs/tests/')) 34 | assertEquals(env.sharedDataDir, '.') 35 | const config = env.engine.schema.config 36 | assertEquals(config.getString('greet'), 'hello from c++') 37 | console.log('translator_test init done') 38 | } 39 | finalizer() { 40 | console.log('translator_test finit') 41 | } 42 | translate(input, segment, env) { 43 | console.log('translator_test translate', input) 44 | assertEquals(env.namespace, 'translator_test') 45 | const config = env.engine.schema.config 46 | assertEquals(config.getString('greet'), 'hello from c++') 47 | const expectedInput = config.getString('expectedInput') 48 | assertEquals(expectedInput, input) 49 | if (input === 'test_input') { 50 | return [ 51 | new Candidate('test', segment.start, segment.end, 'candidate1', 'comment1'), 52 | new Candidate('test', segment.start, segment.end, 'candidate2', 'comment2'), 53 | new Candidate('test', segment.start, segment.end, 'candidate3', 'comment3'), 54 | ] 55 | } 56 | return [] 57 | } 58 | } 59 | globalThis.iife_instance_translator_test_iife_js = new TestTranslator() 60 | })() 61 | -------------------------------------------------------------------------------- /tests/js/fast_filter.iterator.js: -------------------------------------------------------------------------------- 1 | export class FastFilterReturnIterator { 2 | *filter(iter, env) { 3 | return iter 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/js/fast_filter.js: -------------------------------------------------------------------------------- 1 | export class FastFilterWithGenerator { 2 | *filter(iter, env) { 3 | for (let idx = 0, candidate; (candidate = iter.next()); ++idx) { 4 | console.log(`checking candidate at index = ${idx}`) 5 | if (idx % 2 === 0) { 6 | yield candidate 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/js/filter_is_applicable.js: -------------------------------------------------------------------------------- 1 | export class FilterWithIsApplicable { 2 | isApplicable(env) { 3 | console.log('filter_test isApplicable') 4 | return false 5 | } 6 | filter(candidates, env) { 7 | throw new Error('should not be called as isApplicable returns false') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/js/filter_test.js: -------------------------------------------------------------------------------- 1 | import { assert } from './testutils.js' 2 | 3 | export class TestFilter { 4 | constructor(env) { 5 | console.log('filter_test init') 6 | assert(env.namespace === 'filter_test') 7 | assert(env.userDataDir.endsWith('qjs/tests/')) 8 | console.log(`env = ${env}`) 9 | console.log(`env.engine.schema = ${env.engine.schema}`) 10 | console.log(`env.engine.schema.config = ${env.engine.schema.config}`) 11 | const config = env.engine.schema.config 12 | assert(config.getString('greet') === 'hello from c++') 13 | } 14 | finalizer() { 15 | console.log('filter_test finit') 16 | } 17 | filter(candidates, env) { 18 | console.log('filter_test filter', candidates.length) 19 | assert(env.namespace === 'filter_test') 20 | const config = env.engine.schema.config 21 | assert(config.getString('greet') === 'hello from c++') 22 | 23 | const expectingText = config.getString('expectingText') 24 | assert(expectingText === 'text2') 25 | return candidates.filter((it) => it.text === expectingText) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/js/help_menu.js: -------------------------------------------------------------------------------- 1 | // 帮助菜单,/help 触发显示 2 | // ------------------------------------------------------- 3 | // 使用 JavaScript 实现,适配 librime-qjs 插件系统。 4 | // by @[HuangJian](https://github.com/HuangJian) 5 | 6 | /** @type {Array<[string, string]>} */ 7 | const menus = [ 8 | ['帮助菜单', '→ /help'], 9 | ['快捷指令', '→ /deploy /screenshot'], 10 | ['方案选单', '→ F4'], 11 | ['快速计算', '→ /calc 或 /js 组合键,如`/calcsin(pi/2)`候选`1.0`'], 12 | ['拆字反查', '→ uU 组合键,如`uUguili`反查出`魑〘chī〙`'], 13 | ['汉译英上屏', '→ /e* 组合键,如`shuxue/en`上屏`mathematics`'], 14 | ['拼音上屏', '→ /p* 组合键,如`pinyin/py1`上屏`pīn yīn`'], 15 | ['快捷按键', "→ 二三候选 ;' § 上下翻页 ,. § 以词定字 []"], 16 | ['单词大写', '→ AZ 大写字母触发'], 17 | ['日期时间', '→ rq | sj | xq | dt | ts | nl'], 18 | ['中文数字', '→ R快捷键,如`R666`候选`六百六十六元整`'], 19 | ] 20 | 21 | /** 22 | * 帮助菜单翻译器 23 | * @implements {Translator} 24 | */ 25 | export class HelpMenuTranslator { 26 | /** 27 | * Initialize the help menu translator 28 | * @param {Environment} env - The Rime environment 29 | */ 30 | constructor(env) { 31 | console.log('help_menu.js init') 32 | if (env.os.name === 'macOS') { 33 | menus.splice(2, 0, ['词典清除', '→ Fn + ⇧ + ⌫ 组合键']) 34 | } else { 35 | const idx = menus.findIndex(([text, comment]) => text === '快捷指令') 36 | menus.splice(idx, 1) 37 | } 38 | } 39 | 40 | /** 41 | * Clean up the help menu translator 42 | */ 43 | finalizer() { 44 | console.log('help_menu.js finit') 45 | } 46 | 47 | /** 48 | * Translate help menu related input 49 | * @param {string} input - The input string to translate 50 | * @param {Segment} segment - The input segment 51 | * @param {Environment} env - The Rime environment 52 | * @returns {Array} Array of translation candidates 53 | */ 54 | translate(input, segment, env) { 55 | if (input.length < 3 || !'/help'.startsWith(input)) return [] 56 | 57 | segment.prompt = '〔帮助菜单〕' 58 | 59 | const ret = menus.map( 60 | ([text, comment]) => new Candidate('help', segment.start, segment.end, text, comment), 61 | ) 62 | ret.unshift(new Candidate('help', segment.start, segment.end, '版本状态', env.getRimeInfo())) 63 | return ret 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/js/lib.js: -------------------------------------------------------------------------------- 1 | export function greet(name) { 2 | return `Hello ${name}!` 3 | } 4 | 5 | export class MyClass { 6 | constructor(value) { 7 | this.value = value 8 | } 9 | 10 | myMethod() { 11 | return this.value + 1 12 | } 13 | greet(name) { 14 | return greet(name) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/js/lib/string.js: -------------------------------------------------------------------------------- 1 | 2 | const accents = 'āáǎàēéěèīíǐìōóǒòūúǔùǖǘǚǜü' 3 | const without = 'aaaaeeeeiiiioooouuuuvvvvv' 4 | const dict = {} 5 | accents.split('').forEach((char, idx) => (dict[char] = without[idx])) 6 | 7 | export function unaccent(str) { 8 | return str 9 | .split('') 10 | .map((char) => { 11 | return dict[char] || char 12 | }) 13 | .join('') 14 | } 15 | 16 | export function isChineseWord(word) { 17 | // Check for at least one Chinese character (range U+4E00 to U+9FFF) 18 | return /[\u4e00-\u9fff]/.test(word) 19 | } 20 | -------------------------------------------------------------------------------- /tests/js/lib/trie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Node class for Trie data structure 3 | */ 4 | class TrieNode { 5 | constructor() { 6 | this.children = new Map() 7 | this.isEndOfWord = false 8 | this.data = [] // Array to store multiple data entries 9 | } 10 | } 11 | 12 | /** 13 | * Trie data structure implementation 14 | * Efficient for prefix-based operations 15 | */ 16 | export class Trie { 17 | constructor(multipleData = false) { 18 | this.root = new TrieNode() 19 | this.multipleData = multipleData 20 | } 21 | 22 | /** 23 | * Insert a word into the trie with associated data 24 | * @param {string} word - The word to insert 25 | * @param {string} data - Data to store at the end node 26 | */ 27 | insert(word, data) { 28 | let current = this.root 29 | 30 | for (const char of word) { 31 | if (!current.children.has(char)) { 32 | current.children.set(char, new TrieNode()) 33 | } 34 | current = current.children.get(char) 35 | } 36 | 37 | current.isEndOfWord = true 38 | if (!current.data.includes(data)) { 39 | current.data.push(data) 40 | } 41 | } 42 | 43 | // format: sometimes ['sʌmtaimz] adv. 有时, 时常, 往往 44 | parseLine(line) { 45 | const idx = line.indexOf('\t') 46 | if (idx < 1) return null 47 | 48 | return { 49 | text: line.substring(0, idx).trim().toLowerCase(), 50 | info: line.substring(idx + 1).trim(), 51 | } 52 | } 53 | 54 | // format: sometimes ['sʌmtaimz] adv. 有时, 时常, 往往 55 | parseLineRegex(line) { 56 | const matches = line.match(/^(.*?)\s+(.+)\s*$/) 57 | if (!matches) return null 58 | 59 | const [, text, info] = matches 60 | return { 61 | text: text.trim().toLowerCase(), 62 | info: info.trim(), 63 | } 64 | } 65 | 66 | /** 67 | * Search for a word in the trie 68 | * @param {string} word - The word to search for 69 | * @returns {Object} Object of associated data, null if not found 70 | */ 71 | find(word) { 72 | const node = this._traverse(word) 73 | const arr = node?.data || [] 74 | return this.multipleData? arr : arr[0] 75 | } 76 | 77 | /** 78 | * Check if there are any words in the trie that start with the given prefix 79 | * @param {string} prefix - The prefix to search for 80 | * @returns {boolean} True if prefix exists 81 | */ 82 | startsWith(prefix) { 83 | return this._traverse(prefix) !== null 84 | } 85 | 86 | /** 87 | * Get all words with the given prefix 88 | * @param {string} prefix - The prefix to search for 89 | * @returns {Array} Array of objects containing words and their associated data 90 | */ 91 | prefixSearch(prefix) { 92 | const result = [] 93 | const node = this._traverse(prefix) 94 | 95 | if (node !== null) { 96 | this._collectWords(node, prefix, result) 97 | } 98 | 99 | return result 100 | } 101 | 102 | /** 103 | * Helper method to traverse the trie 104 | * @private 105 | */ 106 | _traverse(word) { 107 | let current = this.root 108 | 109 | for (const char of word) { 110 | if (!current.children.has(char)) { 111 | return null 112 | } 113 | current = current.children.get(char) 114 | } 115 | 116 | return current 117 | } 118 | 119 | /** 120 | * Helper method to collect all words with a given prefix 121 | * @private 122 | */ 123 | _collectWords(node, prefix, result) { 124 | if (node.isEndOfWord) { 125 | result.push({ 126 | text: prefix, 127 | info: this.multipleData ? node.data : node.data[0], 128 | }) 129 | } 130 | 131 | for (const [char, childNode] of node.children) { 132 | this._collectWords(childNode, prefix + char, result) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tests/js/main.js: -------------------------------------------------------------------------------- 1 | // test in commandline: `./qjs ./main.js` 2 | import { MyClass } from './lib.js' 3 | 4 | globalThis.MyClass = MyClass // necessary to get the constructor in c++ 5 | 6 | const obj = new MyClass(10) 7 | console.log(obj.greet('QuickJS')) 8 | console.log(obj.myMethod()) 9 | 10 | console.log(obj.greet?.name.includes('greet')) 11 | console.log(obj.hello?.name.includes('greet')) 12 | -------------------------------------------------------------------------------- /tests/js/node-modules.test.js: -------------------------------------------------------------------------------- 1 | // test in commandline: `./qjs ./node-modules.test.js` 2 | 3 | console.log(`log from node-modules.test.js`) 4 | 5 | import { addDays, format } from 'date-fns' 6 | const today = new Date(2025, 2, 17) // month starts from 0 7 | assertEquals(format(today, 'yyyy-MM-dd'), '2025-03-17', 'should get correct date') 8 | const tomorrow = addDays(today, 1) 9 | assertEquals(format(tomorrow, 'yyyy-MM-dd'), '2025-03-18','should get correct tomorrow') 10 | 11 | 12 | import { Solar } from 'lunar-typescript' 13 | import { assertEquals } from './testutils' 14 | 15 | const dt = Solar.fromYmdHms(2025, 3, 17, 11, 20, 0) // month starts from 1 16 | console.log(dt.toFullString()) 17 | assertEquals(dt.toFullString(), '2025-03-17 11:20:00 星期一 双鱼座', 'should get correct date time') 18 | 19 | const expectedLunarText = 20 | `二〇二五年二月十八 乙巳(蛇)年 己卯(兔)月 乙酉(鸡)日 午(马)时 纳音[覆灯火 城头土 泉中水 杨柳木] 星期一 ` + 21 | `北方玄武 星宿[危月燕](凶) 彭祖百忌[乙不栽植千株不长 酉不会客醉坐颠狂] ` + 22 | `喜神方位[乾](西北) 阳贵神方位[坤](西南) 阴贵神方位[坎](正北) 福神方位[坤](西南) 财神方位[艮](东北) 冲[(己卯)兔] 煞[东]` 23 | console.log(dt.getLunar().toFullString()) 24 | assertEquals(dt.getLunar().toFullString(), expectedLunarText, 'should get correct lunar text') 25 | 26 | // to bundle it to IIFE format to run in JavaScriptCore 27 | export class DummyClass {} 28 | -------------------------------------------------------------------------------- /tests/js/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "js", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "date-fns": "^4.1.0", 13 | "lunar-typescript": "^1.7.8" 14 | } 15 | }, 16 | "node_modules/date-fns": { 17 | "version": "4.1.0", 18 | "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz", 19 | "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", 20 | "funding": { 21 | "type": "github", 22 | "url": "https://github.com/sponsors/kossnocorp" 23 | } 24 | }, 25 | "node_modules/lunar-typescript": { 26 | "version": "1.7.8", 27 | "resolved": "https://registry.npmmirror.com/lunar-typescript/-/lunar-typescript-1.7.8.tgz", 28 | "integrity": "sha512-i1rjc1eDHUF4k8NfpYxar3ZKtxyVHOGnR+yVTMS8jMy74ugq7KeHI+8T1XIvB3uojpBq6asywe7z25PmI3VLPw==" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "description": "", 13 | "dependencies": { 14 | "date-fns": "^4.1.0", 15 | "lunar-typescript": "^1.7.8" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/js/processor_test.js: -------------------------------------------------------------------------------- 1 | import { assertEquals } from './testutils.js' 2 | 3 | export class TestProcessor { 4 | constructor(env) { 5 | console.log('[processor_test] init') 6 | const config = env.engine.schema.config 7 | const initTestValue = config.getString('init_test') 8 | if (initTestValue) { 9 | console.log(`[processor_test] init_test value: ${initTestValue}`) 10 | } 11 | } 12 | 13 | finalizer() { 14 | console.log('[processor_test] finit') 15 | } 16 | 17 | process(keyEvent, env) { 18 | assertEquals(env.engine.context.lastSegment?.prompt, 'prompt', 'should have lastSegment with prompt') 19 | 20 | console.log(`[processor_test] process: ${keyEvent.repr}`) 21 | const repr = keyEvent.repr 22 | if (repr === 'space') { 23 | return 'kAccepted' 24 | } else if (repr === 'Return') { 25 | return 'kRejected' 26 | } 27 | 28 | return 'kNoop' 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/js/runtime-error.js: -------------------------------------------------------------------------------- 1 | import { MyClass } from './lib.js' 2 | 3 | globalThis.funcWithRuntimeError = function () { 4 | // Possibly unhandled promise rejection: ReferenceError: abcdefg is not defined 5 | // at (./runtime-error.js:7:21) 6 | // at (./runtime-error.js:11:1) 7 | const obj = new MyClass(abcdefg) 8 | obj.hi() 9 | } 10 | 11 | funcWithRuntimeError() 12 | -------------------------------------------------------------------------------- /tests/js/sort_by_pinyin.js: -------------------------------------------------------------------------------- 1 | import { unaccent } from './lib/string.js' 2 | 3 | /** 4 | * 根据候选项的拼音和输入字符的匹配程度,重新排序候选项。 5 | * 仅对带拼音的候选项进行就地重排,其它类型的候选项(长句子、emoji、英语单词等)保持原顺序。 6 | * @implements {Filter} 7 | * @author https://github.com/HuangJian 8 | */ 9 | export class SortCandidatesByPinyinFilter { 10 | /** 11 | * Initialize the filter 12 | * @param {Environment} env - The Rime environment 13 | */ 14 | constructor(env) { 15 | console.log('sort_by_pinyin.js init') 16 | } 17 | 18 | /** 19 | * Clean up when the filter is unloaded 20 | */ 21 | finalizer() { 22 | console.log('sort_by_pinyin.js finit') 23 | } 24 | 25 | /** 26 | * the number of top candidates to sort 27 | */ 28 | #topN = 100 29 | 30 | /** 31 | * Sort the candidates by pinyin 32 | * @param {Array} candidates - Array of candidates to sort 33 | * @param {Environment} env - The Rime environment 34 | * @returns {Array} The sorted candidates 35 | */ 36 | filter(candidates, env) { 37 | const userPhrases = [] 38 | const userPhrasesIndices = [] 39 | const candidatesWithPinyin = [] 40 | const candidatesWithPinyinIndices = [] 41 | 42 | const input = env.engine.context.input.replace(/\/.*$/, '') // 去掉 /py /en 等快捷键 43 | 44 | const size = candidates.length > this.#topN ? this.#topN : candidates.length 45 | candidates.slice(0, size).forEach((candidate, idx) => { 46 | const pinyin = this.extractPinyin(candidate.comment)?.replaceAll(' ', '') 47 | if (candidate.type === 'user_phrase') { 48 | const weight = this.getWeightByPinyin(pinyin, input, true) + size - idx 49 | userPhrasesIndices.push(idx) 50 | userPhrases.push({ candidate, weight }) 51 | } else if (pinyin) { 52 | const weight = this.getWeightByPinyin(pinyin, input, false) + size - idx 53 | candidatesWithPinyinIndices.push(idx) 54 | candidatesWithPinyin.push({ candidate, weight }) 55 | } 56 | }) 57 | 58 | // 就地重排用户词典的候选词 59 | userPhrases.sort((a, b) => b.weight - a.weight) 60 | userPhrasesIndices.forEach((originalIndex, idx) => { 61 | candidates[originalIndex] = userPhrases[idx].candidate 62 | }) 63 | 64 | // 就地重排其它带拼音的候选词 65 | candidatesWithPinyin.sort((a, b) => b.weight - a.weight) 66 | candidatesWithPinyinIndices.forEach((originalIndex, idx) => { 67 | candidates[originalIndex] = candidatesWithPinyin[idx].candidate 68 | }) 69 | 70 | return candidates 71 | } 72 | /** 73 | * 计算候选项的权重分数,用于智能排序。 74 | * 75 | * @param {string | undefined} pinyin - 候选项的不带调拼音,不包含空格 76 | * @param {string} input - 用户输入的编码,不包含 /py 等快捷键 77 | * @param {boolean} isInUserPhrase - 候选项是否在用户词典中 78 | * @returns {number} 权重分数,规则如下: 79 | * - 拼音完全匹配:+10,000 80 | * - 拼音前缀匹配:+5,000 81 | * - 拼音部分包含:+1,000 + 拼音长度 82 | * - 找不到拼音但在用户词典中:视为完全匹配 +10,000 83 | * - 其它情况:0 84 | */ 85 | getWeightByPinyin(pinyin, input, isInUserPhrase) { 86 | if (pinyin === input) { 87 | return 10000 88 | } 89 | if (isInUserPhrase && !pinyin) { 90 | return 10000 91 | } 92 | if (pinyin?.startsWith(input)) { 93 | return 5000 94 | } 95 | if (pinyin?.includes(input)) { 96 | return 1000 + pinyin.length 97 | } 98 | return 0 99 | } 100 | 101 | /** 102 | * Extract the pinyin from the comment of the candidate 103 | * @param {string} comment the comment of the candidate 104 | * @returns {string | undefined} the pinyin 105 | */ 106 | extractPinyin(comment) { 107 | const match = comment.match(/〖(.+?)〗/) // cn2en 插件提供的带调拼音 108 | if (match) { 109 | return unaccent(match[1]) 110 | } 111 | const match2 = comment.match(/[(.*?)]/) || [] // 白霜拼音提供的不带调拼音 112 | return match2[1] 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/js/testutils.js: -------------------------------------------------------------------------------- 1 | export let totalTests = 0 2 | export let passedTests = 0 3 | 4 | export function assert(condition, message = '') { 5 | totalTests++ 6 | if (condition) { 7 | passedTests++ 8 | console.log('✓ ' + message) 9 | } else { 10 | console.log('✗ ' + message) 11 | console.log(' Expected true, but got false') 12 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 13 | } 14 | } 15 | 16 | export function assertEquals(actual, expected, message = '') { 17 | totalTests++ 18 | const actualStr = JSON.stringify(actual) 19 | const expectedStr = JSON.stringify(expected) 20 | if (actualStr === expectedStr) { 21 | passedTests++ 22 | console.log('✓ ' + message) 23 | } else { 24 | console.log('✗ ' + message) 25 | console.log(' Expected: ' + expectedStr) 26 | console.log(' Actual: ' + actualStr) 27 | throw new Error('Assertion failed' + (message ? ': ' + message : '')) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/js/translator_no_return.js: -------------------------------------------------------------------------------- 1 | export class BadTranslator { 2 | 3 | translate() { 4 | console.log('no return') 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/js/translator_test.js: -------------------------------------------------------------------------------- 1 | import { assert, assertEquals } from './testutils.js' 2 | 3 | export class TestTranslator { 4 | constructor(env) { 5 | console.log('translator_test init') 6 | assertEquals(env.namespace, 'translator_test') 7 | assert(env.userDataDir.endsWith('qjs/tests/')) 8 | assertEquals(env.sharedDataDir, '.') 9 | const config = env.engine.schema.config 10 | assertEquals(config.getString('greet'), 'hello from c++') 11 | console.log('translator_test init done') 12 | } 13 | 14 | finalizer() { 15 | console.log('translator_test finit') 16 | } 17 | 18 | translate(input, segment, env) { 19 | console.log('translator_test translate', input) 20 | assertEquals(env.namespace, 'translator_test') 21 | const config = env.engine.schema.config 22 | assertEquals(config.getString('greet'), 'hello from c++') 23 | 24 | // Check if the input matches the expected input from the test 25 | const expectedInput = config.getString('expectedInput') 26 | assertEquals(expectedInput, input) 27 | 28 | // Return candidates based on the input 29 | if (input === 'test_input') { 30 | return [ 31 | new Candidate('test', segment.start, segment.end, 'candidate1', 'comment1'), 32 | new Candidate('test', segment.start, segment.end, 'candidate2', 'comment2'), 33 | new Candidate('test', segment.start, segment.end, 'candidate3', 'comment3'), 34 | ] 35 | } 36 | return [] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/processor.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include "environment.h" 10 | #include "qjs_processor.h" 11 | #include "test_switch.h" 12 | 13 | using namespace rime; 14 | 15 | template 16 | class QuickJSProcessorTest : public ::testing::Test { 17 | protected: 18 | static void addSegment(Engine* engine, const std::string& prompt) { 19 | Segment segment(0, static_cast(prompt.length())); 20 | segment.prompt = prompt; 21 | engine->context()->composition().AddSegment(segment); 22 | } 23 | }; 24 | 25 | SETUP_JS_ENGINES(QuickJSProcessorTest); 26 | 27 | TYPED_TEST(QuickJSProcessorTest, ProcessKeyEvent) { 28 | the engine(Engine::Create()); 29 | ASSERT_TRUE(engine->schema() != nullptr); 30 | 31 | auto* config = engine->schema()->config(); 32 | ASSERT_TRUE(config != nullptr); 33 | config->SetString("greet", "hello from c++"); 34 | 35 | this->addSegment(engine.get(), "prompt"); 36 | 37 | Ticket ticket(engine.get(), "processor_test", "qjs_processor@processor_test"); 38 | auto env = std::make_unique(engine.get(), "processor_test"); 39 | auto processor = New>(ticket, env.get()); 40 | 41 | // Test key event that should be accepted 42 | KeyEvent acceptEvent("space"); 43 | EXPECT_EQ(processor->processKeyEvent(acceptEvent, env.get()), kAccepted); 44 | 45 | // Test key event that should be rejected 46 | KeyEvent rejectEvent("Return"); 47 | EXPECT_EQ(processor->processKeyEvent(rejectEvent, env.get()), kRejected); 48 | 49 | // Test key event that should result in noop 50 | KeyEvent noopEvent("invalid_key"); 51 | EXPECT_EQ(processor->processKeyEvent(noopEvent, env.get()), kNoop); 52 | } 53 | 54 | TYPED_TEST(QuickJSProcessorTest, NonExistentModule) { 55 | the engine(Engine::Create()); 56 | ASSERT_TRUE(engine->schema() != nullptr); 57 | 58 | this->addSegment(engine.get(), "prompt"); 59 | 60 | // Create a ticket with a non-existent module 61 | Ticket ticket(engine.get(), "non_existent", "qjs_processor@non_existent"); 62 | auto env = std::make_unique(engine.get(), "non_existent"); 63 | auto processor = New>(ticket, env.get()); 64 | 65 | // Test key event - should return noop due to unloaded module 66 | KeyEvent event("space"); 67 | EXPECT_EQ(processor->processKeyEvent(event, env.get()), kNoop); 68 | } 69 | -------------------------------------------------------------------------------- /tests/qjs/qjs_iterator.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | // #include // Include Boost.Stacktrace 6 | 7 | class AbstractIterator { 8 | public: 9 | virtual ~AbstractIterator() = default; 10 | virtual bool next() = 0; 11 | virtual JSValue peek() = 0; 12 | }; 13 | 14 | template 15 | class JSIteratorWrapper { 16 | public: 17 | JSIteratorWrapper(const JSIteratorWrapper&) = delete; 18 | JSIteratorWrapper(JSIteratorWrapper&&) = delete; 19 | JSIteratorWrapper& operator=(const JSIteratorWrapper&) = delete; 20 | JSIteratorWrapper& operator=(JSIteratorWrapper&&) = delete; 21 | 22 | explicit JSIteratorWrapper(JSContext* ctx) : ctx_{ctx} { 23 | static_assert(std::is_base_of_v, 24 | "Template parameter T must inherit from AbstractIterator"); 25 | 26 | auto* rt = JS_GetRuntime(ctx); 27 | if (classId == 0) { // Only register the class once 28 | JS_NewClassID(rt, &classId); 29 | JS_NewClass(rt, classId, &CLASS_DEF); 30 | 31 | // Create and store the prototype globally 32 | proto_ = JS_NewObject(ctx); 33 | if (!JS_IsException(proto_)) { 34 | JS_SetPropertyFunctionList(ctx, proto_, 35 | static_cast(PROTO_FUNCS), 36 | sizeof(PROTO_FUNCS) / sizeof(PROTO_FUNCS[0])); 37 | // Store prototype in class registry 38 | JS_SetClassProto(ctx, classId, JS_DupValue(ctx, proto_)); 39 | } 40 | } else { 41 | // Get the stored prototype 42 | proto_ = JS_GetClassProto(ctx, classId); 43 | } 44 | } 45 | 46 | ~JSIteratorWrapper() { 47 | if (!JS_IsUndefined(proto_)) { 48 | JS_FreeValue(ctx_, proto_); 49 | } 50 | } 51 | 52 | [[nodiscard]] JSValue createIterator(T* itor) const { 53 | // Create a shared_ptr to manage the iterator's lifetime 54 | auto iter = std::make_shared(*itor); 55 | delete itor; // Delete the original pointer since we've copied it 56 | 57 | JSValue obj = JS_NewObjectProtoClass(ctx_, proto_, classId); 58 | if (JS_IsException(obj)) { 59 | return obj; 60 | } 61 | 62 | // Store the shared_ptr in the opaque pointer 63 | auto* ptr = new std::shared_ptr(iter); 64 | if (JS_SetOpaque(obj, ptr) == -1) { 65 | delete ptr; 66 | JS_FreeValue(ctx_, obj); 67 | return JS_EXCEPTION; 68 | } 69 | return obj; 70 | } 71 | 72 | private: 73 | // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 74 | static JSClassID classId; 75 | JSContext* ctx_{nullptr}; 76 | JSValue proto_{JS_UNDEFINED}; 77 | 78 | static void finalizer(JSRuntime* rt, JSValue val) { 79 | void* ptr = JS_GetOpaque(val, classId); 80 | if (ptr != nullptr) { 81 | // Clean up the shared_ptr 82 | auto* iterPtr = static_cast*>(ptr); 83 | delete iterPtr; 84 | } 85 | } 86 | 87 | static JSValue next(JSContext* ctx, JSValueConst thisVal, int /*unused*/, JSValueConst*) { 88 | auto* ptr = static_cast*>(JS_GetOpaque(thisVal, classId)); 89 | return ptr && ptr->get() ? JS_NewBool(ctx, (*ptr)->next()) 90 | : JS_ThrowTypeError(ctx, "Invalid iterator"); 91 | } 92 | 93 | static JSValue peek(JSContext* ctx, JSValueConst thisVal, int /*unused*/, JSValueConst*) { 94 | auto* ptr = static_cast*>(JS_GetOpaque(thisVal, classId)); 95 | return ptr && ptr->get() ? (*ptr)->peek() : JS_ThrowTypeError(ctx, "Invalid iterator"); 96 | } 97 | 98 | static constexpr JSClassDef CLASS_DEF{ 99 | .class_name = "JSIteratorWrapper", 100 | .finalizer = finalizer, 101 | .gc_mark = nullptr, 102 | .call = nullptr, 103 | .exotic = nullptr, 104 | }; 105 | 106 | static constexpr JSCFunctionListEntry PROTO_FUNCS[] = {JS_CFUNC_DEF("next", 0, next), 107 | JS_CFUNC_DEF("peek", 0, peek)}; 108 | 109 | void registerClass() { 110 | proto_ = JS_NewObject(ctx_); 111 | if (JS_IsException(proto_)) { 112 | return; 113 | } 114 | 115 | // Set the prototype functions 116 | JS_SetPropertyFunctionList(ctx_, proto_, static_cast(PROTO_FUNCS), 117 | sizeof(PROTO_FUNCS) / sizeof(PROTO_FUNCS[0])); 118 | 119 | // Set the class prototype 120 | JS_SetClassProto(ctx_, classId, proto_); 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /tests/qjs/report-js-error.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include "../test_switch.h" 7 | 8 | std::string trim(const std::string& str) { 9 | const auto start = str.find_first_not_of(" \t\n\r"); 10 | if (start == std::string::npos) { 11 | return ""; 12 | } 13 | 14 | const auto end = str.find_last_not_of(" \t\n\r"); 15 | return str.substr(start, end - start + 1); 16 | } 17 | 18 | template 19 | class QuickJSErrorTest : public ::testing::Test {}; 20 | 21 | SETUP_JS_ENGINES(QuickJSErrorTest); 22 | 23 | TYPED_TEST(QuickJSErrorTest, TestJsRuntimeError) { 24 | auto& jsEngine = JsEngine::instance(); 25 | auto module = jsEngine.loadJsFile("runtime-error.js"); 26 | auto globalObj = jsEngine.getGlobalObject(); 27 | auto func = jsEngine.getObjectProperty(globalObj, "funcWithRuntimeError"); 28 | ASSERT_TRUE(jsEngine.isFunction(func)); 29 | 30 | auto result = jsEngine.callFunction(func, JS_UNDEFINED, 0, nullptr); 31 | ASSERT_TRUE(jsEngine.isException(result)); 32 | 33 | // The exception is alredy captured in the js engine logger. The log should be: 34 | // ReferenceError: abcdefg is not defined 35 | // at (runtime-error.js:7:21) 36 | 37 | jsEngine.freeValue(module, globalObj, func, result); 38 | } 39 | -------------------------------------------------------------------------------- /tests/qjs/vector-iterator.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "qjs_iterator.hpp" 9 | 10 | using std::move; 11 | 12 | class VectorIterator : public AbstractIterator { 13 | public: 14 | explicit VectorIterator(JSContext* context, std::vector& vec) 15 | : context_{context}, vec_{vec} {} 16 | 17 | bool next() override { return current_ < vec_.size(); } 18 | 19 | JSValue peek() override { 20 | int value = next() ? vec_[current_++] : -1; 21 | return JS_NewInt32(context_, value); 22 | } 23 | 24 | private: 25 | JSContext* context_{nullptr}; 26 | std::vector vec_; 27 | size_t current_{0}; 28 | }; 29 | 30 | // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 31 | template <> 32 | JSClassID JSIteratorWrapper::classId = 0; // Initialize with 0 33 | 34 | class QuickJSGeneratorTest : public ::testing::Test { 35 | protected: 36 | void SetUp() override { 37 | rt_ = JS_NewRuntime(); 38 | ctx_ = JS_NewContext(rt_); 39 | wrapper_ = std::make_unique>(ctx_); 40 | } 41 | 42 | void TearDown() override { 43 | // free the wrapper before freeing the context and runtime 44 | wrapper_.reset(); 45 | JS_FreeContext(ctx_); 46 | JS_FreeRuntime(rt_); 47 | } 48 | 49 | JSContext* getContext() { return ctx_; } 50 | 51 | std::unique_ptr>& getWrapper() { return wrapper_; } 52 | 53 | private: 54 | std::unique_ptr> wrapper_; 55 | JSRuntime* rt_{nullptr}; 56 | JSContext* ctx_{nullptr}; 57 | }; 58 | 59 | // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables, readability-function-cognitive-complexity) 60 | TEST_F(QuickJSGeneratorTest, TestVectorIterator) { 61 | // NOLINTNEXTLINE(readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers) 62 | std::vector numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 63 | 64 | auto* context = getContext(); 65 | 66 | auto* vecIterator = new VectorIterator(context, numbers); 67 | JSValue iterator = getWrapper()->createIterator(vecIterator); 68 | // vecIterator is now managed by the shared_ptr, don't delete it manually 69 | ASSERT_FALSE(JS_IsException(iterator)); 70 | 71 | constexpr std::string_view SCRIPT = R"( 72 | function* filterEvenNumbers(iter) { 73 | while (iter.next()) { 74 | const num = iter.peek(); 75 | if (num % 2 === 0) { 76 | yield num; 77 | } 78 | } 79 | } 80 | )"; 81 | 82 | JSValue result = JS_Eval(context, SCRIPT.data(), SCRIPT.size(), "", JS_EVAL_TYPE_GLOBAL); 83 | ASSERT_FALSE(JS_IsException(result)); 84 | 85 | JSValue global = JS_GetGlobalObject(context); 86 | JSValue filterFunc = JS_GetPropertyStr(context, global, "filterEvenNumbers"); 87 | ASSERT_FALSE(JS_IsException(filterFunc)); 88 | 89 | JSValue generator = JS_Call(context, filterFunc, JS_UNDEFINED, 1, &iterator); 90 | ASSERT_FALSE(JS_IsException(generator)); 91 | 92 | JSValue nextMethod = JS_GetPropertyStr(context, generator, "next"); 93 | 94 | std::vector filteredNumbers; 95 | while (true) { 96 | JSValue nextResult = JS_Call(context, nextMethod, generator, 0, nullptr); 97 | if (JS_IsException(nextResult)) { 98 | JS_FreeValue(context, nextResult); 99 | break; 100 | } 101 | 102 | JSValue done = JS_GetPropertyStr(context, nextResult, "done"); 103 | if (JS_ToBool(context, done) != 0) { 104 | JS_FreeValue(context, done); 105 | JS_FreeValue(context, nextResult); 106 | break; 107 | } 108 | JS_FreeValue(context, done); 109 | 110 | JSValue value = JS_GetPropertyStr(context, nextResult, "value"); 111 | int32_t num = 0; 112 | JS_ToInt32(context, &num, value); 113 | filteredNumbers.push_back(num); 114 | 115 | JS_FreeValue(context, value); 116 | JS_FreeValue(context, nextResult); 117 | } 118 | 119 | ASSERT_EQ(filteredNumbers, (std::vector{2, 4, 6, 8, 10})); 120 | 121 | for (auto obj : {global, filterFunc, generator, nextMethod, result, iterator}) { 122 | JS_FreeValue(context, obj); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/rime-qjs-test-main.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "engines/common.h" 11 | 12 | #ifdef _WIN32 13 | #include 14 | #endif 15 | 16 | void setJavaScriptCoreOptionsToDebug() { 17 | #if defined(_ENABLE_JAVASCRIPTCORE) 18 | LOG(INFO) << "setting the undocumented JavaScriptCore options to debug"; 19 | 20 | setenv("JSC_dumpOptions", "1", 1); // Logs JSC runtime options at startup 21 | 22 | setenv("JSC_logGC", "2", 1); // Enable GC logging: 0 = None, 1 = Basic, 2 = Verbose 23 | setenv("JSC_useSigillCrashAnalyzer", "1", 1); // Enhances crash logs for JSC-related crashes 24 | 25 | // setenv("JSC_dumpDFGDisassembly", "1", 1); // Dumps DFG JIT assembly code 26 | // setenv("JSC_dumpFTLDisassembly", "1", 1); // Dumps FTL JIT assembly code 27 | 28 | // setenv("JSC_showDisassembly", "1", 1); // Logs JIT-compiled code disassembly (advanced debugging) 29 | 30 | // Seems not working: Set JSGC_MAX_HEAP_SIZE to 500MB (in bytes) before creating any JSC context 31 | // setenv("JSGC_MAX_HEAP_SIZE", "524288000", 1); // 500MB = 500 * 1024 * 1024 32 | #endif 33 | } 34 | 35 | using rime::kDefaultModules; 36 | 37 | class GlobalEnvironment : public testing::Environment { 38 | private: 39 | std::string userDataDir_; 40 | 41 | public: 42 | void SetUp() override { 43 | std::filesystem::path path(__FILE__); 44 | path.remove_filename(); 45 | userDataDir_ = path.generic_string(); 46 | LOG(INFO) << "setting up user data dir: " << userDataDir_; 47 | 48 | RimeTraits traits = { 49 | .data_size = sizeof(RimeTraits) - sizeof((traits).data_size), 50 | .shared_data_dir = ".", 51 | .user_data_dir = userDataDir_.c_str(), 52 | .distribution_name = nullptr, 53 | .distribution_code_name = nullptr, 54 | .distribution_version = nullptr, 55 | .app_name = "rime.test", 56 | .modules = nullptr, 57 | .min_log_level = 0, 58 | .log_dir = nullptr, 59 | .prebuilt_data_dir = ".", 60 | .staging_dir = ".", 61 | }; 62 | rime_get_api()->setup(&traits); 63 | rime_get_api()->initialize(&traits); 64 | 65 | setJavaScriptCoreOptionsToDebug(); 66 | } 67 | 68 | void TearDown() override { 69 | // `JsEngine::shutdown();` is not needed since it's called in module.cc 70 | rime_get_api()->finalize(); 71 | } 72 | }; 73 | 74 | int main(int argc, char** argv) noexcept { 75 | testing::InitGoogleTest(&argc, argv); 76 | try { 77 | testing::AddGlobalTestEnvironment(new GlobalEnvironment); 78 | } catch (const std::bad_alloc& e) { 79 | LOG(ERROR) << "Failed to allocate GlobalEnvironment: " << e.what(); 80 | std::exit(1); 81 | } 82 | 83 | #ifdef _WIN32 84 | // Enables UTF-8 output in the Windows console 85 | // Only works when printing logs directly to console using `.\librime-qjs-tests.exe` 86 | // Does not work when redirecting output to a file using `.\librime-qjs-tests.exe > tests.log 2>&1` 87 | SetConsoleOutputCP(CP_UTF8); 88 | #endif 89 | 90 | return RUN_ALL_TESTS(); 91 | } 92 | -------------------------------------------------------------------------------- /tests/test_helper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | inline std::string getFolderPath(const std::string& sourcePath) { 6 | size_t found = sourcePath.find_last_of("/\\"); 7 | return sourcePath.substr(0, found); 8 | } 9 | -------------------------------------------------------------------------------- /tests/test_switch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "engines/common.h" 4 | 5 | #ifdef _ENABLE_JAVASCRIPTCORE 6 | 7 | #define SETUP_JS_ENGINES(testSuite) \ 8 | using JsTypes = ::testing::Types; \ 9 | TYPED_TEST_SUITE(testSuite, JsTypes); 10 | 11 | #else 12 | 13 | #define SETUP_JS_ENGINES(testSuite) \ 14 | using JsTypes = ::testing::Types; \ 15 | TYPED_TEST_SUITE(testSuite, JsTypes); 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /tests/translation.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "environment.h" 6 | #include "fake_translation.hpp" 7 | #include "qjs_translation.h" 8 | #include "test_switch.h" 9 | 10 | using namespace rime; 11 | 12 | template 13 | class QuickJSTranslationTest : public ::testing::Test { 14 | protected: 15 | static an createMockTranslation() { 16 | auto translation = New(); 17 | translation->append(New("mock", 0, 1, "text1", "comment1")); 18 | translation->append(New("mock", 0, 1, "text2", "comment2")); 19 | translation->append(New("mock", 0, 1, "text3", "comment3")); 20 | return translation; 21 | } 22 | }; 23 | 24 | SETUP_JS_ENGINES(QuickJSTranslationTest); 25 | 26 | TYPED_TEST(QuickJSTranslationTest, Initialize) { 27 | auto& jsEngine = JsEngine::instance(); 28 | auto translation = this->createMockTranslation(); 29 | Environment env(nullptr, "test"); 30 | auto qjsTranslation = 31 | New>(translation, TypeParam(), TypeParam(), &env); 32 | EXPECT_TRUE(qjsTranslation->exhausted()); 33 | EXPECT_FALSE(qjsTranslation->Next()); 34 | EXPECT_EQ(qjsTranslation->Peek(), nullptr); 35 | } 36 | 37 | // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables, readability-function-cognitive-complexity) 38 | TYPED_TEST(QuickJSTranslationTest, FilterCandidates) { 39 | auto translation = this->createMockTranslation(); 40 | const char* jsCode = R"( 41 | function filterCandidates(candidates, env) { 42 | console.log(`filterCandidates: ${candidates.length}`) 43 | console.log(`env.namespace: ${env.namespace}`) 44 | const ret = candidates.filter((it, idx) => { 45 | console.log(`it.text = ${it.text}`) 46 | return it.text === 'text2' 47 | }) 48 | console.log(`ret.length: ${ret.length}`) 49 | return ret 50 | } 51 | )"; 52 | 53 | auto& jsEngine = JsEngine::instance(); 54 | auto result = jsEngine.eval(jsCode, ""); 55 | auto global = jsEngine.getGlobalObject(); 56 | auto filterFunc = jsEngine.getObjectProperty(jsEngine.toObject(global), "filterCandidates"); 57 | 58 | Environment env(nullptr, "test"); 59 | auto qjsTranslation = 60 | New>(translation, TypeParam(), filterFunc, &env); 61 | auto candidate = qjsTranslation->Peek(); 62 | 63 | ASSERT_TRUE(candidate != nullptr); 64 | EXPECT_EQ(candidate->text(), "text2"); 65 | ASSERT_TRUE(qjsTranslation->Next()); 66 | EXPECT_TRUE(qjsTranslation->exhausted()); 67 | candidate = qjsTranslation->Peek(); 68 | ASSERT_TRUE(candidate == nullptr); 69 | ASSERT_FALSE(qjsTranslation->Next()); 70 | 71 | jsEngine.freeValue(global); 72 | jsEngine.freeValue(result); 73 | jsEngine.freeValue(filterFunc); 74 | } 75 | 76 | TYPED_TEST(QuickJSTranslationTest, EmptyTranslation) { 77 | auto& jsEngine = JsEngine::instance(); 78 | auto translation = New(); 79 | Environment env(nullptr, "test"); 80 | auto qjsTranslation = 81 | New>(translation, TypeParam(), TypeParam(), &env); 82 | EXPECT_TRUE(qjsTranslation->exhausted()); 83 | EXPECT_FALSE(qjsTranslation->Next()); 84 | EXPECT_EQ(qjsTranslation->Peek(), nullptr); 85 | } 86 | 87 | TYPED_TEST(QuickJSTranslationTest, NoReturnValueShouldNotCrash) { 88 | auto& jsEngine = JsEngine::instance(); 89 | auto translation = this->createMockTranslation(); 90 | 91 | const char* jsCode = "function noReturn() { }"; 92 | auto result = jsEngine.eval(jsCode, ""); 93 | auto global = jsEngine.getGlobalObject(); 94 | auto filterFunc = jsEngine.getObjectProperty(jsEngine.toObject(global), "noReturn"); 95 | 96 | Environment env(nullptr, "test"); 97 | auto qjsTranslation = 98 | New>(translation, TypeParam(), filterFunc, &env); 99 | EXPECT_TRUE(qjsTranslation->exhausted()); 100 | EXPECT_FALSE(qjsTranslation->Next()); 101 | EXPECT_EQ(qjsTranslation->Peek(), nullptr); 102 | 103 | jsEngine.freeValue(filterFunc, result, global); 104 | } 105 | -------------------------------------------------------------------------------- /tests/types.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "dict_data_helper.hpp" 10 | #include "environment.h" 11 | #include "qjs_types.h" 12 | #include "test_helper.hpp" 13 | #include "test_switch.h" 14 | 15 | using namespace rime; 16 | 17 | template 18 | class QuickJSTypesTest : public ::testing::Test { 19 | private: 20 | DictionaryDataHelper trieDataHelper_ = 21 | DictionaryDataHelper(getFolderPath(__FILE__).c_str(), "dummy_dict.txt"); 22 | 23 | protected: 24 | void SetUp() override { trieDataHelper_.createDummyTextFile(); } 25 | 26 | void TearDown() override { 27 | auto folder = getFolderPath(__FILE__); 28 | trieDataHelper_.cleanupDummyFiles(); 29 | std::remove((folder + "/dumm.bin").c_str()); // the file generated in js 30 | std::filesystem::remove_all(folder + "/dumm.ldb"); // the leveldb folder generated in js 31 | } 32 | }; 33 | 34 | SETUP_JS_ENGINES(QuickJSTypesTest); 35 | 36 | // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables, readability-function-cognitive-complexity) 37 | TYPED_TEST(QuickJSTypesTest, WrapUnwrapRimeTypes) { 38 | the engine(Engine::Create()); 39 | ASSERT_TRUE(engine->schema() != nullptr); 40 | auto* config = engine->schema()->config(); 41 | ASSERT_TRUE(config != nullptr); 42 | config->SetBool("key1", true); 43 | config->SetBool("key2", false); 44 | constexpr int A_INT_NUMBER = 666; 45 | config->SetInt("key3", A_INT_NUMBER); 46 | constexpr double A_DOUBLE_NUMBER = 0.999; 47 | config->SetDouble("key4", A_DOUBLE_NUMBER); 48 | config->SetString("key5", "string"); 49 | 50 | auto list = New(); 51 | list->Append(New("item1")); 52 | list->Append(New("item2")); 53 | list->Append(New("item3")); 54 | config->SetItem("list", list); 55 | 56 | auto* context = engine->context(); 57 | ASSERT_TRUE(context != nullptr); 58 | context->set_input("hello"); 59 | 60 | auto& jsEngine = JsEngine::instance(); 61 | 62 | auto env = std::make_unique(engine.get(), "namespace"); 63 | TypeParam environment = jsEngine.wrap(env.get()); 64 | 65 | auto folderPath = getFolderPath(__FILE__); 66 | auto jsEnvironment = jsEngine.toObject(environment); 67 | jsEngine.setObjectProperty(jsEnvironment, "currentFolder", jsEngine.wrap(folderPath)); 68 | 69 | an candidate = New("mock", 0, 1, "text", "comment"); 70 | jsEngine.setObjectProperty(jsEnvironment, "candidate", jsEngine.wrap(candidate)); 71 | 72 | auto result = jsEngine.loadJsFile("types_test"); 73 | auto global = jsEngine.getGlobalObject(); 74 | auto jsFunc = jsEngine.getObjectProperty(jsEngine.toObject(global), "checkArgument"); 75 | auto retValue = 76 | jsEngine.callFunction(jsEngine.toObject(jsFunc), jsEngine.toObject(global), 1, &environment); 77 | 78 | auto retJsEngine = jsEngine.getObjectProperty(jsEngine.toObject(retValue), "engine"); 79 | auto* retEngine = jsEngine.template unwrap(retJsEngine); 80 | ASSERT_EQ(retEngine, engine.get()); 81 | ASSERT_EQ(retEngine->schema()->schema_name(), engine->schema()->schema_name()); 82 | auto retJsCandidate = jsEngine.getObjectProperty(jsEngine.toObject(retValue), "candidate"); 83 | an retCandidate = jsEngine.template unwrap(retJsCandidate); 84 | ASSERT_EQ(retCandidate->text(), "new text"); 85 | ASSERT_EQ(retCandidate.get(), candidate.get()); 86 | 87 | string greet; 88 | bool success = retEngine->schema()->config()->GetString("greet", &greet); 89 | ASSERT_TRUE(success); 90 | ASSERT_EQ(greet, "hello from js"); 91 | 92 | Context* retContext = retEngine->context(); 93 | ASSERT_EQ(retContext->input(), "world"); 94 | 95 | // js code: env.newCandidate = new Candidate('js', 32, 100, 'the text', 'the comment', 888) 96 | auto retJsNewCandidate = jsEngine.getObjectProperty(jsEngine.toObject(retValue), "newCandidate"); 97 | auto newCandidate = jsEngine.template unwrap(retJsNewCandidate); 98 | ASSERT_EQ(newCandidate->type(), "js"); 99 | ASSERT_EQ(newCandidate->start(), 32); 100 | ASSERT_EQ(newCandidate->end(), 100); 101 | ASSERT_EQ(newCandidate->text(), "the text"); 102 | ASSERT_EQ(newCandidate->comment(), "the comment"); 103 | ASSERT_EQ(newCandidate->quality(), 888); 104 | 105 | jsEngine.freeValue(jsEnvironment, result, global, jsFunc, retValue, retJsEngine, retJsCandidate, 106 | retJsNewCandidate); 107 | } 108 | -------------------------------------------------------------------------------- /tools/clang-format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # usage: 4 | # lint - `bash tools/clang-format.sh lint` 5 | # format - `bash tools/clang-format.sh format` 6 | 7 | root="$(cd "$(dirname "$0")" && pwd)/.." 8 | 9 | options="-Werror --dry-run" # default to lint only mode 10 | 11 | if [ "$1" = "format" ]; then 12 | options="-Werror -i" 13 | fi 14 | 15 | ## run on a file to validate the configuration of .clang-format 16 | clang-format ${options} ${root}/src/module.cc 17 | 18 | find ${root}/src \ 19 | -type f \( -name '*.h' -o -name '*.hpp' -o -name '*.cc' -o -name '*.cpp' \) | \ 20 | xargs clang-format ${options} || { echo Please lint your code by '"'"bash ./tools/clang-format.sh lint"'"'.; false; } 21 | 22 | find ${root}/tests \ 23 | -type f \( -name '*.h' -o -name '*.hpp' -o -name '*.cc' -o -name '*.cpp' \) | \ 24 | xargs clang-format ${options} || { echo Please lint your code by '"'"bash ./tools/clang-format.sh lint"'"'.; false; } 25 | -------------------------------------------------------------------------------- /tools/clang-tidy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # usage: 4 | # - lint the modified files: `bash tools/clang-tidy.sh modified` 5 | # - lint all the files: `bash tools/clang-tidy.sh all` 6 | 7 | root="$(cd "$(dirname "$0")" && pwd)/.." 8 | 9 | mode="modified" 10 | if [ "$1" = "all" ]; then 11 | mode="all" 12 | fi 13 | 14 | cmake ${root} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON 15 | 16 | mv ${root}/compile_commands.json ${root}/build 17 | 18 | options="-p ${root}/build \ 19 | --config-file=${root}/.clang-tidy \ 20 | --warnings-as-errors=* \ 21 | --header-filter=\"${root}/(src|tests)/.*\" \ 22 | --system-headers=0 \ 23 | --use-color \ 24 | -extra-arg=-I${root}/src \ 25 | -extra-arg=-I${root}/src/engines \ 26 | -extra-arg=-I${root}/src/gears \ 27 | -extra-arg=-I${root}/src/types \ 28 | -extra-arg=-I${root}/src/misc \ 29 | -extra-arg=-I${root}/src/patch/quickjs \ 30 | -extra-arg=-I${root}/tests \ 31 | -extra-arg=-isystem${root}/../../src \ 32 | -extra-arg=-isystem${root}/../../build/src \ 33 | -extra-arg=-isystem${root}/../../include \ 34 | -extra-arg=-isystem${root}/../../include/glog \ 35 | -extra-arg=-isystem${root}/thirdparty/quickjs \ 36 | -extra-arg=-isystem/usr/local/include \ 37 | -extra-arg=-stdlib=libc++ \ 38 | -extra-arg=-D_ENABLE_JAVASCRIPTCORE \ 39 | -extra-arg=-D_GNU_SOURCE \ 40 | -extra-arg=-DGLOG_EXPORT=__attribute__((visibility(\"default\"))) \ 41 | -extra-arg=-DGLOG_NO_EXPORT=__attribute__((visibility(\"default\"))) \ 42 | -extra-arg=-DGLOG_DEPRECATED=__attribute__((deprecated))" 43 | 44 | ignore_files="test_switch.h" 45 | 46 | process_file() { 47 | if [[ $1 =~ $ignore_files ]]; then 48 | echo "Ignoring $1..." 49 | return 50 | fi 51 | echo "Processing $1..." 52 | clang-tidy ${options} "$1" 53 | } 54 | 55 | export -f process_file 56 | export options 57 | export ignore_files 58 | 59 | if [ "$mode" = "all" ]; then 60 | echo "Linting all files..." 61 | 62 | find ${root}/{src,tests} \ 63 | -type f \( -name '*.h' -o -name '*.hpp' -o -name '*.c' -o -name '*.cc' -o -name '*.cpp' \) | \ 64 | xargs -P $(sysctl -n hw.ncpu) -I {} bash -c 'process_file "{}"' 65 | else 66 | echo "Linting modified files..." 67 | 68 | git diff --name-only HEAD | \ 69 | grep -E '\.(cpp|cc|c|h|hpp)$' | \ 70 | xargs -P $(sysctl -n hw.ncpu) -I {} bash -c 'process_file "{}"' 71 | fi 72 | -------------------------------------------------------------------------------- /tools/expand-macro.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## usage: `bash ./tools/expand-macro.sh src/types/qjs_candidate.h` 4 | 5 | root="$(cd "$(dirname "$0")" && pwd)/.." 6 | 7 | target="${root}/build/expanded" 8 | mkdir -p "${target}" 9 | 10 | file=$1 11 | filename=$(basename "$file") 12 | 13 | /opt/local/libexec/llvm-20/bin/clang++ \ 14 | -E -nostdinc++ \ 15 | -isystem /opt/local/libexec/llvm-20/include/c++/v1 \ 16 | -isystem /opt/local/libexec/llvm-20/lib/clang/20/include \ 17 | -isystem /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include \ 18 | -isystem /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Cryptexes/OS/System/Library/Frameworks \ 19 | -F/System/Library/Frameworks \ 20 | -F/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks \ 21 | -I${root}/src \ 22 | -I${root}/src/gears \ 23 | -I${root}/src/types \ 24 | -I${root}/src/misc \ 25 | -I${root}/src/engines \ 26 | -I${root}/src/engines/quickjs \ 27 | -I${root}/src/engines/javascriptcore \ 28 | -I${root}/../../src \ 29 | -I${root}/../../build/src \ 30 | -I${root}/../../include \ 31 | -I${root}/../../include/glog \ 32 | -I${root}/thirdparty/quickjs \ 33 | -stdlib=libc++ \ 34 | -D_ENABLE_JAVASCRIPTCORE \ 35 | -D_GNU_SOURCE \ 36 | -DGLOG_EXPORT=__attribute__\(\(visibility\(\"default\"\)\)\) \ 37 | -DGLOG_NO_EXPORT=__attribute__\(\(visibility\(\"default\"\)\)\) \ 38 | -DGLOG_DEPRECATED=__attribute__\(\(deprecated\)\) \ 39 | -Wno-deprecated \ 40 | -Wno-macro-redefined \ 41 | -Wno-variadic-macros \ 42 | -std=c++17 \ 43 | ${root}/${file} \ 44 | -o ${target}/${filename}.i && 45 | echo "expanded the macros of ${file} to ${target}/${filename}.i" 46 | -------------------------------------------------------------------------------- /tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "librime-qjs-tools", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "check-api-coverage.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "BSD-3-Clause", 12 | "description": "" 13 | } 14 | -------------------------------------------------------------------------------- /tools/update-version.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | :: Get the script directory and set root path 5 | set "root=%~dp0.." 6 | 7 | :: Get version from CMake output 8 | for /f "tokens=4" %%i in ('cmake "%root%" 2^>^&1 ^| findstr "LibrimeQjs version:"') do ( 9 | set "version=%%i" 10 | ) 11 | 12 | :: Check if version was found 13 | if not defined version ( 14 | echo Error: Could not extract version number 15 | exit /b 1 16 | ) 17 | 18 | :: Get git reference if Nightly parameter is passed 19 | if "%1"=="Nightly" ( 20 | for /f "tokens=*" %%i in ('git describe --always') do set "gitref=%%i" 21 | set "version=%version%+!gitref!" 22 | ) 23 | 24 | :: Update version in rime.d.ts using PowerShell (more reliable than batch file string replacement) 25 | powershell -Command "(Get-Content '%root%\contrib\rime.d.ts') -replace 'LIB_RIME_QJS_VERSION', '%version%' | Set-Content '%root%\contrib\rime.d.ts'" 26 | 27 | echo Updated version to %version% in rime.d.ts 28 | -------------------------------------------------------------------------------- /tools/update-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the version from CMake output 4 | root="$(cd "$(dirname "$0")" && pwd)/.." 5 | version=$(cmake ${root} 2>&1 | grep "LibrimeQjs version:" | awk '{print $4}') 6 | gitref=$(git describe --always) 7 | 8 | if [ -z "$version" ]; then 9 | echo "Error: Could not extract version number" 10 | exit 1 11 | fi 12 | 13 | if [ "$1" = "Nightly" ]; then 14 | version="${version}+${gitref}" 15 | fi 16 | 17 | # Update version in rime.d.ts 18 | sed -i '' "s/LIB_RIME_QJS_VERSION/$version/" contrib/rime.d.ts 19 | 20 | echo "Updated version to $version in rime.d.ts" 21 | --------------------------------------------------------------------------------