├── .clang-format ├── .gitignore ├── .vscode ├── c_cpp_properties.json ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── archlinux ├── PKGBUILD └── cmath-include.patch ├── cpp ├── CMakeLists.txt └── src │ ├── module.cc │ ├── python_filter.cc │ ├── python_filter.h │ ├── python_simple_filter.cc │ ├── python_simple_filter.h │ ├── python_simple_translator.cc │ ├── python_simple_translator.h │ ├── python_translator.cc │ └── python_translator.h ├── examples ├── charset.simple_filter.py ├── complex.filter.py ├── datetime.simple_translator.py └── putonghua_cloud_input.translator.py ├── images ├── charset-0.png ├── charset-1.png ├── complex-0.png ├── complex-1.png ├── datetime.png └── putonghua_cloud_input.png └── python ├── setup.py └── src └── rimeext ├── Filter.py ├── Translator.py └── __init__.py /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Chromium 3 | IndentWidth: 4 4 | ColumnLimit: 0 5 | Cpp11BracedListStyle: 'false' 6 | SortIncludes: false 7 | SpaceAfterCStyleCast: 'true' 8 | SpaceBeforeCpp11BracedList: 'true' 9 | SpacesBeforeTrailingComments: '2' 10 | SpacesInParentheses: 'true' 11 | ... 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dict.yaml 2 | dist 3 | *.egg-info 4 | .mypy_cache 5 | .pytest_cache 6 | __pycache__ 7 | 8 | /cpp/cpp 9 | /python/README.md 10 | /archlinux/*.gz 11 | /archlinux/*.zst 12 | /archlinux/src 13 | /archlinux/pkg 14 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${default}", 7 | "${workspaceFolder}/../librime/src", 8 | "${workspaceFolder}/../librime/build/src", 9 | "/usr/include/python3.9" 10 | ], 11 | "compilerPath": "/usr/bin/clang", 12 | "cStandard": "c17", 13 | "cppStandard": "c++17", 14 | "intelliSenseMode": "linux-clang-x64" 15 | } 16 | ], 17 | "version": 4 18 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-vscode.cpptools", 4 | "xaver.clang-format", 5 | "streetsidesoftware.code-spell-checker", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "vector": "cpp", 4 | "iosfwd": "cpp", 5 | "iterator": "cpp", 6 | "string": "cpp", 7 | "filesystem": "cpp", 8 | "type_traits": "cpp", 9 | "cctype": "cpp", 10 | "clocale": "cpp", 11 | "cmath": "cpp", 12 | "cstdarg": "cpp", 13 | "cstddef": "cpp", 14 | "cstdio": "cpp", 15 | "cstdlib": "cpp", 16 | "cstring": "cpp", 17 | "ctime": "cpp", 18 | "cwchar": "cpp", 19 | "cwctype": "cpp", 20 | "any": "cpp", 21 | "array": "cpp", 22 | "atomic": "cpp", 23 | "strstream": "cpp", 24 | "bit": "cpp", 25 | "*.tcc": "cpp", 26 | "bitset": "cpp", 27 | "chrono": "cpp", 28 | "cinttypes": "cpp", 29 | "codecvt": "cpp", 30 | "compare": "cpp", 31 | "complex": "cpp", 32 | "concepts": "cpp", 33 | "condition_variable": "cpp", 34 | "cstdint": "cpp", 35 | "deque": "cpp", 36 | "forward_list": "cpp", 37 | "list": "cpp", 38 | "map": "cpp", 39 | "set": "cpp", 40 | "unordered_map": "cpp", 41 | "unordered_set": "cpp", 42 | "exception": "cpp", 43 | "algorithm": "cpp", 44 | "functional": "cpp", 45 | "memory": "cpp", 46 | "memory_resource": "cpp", 47 | "numeric": "cpp", 48 | "optional": "cpp", 49 | "random": "cpp", 50 | "ratio": "cpp", 51 | "source_location": "cpp", 52 | "string_view": "cpp", 53 | "system_error": "cpp", 54 | "tuple": "cpp", 55 | "utility": "cpp", 56 | "hash_map": "cpp", 57 | "hash_set": "cpp", 58 | "slist": "cpp", 59 | "fstream": "cpp", 60 | "future": "cpp", 61 | "initializer_list": "cpp", 62 | "iomanip": "cpp", 63 | "iostream": "cpp", 64 | "istream": "cpp", 65 | "limits": "cpp", 66 | "mutex": "cpp", 67 | "new": "cpp", 68 | "numbers": "cpp", 69 | "ostream": "cpp", 70 | "semaphore": "cpp", 71 | "shared_mutex": "cpp", 72 | "sstream": "cpp", 73 | "stdexcept": "cpp", 74 | "stop_token": "cpp", 75 | "streambuf": "cpp", 76 | "thread": "cpp", 77 | "cfenv": "cpp", 78 | "typeindex": "cpp", 79 | "typeinfo": "cpp", 80 | "valarray": "cpp", 81 | "variant": "cpp", 82 | "__bit_reference": "cpp", 83 | "__bits": "cpp", 84 | "__config": "cpp", 85 | "__debug": "cpp", 86 | "__errc": "cpp", 87 | "__functional_base": "cpp", 88 | "__hash_table": "cpp", 89 | "__locale": "cpp", 90 | "__mutex_base": "cpp", 91 | "__node_handle": "cpp", 92 | "__nullptr": "cpp", 93 | "__split_buffer": "cpp", 94 | "__string": "cpp", 95 | "__threading_support": "cpp", 96 | "__tree": "cpp", 97 | "__tuple": "cpp", 98 | "ios": "cpp", 99 | "locale": "cpp", 100 | "queue": "cpp", 101 | "stack": "cpp", 102 | "__functional_03": "cpp", 103 | "csignal": "cpp", 104 | "coroutine": "cpp" 105 | }, 106 | "[cpp]": { 107 | "editor.defaultFormatter": "xaver.clang-format", 108 | "editor.formatOnSave": true 109 | }, 110 | "[python]": { 111 | "editor.formatOnSave": true 112 | }, 113 | "C_Cpp.default.cppStandard": "c++17", 114 | "python.formatting.provider": "autopep8", 115 | "python.formatting.autopep8Args": [ 116 | "--max-line-length=65536", 117 | "--ignore=E302" 118 | ] 119 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # librime-python 2 | 3 | **为什么你需要 librime-python?** 4 | 5 | 1\. Python 是解释执行的脚本语言,因此可以在 rime 输入法的基础上动态地添加功能,而无需重新编译源代码、重启输入法程序,甚至无需重新部署。 6 | 7 | 2\. 与 librime 本身使用的 C++ 语言相比,Python 提供了丰富的库函数,因此可以非常方便地拓展 rime 输入法的功能。Python 常见的应用场景包括字符串处理(自动纠错)、连接 HTTP 服务器(云输入)、深度学习(智能输入引擎)等。 8 | 9 | ## 一、示例 10 | 11 | ### 1.1. `simple_translator` 12 | 13 | 输出当前日期:[`python_simple_translator@datetime`](examples/datetime.simple_translator.py) 14 | 15 | python_simple_translator@datetime 16 | 17 | ### 1.2. `translator` 18 | 19 | 汉语拼音(普通话)云输入:[`python_translator@putonghua_cloud_input`](examples/putonghua_cloud_input.translator.py) 20 | 21 | python_translator@putonghua_cloud_input 22 | 23 | ### 1.3. `simple_filter` 24 | 25 | 过滤拓展区汉字:[`python_simple_filter@charset`](examples/charset.simple_filter.py) 26 | 27 | 过滤前 28 | 29 | rime-loengfan 30 | 31 | 过滤后 32 | 33 | python_simple_filter@charset 34 | 35 | ### 1.4. `filter` 36 | 37 | 一个复杂的例子,删除长度为 2 的候选,并将长度为 3 的候选转为简体:[`python_filter@complex`](examples/complex.filter.py) 38 | 39 | 过滤前 40 | 41 | rime-cantonese 42 | 43 | 过滤后 44 | 45 | python_filter@charset 46 | 47 | **FIXME**:由上图可以看出,长度为 2 的候选并没有被完全刪除,下一个版本修复。 48 | 49 | ## 二、用法 50 | 51 | | 在输入方案中指定的组件名称 | 对应的 Python 脚本文件名 | 52 | | :- | :- | 53 | | `python_simple_translator@` | `.simple_translator.py` | 54 | | `python_translator@` | `.translator.py` | 55 | | `python_simple_filter@` | `.simple_filter.py` | 56 | | `python_filter@` | `.filter.py` | 57 | 58 | 在 Python 脚本文件中按照下面的要求定义 `rime_main` 函数,即可实现相应组件的功能。 59 | 60 | ### 2.1. `simple_translator` 61 | 62 | `simple_translator` 要求 `rime_main` 函数的输入为字符串,输出为字符串的列表。 63 | 64 | 其中,输入的字符串为待翻译的输入码;输出的列表为多个翻译结果,每一项表示一个翻译结果。 65 | 66 | ```python 67 | def rime_main(s: str) -> Optional[List[str]]: 68 | ... 69 | ``` 70 | 71 | ### 2.2. `translator` 72 | 73 | `translator` 要求 `rime_main` 函数的输入为 `TranslatorQuery`,输出为 `TranslatorAnswer` 的列表。 74 | 75 | 其中,输入的 `TranslatorQuery` 为待翻译的內容,其中包含待翻译的输入码等多项信息;输出的列表为多个翻译结果,每一项为一个 `TranslatorAnswer`,其中包含翻译结果、`comment` 等多项信息。 76 | 77 | ```python 78 | from rimeext import TranslatorQuery, TranslatorAnswer 79 | 80 | def rime_main(translator_query: TranslatorQuery) -> Optional[List[TranslatorAnswer]]: 81 | ... 82 | ``` 83 | 84 | ### 2.3. `simple_filter` 85 | 86 | `simple_filter` 要求 `rime_main` 函数的输入为字符串,输出为布尔值。 87 | 88 | 其中,输入的字符串为翻译结果;输出的布尔值表示是否保留该项翻译结果,为真则保留,否则删除。 89 | 90 | ```python 91 | def rime_main(s: str) -> bool: 92 | ... 93 | ``` 94 | 95 | ### 2.4. `filter` 96 | 97 | `filter` 要求 `rime_main` 函数的输入为 `FilterQuery`,输出为 `FilterAnswer`。 98 | 99 | 其中,输入的 `FilterQuery` 为翻译结果,其中包含翻译结果文本、`comment` 等多项信息;输出的 `FilterAnswer` 有三种:跳过(保持该翻译结果不变)、删除(将该翻译结果从候选列表中移除)和重写(修改翻译结果中的某些信息)。 100 | 101 | ```python 102 | from rimeext import FilterQuery, FilterAnswer 103 | 104 | def rime_main(filter_query: FilterQuery) -> FilterAnswer: 105 | ... 106 | ``` 107 | 108 | ## 三、安裝 109 | 110 | 安裝 librime-python 分为两部分:Python 部分需要安装 rimeext 库,C++ 部分需要编译并安装带有 pythonext 插件的 librime。 111 | 112 | ### 3.1. Arch Linux 113 | 114 | Python 部分: 115 | 116 | ```sh 117 | cd python 118 | pip install . 119 | ``` 120 | 121 | C++ 部分: 122 | 123 | ```sh 124 | cd archlinux 125 | makepkg -si 126 | ``` 127 | 128 | 在编译时,Boost.Python 1.76.0 可能会报错,经过判断这是 Boost.Python 库本身的问题(见 [boostorg/python issue #359](https://github.com/boostorg/python/issues/359)),这时自行修改两个系统头文件即可正常编译。 129 | 130 | **FIXME**:在 `PKGBUILD` 中,编译选项 `-DBUILD_MERGED_PLUGINS=Off` 被修改为 `On`,这是因为如果不修改,用户在 Python 脚本中 `import math` 会产生如下 `ImportError`。 131 | 132 | ``` 133 | ImportError: /usr/lib/python3.9/lib-dynload/math.cpython-39-x86_64-linux-gnu.so: undefined symbol: PyLong_AsLongLongAndOverflow 134 | ``` 135 | 136 | 下一个版本修复。 137 | 138 | ### 3.2. Windows 139 | 140 | **FIXME**:在 Windows 上遇到各种编译错误而失败,下一个版本修复。 141 | 142 | ## 四、用户 Q&A 143 | 144 | **Q1. Python 的脚本文件放在哪里?** 145 | 146 | rime 用户文件夹或 rime 系统文件夹。 147 | 148 | **Q2. 我更新了 Python 脚本,应如何通知 rime 输入法?** 149 | 150 | 重新部署,或者切换到其他方案再切回来。 151 | 152 | **Q3. 我在某个方案中使用了 librime-python,但一选择那个方案,输入法就会崩溃,怎么知道哪里出了问题呢?** 153 | 154 | 查看 rime 的错误日志。 155 | 156 | **Q4. 不同的 Python 脚本文件中的变量会相互冲突吗?** 157 | 158 | **FIXME**:目前会相互冲突,因为 pybind11 不支持 [sub-interpreter](https://pybind11.readthedocs.io/en/stable/advanced/embedding.html#sub-interpreter-support),下一个版本修复。 159 | 160 | **Q5. `rime_main` 函数会被调用几次?在什么时候被调用呢?** 161 | 162 | **TODO**:下一个版本补充。 163 | 164 | **Q6. 我想实现一个计数器的功能,应该如何使用全局变量?** 165 | 166 | **TODO**:下一个版本补充。 167 | 168 | ## 五、开发指南 169 | 170 | ### 5.1. 项目原理 171 | 172 | 利用 [pybind11](https://pybind11.readthedocs.io/en/stable/index.html)。pybind11 提供了对 [Python/C API](https://docs.python.org/3/c-api/) 的 C++ 封装。对本项目而言,pybind11 提供的最重要的功能是在 C++ 代码中调用 Python 解释器。 173 | 174 | ### 5.2. 项目结构 175 | 176 | 分为 Python 部分和 C++ 部分。Python 部分定义了数个类,但并不是对 rime C++ API 的绑定,而是 librime-python 自定义的简化版的接口,因此不依赖于 C++ 代码。 177 | 178 | ### 5.3. 代码准则 179 | 180 | 如果你使用任何编辑器,请确保编辑器会读取项目根目录的 `.clang-format`,并在保存时自动格式化代码。 181 | 182 | 本项目是 Python 插件,因此遵守 Python 之禅:Explicit is better that implicit。 183 | 184 | 特别地,对于字符串类型,使用 `std::string` 的写法,而不使用 rime 定义的别名 `rime::string`,这是为了保证与 Python API 交互部分的代码不会引起误解。 185 | 186 | 由于本项目是输入法插件,当插件在运行时产生异常时,异常必须在插件内部处理完毕,不应该使输入法崩溃,且结果必须等价于插件不存在。 187 | 188 | 使用正确的现代 C++17 语法。 189 | 190 | ## 六、开发 Q&A 191 | 192 | **Q1. Python 部分使用了哪些外部库?** 193 | 194 | Python 部分没有使用外部库,只使用了标准库,但是用户可以安装并引入其他外部库。 195 | 196 | **Q2. C++ 部分使用了哪些外部库?** 197 | 198 | 使用了 pybind11。 199 | 200 | **Q3. librime-python 用的是 C++17,但 librime 是 C++14,是否会出现问题?** 201 | 202 | [不会](https://stackoverflow.com/a/49119902)。 203 | 204 | **Q4. Python 部分是对 librime API 的绑定吗?** 205 | 206 | 不是。Python 部分是独立于 C++ 的。是 librime-python 自定义的简化版的接口。 207 | 208 | **Q5. 未来还需要做什么?** 209 | 210 | 1\. 修复上文提到的 bug; 211 | 212 | 2\. 为 librime-python 自定义的简化版的接口添加更多功能,如读取输入法开关状态等; 213 | 214 | 3\. 其他功能添加和优化。 215 | -------------------------------------------------------------------------------- /archlinux/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Felix Yan 2 | # Contributor: GONG Chen 3 | # Contributor: 網軍總司令 4 | # Contributor: Ayaka Mikazuki 5 | 6 | # Forked from https://github.com/archlinux/svntogit-community/blob/302200f/trunk/PKGBUILD 7 | 8 | pkgname=librime 9 | pkgver=1.7.3 10 | _octagramcommit=f92e083052b9983ee3cbddcda5ed60bb3c068e24 11 | _luacommit=d45a41af2f9d731e3c1516a191cc3160e3cb8377 12 | _charcodecommit=b569184772b12965e3ebe1dfd431026951fed81c 13 | pkgrel=6 14 | epoch=1 15 | pkgdesc="Rime input method engine" 16 | arch=('x86_64') 17 | url="https://github.com/rime/librime" 18 | license=('custom:BSD3') 19 | depends=('boost-libs' 'capnproto' 'opencc' 'yaml-cpp' 'leveldb' 'librime-data' 'lua' 'google-glog' 'marisa' 'pybind11') 20 | makedepends=('cmake' 'boost' 'gtest' 'ninja') 21 | source=("https://github.com/rime/librime/archive/$pkgver/$pkgname-$pkgver.tar.gz" 22 | "https://github.com/lotem/librime-octagram/archive/$_octagramcommit/librime-octagram-$_octagramcommit.tar.gz" 23 | "https://github.com/hchunhui/librime-lua/archive/$_luacommit/librime-lua-$_luacommit.tar.gz" 24 | "https://github.com/rime/librime-charcode/archive/$_charcodecommit/librime-lua-$_charcodecommit.tar.gz" 25 | cmath-include.patch) 26 | sha512sums=('8767d17c3d14a5a1bbb8269fab1627b907de72c288b362fdbc6191223937da21e8b18471b4ae8f83ce5afc0ec5c3ab12fbcb49930eb9969c1764c7390d9ee4b0' 27 | '737d1c58982d2f79a6e8b2548eefa1dddc036dd6e6d5436e7d6b4f3adfa2e9d8e45b29a13c1b8207a93cb77f3b5dbd9d18436f44d4e8040eb95b962de582b386' 28 | '2a3d3b49d53066fe96dd008e8064718082225e6bf185574a25b8e98175d9936abcfa1fdc56e48f9c72a2deb46f8157d6132fd119ff8e0a3d52fbe9e2ea21386c' 29 | '6670a2b089479cf4fb23012e61675065d483ab6123f6dcad136b226dbe361a16bc8f33caece2e139c8d89161a73a2126afe2bed3759996153de6e4888a95a430' 30 | '531b4143a7636e6f2cdb67ed2d9b6aa5d43c50dfbbad626757b435debf863f9d466a5923611d9213b0c897af919870d367e88889ea2bbc1037e11fa88a0c7602') 31 | 32 | prepare() { 33 | cd $pkgname-$pkgver 34 | ln -sf "$srcdir"/librime-octagram-$_octagramcommit plugins/librime-octagram 35 | ln -sf "$srcdir"/librime-lua-$_luacommit plugins/librime-lua 36 | ln -sf "$srcdir"/librime-charcode-$_charcodecommit plugins/librime-charcode 37 | ln -sf "$srcdir"/../../cpp plugins/librime-python 38 | 39 | patch -Np1 -d . -i "$srcdir"/cmath-include.patch 40 | } 41 | 42 | build() { 43 | cd $pkgname-$pkgver 44 | export CXXFLAGS="$CXXFLAGS -DNDEBUG" 45 | cmake . -GNinja -Bbuild -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_MERGED_PLUGINS=On -DENABLE_EXTERNAL_PLUGINS=On 46 | cmake --build build 47 | } 48 | 49 | check() { 50 | cd $pkgname-$pkgver/build 51 | ninja test 52 | } 53 | 54 | package() { 55 | cd $pkgname-$pkgver/build 56 | DESTDIR="$pkgdir" ninja install 57 | } 58 | -------------------------------------------------------------------------------- /archlinux/cmath-include.patch: -------------------------------------------------------------------------------- 1 | From 57cffcd02ac70148e21ce982be834876b3df87db Mon Sep 17 00:00:00 2001 2 | From: HanatoK 3 | Date: Sat, 1 May 2021 01:04:38 -0500 4 | Subject: [PATCH] Fix FTBFS. 5 | 6 | Previous boost library (1.75) may implicitly include cmath, but the 7 | latest version (1.76) does not, so the calls to exp are undefined. This 8 | commit include cmath in script_translator.cc and table_translator.cc to 9 | fix the issue (#462). 10 | --- 11 | src/rime/gear/script_translator.cc | 1 + 12 | src/rime/gear/table_translator.cc | 1 + 13 | 2 files changed, 2 insertions(+) 14 | 15 | diff --git a/src/rime/gear/script_translator.cc b/src/rime/gear/script_translator.cc 16 | index 4a45f05e..25061659 100644 17 | --- a/src/rime/gear/script_translator.cc 18 | +++ b/src/rime/gear/script_translator.cc 19 | @@ -8,6 +8,7 @@ 20 | // 21 | #include 22 | #include 23 | +#include 24 | #include 25 | #include 26 | #include 27 | diff --git a/src/rime/gear/table_translator.cc b/src/rime/gear/table_translator.cc 28 | index 162ac024..c95e5e24 100644 29 | --- a/src/rime/gear/table_translator.cc 30 | +++ b/src/rime/gear/table_translator.cc 31 | @@ -6,6 +6,7 @@ 32 | // 33 | #include 34 | #include 35 | +#include 36 | #include 37 | #include 38 | #include 39 | -------------------------------------------------------------------------------- /cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(PYTHONEXT_VERSION 1.0.0) 2 | set(PYTHONEXT_SOVERSION 1) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED True) 6 | 7 | find_package(PythonLibs REQUIRED) 8 | include_directories(${PYTHON_INCLUDE_DIRS}) 9 | 10 | find_package(pybind11 REQUIRED) 11 | include_directories(${pybind11_INCLUDE_DIRS}) 12 | 13 | aux_source_directory(src pythonext_src) 14 | 15 | set(pythonext_library rime-pythonext) 16 | set(pythonext_deps ${rime_library} ${PYTHON_LIBRARIES} ${pybind11_LIBRARIES}) 17 | set(pythonext_modules "pythonext") 18 | 19 | add_library(rime-pythonext-objs OBJECT ${pythonext_src}) 20 | if(BUILD_SHARED_LIBS) 21 | set_target_properties(rime-pythonext-objs PROPERTIES POSITION_INDEPENDENT_CODE ON) 22 | endif() 23 | 24 | set(plugin_name ${pythonext_library} PARENT_SCOPE) 25 | set(plugin_objs $ PARENT_SCOPE) 26 | set(plugin_deps ${pythonext_deps} PARENT_SCOPE) 27 | set(plugin_modules ${pythonext_modules} PARENT_SCOPE) 28 | -------------------------------------------------------------------------------- /cpp/src/module.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "python_simple_translator.h" 8 | #include "python_translator.h" 9 | #include "python_simple_filter.h" 10 | #include "python_filter.h" 11 | 12 | namespace py = pybind11; 13 | 14 | template 15 | struct ComponentName { static const char* const name; }; 16 | 17 | template <> 18 | const char* const ComponentName::name { "simple_translator" }; 19 | template <> 20 | const char* const ComponentName::name { "translator" }; 21 | template <> 22 | const char* const ComponentName::name { "simple_filter" }; 23 | template <> 24 | const char* const ComponentName::name { "filter" }; 25 | 26 | /** 27 | * Determine the path to the Python script file according to the name string of a rime component. 28 | * 29 | * @param componentName The name of the component (e.g. translator, filter). 30 | * @param tagName The name string of the rime component. 31 | * @return Path to the Python script. 32 | * @exception Throws `std::runtime_error` if the file does not exist. 33 | */ 34 | static std::string getPythonScriptRealPath( const char* const componentName, const std::string& tagName ) { 35 | // determine the file name 36 | const std::string fileName { tagName + "." + componentName + ".py" }; 37 | 38 | // the file should either exist in the user directory or the shared directory 39 | const std::filesystem::path userDir { std::filesystem::canonical( RimeGetUserDataDir() ) }; 40 | const std::filesystem::path sharedDir { std::filesystem::canonical( RimeGetSharedDataDir() ) }; 41 | const std::filesystem::path userFile { userDir / fileName }; 42 | const std::filesystem::path sharedFile { sharedDir / fileName }; 43 | const bool userFileExists { std::filesystem::exists( userFile ) }; 44 | const bool sharedFileExists { std::filesystem::exists( sharedFile ) }; 45 | 46 | if ( !userFileExists && !sharedFileExists ) { 47 | LOG( ERROR ) << "Expected file " << fileName << " in the user directory (" 48 | << userDir.string() << ") or in the shared directory (" 49 | << sharedDir.string() << ")."; 50 | std::terminate(); 51 | } 52 | 53 | // prefer the file in the user directory 54 | return ( userFileExists ? userFile : sharedFile ).string(); 55 | } 56 | 57 | template 58 | class PythonComponent : public T::Component { 59 | public: 60 | PythonComponent() {}; 61 | 62 | T* Create( const rime::Ticket& ticket ) { 63 | const rime::Ticket ticket_ { ticket.engine, ticket.name_space, ticket.name_space }; 64 | const char* const componentName { ComponentName::name }; 65 | const std::string& tagName { ticket_.name_space }; 66 | 67 | // load Python script file 68 | const std::string pythonScriptPath { getPythonScriptRealPath( componentName, tagName ) }; 69 | LOG( INFO ) << "reading Python script file " << pythonScriptPath << "."; 70 | 71 | try { 72 | py::eval_file( pythonScriptPath, py::globals(), py::globals() ); 73 | py::function py_entry { py::eval( "rime_main", py::globals(), py::globals() ) }; 74 | return new T { ticket_, py_entry }; 75 | } catch ( const py::error_already_set& e ) { 76 | LOG( ERROR ) << e.what(); 77 | std::terminate(); 78 | } 79 | } 80 | }; 81 | 82 | static void rime_pythonext_initialize() { 83 | LOG( INFO ) << "registering components from module 'pythonext'."; 84 | 85 | py::initialize_interpreter(); 86 | 87 | try { 88 | py::exec( "import rimeext", py::globals(), py::globals() ); 89 | } catch ( const py::error_already_set& e ) { 90 | LOG( ERROR ) << e.what(); 91 | std::terminate(); 92 | } 93 | 94 | rime::Registry& r { rime::Registry::instance() }; 95 | r.Register( "python_simple_translator", new PythonComponent() ); 96 | r.Register( "python_translator", new PythonComponent() ); 97 | r.Register( "python_simple_filter", new PythonComponent() ); 98 | r.Register( "python_filter", new PythonComponent() ); 99 | } 100 | 101 | static void rime_pythonext_finalize() { 102 | py::finalize_interpreter(); 103 | } 104 | 105 | RIME_REGISTER_MODULE( pythonext ) 106 | -------------------------------------------------------------------------------- /cpp/src/python_filter.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "python_filter.h" 6 | 7 | namespace pythonext { 8 | 9 | // PythonFilter 10 | 11 | PythonFilter::PythonFilter( const rime::Ticket& ticket, py::function py_entry ) 12 | : Filter { ticket }, py_entry { py_entry }, FilterQuery { py::eval( "rimeext.FilterQuery", py::globals(), py::globals() ) } {} 13 | 14 | rime::an PythonFilter::Apply( rime::an translation, rime::CandidateList* candidates ) { 15 | return rime::New( this, translation ); 16 | } 17 | 18 | void PythonFilter::FilterFunc( rime::an cand, rime::CandidateQueue* cand_queue ) { 19 | const std::string& candidate_type { cand->type() }; 20 | const std::string& text { cand->text() }; 21 | const std::string comment { cand->comment() }; // return value is not a reference 22 | const std::string preedit { cand->preedit() }; // return value is not a reference 23 | 24 | try { 25 | const py::object filter_result { py_entry( FilterQuery( candidate_type, text, comment, preedit ) ) }; 26 | 27 | // Case 1: Skip the current candidate. The candidate will be remained intact 28 | const bool should_skip { filter_result.attr( "should_skip" ).cast() }; 29 | if ( should_skip ) { 30 | cand_queue->emplace_back( cand ); 31 | return; 32 | } 33 | 34 | // Case 2: Remove the current candidate from the candidate list 35 | const bool should_remove { filter_result.attr( "should_remove" ).cast() }; 36 | if ( should_remove ) 37 | return; 38 | 39 | // Case 3: Change certain attributes of the current candidate 40 | const std::string new_candidate_type { filter_result.attr( "candidate_type" ).cast() }; 41 | const std::string new_text { filter_result.attr( "text" ).cast() }; 42 | const std::string new_comment { filter_result.attr( "comment" ).cast() }; 43 | 44 | const rime::an new_cand { rime::New( cand, new_candidate_type, new_text, new_comment ) }; 45 | 46 | cand_queue->emplace_back( new_cand ); 47 | } catch ( const py::error_already_set& e ) { 48 | LOG( ERROR ) << e.what(); 49 | } 50 | } 51 | 52 | // PythonFilterTranslation 53 | 54 | PythonFilterTranslation::PythonFilterTranslation( PythonFilter* filter, rime::an translation ) 55 | : filter_ { filter }, PrefetchTranslation { translation } {} 56 | 57 | bool PythonFilterTranslation::Replenish() { 58 | const rime::an cand { translation_->Peek() }; 59 | translation_->Next(); 60 | filter_->FilterFunc( cand, &cache_ ); 61 | return !cache_.empty(); 62 | } 63 | 64 | } // namespace pythonext 65 | -------------------------------------------------------------------------------- /cpp/src/python_filter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace py = pybind11; 10 | 11 | namespace pythonext { 12 | 13 | class PythonFilter : public rime::Filter { 14 | public: 15 | PythonFilter( const rime::Ticket& ticket, py::function py_entry ); 16 | virtual rime::an Apply( rime::an translation, rime::CandidateList* candidates ) override; 17 | void FilterFunc( rime::an cand, rime::CandidateQueue* cand_queue ); 18 | 19 | private: 20 | const py::function py_entry; 21 | const py::object FilterQuery; 22 | }; 23 | 24 | class PythonFilterTranslation : public rime::PrefetchTranslation { 25 | public: 26 | PythonFilterTranslation( PythonFilter* filter, rime::an translation ); 27 | 28 | protected: 29 | virtual bool Replenish() override; 30 | PythonFilter* const filter_; 31 | }; 32 | 33 | } // namespace pythonext 34 | -------------------------------------------------------------------------------- /cpp/src/python_simple_filter.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "python_simple_filter.h" 5 | 6 | namespace pythonext { 7 | 8 | // PythonSimpleFilter 9 | 10 | PythonSimpleFilter::PythonSimpleFilter( const rime::Ticket& ticket, py::function py_entry ) 11 | : Filter { ticket }, py_entry { py_entry } {} 12 | 13 | rime::an PythonSimpleFilter::Apply( rime::an translation, rime::CandidateList* candidates ) { 14 | return rime::New( this, translation ); 15 | } 16 | 17 | bool PythonSimpleFilter::FilterFunc( rime::an cand ) noexcept { 18 | const std::string& text { cand->text() }; 19 | 20 | try { 21 | return py_entry( text ).cast(); 22 | } catch ( const py::error_already_set& e ) { 23 | LOG( ERROR ) << e.what(); 24 | } 25 | 26 | return true; 27 | } 28 | 29 | // PythonSimpleFilterTranslation 30 | 31 | PythonSimpleFilterTranslation::PythonSimpleFilterTranslation( PythonSimpleFilter* filter, rime::an translation ) 32 | : filter_ { filter }, translation_ { translation } { 33 | LocateNextCandidate(); 34 | } 35 | 36 | bool PythonSimpleFilterTranslation::Next() { 37 | if ( exhausted() ) 38 | return false; 39 | if ( !translation_->Next() ) { 40 | set_exhausted( true ); 41 | return false; 42 | } 43 | return LocateNextCandidate(); 44 | } 45 | 46 | rime::an PythonSimpleFilterTranslation::Peek() { 47 | return translation_->Peek(); 48 | } 49 | 50 | bool PythonSimpleFilterTranslation::LocateNextCandidate() { 51 | while ( !translation_->exhausted() ) { 52 | const rime::an cand { translation_->Peek() }; 53 | if ( cand && filter_->FilterFunc( cand ) ) 54 | return true; 55 | translation_->Next(); 56 | } 57 | set_exhausted( true ); 58 | return false; 59 | } 60 | 61 | } // namespace pythonext 62 | -------------------------------------------------------------------------------- /cpp/src/python_simple_filter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace py = pybind11; 10 | 11 | namespace pythonext { 12 | 13 | class PythonSimpleFilter : public rime::Filter { 14 | public: 15 | PythonSimpleFilter( const rime::Ticket& ticket, py::function py_entry ); 16 | virtual rime::an Apply( rime::an translation, rime::CandidateList* candidates ) override; 17 | bool FilterFunc( rime::an cand ) noexcept; 18 | 19 | private: 20 | const py::function py_entry; 21 | }; 22 | 23 | class PythonSimpleFilterTranslation : public rime::Translation { 24 | public: 25 | PythonSimpleFilterTranslation( PythonSimpleFilter* filter, rime::an translation ); 26 | virtual bool Next() override; 27 | virtual rime::an Peek() override; 28 | 29 | protected: 30 | bool LocateNextCandidate(); 31 | rime::an translation_; 32 | PythonSimpleFilter* const filter_; 33 | }; 34 | 35 | } // namespace pythonext 36 | -------------------------------------------------------------------------------- /cpp/src/python_simple_translator.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "python_simple_translator.h" 5 | 6 | namespace pythonext { 7 | 8 | PythonSimpleTranslator::PythonSimpleTranslator( const rime::Ticket& ticket, py::function py_entry ) 9 | : Translator { ticket }, py_entry { py_entry } {} 10 | 11 | rime::an PythonSimpleTranslator::Query( const std::string& input, const rime::Segment& segment ) noexcept { 12 | if ( !segment.HasTag( "abc" ) ) 13 | return nullptr; 14 | 15 | const size_t segment_start { segment.start }; 16 | const size_t segment_end { segment.end }; 17 | 18 | try { 19 | // skip if return value is None 20 | // Note: cannot assign to type `py::list` directly 21 | // https://pybind11.readthedocs.io/en/stable/advanced/pycpp/object.html#assigning-py-none-to-wrappers 22 | const py::object output { py_entry( input ) }; 23 | if ( output.is_none() ) 24 | return nullptr; 25 | 26 | const py::list output_list { output }; 27 | 28 | // skip if returned list is empty 29 | if ( output_list.empty() ) 30 | return nullptr; 31 | 32 | // create translation list from the returned list 33 | const rime::an translation { rime::New() }; 34 | for ( const py::handle& item : output_list ) { 35 | const std::string item_string { item.cast() }; 36 | translation->Append( rime::New( "python_simple", segment_start, segment_end, item_string ) ); 37 | } 38 | return translation; 39 | } catch ( const py::error_already_set& e ) { 40 | LOG( ERROR ) << e.what(); 41 | } 42 | 43 | return nullptr; 44 | } 45 | 46 | } // namespace pythonext 47 | -------------------------------------------------------------------------------- /cpp/src/python_simple_translator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace py = pybind11; 9 | 10 | namespace pythonext { 11 | 12 | class PythonSimpleTranslator : public rime::Translator { 13 | public: 14 | PythonSimpleTranslator( const rime::Ticket& ticket, py::function py_entry ); 15 | virtual rime::an Query( const std::string& input, const rime::Segment& segment ) noexcept override; 16 | 17 | private: 18 | const py::function py_entry; 19 | }; 20 | 21 | } // namespace pythonext 22 | -------------------------------------------------------------------------------- /cpp/src/python_translator.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "python_translator.h" 5 | 6 | namespace pythonext { 7 | 8 | PythonTranslator::PythonTranslator( const rime::Ticket& ticket, py::function py_entry ) 9 | : Translator { ticket }, py_entry { py_entry }, TranslatorQuery { py::eval( "rimeext.TranslatorQuery", py::globals(), py::globals() ) } {} 10 | 11 | rime::an PythonTranslator::Query( const std::string& input, const rime::Segment& segment ) noexcept { 12 | if ( !segment.HasTag( "abc" ) ) 13 | return nullptr; 14 | 15 | const size_t segment_start { segment.start }; 16 | 17 | try { 18 | // skip if return value is None 19 | const py::object output { py_entry( TranslatorQuery( input ) ) }; 20 | if ( output.is_none() ) 21 | return nullptr; 22 | 23 | const py::list output_list { output }; 24 | 25 | // skip if returned list is empty 26 | if ( output_list.empty() ) 27 | return nullptr; 28 | 29 | // create translation list from the returned list 30 | const rime::an translation { rime::New() }; 31 | for ( const py::handle& candidate : output_list ) { 32 | // get all the 5 attributes from the object 33 | const std::string text { candidate.attr( "text" ).cast() }; 34 | const size_t length { candidate.attr( "length" ).cast() }; 35 | const std::string candidate_type { candidate.attr( "candidate_type" ).cast() }; 36 | const std::string comment { candidate.attr( "comment" ).cast() }; 37 | const std::string preedit { candidate.attr( "preedit" ).cast() }; 38 | 39 | translation->Append( rime::New( candidate_type, segment_start, segment_start + length, text, comment, preedit ) ); 40 | } 41 | return translation; 42 | } catch ( const py::error_already_set& e ) { 43 | LOG( ERROR ) << e.what(); 44 | } 45 | 46 | return nullptr; 47 | } 48 | 49 | } // namespace pythonext 50 | -------------------------------------------------------------------------------- /cpp/src/python_translator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace py = pybind11; 9 | 10 | namespace pythonext { 11 | 12 | class PythonTranslator : public rime::Translator { 13 | public: 14 | PythonTranslator( const rime::Ticket& ticket, py::function py_entry ); 15 | virtual rime::an Query( const std::string& input, const rime::Segment& segment ) noexcept override; 16 | 17 | private: 18 | const py::function py_entry; 19 | const py::object TranslatorQuery; 20 | }; 21 | 22 | } // namespace pythonext 23 | -------------------------------------------------------------------------------- /examples/charset.simple_filter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 过滤拓展区汉字 3 | 4 | import re 5 | 6 | # https://ayaka.shn.hk/hanregex/ 7 | # CJK Unified Ideographs Extension A to G 8 | extblk = re.compile(r'[\u3400-\u4dbf\U00020000-\U0002a6df\U0002a700-\U0002ebef\U00030000-\U0003134f]') 9 | 10 | def rime_main(s: str) -> bool: 11 | return not extblk.match(s) 12 | -------------------------------------------------------------------------------- /examples/complex.filter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 删除长度为 2 的候选,并将长度为 3 的候选转为简体 3 | 4 | from opencc import OpenCC 5 | from rimeext import FilterQuery, FilterAnswer 6 | 7 | t2s = OpenCC('t2s').convert 8 | 9 | def rime_main(filter_query: FilterQuery) -> FilterAnswer: 10 | text = filter_query.text 11 | comment = filter_query.comment 12 | if len(text) == 2: 13 | return filter_query.do_remove() 14 | if len(text) == 3: 15 | return filter_query.do_rewrite(text=t2s(text), comment=comment + ' [简]') 16 | return filter_query.do_skip() 17 | -------------------------------------------------------------------------------- /examples/datetime.simple_translator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 输入 date 时输出当前日期 3 | 4 | from datetime import datetime 5 | from typing import List, Optional 6 | 7 | def rime_main(s: str) -> Optional[List[str]]: 8 | if s == 'date': 9 | date = datetime.now().strftime('%Y-%m-%d') 10 | return [date] 11 | -------------------------------------------------------------------------------- /examples/putonghua_cloud_input.translator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 汉语拼音(普通话)云输入 3 | 4 | from itertools import repeat 5 | import requests 6 | from rimeext import TranslatorQuery, TranslatorAnswer 7 | from typing import List, Optional 8 | import unicodedata 9 | 10 | def format_pinyin(s: str) -> str: 11 | ''' 12 | >>> format_pinyin('hao3') 13 | 'hǎo' 14 | >>> format_pinyin('nv3') 15 | 'nǚ' 16 | ''' 17 | s = s.replace('v', 'ü') 18 | if s.endswith('5'): 19 | s = s[:-1] 20 | elif s.endswith(tuple('1234')): 21 | tone = int(s[-1]) 22 | tone_mark = '\u0304\u0301\u030c\u0300'[tone - 1] 23 | s = s[:-1] 24 | for component in ['iu', 'ui', 'a', 'o', 'e', 'i', 'u', 'ü']: 25 | if component in s: 26 | s = s.replace(component, component + tone_mark) 27 | break 28 | s = unicodedata.normalize('NFC', s) 29 | return s 30 | 31 | def format_pinyin_sequence(s: str) -> str: 32 | ''' 33 | >>> format_pinyin_sequence('ni3 hao3') 34 | 'nǐ hǎo' 35 | ''' 36 | return ' '.join(map(format_pinyin, s.split(' '))) 37 | 38 | def rime_main(translator_query: TranslatorQuery) -> Optional[List[TranslatorAnswer]]: 39 | input_str = translator_query.text 40 | 41 | num_of_items = 5 42 | 43 | payload = { 44 | 'text': input_str, 45 | 'itc': 'zh-hant-t-i0-pinyin', 46 | 'num': num_of_items, 47 | 'cp': 0, 48 | 'cs': 1, 49 | 'ie': 'utf-8', 50 | 'oe': 'utf-8', 51 | 'app': 'test', 52 | } 53 | r = requests.get('http://inputtools.google.com/request', params=payload) 54 | r.raise_for_status() 55 | response = r.json() 56 | 57 | if response[0] != 'SUCCESS': 58 | return 59 | 60 | _, texts, _, d = response[1][0] 61 | annotations = d.get('annotation', repeat(None)) 62 | matched_lengths = d.get('matched_length', repeat(None)) 63 | return [translator_query.create_answer( 64 | text, 65 | length=matched_length, 66 | comment=format_pinyin_sequence(annotation)) 67 | for text, annotation, matched_length 68 | in zip(texts, annotations, matched_lengths)] 69 | -------------------------------------------------------------------------------- /images/charset-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayaka14732/librime-python/c210fcc77366c54bf5a383c5b14f10afbf608042/images/charset-0.png -------------------------------------------------------------------------------- /images/charset-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayaka14732/librime-python/c210fcc77366c54bf5a383c5b14f10afbf608042/images/charset-1.png -------------------------------------------------------------------------------- /images/complex-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayaka14732/librime-python/c210fcc77366c54bf5a383c5b14f10afbf608042/images/complex-0.png -------------------------------------------------------------------------------- /images/complex-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayaka14732/librime-python/c210fcc77366c54bf5a383c5b14f10afbf608042/images/complex-1.png -------------------------------------------------------------------------------- /images/datetime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayaka14732/librime-python/c210fcc77366c54bf5a383c5b14f10afbf608042/images/datetime.png -------------------------------------------------------------------------------- /images/putonghua_cloud_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayaka14732/librime-python/c210fcc77366c54bf5a383c5b14f10afbf608042/images/putonghua_cloud_input.png -------------------------------------------------------------------------------- /python/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup, find_packages 4 | from os import path 5 | 6 | here = path.abspath(path.dirname(__file__)) 7 | 8 | long_description = 'Please see [ayaka14732/librime-python](https://github.com/ayaka14732/librime-python).' 9 | 10 | setup( 11 | name='rimeext', 12 | version='0.0.1', 13 | description='Rime Python extension', 14 | long_description=long_description, 15 | long_description_content_type='text/markdown', 16 | url='https://github.com/ayaka14732/librime-python', 17 | author='ayaka14732', 18 | author_email='ayaka@mail.shn.hk', 19 | classifiers=[ 20 | 'Development Status :: 4 - Beta', 21 | 'Intended Audience :: Developers', 22 | 'Topic :: Software Development :: Libraries :: Python Modules', 23 | 'Topic :: Text Processing :: Linguistic', 24 | 'Natural Language :: Chinese (Simplified)', 25 | 'Natural Language :: Chinese (Traditional)', 26 | 'Natural Language :: Cantonese', 27 | 'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', 28 | 'Programming Language :: Python :: 3', 29 | 'Programming Language :: Python :: 3.7', 30 | 'Programming Language :: Python :: 3.8', 31 | 'Programming Language :: Python :: 3.9', 32 | 'Programming Language :: Python :: 3.10', 33 | ], 34 | keywords='linguistics input-method', 35 | packages=find_packages('src'), 36 | package_dir={'': 'src'}, 37 | python_requires='>=3.7, <4', 38 | entry_points={}, 39 | project_urls={ 40 | 'Bug Reports': 'https://github.com/ayaka14732/librime-python/issues', 41 | 'Source': 'https://github.com/ayaka14732/librime-python', 42 | } 43 | ) 44 | -------------------------------------------------------------------------------- /python/src/rimeext/Filter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from typing import Optional 4 | 5 | class FilterAnswer: 6 | ''' 7 | The answer to a filter query. 8 | ''' 9 | 10 | def __init__(self, should_skip: bool = False, should_remove: bool = False, candidate_type: Optional[str] = None, text: Optional[str] = None, comment: Optional[str] = None, direct_call: bool = True): 11 | assert not direct_call, 'Please use `Skip`, `Remove` or `Rewrite` instead of calling this constructor directly' 12 | 13 | self.should_skip = should_skip 14 | 15 | self.should_remove = should_remove 16 | 17 | self.candidate_type = candidate_type 18 | self.text = text 19 | self.comment = comment 20 | 21 | @classmethod 22 | def Skip(cls): 23 | return cls(should_skip=True, direct_call=False) 24 | 25 | @classmethod 26 | def Remove(cls): 27 | return cls(should_remove=True, direct_call=False) 28 | 29 | @classmethod 30 | def Rewrite(cls, **kwargs): 31 | return cls(**kwargs, direct_call=False) 32 | 33 | class FilterQuery: 34 | ''' 35 | The input to the filter. 36 | ''' 37 | 38 | def __init__(self, candidate_type: str, text: str, comment: str, preedit: str): 39 | self.candidate_type = candidate_type 40 | self.text = text 41 | self.comment = comment 42 | self.preedit = preedit 43 | 44 | def __repr__(self) -> str: 45 | return f'' 46 | 47 | def do_skip(self) -> FilterAnswer: 48 | '''Skip the current candidate. The candidate will be remained intact.''' 49 | return FilterAnswer.Skip() 50 | 51 | def do_remove(self) -> FilterAnswer: 52 | '''Remove the current candidate from the candidate list.''' 53 | return FilterAnswer.Remove() 54 | 55 | def do_rewrite(self, candidate_type: Optional[str] = None, text: Optional[str] = None, comment: Optional[str] = None) -> FilterAnswer: 56 | '''Change certain attributes of the current candidate.''' 57 | # default values 58 | if candidate_type is None: 59 | candidate_type = 'python_filtered' 60 | if text is None: 61 | text = self.text 62 | if comment is None: 63 | comment = self.comment 64 | 65 | # type checking 66 | assert isinstance(candidate_type, str) 67 | assert isinstance(text, str) 68 | assert isinstance(comment, str) 69 | 70 | return FilterAnswer.Rewrite(text=text, candidate_type=candidate_type, comment=comment) 71 | -------------------------------------------------------------------------------- /python/src/rimeext/Translator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from typing import Optional 4 | 5 | class TranslatorAnswer: 6 | ''' 7 | The answer to a translator query. 8 | ''' 9 | 10 | def __init__(self, text: Optional[str] = None, length: Optional[int] = None, candidate_type: Optional[str] = None, comment: Optional[str] = None, preedit: Optional[str] = None): 11 | self.text = text 12 | self.length = length 13 | self.candidate_type = candidate_type 14 | self.comment = comment 15 | self.preedit = preedit 16 | 17 | def __repr__(self) -> str: 18 | return f'' 19 | 20 | class TranslatorQuery: 21 | ''' 22 | The input to the translator. 23 | ''' 24 | 25 | def __init__(self, text: str): 26 | self.text = text 27 | 28 | def __repr__(self) -> str: 29 | return f'' 30 | 31 | def create_answer(self, text: str, length: Optional[int] = None, candidate_type: Optional[str] = None, comment: Optional[str] = None, preedit: Optional[str] = None) -> TranslatorAnswer: 32 | # default values 33 | if length is None: 34 | length = len(self.text) 35 | if candidate_type is None: 36 | candidate_type = 'python' 37 | if comment is None: 38 | comment = '' 39 | if preedit is None: 40 | preedit = '' 41 | 42 | # type checking 43 | assert isinstance(text, str) 44 | assert isinstance(length, int) 45 | assert isinstance(candidate_type, str) 46 | assert isinstance(comment, str) 47 | assert isinstance(preedit, str) 48 | 49 | return TranslatorAnswer(text=text, length=length, candidate_type=candidate_type, comment=comment, preedit=preedit) 50 | -------------------------------------------------------------------------------- /python/src/rimeext/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .Translator import TranslatorQuery, TranslatorAnswer 4 | from .Filter import FilterQuery, FilterAnswer 5 | --------------------------------------------------------------------------------