Compare commits

...

201 Commits

Author SHA1 Message Date
Patrick Goldinger
53ab0a3fa0 Release v0.4.0-alpha02 2022-08-03 23:14:59 +02:00
Patrick Goldinger
aeeff67d2e Adjust Settings UI home message and version number (#1942) 2022-08-03 23:12:33 +02:00
florisboard-bot
4343703eb3 Update translations from Crowdin 2022-08-03 23:00:58 +02:00
Patrick Goldinger
5f09bdbce2 Add implementation for notifySuggestionReverted() 2022-08-03 12:18:25 +02:00
Kostas Giapis
3a3e3625f2 Fix uppercase Greek vowels popups (#1981)
Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2022-08-02 21:47:42 +02:00
Patrick Goldinger
bdf14c1997 Add numeric row manual shifting symbols (#1988) 2022-08-01 13:09:04 +02:00
Patrick Goldinger
d6e064ae00 Fix composer not allowing multiple code points (#1984) 2022-08-01 11:54:41 +02:00
Patrick Goldinger
1012094568 Fix accent ordering of z in Polish popup mapping (#1960) 2022-07-31 09:29:46 +02:00
Patrick Goldinger
4117537ff2 Disable unnecessary app icon sync in Settings UI for Android 10+ 2022-07-29 20:18:18 +02:00
Patrick Goldinger
2a72cb70d6 Remove deprecated Accompanist Insets library 2022-07-28 22:36:05 +02:00
Patrick Goldinger
b1cd9d9389 Rework and improve splash screen of Settings UI
Especially fixes the splash screen for Android 7-11 devices, which utilize the SplashScreen compat library and where the app icon did not draw correctly.

Additionally an unnecessary intermediate splash screen background step has been removed, which should improve Settings UI cold startup time slightly and make it seem more snappy.
2022-07-28 22:25:15 +02:00
Patrick Goldinger
568dfc973d Upgrade Compose to 1.2.0 / Upgrade other dependencies 2022-07-28 16:53:26 +02:00
Patrick Goldinger
2e74cec0db Upgrade Kotlin to 1.7.10 and Compose Compiler to 1.3.0-rc01 2022-07-28 15:51:57 +02:00
Patrick Goldinger
db378159d6 Decouple Jetpack Compose Compiler version from other Compose packages
This change has been done as Google decouples the Compiler release from the rest of Compose packages to allow for faster upgrades of the Kotlin version.

Source: https://android-developers.googleblog.com/2022/06/independent-versioning-of-Jetpack-Compose-libraries.html
2022-07-28 15:37:38 +02:00
Patrick Goldinger
50ff2d8f1b Merge pull request #1974 from florisboard/networkutils-and-clipboard-fixes
Clipboard and NetworkUtils regex fixes
2022-07-27 20:47:34 +02:00
Patrick Goldinger
061495fb27 Improve host regex accuracy for clipboard URL extraction (#1971) 2022-07-27 17:01:47 +02:00
Patrick Goldinger
40cb59ddfd Fix extracted URLs not checking for duplicates (#1971) 2022-07-27 16:51:06 +02:00
Patrick Goldinger
38affddc9e Fix extracted phone numbers not stripping parentheses (#1971) 2022-07-27 16:47:24 +02:00
Patrick Goldinger
17dcb90473 Partly disable smart clipboard on Android 7.0 and 7.1 (#1970)
Android 7.0 and 7.1 do not support named regex groups natively, which causes a crash.
2022-07-27 14:44:32 +02:00
Patrick Goldinger
772402b46f Fix domains get extracted from emails in clipboard (#1971) 2022-07-27 13:58:40 +02:00
Patrick Goldinger
29bd8a289c Add clipboard phone number detection (#1889, #1971) 2022-07-27 13:42:17 +02:00
Patrick Goldinger
79d9e73608 Fix phantom spacing for 1 letter words (#1940) 2022-07-26 15:44:12 +02:00
Patrick Goldinger
b04f8d75f3 Adjust AutoTextKey behavior to respect subtype locale (#1840) 2022-07-26 15:43:19 +02:00
Kostas Giapis
32b1d123d2 Add circumflex popups to Turkish layout (#1962)
Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2022-07-26 15:37:45 +02:00
Patrick Goldinger
b576cafaa4 Upgrade dependencies and adapt API changes 2022-07-24 17:50:15 +02:00
Patrick Goldinger
312ef93ffc Fix auto-spacing incorrectly triggered (#1947)
Issue only occurred for non-Appender composers
2022-07-19 00:07:34 +02:00
Patrick Goldinger
a1dda0c247 Upgrade JetPref to 0.1.0-beta12 2022-07-11 01:27:29 +02:00
Patrick Goldinger
0c36b96922 Update README.md to fix inconsistency with roadmap 2022-07-07 23:56:30 +02:00
Patrick Goldinger
07e92f052b Release v0.4.0-alpha01 2022-07-06 17:56:53 +02:00
florisboard-bot
244c834de9 Update translations from Crowdin 2022-07-06 17:51:08 +02:00
Vlad
61b5c2cffd Correct layout name for RU&UA (ЙЦУКЕН) (#1681) 2022-07-06 16:39:44 +02:00
Leonardo Hernández
1441bd63cb Refactor and improve C++ codebase (#1895)
* close unused fd

dup2 doesn't close old fds, it only duplicates them.

* use `extern "C"` by block instead individual

also formatting changes for function parameters

* fix a memory leak

* cpp refactor: add utils::log() which takes log_priority

* std{out,err} logger: various improvements

- use std::thread rather than pthread
- redirect std{out,err} to stdin to avoid read() calls
- don't use global variables, for avoid spawning unneeded threads use a static function variable
- check for errors in pipe()
- use a lambda function for thread
2022-07-06 16:15:27 +02:00
Patrick Goldinger
e7d0db0fc0 Merge pull request #1913 from florisboard/sug04-prepare-UI-logic-interface
0.4/Phase 1: Prepare UI, suggestions interface and adjust logic
2022-07-06 11:48:00 +02:00
Patrick Goldinger
a87d340b25 Adjust experimental and NYI banners in Typing screen 2022-07-06 02:11:24 +02:00
Patrick Goldinger
3f0d90cb7c Add auto-spacing after punctuation (#375)
Key notes:
- It only works in rich editors
- It intentionally does NOT work in URL, EMAIL and PASSWORD text fields
- May break for exotic characters (aka everything not representable with one char in UTF-16)
- There's no hardcoded language restriction, however it is tailored towards symbols used mostly in Latin-based languages atm
- Performance checking needs to be redone for the commitChar() method
2022-07-06 01:35:02 +02:00
Patrick Goldinger
b9e9f9b122 Implement suggestion user removal 2022-07-05 02:21:57 +02:00
Patrick Goldinger
f2d1cf3baf Rework clipboard suggestions logic and allow for multiple items (#739) 2022-07-05 01:07:58 +02:00
Patrick Goldinger
cf1112327a Rework typing preference screen and integrate spelling 2022-07-04 23:29:15 +02:00
Patrick Goldinger
c2cb28668d Implement candidate auto-commit logic 2022-07-04 19:30:39 +02:00
Patrick Goldinger
75f4fcb91a Expand provider API with suggestions removal and notify events 2022-07-04 02:50:18 +02:00
Patrick Goldinger
3d92bd0584 Document EditorContent getters and companion object 2022-07-04 23:10:03 +02:00
Patrick Goldinger
52ca98a14d Improve NlpManager and provider API 2022-07-02 20:40:37 +02:00
Patrick Goldinger
629a73a5cf Document and improve SpellingResult 2022-07-01 20:02:10 +02:00
Patrick Goldinger
077ec43855 Add Liberapay option to FUNDING.yml (#1434) 2022-07-01 17:18:17 +02:00
Patrick Goldinger
3ecd3618cb Add baseline for keyboard and provider logic bridge 2022-07-01 01:27:41 +02:00
Patrick Goldinger
38bc34913b Rework and improve internal APK assets file handling 2022-06-30 00:08:03 +02:00
Patrick Goldinger
c733e5ceea Extend Android asset manager API to simplify usage 2022-06-29 22:24:05 +02:00
Patrick Goldinger
e2536ceb92 Remove duplicate NATIVE_NULLPTR 2022-06-28 23:10:13 +02:00
Patrick Goldinger
c17b6f073d Switch from LiveData to StateFlow in some manager classes 2022-06-28 22:10:22 +02:00
Patrick Goldinger
936b177776 Rework spell checker config and add utility script 2022-06-27 22:42:59 +02:00
Patrick Goldinger
7d8036fe69 Remove Nuspell spell check implementation (#1921) 2022-06-27 18:49:59 +02:00
Patrick Goldinger
6d08d1a265 Add sentence break iterator caching 2022-06-27 16:25:57 +02:00
Patrick Goldinger
48aba1c055 Add skeleton for new NLP provider API 2022-06-26 23:49:42 +02:00
Patrick Goldinger
0b3d3317bf Add secondary text UI implementation for candidates 2022-06-25 16:42:59 +02:00
Patrick Goldinger
d1fbdc581b Update roadmap's milestone 0.4 phase 1 2022-06-24 18:46:08 +02:00
Patrick Goldinger
044170eb4b Fix auto-capitalization issues with invalid initial state (#1915) 2022-06-24 03:25:28 +02:00
Patrick Goldinger
a7c16b3ceb Improve state reset mechanism for restarts (#1916) 2022-06-24 01:18:21 +02:00
Patrick Goldinger
dd12be2275 Rework and document candidate item API 2022-06-24 00:39:44 +02:00
Patrick Goldinger
1049bc543a Move package smartbar from ime.text to ime 2022-06-23 19:50:22 +02:00
Patrick Goldinger
5c5ad3cd32 Remove unused TextProcessor class 2022-06-21 14:48:35 +02:00
Patrick Goldinger
6fce521122 Fix candidate completion logic not behaving as expected 2022-06-20 23:02:39 +02:00
Patrick Goldinger
2af9941ea6 Add Costa Rican colón currency set (#1914) 2022-06-20 22:00:47 +02:00
Patrick Goldinger
9b24f742d1 Disable auto-capitalization for Thai language (#1908) 2022-06-20 00:05:44 +02:00
Patrick Goldinger
b36bcf7733 Tie composing region indicator to suggestion enabled state (#1911) 2022-06-19 23:46:14 +02:00
Patrick Goldinger
9559dbdcd6 Update roadmap for 0.4 milestone 2022-06-19 21:53:42 +02:00
Patrick Goldinger
668dd4b5bf Fix changelog for 0.3.16 accidentally stored in beta metafolder 2022-06-13 10:57:29 +02:00
Patrick Goldinger
b98feab9c4 Release v0.3.16 2022-06-12 22:12:29 +02:00
Patrick Goldinger
5f4711ac3e Update Settings home screen message for 0.3.16 2022-06-12 22:12:09 +02:00
Patrick Goldinger
4a931cbdc0 Release v0.3.16-rc01 2022-06-07 16:09:53 +02:00
florisboard-bot
90b2ddbdf2 Update translations from Crowdin 2022-06-07 16:04:43 +02:00
Patrick Goldinger
3d6cacc753 Merge pull request #1894 from florisboard/finetune-performance-and-decrease-cpu-usage
Improve input feedback controller performance
2022-06-05 19:50:51 +02:00
Patrick Goldinger
3aed315dfc Fix and improve auto capitalization logic and performance 2022-06-05 19:36:27 +02:00
Patrick Goldinger
06233cca8c Prevent unnecessary stack trace collection in KeyboardState 2022-06-04 18:54:58 +02:00
Patrick Goldinger
3da0ab260a Improve language display name performance by caching it 2022-06-04 11:12:12 +02:00
Patrick Goldinger
5d47159151 Improve input feedback controller performance 2022-06-03 03:00:31 +02:00
Patrick Goldinger
7e92a7382e Add clipboard URL and Email detection (#1889) 2022-06-03 02:00:10 +02:00
Patrick Goldinger
e69dcd87df Merge pull request #1884 from tsiflimagas/dvorak-es
Add DvorakES layout
2022-05-31 20:54:19 +02:00
Kostas Giapis
fb9589f642 Add Dvorak (ÑÇ) layout 2022-05-31 01:24:50 +03:00
Patrick Goldinger
b034075437 Upgrade AGP to 7.2.1 2022-05-30 23:56:56 +02:00
Patrick Goldinger
cd10906811 Release v0.3.16-beta04 2022-05-29 13:22:07 +02:00
florisboard-bot
a1e0bd9a0e Update translations from Crowdin 2022-05-29 13:03:36 +02:00
Patrick Goldinger
2dd70db4c2 Merge pull request #1880 from florisboard/fix-kbd-logic-and-settings-issues
Fix minor keyboard logic and Settings issues
2022-05-29 13:01:39 +02:00
Patrick Goldinger
fc835cd1ab Add exact stacktrace error log for backup failure (#1847) 2022-05-28 19:00:00 +02:00
Patrick Goldinger
5d88058354 Fix shift+space swipe not selecting text (#1843) 2022-05-27 19:16:04 +02:00
Patrick Goldinger
cda111f623 Fix auto-capitalization not behaving correctly (#1861) 2022-05-27 18:54:53 +02:00
Linerly
fc5ea6204d Add Indonesian layout (#1879)
* Add layout

* Add popup mappings and subtype presets; use QWERTY

* ...forgot the main thing

* Add Indonesian popup mapping and subtype presets

* oops

* Fix syntax issue in Indonesian subtype preset

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2022-05-26 18:06:13 +02:00
Patrick Goldinger
70ba72bf8b Merge pull request #1878 from florisboard/fix-minor-theme-issues
Fix minor theme issues (kbd + prefs)
2022-05-26 17:34:24 +02:00
Patrick Goldinger
c9f84a5a32 Add translations for emoji key and landscape input Syngg elements 2022-05-26 17:19:03 +02:00
Patrick Goldinger
10f53d1bd9 Fix Snygg Shape crashing for corner radii >= 34dp (#1838) 2022-05-26 16:57:30 +02:00
Patrick Goldinger
7624aac935 Upgrade to JetPref 0.1.0-beta11 2022-05-26 16:05:49 +02:00
Patrick Goldinger
06ffb89198 Merge pull request #1875 from florisboard/improve-devtools
Improve devtools and crash log / Add debug log
2022-05-26 15:20:34 +02:00
Patrick Goldinger
d48ce5ad94 Improve CrashDialogActivity crash log generation and add details 2022-05-24 23:58:26 +02:00
Patrick Goldinger
5c1dd57802 Add debug log view/export screen 2022-05-24 23:57:42 +02:00
Patrick Goldinger
1f4133676a Add internal Devtools helper for generating debug logs 2022-05-24 23:56:54 +02:00
Patrick Goldinger
f83d40d137 Add git commit hash to BuildConfig and debug versionNameSuffix
This change allows us to definitely know which commit a build corresponds to. For debug builds a short commit is included within the version name, for beta and stable builds it is just an internal `BuildConfig` field.
2022-05-24 19:41:38 +02:00
Patrick Goldinger
0999cb7b86 Merge pull request #1873 from florisboard/fix-prefs-reset-on-startup
Fix preferences reset & extensions not properly re-indexing on device startup
2022-05-24 19:35:23 +02:00
Patrick Goldinger
c58d6311e1 Fix extensions not properly reloading when context switches 2022-05-23 22:56:41 +02:00
Patrick Goldinger
8c9016d7e3 Fix prefs sometimes reset on startup (#1726, #1868) 2022-05-23 21:40:31 +02:00
Patrick Goldinger
072d768ef6 Properly upgrade to Gradle 7.4.2 2022-05-22 19:43:50 +02:00
Thanh, H
7eabe77358 Vietnamese Telex: Add mising case (#1862)
* Add missing case uc, ic, oc

* Add uô

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2022-05-22 19:09:00 +02:00
ElishaAz
e4df74bbae Added a button for switching to an IME that supports voice (#1587)
* Added a button for switching to an IME that supports voice

* Moved voice input button to the left of clipboard

* Improve voice key position and show toast on failure

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2022-05-22 10:47:31 +02:00
Péter Gábor
80f7d50770 Update Hungarian popup mapping (#1859)
Addition character assignments and changed TLD strings for Hungarian keyboard layouts.

These are important changes because Hungarian users mostly search accented characters where they belong.
For example, the u ú ü ű are similar shapes but u ú and ü ű are the short and long symbol pairs for two different sounds (the same is true for o ó and ö ő).
So the changes are intended to show up as follows:
- assignments for o and u is kept for compatibility with the old style and non-hungarian layouts but with hungarian language (as I use florisboard)
- assignments added for ö and ü to found their long versions (ő and ű) in the right place too (as others use florisboard)

TLD strings are changed because .hu and .eu is widely used in Hungary and .gov.hu is the official ending for government sites.
2022-05-22 10:16:14 +02:00
Patrick Goldinger
31d901ba1c Merge pull request #1870 from florisboard/add-baseline-profile
Add baseline profile and benchmak tests
2022-05-22 09:59:31 +02:00
Patrick Goldinger
4e2a43a6cd Add extended benchmark and improve app startup 2022-05-21 13:35:18 +02:00
Patrick Goldinger
0408d437e1 Add baseline profile 2022-05-20 17:07:38 +02:00
Patrick Goldinger
5a6c36d421 Merge pull request #1860 from florisboard/project-build-and-meta-rework
Improvement of Gradle files and dependency management + Proguard APK size optimization
2022-05-20 14:44:17 +02:00
Patrick Goldinger
5fc07f9ae3 Add Proguard optimization to beta and stable builds
Decrease resulting APK size for beta and stable builds by 3.2MB (18.3MB -> 15.1MB). Debug builds are not affected.
2022-05-19 19:17:52 +02:00
Patrick Goldinger
91cbe6d8ec Change OSS notices from Google GMS to mikepenz/AboutLibraries plugin
Note: the prior GMS Gradle plugin [oss-licenses-plugin](https://github.com/google/play-services-plugins/tree/master/oss-licenses-plugin) was, despite belonging to the GMS service stack, open-source. However it was a bit clunky to use and it does not keep up with the newest Gradle plugin standards, so it got replaced by Mike Penz' AboutLibraries library/plugin. This plugin allows me to define custom licenses and have an JC integration without needing to maintain much code, which is always a plus.
2022-05-19 02:07:55 +02:00
Patrick Goldinger
c2aa87beab Upgrade to Gradle 7.4.2 2022-05-19 00:29:48 +02:00
Patrick Goldinger
0094699a88 Move dependencies declaration to new Gradle version catalog 2022-05-18 23:55:35 +02:00
Patrick Goldinger
4adaf9a315 Release v0.3.16-beta03 2022-05-17 23:14:42 +02:00
florisboard-bot
01eee827df Update translations from Crowdin 2022-05-17 22:33:25 +02:00
SaeID/Rz
fd87241887 Add new persian layout (#1823)
* Delete extension.json

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Update fa3.json

insert character "ئ" as the default character

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2022-05-17 16:12:19 +02:00
Patrick Goldinger
d63792cb15 Merge pull request #1855 from florisboard/emoji-fixes-and-small-improvements
Emoji minor bug fixes / improvements
2022-05-17 15:39:11 +02:00
Patrick Goldinger
e2c4992b51 Fix symbols emoji category icon not representative (#1421) 2022-05-17 00:14:49 +02:00
Patrick Goldinger
481d929f2b Fix incorrect aspect ratio for emoji keys (#1856) 2022-05-16 23:05:40 +02:00
Patrick Goldinger
3d1f1d1e12 Rework internal EmojiCompat implementation (#1770) 2022-05-16 22:52:13 +02:00
Patrick Goldinger
9f4ef7e1ad Re-add vibration to emoji screen (#1675) 2022-05-16 18:26:34 +02:00
Patrick Goldinger
7d24f0c5ae Merge pull request #1844 from florisboard/glide-typing-fixes-and-improvements
Glide typing + gestures improvements / bug fixes
2022-05-15 18:35:56 +02:00
Patrick Goldinger
67408130b1 Fix glide typing not working for long words (#1851) 2022-05-15 13:07:09 +02:00
Patrick Goldinger
cdb9504e5f Fix glide threshold using incorrect units (#1023) 2022-05-14 12:38:45 +02:00
Patrick Goldinger
977b32de6e Add punctuation rules to keyboard extension (#596, #1828) 2022-05-14 11:37:54 +02:00
Patrick Goldinger
4ae4eb00e9 Auto-disable glide typing for password fields 2022-05-14 02:00:47 +02:00
Patrick Goldinger
063fca6dd1 Adjust popup mappings formatting in extension.json 2022-05-13 19:08:10 +02:00
Patrick Goldinger
a650bfe94c Fix active key not appearing pressed for moving gestures (#1846) 2022-05-13 11:47:59 +02:00
Patrick Goldinger
517448da06 Fix phantom space not resetting in selection update (#916) 2022-05-13 11:38:51 +02:00
Patrick Goldinger
8ba9abdace Fix glide typing broken in some input fields (#1009, #1247)
This applies to apps that report that they are "raw" but in fact they are rich. This new detection works by checking if the initial selection is valid, if so then we assume that the editor is rich.
2022-05-12 20:23:58 +02:00
Patrick Goldinger
193d03e0eb Remove Jetifier flag
See https://developer.android.com/studio/releases/gradle-plugin#jetifier-build-analyzer
2022-05-11 23:28:41 +02:00
Patrick Goldinger
363cfe1443 Migrate app namespace from manifest to build.gradle.kts
This is a preparation for AGP 8.0.0 in the future.
2022-05-11 23:15:59 +02:00
Patrick Goldinger
d9cd36966e Upgrade Gradle to 7.3.3 and AGP to 7.2.0 2022-05-11 22:59:37 +02:00
Patrick Goldinger
02bfae55b9 Release v0.3.16-beta02 2022-05-11 22:36:29 +02:00
florisboard-bot
6edbf64104 Update translations from Crowdin 2022-05-11 22:26:14 +02:00
Thanh, H
1c29319036 Expand Basic Telex for Vietnamese to proper Telex (#1797)
* Update tetex rule for: vowel + side char

* Update full rule for a, ă

* Add rule for e

* Update e+{c, m, n} and ê + {c, m} (2 rules)

* Add full rule for e, ă (mark and un-mark)

* Full rule for i

* Full rule o

* Full rule for u

* Add â+{c} (mark + unmark)

* Add â + {m, n}

* Add â + {p, t}

* Full â (mark + unmark)

* Add ê + {n, p, t} (mark)

* Full ê

* Full ô

* Full ơ

* Full ư

* Add full iê + {m, n, p, t, u, ng}: mark & unmark

* Add uyê + {n, t}: mark, un-mark

* Add ươ + {c, i}

* Add ươm

* Add ươ: mark & unmark

* Add full yê: {m, n, t, u, ng} - mark, unmark

* Remove useless rule: "ưow": "ươ"

* Add ach, ich, êch

* Fix Vietnamese default subtype using incorrect composer ID

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2022-05-11 17:52:12 +02:00
Patrick Goldinger
8c8664cafa Merge pull request #1832 from florisboard/fix-and-improve-kbd-logic
Fix and improve keyboard/input logic
2022-05-10 23:36:05 +02:00
Patrick Goldinger
652c7f4e4b Fix auto-capitalization and re-evaluate on new subtype (#1623)
Switching subtype forces a re-evaluation of the input shift state. Korean never auto-capitalizes anymore.
2022-05-10 00:06:51 +02:00
Patrick Goldinger
753fbc30df Re-implement composers into new editor instance logic 2022-05-09 23:32:10 +02:00
Patrick Goldinger
01de9a4ae1 Add ability to hold shift without caps lock (#1764)
Pressing a char before long press timeout will suppress caps lock -> this allows to use the shift key like on PC
2022-05-09 21:19:27 +02:00
Patrick Goldinger
aa52784174 Fix capitalization issues in text keyboard logic (#227) 2022-05-09 19:36:14 +02:00
Patrick Goldinger
dc72e2162e Fix shift key state concurrency issue in touch logic (#227) 2022-05-09 01:16:13 +02:00
Patrick Goldinger
8ef37cedfb Fix shift long press resetting after releasing (#1834) 2022-05-09 00:14:19 +02:00
Patrick Goldinger
08a77ce0eb Add shift state selector and fix Dvorak layout (#528) 2022-05-08 22:02:39 +02:00
Patrick Goldinger
afc6f21a6a Rework shift state management and possible states
There's now a differentiation between automatic and manual shifting. This is the prerequisite for proper shifting of symbol rows.
2022-05-08 21:30:46 +02:00
Patrick Goldinger
bdc2a9a5d3 Upgrade Accompanist to 0.23.1 2022-05-08 21:08:17 +02:00
Patrick Goldinger
c27365bccd Adjust timing for double tap / key repeat events (#1095)
Double tap events now use a fixed value provided by the system (by default `300ms`) and do not depend on `prefs.keyboard.longPressDelay` anymore.

Additionally key repeat also uses a system value, however both the default system value and the previous hard-coded value are `50ms`, no nothing should change here for the user.
2022-05-08 13:00:36 +02:00
Patrick Goldinger
83d4fc727d Fix backspace sometimes not working in initial state (#1831) 2022-05-07 18:47:45 +02:00
Patrick Goldinger
5bf4819d83 Fix space swipe not checking for raw input editor 2022-05-07 17:50:57 +02:00
Patrick Goldinger
60221743f6 Fix Smartbar suggestion UI not updating correctly 2022-05-07 17:39:55 +02:00
Patrick Goldinger
9d78661ecb Refactor Room to use KSP + export schema correctly 2022-05-07 11:52:49 +02:00
Patrick Goldinger
addedb6f9b Fix long press action being run outside main thread 2022-05-06 19:08:21 +02:00
Patrick Goldinger
e1701b2ba7 Improve devtools input state overlay 2022-05-05 22:51:13 +02:00
Patrick Goldinger
380a0e4f4c Release v0.3.16-beta01 2022-05-03 19:57:46 +02:00
florisboard-bot
7fb24c0471 Update translations from Crowdin 2022-05-03 19:38:57 +02:00
Patrick Goldinger
aaae483ad8 Update ROADMAP.md and home screen message 2022-05-03 19:37:23 +02:00
GoRaN
578652b9ba Update Kurdish layout and overall improvements (#1798)
* Update kurdish.json

* Update kurdish_standard.json

* Update ckb.json

* Update ckb.json

* Update kurdish_standard.json
2022-05-02 23:25:59 +02:00
Patrick Goldinger
b8133b17fe Merge pull request #1822 from florisboard/editor-instance-rework
Major rework of input logic, connection handling and state management
2022-05-02 23:18:37 +02:00
Patrick Goldinger
8226a4fcb9 Add "Always delete word" toggle for glide typing (#1541, #1036) 2022-05-02 22:51:20 +02:00
Patrick Goldinger
ea85a63da9 Fix input lag for mass selection events (fast cursor movement)
which results from the keyboard. Fast cursor movement from the editor currently is still a laggy experience.
2022-05-02 22:13:39 +02:00
Patrick Goldinger
21e784fb09 Fix crashutility namespace incorrect in manifest 2022-05-02 19:48:42 +02:00
Patrick Goldinger
0a1fb5a51e Fix editor instance issues with phantom space 2022-05-01 19:05:18 +02:00
Patrick Goldinger
2a3c3e07e2 Improve input connection raw key event sending 2022-05-01 15:30:37 +02:00
Patrick Goldinger
f8eb86806e Improve expected content handling to reduce expensive calls 2022-04-30 18:56:29 +02:00
Patrick Goldinger
25a7c47c3c Fix enter key behavior for multiline and shift (#1037) 2022-04-29 19:42:00 +02:00
Patrick Goldinger
f08d92fef4 Fix delete key word selection not working properly 2022-04-29 00:07:37 +02:00
Patrick Goldinger
362a84de51 Rework InputEventDispatcher and remove async channel 2022-04-28 23:57:05 +02:00
Patrick Goldinger
64fefef31a Add delete key long press option in gesture prefs (#637) 2022-04-28 01:02:51 +02:00
Patrick Goldinger
b0fced6f98 Fix delete key precise char selection not triggering precisely 2022-04-27 23:54:08 +02:00
Patrick Goldinger
406f79ee36 Fix state bugs in the commitText and deleteBeforeCursor logic 2022-04-27 23:17:37 +02:00
Patrick Goldinger
893fc758b1 Add expected content functionality baseline 2022-04-26 23:58:48 +02:00
Patrick Goldinger
126de62f80 Improve and fix new caching base logic in editor instance 2022-04-25 23:31:08 +02:00
Patrick Goldinger
0ee634a83d Merge input logic and editor instance
but keep logic separate in an abstract and a normal class
2022-04-23 20:47:27 +02:00
Patrick Goldinger
d4f634e8af Split editor instance into input logic and instance
Base for further work on the split and input cache expected caching
2022-04-22 21:20:58 +02:00
Patrick Goldinger
f544e1203d Fix delete call being async / Update selection docs 2022-04-21 01:24:32 +02:00
Patrick Goldinger
d0a373f5f4 Rework and simplify editor content+instance logic 2022-04-20 23:53:45 +02:00
Patrick Goldinger
e4dfed3940 Improve logic in text updater and caching 2022-04-20 02:03:34 +02:00
Patrick Goldinger
7f674f89d2 Raise minimum API level from 23 to 24 2022-04-20 00:05:43 +02:00
Patrick Goldinger
0d593a07e6 Rework base logic of EditorInstance word logic
Not optimized for performance yet, some features are broken in this commit. See the following commits for improvements in the changes of this commit.
2022-04-19 23:56:49 +02:00
Patrick Goldinger
261c8fbec1 Add FlorisEditorInfo wrapper class 2022-04-18 23:58:59 +02:00
Patrick Goldinger
d5262e3ad2 Move EditorInfo helpers into own file 2022-04-17 20:43:51 +02:00
Patrick Goldinger
527543331a Update android.yml to remove gradle caching 2022-04-16 13:02:29 +02:00
Patrick Goldinger
75623f2a95 Fix space key being used as hint from symbols (#1778) 2022-04-16 12:24:16 +02:00
Patrick Goldinger
29f6658256 Merge pull request #1793 from florisboard/gestures-and-glide-fixes
Fix issues with gesture detection and glide/input logic
2022-04-16 12:05:59 +02:00
Patrick Goldinger
dbc6606237 Add Smartbar visibility toggle swipe action (#1794) 2022-04-15 20:00:37 +02:00
Patrick Goldinger
1108456a8e Fix glide typing not setting phantom space flag (#1777) 2022-04-15 19:29:04 +02:00
Patrick Goldinger
2debf36cb6 Improve double-space period behavior (#1792)
Still isn't fixed though, requires new cached input logic
2022-04-15 19:10:46 +02:00
Patrick Goldinger
896101b840 Fix gesture detector comparing px and dp values 2022-04-15 18:23:20 +02:00
Patrick Goldinger
a84854a5b1 Fix space bar eating input when gestures disabled (#1715) 2022-04-15 17:44:44 +02:00
Patrick Goldinger
461966de96 Add devtools "Show key touch boundaries" toggle 2022-04-14 22:22:06 +02:00
Patrick Goldinger
6dd3713641 Fix confirm delete dialog bug when history empty (#1783) 2022-04-13 23:00:06 +02:00
Patrick Goldinger
d51b301364 Change repo icon to be rounded-corner (#1785) 2022-04-13 22:47:01 +02:00
Patrick Goldinger
f8db5d3881 Merge pull request #1780 from florisboard/kotlin-package-structure-changes
Source file package structure changes
2022-04-13 17:56:12 +02:00
Patrick Goldinger
781a00e7c2 Fix linking to source files & Update ROADMAP.md 2022-04-12 20:30:27 +02:00
Patrick Goldinger
a1bb73ba9d Optimize imports on all main source files 2022-04-12 00:21:13 +02:00
Patrick Goldinger
d6390108c2 Move res package to lib/cache and lib/io 2022-04-12 00:13:41 +02:00
Patrick Goldinger
fee954c95f Move snygg package to lib/snygg 2022-04-12 00:02:07 +02:00
Patrick Goldinger
0347eabc04 Rework structure inside app and move custom compose to lib 2022-04-11 23:57:45 +02:00
Patrick Goldinger
c6bcd8a89c Rename common package to lib and move several packages in 2022-04-11 23:35:07 +02:00
Patrick Goldinger
114bcb0d4b Rename java source dir to kotlin 2022-04-11 22:56:20 +02:00
Patrick Goldinger
f4a1a04997 Merge pull request #1775 from florisboard/change-app-icon-v4
Add new flower-shaped app icon
2022-04-11 17:52:32 +02:00
Patrick Goldinger
70a859e00b Adjust Fastlane to use new icon 2022-04-10 21:28:54 +02:00
Patrick Goldinger
8d3b2ef474 Add new flower-shaped app icon (#1735)
Co-authored-by: BloodRaven0 <bloodraven0@users.noreply.github.com>
2022-04-10 21:09:04 +02:00
Patrick Goldinger
76efc0a0e0 Upgrade AGP to 7.1.3 / Upgrade other dependencies 2022-04-10 14:53:48 +02:00
478 changed files with 21085 additions and 18202 deletions

1
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,3 @@
github: [patrickgold]
liberapay: patrickgold
custom: ["https://paypal.me/devpatrickgold"]

BIN
.github/repo_icon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -29,14 +29,6 @@ jobs:
java-version: 11
- name: Setup CMake and Ninja
uses: lukka/get-cmake@v3.20.1
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build with Gradle
# MUST call gradlew separately because of an OSS license plugin issue.
# See https://github.com/google/play-services-plugins/issues/199

3
.gitignore vendored
View File

@@ -43,6 +43,3 @@ crowdin.properties
# C++
.cxx/
# AndroidX Room schema JSONs
/app/schemas/

View File

@@ -64,7 +64,7 @@ For the `code` field of each key, make sure to use the UTF-8 code. An useful too
is [unicode-table.com](https://unicode-table.com/en/). From there, you search for your letter and then use the HTML
code, but without the `&#;`
For internal codes of functional or UI keys, see
[`app/src/main/java/dev/patrickgold/florisboard/ime/text/key/KeyCode.kt`](app/src/main/java/dev/patrickgold/florisboard/ime/text/key/KeyCode.kt)
[`app/src/main/kotlin/dev/patrickgold/florisboard/ime/text/key/KeyCode.kt`](app/src/main/kotlin/dev/patrickgold/florisboard/ime/text/key/KeyCode.kt)
.
The label is equally important and should always match up with the defined code. If `code` and `label` don't match up,

View File

@@ -1,9 +1,9 @@
<img align="left" width="80" height="80"
src="fastlane/metadata/android/en-US/images/icon.png" alt="App icon">
src=".github/repo_icon.png" alt="App icon">
# FlorisBoard [![Crowdin](https://badges.crowdin.net/florisboard/localized.svg)](https://crowdin.florisboard.patrickgold.dev) [![Matrix badge](https://img.shields.io/badge/chat-%23florisboard%3amatrix.org-blue)](https://matrix.to/#/#florisboard:matrix.org) ![FlorisBoard CI](https://github.com/florisboard/florisboard/workflows/FlorisBoard%20CI/badge.svg?event=push)
**FlorisBoard** is a free and open-source keyboard for Android 6.0+
**FlorisBoard** is a free and open-source keyboard for Android 7.0+
devices. It aims at being modern, user-friendly and customizable while
fully respecting your privacy. Currently in early-beta state.
@@ -54,7 +54,8 @@ fully respecting your privacy. Currently in early-beta state.
</tr>
</table>
Beginning with v0.4.0 FlorisBoard will follow [SemVer](https://semver.org/#summary) versioning scheme and enter the public beta on Google Play.
Beginning with v0.4.0 FlorisBoard will follow [SemVer](https://semver.org/#summary) versioning scheme.
Beginning with v0.5.0 FlorisBoard will enter the public beta on Google Play.
## Highlighted features
- Integrated clipboard manager / history
@@ -84,6 +85,8 @@ to get more information on this topic.
[Android Jetpack](https://github.com/androidx)
* [Accompanist Compose UI libraries](https://github.com/google/accompanist/) by
[Google](https://github.com/google)
* [AboutLibraries](https://github.com/mikepenz/AboutLibraries) by
[mikepenz](https://github.com/mikepenz)
* [Google Material icons](https://github.com/google/material-design-icons) by
[Google](https://github.com/google)
* [JetPref preference library](https://github.com/patrickgold/jetpref) by
@@ -97,6 +100,8 @@ to get more information on this topic.
* [Nuspell](https://github.com/nuspell/nuspell) by
[Nuspell](https://github.com/nuspell)
Many thanks to [Nikolay Anzarov](https://www.behance.net/nikolayanzarov) ([@BloodRaven0](https://github.com/BloodRaven0)) for designing and providing the main app icons to this project!
## License
```
Copyright 2020-2022 Patrick Goldinger

View File

@@ -8,64 +8,83 @@ out a bit on the stable track. If you are interested in following the developmen
along the beta track releases! These are generally more unstable but you get new stuff faster and can provide early
feedback, which helps a lot!
## 0.3.x
## 0.4
Releases in this section still follow the old versioning scheme, meaning the patch number is a feature upgrade. As this
naming convention is more confusing than useful, beginning with v0.4.0 development a new release/development cycle will
be introduced.
Major release which mainly focuses on adding proper word suggestions and inline autocorrect (for Latin-based languages
only at first). This is a big effort which will take some time to be fully completed. Additionally general small bug
fixes and improvements will be made alongside the development of the main objective.
### 0.3.14 (almost completed, release candidate phase)
With this release the versioning scheme changes to `0.x.y`, where `x` specifies the major changes, and `y` are just
small bug fixes and improvements for the former major stable release `x`. This is different to `0.3.x`, where the
version scheme just did not make any sense anymore, especially with the latest `0.3.x` releases. As for the beta track,
major developments (`0.x`) will have alpha, beta and release candidate releases on the beta track before it goes live on
the stable track. Small follow-up bug fixes (`0.x.y`) will be published on both the stable and beta track without
release candidates.
- Re-write of the Preference core
- Reduce redundancy in key/default value definitions
- Avoid having to manually add redundant code for adding a new pref
- Goes hand-in-hand with the Settings UI re-write
- Re-write of the Settings UI with Jetpack Compose
- Also re-structure UI into a more list-like panel
- Adjust theme colors of Settings a bit to make it more modern
- Preview the keyboard at any time from within the Settings
- Settings language different than device language
- Re-write the Setup UI in Jetpack Compose
- Simplify screen based on previously discussed ideas and mock-ups
- Improve backend setup logic
- Implement base-UI for extensions and further continue development of existing Flex (FlorisBoard extension) format
- Allows for a continuous experience of customizing FlorisBoard in different areas
- Planned what will use Flex:
- Themes
- Layouts (Characters, symbols, numeric, ...)
- Composers for non-Latin script languages
- Word suggestion dictionaries (in 0.4.0)
- Spell check dictionaries
- User dictionaries (not in 0.3.14)
- Other features that require only data and no logic (not in 0.3.14)
- Maybe full backup of preferences? Not 100% confirmed though and may be pushed back
- Theme rework part I:
- Custom key corner radius
- Custom key border color (not shadow!!)
- Re-work theme internals so they use Flex extension format and FlexCSS
- Improvement of the Smartbar
- Allow to have multiple Smartbars
- Better candidate view (in prep for 0.4.0)
### Word suggestions / Autocorrect
### 0.3.15 & 0.3.16
The development effort of this feature is quite big, thus it is split into multiple phases:
- Hotfix releases for possible bugs in the preference rework, may be skipped.
**Phase 1: Preparations of suggestions UI & interfacing API (first alpha release(s))**
## 0.4.0
- Rework Smartbar suggestions UI
- Allow for primary and optionally secondary label (in a smaller font) to be shown per suggestion
- Better integrate clipboard suggestions into word suggestion flow
- Add long-press suggestion action for user to prevent from showing again
- Generally fix and polish suggestions UI design (3-column mode and scrollable mode)
- Add a `SuggestionProvider` interface API to allow for any specialized implementation to be written
- A provider's main task is to receive updates on the current state of the editor (except for raw inputs) and
provide both current word autocorrect/suggestions or next word suggestions if there is no current word
- The provider can utilize the basic APK asset and file APIs for reading dictionary files, however there is no
standardization in parsing as different languages may require different dictionary structures and thus have
different requirements
- Document API and add dummy implementation to test API
- Try to add toggle for not underlining the current word (composing region) while not loosing the caching benefits
- In parallel: Do local research and preps for phase 2
- Re-adding word suggestions (at least for Latin-based languages at first)
- Importing the dictionaries as well as management relies on the Flex extension core and UI in Kotlin
- Actually parsing and generating suggestions happens in C++ to avoid another OOM catastrophe like in 0.3.9/10
- The actual format of the dictionary and word list source is not decided yet
- Community repository on GitHub for theme sharing across users (may be 0.5.0)
_[Anomaly](https://www.anomaly.ltd/), an Australian software company, will sponsor this project with 1000€ so this phase
gets implemented first, as they want to use FlorisBoard as a base for
their [WCC Language Program project (Gurray)](https://www.anomaly.ltd/portfolio/wcc/gurray/). As this fits in perfectly
with the current dev cycle and this had to be done anyways (some parts like documenting and UI polishing just later in
the 0.4 milestone), I have accepted this. However in general this does not mean this project accepts sponsoring for any
feature to be prioritized, as the project's main goals and planned feature timeline must always come first and human dev
resources are limited._
With this release the versioning scheme changes: the second number now indicates new features, changes in the third "
patch" number now indicates bug fixes and minor feature additions for the stable track. The development cycle for each
0.x release will have `-alphaXX`, `-betaXX` and `-rcXX` (release candidate) releases on the beta track for interested
people to follow along the development. The first release to follow the new scheme will be `0.4.0-alpha01` on the beta
track.
**Phase 2: Add native (C++) Latin word suggestion core (alpha releases)**
## 0.5.0
- Research and experiment with different approaches/data sources for Latin-based language prediction and autocorrect
- Research will mainly be done first locally on Linux to decide what to use
- Implementation will be in C++ using STL libraries and if needed other open-source libraries, with compatibility
and CPU/memory restrictions on Android devices in mind
- Once an experiment runs well locally it will be included in the main project and tested out within the keyboard UI
in different alpha releases
- Especially at the beginning an idea may be scrapped and replaced by something else if found that another approach
is better
- (Based on research) Introduce new dictionary/language model format
- Importing the dictionaries/models as well as management relies on the Flex extension core and UI in Kotlin
- Actually parsing and generating suggestions happens in C++
- The actual format of the dictionary/model source is not decided yet
- Add system in preprocessing stage to properly mark slightly offensive words and prevent extremely offensive words
from being included at all
- Add system in preprocessing stage to filter out email addresses and phone numbers that may be included in the
large datasets which are used for building the models
**Phase 3: Add support for more languages & Allow glide typing to utilize new word prediction system (beta releases)**
- Glide typing: Utilize new prediction system and get rid of current English (US) json dictionary
- Add support for more languages (Latin-based), may need to utilize datasets like Opensubtitles or Wikimedia, although
those need extensive cleaning and are not as reliable
- Focus on improving performance and stabilizing the Latin suggestion core
- Possibly address some language-specific issues and ensure suggested word capitalization is correct
- Finalize Settings and keyboard UI regarding word suggestions.
### Other planned features for 0.4
- General small fixes and improvements
- Community repository on GitHub for extension sharing across users (may be 0.5.0 though)
- Localized emoji suggestions (may be 0.5.0 though)
## 0.5
- Complete rework of the Emoji panel
- Recently used / Emoji history (already implemented with 0.3.14)
@@ -79,18 +98,16 @@ track.
- Rework branding images and texts of FlorisBoard for the app stores
- Focus on stability and experience improvements of the app and keyboard
## 0.6.0
## Backlog
**Features that MAY be added (even in versions mentioned above) or dismissed altogether**
- Full on-board layout editor which allows users to create their own layouts without writing a JSON file
- Import/Export of custom layout files packed in Flex extensions
## Backlog / Features that MAY be added, even in versions not mentioned above if the feature implementation fits perfectly with another feature
- Theme rework part II
- Adaptive themes v2
- Voice-to-text with Mozilla's open-source voice service
- Text translation
- Glide typing better word detection
- Proximity-based key typo detection
- Floating keyboard
- Tablet mode / Optimizations for landscape input

View File

@@ -1,14 +1,37 @@
/*
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Suppress needed until https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@file:Suppress("DSL_SCOPE_VIOLATION")
import java.io.ByteArrayOutputStream
import java.io.File
plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
kotlin("plugin.serialization")
id("com.google.android.gms.oss-licenses-plugin")
id("de.mannodermaus.android-junit5")
alias(libs.plugins.agp.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.mannodermaus.android.junit5)
alias(libs.plugins.mikepenz.aboutlibraries)
}
android {
compileSdk = 31
namespace = "dev.patrickgold.florisboard"
compileSdk = 32
buildToolsVersion = "31.0.0"
ndkVersion = "22.1.7171670"
@@ -18,32 +41,29 @@ android {
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
jvmTarget = "1.8"
freeCompilerArgs = listOf(
"-Xallow-result-return-type",
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
"-Xjvm-default=compatibility",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-Xjvm-default=all-compatibility",
)
}
defaultConfig {
applicationId = "dev.patrickgold.florisboard"
minSdk = 23
minSdk = 24
targetSdk = 31
versionCode = 80
versionName = "0.3.15"
versionCode = 88
versionName = "0.4.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf(
Pair("room.schemaLocation", "$projectDir/schemas"),
Pair("room.incremental", "true"),
Pair("room.expandProjection", "true")
)
}
buildConfigField("String", "BUILD_COMMIT_HASH", "\"${getGitCommitHash()}\"")
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
arg("room.expandProjection", "true")
}
externalNativeBuild {
@@ -66,6 +86,9 @@ android {
jniLibs {
srcDirs("src/main/icu4c/prebuilt/jniLibs")
}
java {
srcDirs("src/main/kotlin")
}
}
}
}
@@ -84,7 +107,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.1.1"
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
}
externalNativeBuild {
@@ -94,9 +117,9 @@ android {
}
buildTypes {
named("debug").configure {
named("debug") {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
versionNameSuffix = "-debug-${getGitCommitHash(short = true)}"
isDebuggable = true
isJniDebuggable = false
@@ -112,11 +135,13 @@ android {
resValue("string", "floris_app_name", "FlorisBoard Debug")
}
create("beta") // Needed because by default the "beta" BuildType does not exist
named("beta").configure {
create("beta") {
applicationIdSuffix = ".beta"
versionNameSuffix = ""
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
versionNameSuffix = "-alpha02"
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
isMinifyEnabled = true
isShrinkResources = true
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_beta_round")
@@ -124,14 +149,31 @@ android {
resValue("string", "floris_app_name", "FlorisBoard Beta")
}
named("release").configure {
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
named("release") {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
isMinifyEnabled = true
isShrinkResources = true
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_release")
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_release_round")
resValue("drawable", "floris_app_icon_foreground", "@drawable/ic_app_icon_release_foreground")
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_stable")
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_stable_round")
resValue("drawable", "floris_app_icon_foreground", "@drawable/ic_app_icon_stable_foreground")
resValue("string", "floris_app_name", "@string/app_name")
}
create("benchmark") {
initWith(getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
ndk {
// For running FlorisBoard on the emulator
abiFilters += listOf("x86", "x86_64")
}
}
}
aboutLibraries {
configPath = "app/src/main/config"
}
testOptions {
@@ -149,36 +191,56 @@ tasks.withType<Test> {
}
dependencies {
implementation("androidx.activity:activity-compose:1.4.0")
implementation("androidx.activity:activity-ktx:1.4.0")
implementation("androidx.autofill:autofill:1.1.0")
implementation("androidx.collection:collection-ktx:1.2.0")
implementation("androidx.compose.material:material:1.1.1")
implementation("androidx.compose.runtime:runtime-livedata:1.1.1")
implementation("androidx.compose.ui:ui:1.1.1")
implementation("androidx.compose.ui:ui-tooling-preview:1.1.1")
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.core:core-splashscreen:1.0.0-beta01")
implementation("androidx.emoji2:emoji2:1.1.0")
implementation("androidx.emoji2:emoji2-views:1.1.0")
implementation("androidx.navigation:navigation-compose:2.4.1")
implementation("com.google.accompanist:accompanist-flowlayout:0.23.0")
implementation("com.google.accompanist:accompanist-insets:0.23.0")
implementation("com.google.accompanist:accompanist-systemuicontroller:0.23.0")
implementation("dev.patrickgold.jetpref:jetpref-datastore-model:0.1.0-beta08")
implementation("dev.patrickgold.jetpref:jetpref-datastore-ui:0.1.0-beta08")
implementation("dev.patrickgold.jetpref:jetpref-material-ui:0.1.0-beta08")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
implementation("androidx.room:room-runtime:2.4.2")
kapt("androidx.room:room-compiler:2.4.2")
implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.systemuicontroller)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.autofill)
implementation(libs.androidx.collection.ktx)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.runtime.livedata)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.emoji2)
implementation(libs.androidx.emoji2.views)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.profileinstaller)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.runtime)
implementation(libs.cache4k)
implementation(libs.jetpref.datastore.model)
implementation(libs.jetpref.datastore.ui)
implementation(libs.jetpref.material.ui)
implementation(libs.kotlinx.coroutines)
implementation(libs.kotlinx.serialization.json)
implementation(libs.mikepenz.aboutlibraries.core)
implementation(libs.mikepenz.aboutlibraries.compose)
testImplementation("io.kotest:kotest-runner-junit5:5.1.0")
testImplementation("io.kotest:kotest-assertions-core:5.1.0")
testImplementation("io.kotest:kotest-property:5.1.0")
testImplementation("io.kotest.extensions:kotest-extensions-robolectric:0.5.0")
testImplementation("nl.jqno.equalsverifier:equalsverifier:3.8.3")
testImplementation(libs.equalsverifier)
testImplementation(libs.kotest.assertions.core)
testImplementation(libs.kotest.extensions.roboelectric)
testImplementation(libs.kotest.property)
testImplementation(libs.kotest.runner.junit5)
androidTestImplementation("androidx.test.ext", "junit", "1.1.2")
androidTestImplementation("androidx.test.espresso", "espresso-core", "3.3.0")
androidTestImplementation(libs.androidx.test.ext)
androidTestImplementation(libs.androidx.test.espresso.core)
}
fun getGitCommitHash(short: Boolean = false): String {
if (!File(".git").exists()) {
return "null"
}
val stdout = ByteArrayOutputStream()
exec {
if (short) {
commandLine("git", "rev-parse", "--short", "HEAD")
} else {
commandLine("git", "rev-parse", "HEAD")
}
standardOutput = stdout
}
return stdout.toString().trim()
}

29
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,29 @@
# Disable obfuscation (we use Proguard exclusively for optimization)
-dontobfuscate
# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
# Keep `INSTANCE.serializer()` of serializable objects.
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault

View File

@@ -0,0 +1,62 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "d573e2ae2cbe7026957bc8d8fda291f3",
"entities": [
{
"tableName": "clipboard_files",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER NOT NULL, `_display_name` TEXT NOT NULL, `_size` INTEGER NOT NULL, `mimeTypes` TEXT NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "_display_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "_size",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mimeTypes",
"columnName": "mimeTypes",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_clipboard_files__id",
"unique": false,
"columnNames": [
"_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_clipboard_files__id` ON `${TABLE_NAME}` (`_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd573e2ae2cbe7026957bc8d8fda291f3')"
]
}
}

View File

@@ -0,0 +1,80 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "6fbec9d7ea017f8aefac4fb84dbd5189",
"entities": [
{
"tableName": "clipboard_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `text` TEXT, `uri` TEXT, `creationTimestampMs` INTEGER NOT NULL, `isPinned` INTEGER NOT NULL, `mimeTypes` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uri",
"columnName": "uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "creationTimestampMs",
"columnName": "creationTimestampMs",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPinned",
"columnName": "isPinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mimeTypes",
"columnName": "mimeTypes",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_clipboard_history__id",
"unique": false,
"columnNames": [
"_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_clipboard_history__id` ON `${TABLE_NAME}` (`_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6fbec9d7ea017f8aefac4fb84dbd5189')"
]
}
}

View File

@@ -0,0 +1,68 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "787af4a2df15bf9d2c0597519d3fb273",
"entities": [
{
"tableName": "words",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `word` TEXT NOT NULL, `frequency` INTEGER NOT NULL, `locale` TEXT, `shortcut` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "word",
"columnName": "word",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "freq",
"columnName": "frequency",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "locale",
"columnName": "locale",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shortcut",
"columnName": "shortcut",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_words__id",
"unique": false,
"columnNames": [
"_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_words__id` ON `${TABLE_NAME}` (`_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '787af4a2df15bf9d2c0597519d3fb273')"
]
}
}

View File

@@ -13,10 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="dev.patrickgold.florisboard">
xmlns:tools="http://schemas.android.com/tools">
<!-- Permission needed to vibrate if the user has key press vibration enabled -->
<uses-permission android:name="android.permission.VIBRATE"/>
@@ -46,14 +44,16 @@
android:theme="@style/FlorisAppTheme"
tools:targetApi="s">
<!-- Allow app to be profiled for benchmarking and baseline profile generation -->
<profileable android:shell="true"/>
<!-- IME service -->
<service
android:name="dev.patrickgold.florisboard.FlorisImeService"
android:label="@string/floris_app_name"
android:permission="android.permission.BIND_INPUT_METHOD"
android:directBootAware="true"
android:exported="true"
tools:targetApi="n">
android:exported="true">
<intent-filter>
<action android:name="android.view.InputMethod"/>
</intent-filter>
@@ -80,7 +80,7 @@
android:launchMode="singleTask"
android:roundIcon="@mipmap/floris_app_icon_round"
android:windowSoftInputMode="adjustResize"
android:theme="@style/FlorisAppTheme"
android:theme="@style/FlorisAppTheme.Splash"
android:exported="false">
<intent-filter>
<data android:scheme="florisboard" android:host="app-ui"/>
@@ -95,7 +95,7 @@
android:launchMode="singleTask"
android:roundIcon="@mipmap/floris_app_icon_round"
android:targetActivity="dev.patrickgold.florisboard.app.FlorisAppActivity"
android:theme="@style/FlorisAppTheme"
android:theme="@style/FlorisAppTheme.Splash"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -105,7 +105,7 @@
<!-- Import File Bridging Activity -->
<activity
android:name="dev.patrickgold.florisboard.app.ui.ext.ImportFileActivity"
android:name="dev.patrickgold.florisboard.app.ext.ImportFileActivity"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__title"
android:launchMode="singleTask"
@@ -123,7 +123,7 @@
<!-- Crash Dialog Activity -->
<activity
android:name="dev.patrickgold.florisboard.crashutility.CrashDialogActivity"
android:name="dev.patrickgold.florisboard.lib.crashutility.CrashDialogActivity"
android:icon="@mipmap/floris_app_icon"
android:label="@string/crash_dialog__title"
android:theme="@style/CrashDialogTheme"/>
@@ -147,6 +147,17 @@
android:resource="@xml/file_paths"/>
</provider>
<!-- Disable default EmojiCompat initializer -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.emoji2.text.EmojiCompatInitializer"
tools:node="remove"/>
</provider>
</application>
</manifest>

View File

@@ -2,10 +2,10 @@
"$": "ime.extension.keyboard",
"meta": {
"id": "org.florisboard.composers",
"version": "0.1.0",
"version": "0.1.1",
"title": "Default composers",
"description": "Default composers which are always available.",
"maintainers": [ "patrickgold <patrick@patrickgold.dev>" ],
"maintainers": [ "patrickgold <patrick@patrickgold.dev>", "thanhhocse96 <thanh.hoquang@pm.me>" ],
"license": "apache-2.0"
},
"composers": [
@@ -13,10 +13,10 @@
{ "$": "hangul-unicode" },
{ "$": "kana-unicode" },
{ "$": "with-rules",
"id": "basic-telex",
"label": "Basic Telex",
"id": "telex",
"label": "Telex",
"rules": {
"aw": "ă", "aa": "â", "dd": "đ", "ee": "ê", "oo": "ô", "ow": "ơ", "uw": "ư", "w": "ư",
"aw": "ă", "aa": "â", "dd": "đ", "ee": "ê", "oo": "ô", "ow": "ơ", "uw": "ư", "w": "ư",
"uow": "ươ",
"af": "à", "ar": "ả", "ax": "ã", "as": "á", "aj": "ạ",
"ăf": "ằ", "ăr": "ẳ", "ăx": "ẵ", "ăs": "ắ", "ăj": "ặ",
@@ -30,7 +30,7 @@
"uf": "ù", "ur": "ủ", "ux": "ũ", "us": "ú", "uj": "ụ",
"ưf": "ừ", "ưr": "ử", "ưx": "ữ", "ưs": "ứ", "ưj": "ự",
"yf": "ỳ", "yr": "ỷ", "yx": "ỹ", "ys": "ý", "yj": "ỵ",
"ăw": "aw", "âa": "aa", "đd": "dd", "êe": "ee", "ôo": "oo", "ơw": "ow", "ưw": "w",
"ăw": "aw", "âa": "aa", "đd": "dd", "êe": "ee", "ôo": "oo", "ơw": "ow", "ưw": "w", "iêe": "iee",
"ươw": "uow",
"àf": "af", "ảr": "ar", "ãx": "ax", "ás": "as", "ạj": "aj",
"ằf": "ăf", "ẳr": "ăr", "ẵx": "ăx", "ắs": "ăs", "ặj": "ăj",
@@ -55,7 +55,623 @@
"ờz": "ơ", "ởz": "ơ", "ỡz": "ơ", "ớz": "ơ", "ợz": "ơ",
"ùz": "u", "ủz": "u", "ũz": "u", "úz": "u", "ụz": "u",
"ừz": "ư", "ửz": "ư", "ữz": "ư", "ứz": "ư", "ựz": "ư",
"ỳz": "y", "ỷz": "y", "ỹz": "y", "ýz": "y", "ỵz": "y"
"ỳz": "y", "ỷz": "y", "ỹz": "y", "ýz": "y", "ỵz": "y",
"acw": "ăc", "amw": "ăm", "anw": "ăn", "apw": "ăp", "atw": "ăt", "angw": "ăng",
"aca": "âc", "ama": "âm", "ana": "ân", "apa": "âp", "ata": "ât", "aua": "âu", "aya": "ây", "anga": "âng",
"eme": "êm", "ene": "ên", "epe": "êp", "ete": "êt", "enhe": "ênh",
"oio": "ôi","omo": "ôm", "ono": "ôn", "opo": "ôp", "oto": "ôt", "ongo": "ông",
"oiw": "ơi", "omw": "ơm", "onw": "ơn", "opw": "ơp", "otw": "ơt",
"uaw": "ưa", "uiw": "ưi", "umw": "ưm", "unw": "ưn", "utw": "ưt", "uuw": "ưu", "ungw": "ưng",
"ieme": "iêm", "iene": "iên", "iepe": "iêp", "iete": "iêt", "ieue": "iêu", "ienge": "iêng",
"uocw": "ươc", "uoiw": "ươi", "uomw": "ươm", "uonw": "ươn", "uotw": "ươt", "uongw": "ương",
"uoco": "uôc", "uoio": "uôi", "uomo": "uôm", "uono": "uôn", "uoto": "uôt", "uongo": "uông",
"uyene": "uyên", "uyete": "uyêt",
"yeme": "yêm", "yene": "yên", "yete": "yêt", "yeue": "yêu", "yenge": "yêng",
"ăca": "âc", "ăma": "âm", "ăna": "ân", "ăpa": "âp", "ăta": "ât", "ănga": "âng",
"âcw": "ăc", "âmw": "ăm", "ânw": "ăn", "âpw": "ăp", "âtw": "ăt", "ângw": "ăng",
"ôiw": "ơi", "ômw": "ơm", "ônw": "ơn", "ôpw": "ơp", "ôtw": "ơt",
"ơio": "ôi", "ơmo": "ôm", "ơno": "ôn", "ơpo": "ôp", "ơto": "ôt",
"ăcw": "acw", "ămw": "amw", "ănw": "anw", "ăpw": "apw", "ătw": "atw", "ăngw": "angw",
"âca": "aca", "âma": "ama", "âna": "ana", "âpa": "apa", "âta": "ata", "âua": "aua", "âya": "aya", "ânga": "anga",
"ême": "eme", "êne": "ene", "êpe": "epe", "ête": "ete",
"ôio": "oio", "ômo": "omo", "ôno": "ono", "ôpo": "opo", "ôto": "oto", "ôngo": "ongo",
"ơmw": "omw", "ơnw": "onw", "ơpw": "opw", "ơtw": "otw",
"ưaw": "uaw", "ưiw": "uiw", "ưmw": "umw", "ưnw": "unw" , "ưtw": "utw", "ưuw": "uuw", "ưngw": "ungw",
"iême": "ieme", "iêne": "iene", "iêpe": "iepe", "iête": "iete", "iêue": "ieue", "iênge": "ienge",
"ươcw": "uocw", "ươiw": "uoiw", "ươmw": "uomw", "ươnw": "uonw", "ươtw": "uotw", "ươngw": "uongw",
"uyêne": "uyene", "uyêt": "uyete",
"yême": "yeme", "yêne": "yene", "yête": "yete", "yêue": "yeue", "yênge": "yenge",
"acs": "ác", "acj": "ạc",
"achs": "ách", "achj": "ạch",
"ais": "ái", "aif": "ài", "air": "ải", "aix": "ãi", "aij": "ại",
"ams": "ám", "amf": "àm", "amr": "ảm", "amx": "ãm", "amj": "ạm",
"ans": "án" , "anf": "àn", "anr": "ản", "anx": "ãn", "anj": "ạn",
"aos": "áo", "aof": "ào", "aor": "ảo", "aox": "ão", "aoj": "ạo",
"aps":"áp", "apj": "ạp",
"ats":"át", "atj": "ạt",
"aus":"áu", "auf": "àu", "aur": "ảu", "aux": "ãu", "auj": "ạu",
"ays": "áy", "ayf": "ày", "ayr": "ảy", "ayx": "ãy", "ayj": "ạy",
"angs": "áng", "angf": "àng", "angr": "ảng", "angx": "ãng", "angj": "ạng",
"anhs": "ánh", "anhf": "ành", "anhr": "ảnh", "anhx": "ãnh", "anhj": "ạnh",
"ács": "acs", "ạcj": "acj",
"áchs": "achs", "ạchj": "achj",
"áis": "ais", "àif": "aif", "ảir": "air", "ãix": "aix", "ạij": "aij",
"áms": "ams", "àmf": "amf", "ảmr": "amr", "ãmx": "amx", "ạmj": "amj",
"áns": "ans", "ànf": "anf", "ảnr": "anr", "ãnx": "anx", "ạnj": "anj",
"áos": "aos", "àof": "aof", "ảor": "aor", "ãox": "aox", "ạoj":"aoj",
"áps": "aps", "ạpj": "apj",
"áts": "ats", "ạtj": "atj",
"áus": "aus", "àuf": "auf", "ảur": "aur", "ãux": "aux", "ạuj": "auj",
"áys": "ays", "àyf": "ayf", "ảyr": "ayr", "ãyx": "ayx", "ạyj": "ayj",
"ángs": "angs", "àngf": "angf", "ảngr": "angr", "ãngx": "angx", "ạngj": "angj",
"ánhs": "anhs", "ànhf": "anhf", "ảnhr": "anhr", "ãnhx": "anhx", "ạnhj": "anhj",
"ecs": "éc", "ecj": "ẹc",
"ems": "ém", "emf": "èm", "emr": "ẻm", "emx": "ẽm", "emj": "ẹm",
"ens": "én", "enf": "èn", "enr": "ẻn", "enx": "ẽn", "enj": "ẹn",
"eps": "ép", "epj": "ẹp",
"ets": "ét", "etj": "ẹt",
"engs": "éng", "engf": "èng", "engr": "ẻng", "engx": "ẽng", "engj": "ẹng",
"enhs": "énh", "enhf": "ènh", "enhr": "ẻnh", "enhx": "ẽnh", "enhj": "ẹnh",
"écs": "ecs", "ẹcj": "ecj",
"éms": "ems", "èmf": "emf", "ẻmr": "emr", "ẽmx": "emx", "ẹmj": "emj",
"éns": "ens", "ènf": "enf","ẻnr": "enr", "ẽnx": "enx", "ẹnj": "enj",
"éps": "eps", "ẹpj": "epj",
"éts": "ets", "ẹtj": "etj",
"éngs": "engs", "èngf": "engf", "ẻngr": "engr", "ẽngx":"engx", "ẹngj": "engj",
"énhs": "enhs", "ènhf": "enhf", "ẻnhr": "enhr", "ẽnh": "enhx", "ẹnh": "enhj",
"ias": "ía", "iaf": "ìa", "iar": "ỉa", "iax": "ĩa", "iaj": "ịa",
"ics": "íc", "icj": "ịc",
"ims": "ím", "imf": "ìm", "imr": "ỉm", "imx": "ĩm", "imj": "ịm",
"ins": "ín", "inf": "ìn", "inr": "ỉn", "inx": "ĩn", "inj": "ịn",
"ips": "íp", "ipj": "ịp",
"its": "ít", "itj": "ịt",
"ius": "íu", "iuf": "ìu", "iur": "ỉu", "iux": "ĩu", "iuj": "ịu",
"ichs": "ích", "ichj": "ịch",
"inhs": "ính", "inhf": "ình", "inhr": "ỉnh", "inhx": "ĩnh", "inhj": "ịnh",
"ías": "ias", "ìaf": "iaf", "ỉar": "iar", "ĩax": "iax", "ịaj": "iaj",
"ícs": "ics", "ịcj": "icj",
"íms": "ims", "ìmf": "imf", "ỉmr": "imr", "ĩmx": "imx", "ịmj": "imj",
"íns": "ins", "ìnf": "inf", "ỉnr": "inr", "ĩnx": "inx", "ịnj": "inj",
"íps": "ips", "ịpj": "ipj",
"íts": "its", "ịtj": "itj",
"íus": "ius", "ìuf": "iuf", "ỉur": "iur", "ĩux": "iux", "ịuj": "iuj",
"íchs": "ichs", "ịchj": "ichj",
"ínhs": "inhs", "ìnhf": "inhf", "ỉnhr": "inhr", "ĩnhx": "inhx", "ịnhj": "inhj",
"oas": "óa", "oaf": "òa", "oar": "ỏa", "oax": "õa", "oaj": "ọa",
"ocs": "óc", "ocj": "ọc",
"ois": "ói", "oif": "òi", "oir": "ỏi", "oix": "õi", "oij": "ọi",
"oms": "óm", "omf": "òm", "omr": "ỏm", "omx": "õm", "omj": "ọm",
"ons": "ón", "onf": "òn", "onr": "ỏn", "onx": "õn", "onj": "ọn",
"ops": "óp", "opj": "ọp",
"ots": "ót", "otj": "ọt",
"ongs": "óng", "ongf": "òng", "ongr": "ỏng", "ongx": "õng", "ongj": "ọng",
"óas": "oas", "òaf": "oaf", "ỏar": "oar", "õax": "oax", "ọaj": "oaj",
"ócs": "ocs", "ọcj": "ocj",
"óis": "ois", "òif": "oif", "ỏir": "oir", "õix": "oix", "ọij": "oij",
"óms": "oms", "òmf": "omf", "ỏmr": "omr", "õmx": "omx", "ọmj": "omj",
"óns": "ons", "ònf": "onf", "ỏnr": "onr", "õnx": "onx", "ọnj": "onj",
"óps": "ops", "ọpj": "opj",
"óts": "ots", "ọtj": "otj",
"óngs": "ongs", "òngf": "ongf", "ỏngr": "ongr", "õngx": "ongx", "ọngj": "ongj",
"uas": "úa", "uaf": "ùa", "uar": "ủa", "uax": "ũa", "uaj": "ụa",
"ucs": "úc", "ucj": "ục",
"uis": "úi", "uif": "ùi", "uir": "ủi", "uix": "ũi", "uij": "ụi",
"ums": "úm", "umf": "ùm", "umr": "ủm", "umx": "ũm", "umj": "ụm",
"uns": "ún", "unf": "ùn", "unr": "ủn", "unx": "ũn", "unj": "ụn",
"ups": "úp", "upj": "ụp",
"uts": "út", "utj": "ụt",
"uus": "úu", "uuf": "ùu", "uur": "ủu", "uux": "ũu", "uuj": "ụu",
"uys": "úy", "uyf": "ùy", "uyr": "ủy", "uyx": "ũy", "uyj": "ụy",
"ungs": "úng", "ungf": "ùng", "ungr": "ủng", "ungx": "ũng", "ungj": "ụng",
"úas": "uas", "ùaf": "uaf", "ủar": "uar", "ũax": "uax", "ụaj": "uaj",
"úcs": "ucs", "ụcj": "ucj",
"úis": "uis", "ùif": "uif", "ủir": "uir", "ũix": "uix", "ụij": "uij",
"úms": "ums", "ùmf": "umf", "ủmr": "umr", "ũmx": "umx", "ụmj": "umj",
"úns": "uns", "ùnf": "unf", "ủnr": "unr", "ũnx": "unx", "ụnj": "unj",
"úps": "ups", "ụpj": "upj",
"úts": "uts", "ụtj": "utj",
"úus": "uus", "ùuf": "uuf", "ủur": "uur", "ũux": "uux", "ụuj": "uuj",
"úu ": "uus", "ùu ": "uuf", "ủu ": "uur", "ũu ": "uux", "ụu ": "uuj",
"úys": "uys", "ùyf": "uyf", "ủyr": "uyr", "ũyx": "uyx", "ụyj": "uyj",
"úngs": "ungs", "ùngf": "ungf", "ủngr": "ungr", "ũngx": "ungx", "ụngj": "ungj",
"ăcs": "ắc", "ăcj": "ặc",
"ácw": "ắc", "ạcw": "ặc",
"ăms": "ắm", "ămf": "ằm", "ămr": "ẳm", "ămx": "ẵm", "ămj": "ặm",
"ámw": "ắm", "àmw": "ằm", "ảmw": "ẳm", "ãmw": "ẵm", "ạmw": "ặm",
"ăns": "ắn", "ănf": "ằn", "ănr": "ẳn", "ănx": "ẵn", "ănj": "ặn",
"ánw": "ắn", "ànw": "ằn", "ảnw": "ẳn", "ãnw": "ẵn", "ạnw": "ặn",
"ăps": "ắp", "ăpj": "ặp",
"ápw": "ắp", "ạpw": "ặp",
"ăts": "ắt", "ătj": "ặt",
"átw": "ắt", "ạtw": "ặt",
"ăngs": "ắng", "ăngf": "ằng", "ăngr": "ẳng", "ăngx": "ẵng", "ăngj": "ặng",
"ángw": "ắng", "àngw": "ằng", "ảngw": "ẳng", "ãngw":"ẵng", "ạngw": "ặng",
"ắcw": "ácw", "ắcs": "ăcs", "ăcsw": "acsw", "ácws": "acws",
"ặcw": "ạcw", "ặcj": "ăcj", "ăcjw": "acjw", "ạcwj": "acwj",
"ắms": "ăms", "ắmw": "ámw", "ămsw": "amsw", "ámws": "amws",
"ằmf": "ămf", "ằmw": "àmw", "ămfw": "amfw", "àmwf": "amwf",
"ẳmr": "ămr", "ẳmw": "ảmw", "ămrw": "amrw", "ảmwr": "amwr",
"ẵmx": "ămx", "ẵmw": "ãmw", "ămxw": "amxw", "ãmwx": "amwx",
"ặmj": "ămj", "ặmw": "ạmw", "ămjw": "amjw", "ạmwj": "amwj",
"ắns": "ăns", "ắnw": "ánw", "ănsw": "answ", "ánws": "anws",
"ằnf": "ănf", "ằnw": "ànw", "ănfw": "anfw", "ànwf": "anwf",
"ẳnr": "ănr", "ẳnw": "ảnw", "ănrw": "anrw", "ảnwr": "anwr",
"ẵnx": "ănx", "ẵnw": "ãnw", "ănxw": "anxw", "ãnwx": "anwx",
"ặnj": "ănj", "ặnw": "ạnw", "ănjw": "anjw", "ạnwj": "anwj",
"ắps": "ăps", "ắpw": "ápw", "ăpsw": "apsw", "ápws": "apws",
"ặpj": "ăpj", "ặpw": "ạpw", "ăpjw": "apjw", "ạpwj": "apwj",
"ắts": "ăts", "ắtw": "átw", "ătsw": "atws", "átws": "atws",
"ặtj": "ătj", "ặtw": "ạtw", "ătjw": "atjw", "ạtwj": "atwj",
"ắngs": "ăngs", "ắngw": "ángw", "ăngsw": "angsw", "ángws": "angws",
"ằngf": "ăngf", "ằngw": "àngw", "ăngfw": "angfw", "àngwf": "angwf",
"ẳngr": "ăngr", "ẳngw": "ảngw", "ăngrw": "angrw", "ảngwr": "angwr",
"ẵngx": "ăngx", "ẵngw": "ãngw", "ăngxw": "angxw", "ãngwx": "angwx",
"ặngj": "ăngj", "ặngw": "ạngw", "ăngjw": "angjw", "ạngwj": "angwj",
"âcs": "ấc", "âcj": "ậc",
"áca": "ấc", "ạca": "ậc",
"âms": "ấm", "âmf": "ầm", "âmr": "ẩm", "âmx": "ẫm", "âmj": "ậm",
"áma": "ấm", "àma": "ầm", "ảma": "ẩm", "ãma": "ẫm", "ạma": "ậm",
"âns": "ấn", "ânf": "ần", "ânr": "ẩn", "ânx": "ẫn", "ânj": "ận",
"ána": "ấn", "àna": "ần", "ảna": "ẩn", "ãna": "ẫn", "ạna": "ận",
"âps": "ấp", "âpj": "ập",
"ápa": "ấp", "ạpa": "ập",
"âts": "ất", "âtj": "ật",
"áta": "ất", "ạta": "ật",
"âus": "ấu", "âuf": "ầu", "âur": "ẩu", "âux": "ẫu", "âuj": "ậu",
"áua": "ấu", "àua": "ầu", "ảua": "ẩu", "ãua": "ẫu", "ạua": "ậu",
"âys": "ấy", "âyf": "ầy", "âyr": "ẩy", "âyx": "ẫy", "âyj": "ậy",
"áya": "ấy", "àya": "ầy", "ảya": "ẩy", "ãya": "ẫy", "ạya": "ậy",
"ângs": "ấng", "ângf": "ầng", "ângr": "ẩng", "ângx": "ẫng", "ângj": "ậng",
"ánga": "ấng", "ànga": "ầng", "ảnga": "ẩng", "ãnga": "ẫng", "ạnga": "ậng",
"ấcs": "âcs", "ấca": "áca", "âcsa": "acsa", "ácas": "acas",
"ậcj": "âcj", "ậca": "ạca", "âcja": "acja", "ạcaj": "acaj",
"ấms": "âms", "ấma": "áma", "âmsa": "amsa", "ámas": "amas",
"ầmf": "âmf", "ầma": "àma", "âmfa": "amfa", "àmaf": "amaf",
"ẩmr": "âmr", "ẩma": "ảma", "âmra": "amra", "ảmar": "amar",
"ẫmx": "âmx", "ẫma": "ãma", "âmxa": "amxa", "ãmax": "amax",
"ậmj": "âmj", "ậma": "ạma", "âmja": "amja", "ạmaj": "amaj",
"ấns": "âns", "ấna": "ána", "ânsa": "ansa", "ánas": "anas",
"ầnf": "ânf", "ầna": "àna", "ânfa": "anfa", "ànaf": "anaf",
"ẩnr": "ânr", "ẩna": "ảna", "ânra": "anra", "ảnar": "anar",
"ẫnx": "ânx", "ẫna": "ãna", "ânxa": "anxa", "ãnax": "anax",
"ậnj": "ânj", "ậna": "ạna", "ânja": "anja", "ạnaj": "anaj",
"ấps": "âps", "ấpa": "ápa", "âpsa": "apsa", "ápas": "apas",
"ậpj": "âpj", "ậpa": "ạpa", "âpja": "apja", "ạpaj": "apaj",
"ấts": "âts", "ấta": "áta", "âtsa": "atas", "átas": "atas",
"ậtj": "âtj", "ậta": "ạta", "âtja": "atja", "ạtaj": "ataj",
"ấus": "âus", "ấua": "áua", "âusa": "ausa", "áuas": "auas",
"ầuf": "âuf", "ầua": "àua", "âufa": "aufa", "àuaf": "auaf",
"ẩur": "âur", "ẩua": "ảua", "âura": "aura", "ảuar": "auar",
"ẫux": "âux", "ẫua": "ãua", "âuxa": "auxa", "ãuax": "auax",
"ậuj": "âuj", "ậua": "ạua", "âuja": "auja", "ạuaj": "auaj",
"ấys": "âys", "ấya": "áya", "âysa": "aysa", "áyas": "ayas",
"ầyf": "âyf", "ầya": "àya", "âyfa": "ayfa", "àyaf": "ayaf",
"ẩyr": "âyr", "ẩya": "ảya", "âyra": "ayra", "ảyar": "ayar",
"ẫyx": "âyx", "ẫya": "ãya", "âyxa": "ayxa", "ãyax": "ayax",
"ậyj": "âyj", "ậya": "ạya", "âyja": "ayja", "ạyaj": "ayaj",
"ấngs": "ângs", "ấnga": "ánga", "ângsa": "angsa", "ángas": "angas",
"ầngf": "ângf", "ầnga": "ànga", "ângfa": "angfa", "àngaf": "angaf",
"ẩngr": "ângr", "ẩnga": "ảnga", "ângra": "angra", "ảngar": "angar",
"ẫngx": "ângx", "ẫnga": "ãnga", "ângxa": "angxa", "ãngax": "angax",
"ậngj": "ângj", "ậnga": "ạnga", "ângja": "angja", "ạngaj": "angaj",
"êcs": "ếc", "êcj": "ệc",
"éce": "ếc", "ẹce": "ệc",
"êms": "ếm", "êmf": "ềm", "êmr": "ểm", "êmx": "ễm", "êmj": "ệm",
"éme": "ếm", "ème": "ềm", "ẻme": "ểm", "ẽme": "ễm", "ẹme": "ệm",
"êns": "ến", "ênf": "ền", "ênr": "ển", "ênx": "ễn", "ênj": "ện",
"éne": "ến", "ène": "ền", "ẻne": "ển", "ẽne": "ễn", "ẹne": "ện",
"êps": "ếp", "êpj": "ệp",
"épe": "ếp", "ẹpe": "ệp",
"êts": "ết", "êtj": "ệt",
"éte": "ết", "ẹte": "ệt",
"êchs": "ếch", "êchj": "ệch",
"echs": "éch", "echj": "ẹch",
"éche": "ếch", "ẹche": "ệch",
"ênhs": "ếnh", "ênhf": "ềnh", "ênhr": "ểnh", "ênhx": "ễnh", "ênhj": "ệnh",
"énhe": "ếnh", "ènhe": "ềnh", "ẻnhe": "ểnh", "ẽnhe": "ễnh", "ẹnhe": "ệnh",
"ếms": "êms", "ếme": "éme", "êmse": "emse", "émes": "emes",
"ềmf": "êmf", "ềme": "ème", "êmfe": "emfe", "èmef": "emef",
"ểmr": "êmr", "ểme": "ẻme", "êmre": "emre", "ẻmer": "emer",
"ễmx": "êmx", "ễme": "ẽme", "êmxe": "emxe", "ẽmex": "emex",
"ệmj": "êmj", "ệme": "ẹme", "êmje": "emje", "ẹmej": "emej",
"ếns": "êns", "ếne": "éne", "ênse": "ense", "énes": "enes",
"ềnf": "ênf", "ềne": "ène", "ênfe": "enfe", "ènef": "enef",
"ểnr": "ênr", "ểne": "ẻne", "ênre": "enre", "ẻner": "ener",
"ễnx": "ênx", "ễne": "ẽne", "ênxe": "enxe", "ẽnex": "enex",
"ệnj": "ênj", "ệne": "ẹne", "ênje": "enje", "ẹnej": "enej",
"ếps": "êps", "ếpe": "épe", "êpse": "epse", "épes": "epes",
"ệpj": "êpj", "ệpe": "ẹpe", "êpje": "epje", "ẹpej": "epej",
"ếts": "êts", "ếte": "éte", "êtse": "etes", "étes": "etes",
"ệtj": "êtj", "ệte": "ẹte", "êtje": "etje", "ẹtej": "etej",
"ếchs": "êchs", "ếche": "éche", "êchse": "eches", "éches": "eches",
"ệchj": "êchj", "ệche": "ẹche", "êchje": "echje", "ẹchej": "echej",
"ếnhs": "ênhs", "ếnhe": "énhe", "ênhse": "enhse", "énhes": "enhes",
"ềnhf": "ênhf", "ềnhe": "ènhe", "ênhfe": "enhfe", "ènhef": "enhef",
"ểnhr": "ênhr", "ểnhe": "ẻnhe", "ênhre": "enhre", "ẻnher": "enher",
"ễnhx": "ênhx", "ễnhe": "ẽnhe", "ênhxe": "enhxe", "ẽnhex": "enhex",
"ệnhj": "ênhj", "ệnhe": "ẹnhe", "ênhje": "enhje", "ẹnhej": "enhej",
"ôcs": "ốc", "ôcj": "ộc",
"óco": "ốc", "ọco": "ộc",
"ôis": "ối", "ôif": "ồi", "ôir": "ổi", "ôix": "ỗi", "ôij": "ội",
"óio": "ối", "òio": "ồi", "ỏio": "ổi", "õio": "ỗi", "ọio": "ội",
"ôms": "ốm", "ômf": "ồm", "ômr": "ổm", "ômx": "ỗm", "ômj": "ộm",
"ómo": "ốm", "òmo": "ồm", "ỏmo": "ổm", "õmo": "ỗm", "ọmo": "ộm",
"ôns": "ốn", "ônf": "ồn", "ônr": "ổn", "ônx": "ỗn", "ônj": "ộn",
"óno": "ốn", "òno": "ồn", "ỏno": "ổn", "õno": "ỗn", "ọno": "ộn",
"ôps": "ốp", "ôpj": "ộp",
"ópo": "ốp", "ọpo": "ộp",
"ôts": "ốt", "ôtj": "ột",
"óto": "ốt", "ọto": "ột",
"ôngs": "ống", "ôngf": "ồng", "ôngr": "ổng", "ôngx": "ỗng", "ôngj": "ộng",
"óngo": "ống", "òngo": "ồng", "ỏngo": "ổng", "õngo": "ỗng", "ọngo": "ộng",
"ốcs": "ôcs", "ốco": "óco", "ôcso": "ocso", "ócos": "ocos",
"ộcj": "ôcj", "ộco": "ọco", "ôcjo": "ocjo", "ọcoj": "ocoj",
"ốis": "ôis", "ốio": "óio", "ôiso": "oiso", "óios": "oios",
"ồif": "ôif", "ồio": "òio", "ôifo": "oifo", "òiof": "oiof",
"ổir": "ôir", "ổio": "ỏio", "ôiro": "oiro", "ỏior": "oior",
"ỗix": "ôix", "ỗio": "õio", "ôixo": "oixo", "õiox": "oiox",
"ộij": "ôij", "ộio": "ọio", "ôijo": "oijo", "ọioj": "oioj",
"ốms": "ôms", "ốmo": "ómo", "ômso": "omso", "ómos": "omos",
"ồmf": "ômf", "ồmo": "òmo", "ômfo": "omfo", "òmof": "omof",
"ổmr": "ômr", "ổmo": "ỏmo", "ômro": "omro", "ỏmor": "omor",
"ỗmx": "ômx", "ỗmo": "õmo", "ômxo": "omxo", "õmox": "omox",
"ộmj": "ômj", "ộmo": "ọmo", "ômjo": "omjo", "ọmoj": "omoj",
"ốns": "ôns", "ốno": "óno", "ônso": "onso", "ónos": "onos",
"ồnf": "ônf", "ồno": "òno", "ônfo": "onfo", "ònof": "onof",
"ổnr": "ônr", "ổno": "ỏno", "ônro": "onro", "ỏnor": "onor",
"ỗnx": "ônx", "ỗno": "õno", "ônxo": "onxo", "õnox": "onox",
"ộnj": "ônj", "ộno": "ọno", "ônjo": "onjo", "ọnoj": "onoj",
"ốps": "ôps", "ốpo": "ópo", "ôpso": "opso", "ópos": "opos",
"ộpj": "ôpj", "ộpo": "ọpo", "ôpjo": "opjo", "ọpoj": "opoj",
"ốts": "ôts", "ốto": "óto", "ôtso": "otso", "ótos": "otos",
"ộtj": "ôtj", "ộto": "ọto", "ôtjo": "otjo", "ọtoj": "otoj",
"ốngs": "ôngs", "ốngo": "óngo", "ôngso": "ongso", "óngos": "ongos",
"ồngf": "ôngf", "ồngo": "òngo", "ôngfo": "ongfo", "òngof": "ongof",
"ổngr": "ôngr", "ổngo": "ỏngo", "ôngro": "ongro", "ỏngor": "ongor",
"ỗngx": "ôngx", "ỗngo": "õngo", "ôngxo": "ongxo", "õngox": "ongox",
"ộngj": "ôngj", "ộngo": "ọngo", "ôngjo": "ongjo", "ọngoj": "ongoj",
"ơis": "ới", "ơif": "ời", "ơir": "ởi", "ơix": "ỡi", "ơij": "ợi",
"óiw": "ới", "òiw": "ời", "ỏiw": "ởi", "õiw": "ỡi", "ọiw": "ợi",
"ơms": "ớm", "ơmf": "ờm", "ơmr": "ởm", "ơmx": "ỡm", "ơmj": "ợm",
"ómw": "ớm", "òmw": "ờm", "ỏmw": "ởm", "õmw": "ỡm", "ọmw": "ợm",
"ơns": "ớn", "ơnf": "ờn", "ơnr": "ởn", "ơnx": "ỡn", "ơnj": "ợn",
"ónw": "ớn", "ònw": "ờn", "ỏnw": "ởn", "õnw": "ỡn", "ọnw": "ợn",
"ơps": "ớp", "ơpj": "ợp",
"ópw": "ớp", "ọpw": "ợp",
"ơts": "ớt", "ơtj": "ợt",
"ótw": "ớt", "ọtw": "ợt",
"ớis": "ơis", "ớiw": "óiw", "ơisw": "oisw", "óiws": "oiws",
"ờif": "ơif", "ờiw": "òiw", "ơifw": "oifw", "òiwf": "oiwf",
"ởir": "ơir", "ởiw": "ỏiw", "ơirw": "oirw", "ỏiwr": "oiwr",
"ỡix": "ơix", "ỡiw": "õiw", "ơixw": "oixw", "õiwx": "oiwx",
"ợij": "ơij", "ợiw": "ọiw", "ơijw": "oijw", "ọiwj": "oiwj",
"ớms": "ơms", "ớmw": "ómw", "ơmsw": "omsw", "ómws": "omws",
"ờmf": "ơmf", "ờmw": "òmw", "ơmfw": "omfw", "òmwf": "omwf",
"ởmr": "ơmr", "ởmw": "ỏmw", "ơmrw": "omrw", "ỏmwr": "omwr",
"ỡmx": "ơmx", "ỡmw": "õmw", "ơmxw": "omxw", "õmwx": "omwx",
"ợmj": "ơmj", "ợmw": "ọmw", "ơmjw": "omjw", "ọmwj": "omwj",
"ớns": "ơns", "ớnw": "ónw", "ơnsw": "onsw", "ónws": "onws",
"ờnf": "ơnf", "ờnw": "ònw", "ơnfw": "onfw", "ònwf": "onwf",
"ởnr": "ơnr", "ởnw": "ỏnw", "ơnrw": "onrw", "ỏnwr": "onwr",
"ỡnx": "ơnx", "ỡnw": "õnw", "ơnxw": "onxw", "õnwx": "onwx",
"ợnj": "ơnj", "ợnw": "ọnw", "ơnjw": "onjw", "ọnwj": "onwj",
"ớps": "ơps", "ớpw": "ópw", "ơpsw": "opsw", "ópws": "opws",
"ợpj": "ơpj", "ợpw": "ọpw", "ơpjw": "opjw", "ọpwj": "opwj",
"ớts": "ơts", "ớtw": "ótw", "ơtsw": "otsw", "ótws": "otws",
"ợtj": "ơtj", "ợtw": "ọtw", "ơtjw": "otjw", "ọtwj": "otwj",
"ưas": "ứa", "ưaf": "ừa", "ưar": "ửa", "ưax": "ữa", "ưaj": "ựa",
"úaw": "ứa", "ùaw": "ừa", "ủaw": "ửa", "ũaw": "ữa", "ụaw": "ựa",
"ưis": "ứi", "ưif": "ừi", "ưir": "ửi", "ưix": "ữi", "ưij": "ựi",
"úiw": "ứi", "ùiw": "ừi", "ủiw": "ửi", "ũiw": "ữi", "ụiw": "ựi",
"ưms": "ứm", "ưmf": "ừm", "ưmr": "ửm", "ưmx": "ữm", "ưmj": "ựm",
"úmw": "ứm", "ùmw": "ừm", "ủmw": "ửm", "ũmw": "ữm", "ụmw": "ựm",
"ưns": "ứn", "ưnf": "ừn", "ưnr": "ửn", "ưnx": "ữn", "ưnj": "ựn",
"únw": "ứn", "ùnw": "ừn", "ủnw": "ửn", "ũnw": "ữn", "ụnw": "ựn",
"ưts": "ứt", "ưtj": "ựt",
"útw": "ứt", "ụtw": "ựt",
"ưus": "ứu", "ưuf": "ừu", "ưur": "ửu", "ưux": "ữu", "ưuj": "ựu",
"úuw": "ứu", "ùuw": "ừu", "ủuw": "ửu", "ũuw": "ữu", "ụuw": "ựu",
"ưngs": "ứng", "ưngf": "ừng", "ưngr": "ửng", "ưngx": "ững", "ưngj": "ựng",
"úngw": "ứng", "ùngw": "ừng", "ủngw": "ửng", "ũngw": "ững", "ụngw": "ựng",
"ứas": "ưas", "ứaw": "úaw", "ưasw": "uasw", "úaws": "uaws",
"ừaf": "ưaf", "ừaw": "ùaw", "ưafw": "uafw", "ùawf": "uawf",
"ửar": "ưar", "ửaw": "ủaw", "ưarw": "uarw", "ủawr": "uawr",
"ữax": "ưax", "ữaw": "ũaw", "ưaxw": "uaxw", "ũawx": "uawx",
"ựaj": "ưaj", "ựaw": "ụaw", "ưajw": "uajw", "ụawj": "uawj",
"ứis": "ưis", "ứiw": "úiw", "ưisw": "uisw", "úiws": "uiws",
"ừif": "ưif", "ừiw": "ùiw", "ưifw": "uifw", "ùiwf": "uiwf",
"ửir": "ưir", "ửiw": "ủiw", "ưirw": "uirw", "ủiwr": "uiwr",
"ữix": "ưix", "ữiw": "ũiw", "ưixw": "uixw", "ũiwx": "uiwx",
"ựij": "ưij", "ựiw": "ụiw", "ưijw": "uijw", "ụiwj": "uiwj",
"ứms": "ưms", "ứmw": "úmw", "ưmsw": "umsw", "úmws": "umws",
"ừmf": "ưmf", "ừmw": "ùmw", "ưmfw": "umfw", "ùmwf": "umwf",
"ửmr": "ưmr", "ửmw": "ủmw", "ưmrw": "umrw", "ủmwr": "umwr",
"ữmx": "ưmx", "ữmw": "ũmw", "ưmxw": "umxw", "ũmwx": "umwx",
"ựmj": "ưmj", "ựmw": "ụmw", "ưmjw": "umjw", "ụmwj": "umwj",
"ứns": "ưns", "ứnw": "únw", "ưnsw": "unsw", "únws": "unws",
"ừnf": "ưnf", "ừnw": "ùnw", "ưnfw": "unfw", "ùnwf": "unwf",
"ửnr": "ưnr", "ửnw": "ủnw", "ưnrw": "unrw", "ủnwr": "unwr",
"ữnx": "ưnx", "ữnw": "ũnw", "ưnxw": "unxw", "ũnwx": "unwx",
"ựnj": "ưnj", "ựnw": "ụnw", "ưnjw": "unjw", "ụnwj": "unwj",
"ứts": "ưts", "ứtw": "útw", "ưtsw": "utsw", "útws": "utws",
"ựtj": "ưtj", "ựtw": "ụtw", "ưtjw": "utjw", "ụtwj": "utwj",
"ứus": "ưus", "ứuw": "úuw", "ưusw": "uusw", "úuws": "uuws",
"ừuf": "ưuf", "ừuw": "ùuw", "ưufw": "uufw", "ùuwf": "uuwf",
"ửur": "ưur", "ửuw": "ủuw", "ưurw": "uurw", "ủuwr": "uuwr",
"ữux": "ưux", "ữuw": "ũuw", "ưuxw": "uuxw", "ũuwx": "uuwx",
"ựuj": "ưuj", "ựuw": "ụuw", "ưujw": "uujw", "ụuwj": "uuwj",
"ứngs": "ưngs", "ứngw": "úngw", "ưngsw": "ungsw", "úngws": "ungws",
"ừngf": "ưngf", "ừngw": "ùngw", "ưngfw": "ungfw", "ùngwf": "ungwf",
"ửngr": "ưngr", "ửngw": "ủngw", "ưngrw": "ungrw", "ủngwr": "ungwr",
"ữngx": "ưngx", "ữngw": "ũngw", "ưngxw": "ungxw", "ũngwx": "ungwx",
"ựngj": "ưngj", "ựngw": "ụngw", "ưngjw": "ungjw", "ụngwj": "ungwj",
"iems": "iém", "iemf": "ièm", "iemr": "iẻm", "iemx": "iẽm", "iemj": "iẹm",
"iéme": "iếm", "ième": "iềm", "iẻme": "iểm", "iẽme": "iễm", "iẹme": "iệm",
"iêms": "iếm", "iêmf": "iềm", "iêmr": "iểm", "iêmx": "iễm", "iêmj": "iệm",
"iens": "ién", "ienf": "ièn", "ienr": "iẻn", "ienx": "iẽn", "ienj": "iẹn",
"iéne": "iến", "iène": "iền", "iẻne": "iển", "iẽne": "iễn", "iẹne": "iện",
"iêns": "iến", "iênf": "iền", "iênr": "iển", "iênx": "iễn", "iênj": "iện",
"ieps": "iép", "iepj": "iẹp",
"iépe": "iếp", "iẹpe": "iệp",
"iêps": "iếp", "iêpj": "iệp",
"iets": "iét", "ietj": "iẹt",
"iéte": "iết", "iẹte": "iệt",
"iêts": "iết", "iêtj": "iệt",
"ieus": "iéu", "ieuf": "ièu", "ieur": "iẻu", "ieux": "iẽu", "ieuj": "iẹu",
"iéue": "iếu", "ièue": "iều", "iẻue": "iểu", "iẽue": "iễu", "iẹue": "iệu",
"iêus": "iếu", "iêuf": "iều", "iêur": "iểu", "iêux": "iễu", "iêuj": "iệu",
"iengs": "iéng", "iengf": "ièng", "iengr": "iẻng", "iengx": "iẽng", "iengj": "iẹng",
"iénge": "iếng", "iènge": "iềng", "iẻnge": "iểng", "iẽnge": "iễng", "iẹnge": "iệng",
"iêngs": "iếng", "iêngf": "iềng", "iêngr": "iểng", "iêngx": "iễng", "iêngj": "iệng",
"iếms": "iêms", "iếme": "iéme", "iêmse": "iemse", "iémes": "iemes",
"iềmf": "iêmf", "iềme": "ième", "iêmfe": "iemfe", "ièmef": "iemef",
"iểmr": "iêmr", "iểme": "iẻme", "iêmre": "iemre", "iẻmer": "iemer",
"iễmx": "iêmx", "iễme": "iẽme", "iêmxe": "iemxe", "iẽmex": "iemex",
"iệmj": "iêmj", "iệme": "iẹme", "iêmje": "iemje", "iẹmej": "iemej",
"iếns": "iêns", "iếne": "iéne", "iênse": "iense", "iénes": "ienes",
"iềnf": "iênf", "iềne": "iène", "iênfe": "ienfe", "iènef": "ienef",
"iểnr": "iênr", "iểne": "iẻne", "iênre": "ienre", "iẻner": "iener",
"iễnx": "iênx", "iễne": "iẽne", "iênxe": "ienxe", "iẽnex": "ienex",
"iệnj": "iênj", "iệne": "iẹne", "iênje": "ienje", "iẹnej": "ienej",
"iếps": "iêps", "iếpe": "iépe", "iêpse": "iepse", "iépes": "iepes",
"iệpj": "iêpj", "iệpe": "iẹpe", "iêpje": "iepje", "iẹpej": "iepej",
"iếts": "iêts", "iếte": "iéte", "iêtse": "ietse", "iétes": "ietes",
"iệtj": "iêtj", "iệte": "iẹte", "iêtje": "ietje", "iẹtej": "ietej",
"iếus": "iêus", "iếue": "iéue", "iêuse": "ieuse", "iéues": "ieues",
"iềuf": "iêuf", "iềue": "ièue", "iêufe": "ieufe", "ièuef": "ieuef",
"iểur": "iêur", "iểue": "iẻue", "iêure": "ieure", "iẻuer": "ieuer",
"iễux": "iêux", "iễue": "iẽue", "iêuxe": "ieuxe", "iẽuex": "ieuex",
"iệuj": "iêuj", "iệue": "iẹue", "iêuje": "ieuje", "iẹuej": "ieuej",
"iếngs": "iêngs", "iếnge": "iénge", "iêngse": "iengse", "iénges": "ienges",
"iềngf": "iêngf", "iềnge": "iènge", "iêngfe": "iengfe", "ièngef": "iengef",
"iểngr": "iêngr", "iểnge": "iẻnge", "iêngre": "iengre", "iẻnger": "ienger",
"iễngx": "iêngx", "iễnge": "iẽnge", "iêngxe": "iengxe", "iẽngex": "iengex",
"iệngj": "iêngj", "iệnge": "iẹnge", "iêngje": "iengje", "iẹngej": "iengej",
"uyens": "uyén", "uyenf": "uyèn", "uyenr": "uyẻn", "uyenx": "uyẽn", "uyenj": "uyẹn",
"uyéne": "uyến", "uyène": "uyền", "uyẻne": "uyển", "uyẽne": "uyễn", "uyẹne": "uyện",
"uyêns": "uyến", "uyênf": "uyền", "uyênr": "uyển", "uyênx": "uyễn", "uyênj": "uyện",
"uyets": "uyét", "uyetf": "uyèt", "uyetr": "uyẻt", "uyetx": "uyẽt", "uyetj": "uyẹt",
"uyéte": "uyết", "uyète": "uyềt", "uyẻte": "uyểt", "uyẽte": "uyễt", "uyẹte": "uyệt",
"uyêts": "uyết", "uyêtf": "uyềt", "uyêtr": "uyểt", "uyêtx": "uyễt", "uyêtj": "uyệt",
"uyếns": "uyêns", "uyếne": "uyéne", "uyênse": "uyense", "uyénes": "uyenes",
"uyềnf": "uyênf", "uyềne": "uyène", "uyênfe": "uyenfe", "uyènef": "uyenef",
"uyểnr": "uyênr", "uyểne": "uyẻne", "uyênre": "uyenre", "uyẻner": "uyener",
"uyễnx": "uyênx", "uyễne": "uyẽne", "uyênxe": "uyenxe", "uyẽnex": "uyenex",
"uyệnj": "uyênj", "uyệne": "uyẹne", "uyênje": "uyenje", "uyẹnej": "uyenej",
"uyếts": "uyêts", "uyếte": "uyéte", "uyêtse": "uyetse", "uyétes": "uyetes",
"uyềtf": "uyêtf", "uyềte": "uyète", "uyêtfe": "uyetfe", "uyètef": "uyetef",
"uyểtr": "uyêtr", "uyểte": "uyẻte", "uyêtre": "uyetre", "uyẻter": "uyeter",
"uyễtx": "uyêtx", "uyễte": "uyẽte", "uyêtxe": "uyetxe", "uyẽtex": "uyetex",
"uyệtj": "uyêtj", "uyệte": "uyẹte", "uyêtje": "uyetje", "uyẹtej": "uyetej",
"uyts": "uýt", "uytj": "uỵt",
"uynhs": "uýnh", "uynhf": "uỳnh", "uynhr": "uỷnh", "uynhx": "uỹnh", "uynhj": "uỵnh",
"uýts": "uyts", "uỵtj": "uytj",
"uýnhs": "uynhs", "uỳnhf": "uynhf", "uỷnhr": "uynhr", "uỹnh": "uynhx", "uỵnhj": "uynhj",
"uits": "uít", "uitj": "uịt",
"uíts": "uits", "uịtj": "uitj",
"uos": "úo", "uof": "ùo", "uor": "ủo", "uox": "ũo", "uoj": "ụo",
"úoc": "uóc", "ụoc": "uọc",
"uocs": "uóc", "uocj": "uọc",
"uócw": "ước", "uọcw": "ược",
"ươcs": "ước", "ươcj": "ược",
"úoi": "uói", "ùoi" : "uòi", "ủoi": "uỏi", "ũoi": "uõi", "ụoi": "uọi",
"uois": "uói", "uoif": "uòi", "uoir": "uỏi", "uoix": "uõi", "uoij": "uọi",
"uóiw": "ưới", "uòiw": "ười", "uỏiw": "ưởi", "uõiw": "ưỡi", "uọiw": "ượi",
"ươis": "ưới", "ươif": "ười", "ươir": "ưởi", "ươix": "ưỡi", "ươij": "ượi",
"úom": "uóm", "ùom" : "uòm", "ủom": "uỏm", "ũom": "uõm", "ụom": "uọm",
"uoms": "uóm", "uomf": "uòm", "uomr": "uỏm", "uomx": "uõm", "uomj": "uọm",
"uómw": "ướm", "uòmw": "ườm", "uỏmw": "ưởm", "uõmw": "ưỡm", "uọmw": "ượm",
"ươms": "ướm", "ươmf": "ườm", "ươmr": "ưởm", "ươmx": "ưỡm", "ươmj": "ượm",
"úon": "uón", "ùon" : "uòn", "ủon": "uỏn", "ũon": "uõn", "ụon": "uọn",
"uons": "uón", "uonf": "uòn", "uonr": "uỏn", "uonx": "uõn", "uonj": "uọn",
"uónw": "ướn", "uònw": "ườn", "uỏnw": "ưởn", "uõnw": "ưỡn", "uọnw": "ượn",
"ươns": "ướn", "ươnf": "ườn", "ươnr": "ưởn", "ươnx": "ưỡn", "ươnj": "ượn",
"úop": "uóp", "ụop": "uọp",
"uops": "uóp", "uopj": "uọp",
"uópw": "ướp", "uọpw": "ượp",
"ươps": "ướp", "ươpj": "ượp",
"úot": "uót", "ụot": "uọt",
"uots": "uót", "uotj": "uọt",
"uótw": "ướt", "uọtw": "ượt",
"ươts": "ướt", "ươtj": "ượt",
"úou": "uóu", "ùou" : "uòu", "ủou": "uỏu", "ũou": "uõu", "ụou": "uọu",
"uous": "uóu", "uouf": "uòu", "uour": "uỏu", "uoux": "uõu", "uouj": "uọu",
"uóuw": "ướu", "uòuw": "ườu", "uỏuw": "ưởu", "uõuw": "ưỡu", "uọuw": "ượu",
"ươus": "ướu", "ươuf": "ườu", "ươur": "ưởu", "ươux": "ưỡu", "ươuj": "ượu",
"úong": "uóng", "ùong" : "uòng", "ủong": "uỏng", "ũong": "uõng", "ụong": "uọng",
"uongs": "uóng", "uongf": "uòng", "uongr": "uỏng", "uongx": "uõng", "uongj": "uọng",
"uóngw": "ướng", "uòngw": "ường", "uỏngw": "ưởng", "uõngw": "ưỡng", "uọngw": "ượng",
"ươngs": "ướng", "ươngf": "ường", "ươngr": "ưởng", "ươngx": "ưỡng", "ươngj": "ượng",
"úos": "uos", "ùof": "uof", "ủor": "uor", "ũo": "uox", "ụoj": "uoj",
"uócs": "uocs", "uọcj": "uocj",
"ướcs": "ươcs", "ướcw": "uócw", "ươcsw": "uocsw", "uócws": "uocws",
"ượcj": "ươcj", "ượcw": "uọcw", "ươcjw": "uocjw", "uọcwj": "uocwj",
"uóis": "uois", "uòif": "uoif", "uỏir": "uoir", "uõix": "uoix", "uọij": "uoij",
"ướis": "ươis", "ướiw": "uóiw", "ươisw": "uoisw", "uóiws": "uoiws",
"ườif": "ươif", "ườiw": "uòiw", "ươifw": "uoifw", "uòiwf": "uoiwf",
"ưởir": "ươir", "ưởiw": "uỏiw", "ươirw": "uoirw", "uỏiwr": "uoiwr",
"ưỡix": "ươix", "ưỡiw": "uõiw", "ươixw": "uoixw", "uõiwx": "uoiwx",
"ượij": "ươij", "ượiw": "uóiw", "ươijw": "uoijw", "uọiwj": "uoiwj",
"uóms": "uoms", "uòmf": "uomf", "uỏmr": "uomr", "uõmx": "uomx", "uọmj": "uomj",
"ướms": "ươms", "ướmw": "uómw", "ươmsw": "uomsw", "uómws": "uomws",
"ườmf": "ươmf", "ườmw": "uòmw", "ươmfw": "uomfw", "uòmwf": "uomwf",
"ưởmr": "ươmr", "ưởmw": "uỏmw", "ươmrw": "uomrw", "uỏmwr": "uomwr",
"ưỡmx": "ươmx", "ưỡmw": "uõmw", "ươmxw": "uomxw", "uõmwx": "uomwx",
"ượmj": "ươmj", "ượmw": "uómw", "ươmjw": "uomjw", "uọmwj": "uomwj",
"uóns": "uons", "uònf": "uonf", "uỏnr": "uonr", "uõnx": "uonx", "uọnj": "uonj",
"ướns": "ươns", "ướnw": "uónw", "ươnsw": "uonsw", "uónws": "uonws",
"ườnf": "ươnf", "ườnw": "uònw", "ươnfw": "uonfw", "uònwf": "uonwf",
"ưởnr": "ươnr", "ưởnw": "uỏnw", "ươnrw": "uonrw", "uỏnwr": "uonwr",
"ưỡnx": "ươnx", "ưỡnw": "uõnw", "ươnxw": "uonxw", "uõnwx": "uonwx",
"ượnj": "ươnj", "ượnw": "uónw", "ươnjw": "uonjw", "uọnwj": "uonwj",
"uóps": "uops", "uọpj": "uopj",
"ướps": "ươps", "ướpw": "uópw", "ươpsw": "uopsw", "uópws": "uopws",
"ượpj": "ươpj", "ượpw": "uọpw", "ươpjw": "uopjw", "uọpwj": "uopwj",
"uóts": "uots", "uọtj": "uotj",
"ướts": "ươts", "ướtw": "uótw", "ươtsw": "uotsw", "uótws": "uotws",
"ượtj": "ươtj", "ượtw": "uọtw", "ươtjw": "uotjw", "uọtwj": "uotwj",
"uóus": "uous", "uòuf": "uouf", "uỏur": "uour", "uõux": "uoux", "uọuj": "uouj",
"ướus": "ươus", "ướuw": "uóuw", "ươusw": "uousw", "uóuws": "uouws",
"ườuf": "ươuf", "ườuw": "uòuw", "ươufw": "uoufw", "uòuwf": "uouwf",
"ưởur": "ươur", "ưởuw": "uỏuw", "ươurw": "uourw", "uỏuwr": "uouwr",
"ưỡux": "ươux", "ưỡuw": "uõuw", "ươuxw": "uouxw", "uõuwx": "uouwx",
"ượuj": "ươuj", "ượuw": "uóuw", "ươujw": "uoujw", "uọuwj": "uouwj",
"uóngs": "uongs", "uòngf": "uongf", "uỏngr": "uongr", "uõngx": "uongx", "uọngj": "uongj",
"ướngs": "ươngs", "ướngw": "uóngw", "ươngsw": "uongsw", "uóngws": "uongws",
"ườngf": "ươngf", "ườngw": "uòngw", "ươngfw": "uongfw", "uòngwf": "uongwf",
"ưởngr": "ươngr", "ưởngw": "uỏngw", "ươngrw": "uongrw", "uỏngwr": "uongwr",
"ưỡngx": "ươngx", "ưỡngw": "uõngw", "ươngxw": "uongxw", "uõngwx": "uongwx",
"ượngj": "ươngj", "ượngw": "uóngw", "ươngjw": "uongjw", "uọngwj": "uongwj",
"uóco": "uốc", "uọco": "uộc",
"uôcs": "uốc", "uôcj": "uộc",
"uóio": "uối", "uòio": "uồi", "uỏio": "uổi", "uõio": "uỗi", "uọio": "uội",
"uôis": "uối", "uôif": "uồi", "uôir": "uổi", "uôix": "uỗi", "uôij": "uội",
"uómo": "uốm", "uòmo": "uồm", "uỏmo": "uổm", "uõmo": "uỗm", "uọmo": "uộm",
"uôms": "uốm", "uômf": "uồm", "uômr": "uổm", "uômx": "uỗm", "uômj": "uộm",
"uóno": "uốn", "uòno": "uồn", "uỏno": "uổn", "uõno": "uỗn", "uọno": "uộn",
"uôns": "uốn", "uônf": "uồn", "uônr": "uổn", "uônx": "uỗn", "uônj": "uộn",
"uópo": "uốp", "uọpo": "uộp",
"uôps": "uốp", "uôpj": "uộp",
"uóto": "uốt", "uọto": "uột",
"uôts": "uốt", "uôtj": "uột",
"uóuo": "uốu", "uòuo": "uồu", "uỏuo": "uổu", "uõuo": "uỗu", "uọuo": "uộu",
"uôus": "uốu", "uôuf": "uồu", "uôur": "uổu", "uôux": "uỗu", "uôuj": "uộu",
"uóngo": "uống", "uòngo": "uồng", "uỏngo": "uổng", "uõngo": "uỗng", "uọngo": "uộng",
"uôngs": "uống", "uôngf": "uồng", "uôngr": "uổng", "uôngx": "uỗng", "uôngj": "uộng",
"uốcs": "uôcs", "uốco": "uóco", "uôcso": "uocso", "uócos": "uocos",
"uộcj": "uôcj", "uộco": "uọco", "uôcjo": "uocjo", "uọcoj": "uocoj",
"uốis": "uôis", "uốio": "uóio", "uôiso": "uoiso", "uóios": "uoios",
"uồif": "uôif", "uồio": "uòio", "uôifo": "uoifo", "uòiof": "uoiof",
"uổir": "uôir", "uổio": "uỏio", "uôiro": "uoiro", "uỏior": "uoior",
"uỗix": "uôix", "uỗio": "uõio", "uôixo": "uoixo", "uõiox": "uoiox",
"uộij": "uôij", "uộio": "uóio", "uôijo": "uoijo", "uọioj": "uoioj",
"uốms": "uôms", "uốmo": "uómo", "uômso": "uomso", "uómos": "uomos",
"uồmf": "uômf", "uồmo": "uòmo", "uômfo": "uomfo", "uòmof": "uomof",
"uổmr": "uômr", "uổmo": "uỏmo", "uômro": "uomro", "uỏmor": "uomor",
"uỗmx": "uômx", "uỗmo": "uõmo", "uômxo": "uomxo", "uõmox": "uomox",
"uộmj": "uômj", "uộmo": "uómo", "uômjo": "uomjo", "uọmoj": "uomoj",
"uốns": "uôns", "uốno": "uóno", "uônso": "uonso", "uónos": "uonos",
"uồnf": "uônf", "uồno": "uòno", "uônfo": "uonfo", "uònof": "uonof",
"uổnr": "uônr", "uổno": "uỏno", "uônro": "uonro", "uỏnor": "uonor",
"uỗnx": "uônx", "uỗno": "uõno", "uônxo": "uonxo", "uõnox": "uonox",
"uộnj": "uônj", "uộno": "uóno", "uônjo": "uonjo", "uọnoj": "uonoj",
"uốps": "uôps", "uốpo": "uópo", "uôpso": "uopso", "uópos": "uopos",
"uộpj": "uôpj", "uộpo": "uọpo", "uôpjo": "uopjo", "uọpoj": "uopoj",
"uốts": "uôts", "uốto": "uóto", "uôtso": "uotso", "uótos": "uotos",
"uộtj": "uôtj", "uộto": "uọto", "uôtjo": "uotjo", "uọtoj": "uotoj",
"uốus": "uôus", "uốuo": "uóuo", "uôuso": "uouso", "uóuos": "uouos",
"uồuf": "uôuf", "uồuo": "uòuo", "uôufo": "uoufo", "uòuof": "uouof",
"uổur": "uôur", "uổuo": "uỏuo", "uôuro": "uouro", "uỏuor": "uouor",
"uỗux": "uôux", "uỗuo": "uõuo", "uôuxo": "uouxo", "uõuox": "uouox",
"uộuj": "uôuj", "uộuo": "uóuo", "uôujo": "uoujo", "uọuoj": "uouoj",
"uốngs": "uôngs", "uốngo": "uóngo", "uôngso": "uongso", "uóngos": "uongos",
"uồngf": "uôngf", "uồngo": "uòngo", "uôngfo": "uongfo", "uòngof": "uongof",
"uổngr": "uôngr", "uổngo": "uỏngo", "uôngro": "uongro", "uỏngor": "uongor",
"uỗngx": "uôngx", "uỗngo": "uõngo", "uôngxo": "uongxo", "uõngox": "uongox",
"uộngj": "uôngj", "uộngo": "uóngo", "uôngjo": "uongjo", "uọngoj": "uongoj",
"yes": "ýe", "yef": "ỳe", "yer": "ỷe", "yex": "ỹe", "yej": "ỵe",
"ýem": "yém", "ỳem": "yèm", "ỷem": "yẻm", "ỹem": "yẽm", "ỵem": "yẹm",
"yems": "yém", "yemf": "yèm", "yemr": "yẻm", "yemx": "yẽm", "yemj": "yẹm",
"yêms": "yếm", "yêmf": "yềm", "yêmr": "yểm", "yêmx": "yễm", "yêmj": "yệm",
"yéme": "yếm", "yème": "yềm", "yẻme": "yểm", "yẽme": "yễm", "yẹme": "yệm",
"ýen": "yén", "ỳen": "yèn", "ỷen": "yẻn", "ỹen": "yẽn", "ỵen": "yẹn",
"yens": "yén", "yenf": "yèn", "yenr": "yẻn", "yenx": "yẽn", "yenj": "yẹn",
"yêns": "yến", "yênf": "yền", "yênr": "yển", "yênx": "yễn", "yênj": "yện",
"yéne": "yến", "yène": "yền", "yẻne": "yển", "yẽne": "yễn", "yẹne": "yện",
"ýet": "yét", "ỵet": "yẹt",
"yets": "yét", "yetj": "yẹt",
"yêts": "yết", "yêtj": "yệt",
"yéte": "yết", "yẹte": "yệt",
"ýeu": "yéu", "ỳeu": "yèu", "ỷeu": "yẻu", "ỹeu": "yẽu", "ỵeu": "yẹu",
"yeus": "yéu", "yeuf": "yèu", "yeur": "yẻu", "yeux": "yẽu", "yeuj": "yẹu",
"yêus": "yếu", "yêuf": "yều", "yêur": "yểu", "yêux": "yễu", "yêuj": "yệu",
"yéue": "yếu", "yèue": "yều", "yẻue": "yểu", "yẽue": "yễu", "yẹue": "yệu",
"ýeng": "yéng", "ỳeng": "yèng", "ỷeng": "yẻng", "ỹeng": "yẽng", "ỵeng": "yẹng",
"yengs": "yéng", "yengf": "yèng", "yengr": "yẻng", "yengx": "yẽng", "yengj": "yẹng",
"yêngs": "yếng", "yêngf": "yềng", "yêngr": "yểng", "yêngx": "yễng", "yêngj": "yệng",
"yénge": "yếng", "yènge": "yềng", "yẻnge": "yểng", "yẽnge": "yễng", "yẹnge": "yệng",
"ýes": "yes", "ỳef": "yef", "ỷer": "yer", "ỹex": "yex", "ỵej": "yej",
"yếms": "yêms", "yếme": "yéme", "yêmse": "yemse", "yémes": "yemes",
"yềmf": "yêmf", "yềme": "yème", "yêmfe": "yemfe", "yèmef": "yemef",
"yểmr": "yêmr", "yểme": "yẻme", "yêmre": "yemre", "yẻmer": "yemer",
"yễmx": "yêmx", "yễme": "yẽme", "yêmxe": "yemxe", "yẽmex": "yemex",
"yệmj": "yêmj", "yệme": "yẹme", "yêmje": "yemje", "yẹmej": "yemej",
"yếns": "yêns", "yếne": "yéne", "yênse": "yense", "yénes": "yenes",
"yềnf": "yênf", "yềne": "yène", "yênfe": "yenfe", "yènef": "yenef",
"yểnr": "yênr", "yểne": "yẻne", "yênre": "yenre", "yẻner": "yener",
"yễnx": "yênx", "yễne": "yẽne", "yênxe": "yenxe", "yẽnex": "yenex",
"yệnj": "yênj", "yệne": "yẹne", "yênje": "yenje", "yẹnej": "yenej",
"yếts": "yêts", "yếte": "yéte", "yêtse": "yetse", "yétes": "yetes",
"yệtj": "yêtj", "yệte": "yẹte", "yêtje": "yetje", "yẹtej": "yetej",
"yếus": "yêus", "yếue": "yéue", "yêuse": "yeuse", "yéues": "yeues",
"yềuf": "yêuf", "yềue": "yèue", "yêufe": "yeufe", "yèuef": "yeuef",
"yểur": "yêur", "yểue": "yẻue", "yêure": "yeure", "yẻuer": "yeuer",
"yễux": "yêux", "yễue": "yẽue", "yêuxe": "yeuxe", "yẽuex": "yeuex",
"yệuj": "yêuj", "yệue": "yẹue", "yêuje": "yeuje", "yẹuej": "yeuej",
"yếngs": "yêngs", "yếnge": "yénge", "yêngse": "yengse", "yénges": "yenges",
"yềngf": "yêngf", "yềnge": "yènge", "yêngfe": "yengfe", "yèngef": "yengef",
"yểngr": "yêngr", "yểnge": "yẻnge", "yêngre": "yengre", "yẻnger": "yenger",
"yễngx": "yêngx", "yễnge": "yẽnge", "yêngxe": "yengxe", "yẽngex": "yengex",
"yệngj": "yêngj", "yệnge": "yẹnge", "yêngje": "yengje", "yẹngej": "yengej"
}
}
]

View File

@@ -45,6 +45,18 @@
{ "code": 165, "label": "¥" }
]
},
{
"id": "costa_rican_colon",
"label": "Costa Rican colón (₡)",
"slots": [
{ "code": 8353, "label": "₡" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"id": "dollar",
"label": "Dollar ($)",

View File

@@ -111,6 +111,13 @@
"direction": "ltr",
"modifier": "org.florisboard.layouts:dvorak"
},
{
"id": "dvorak_es",
"label": "Dvorak (ÑÇ)",
"authors": [ "tsiflimagas" ],
"direction": "ltr",
"modifier": "org.florisboard.layouts:dvorak"
},
{
"id": "esperanto",
"label": "Esperanto",
@@ -178,6 +185,12 @@
"authors": [ "nd500" ],
"direction": "ltr"
},
{
"id": "indonesian",
"label": "Indonesian (QWERTY)",
"authors": [ "Linerly" ],
"direction": "ltr"
},
{
"id": "ipa",
"label": "International Phonetic Alphabet",
@@ -186,13 +199,13 @@
},
{
"id": "jcuken_russian",
"label": "Russian (JCUKEN)",
"label": "Russian (ЙЦУКЕН)",
"authors": [ "williamtheaker" ],
"direction": "ltr"
},
{
"id": "jcuken_ukrainian",
"label": "Ukrainian (JCUKEN)",
"label": "Ukrainian (ЙЦУКЕН)",
"authors": [ "williamtheaker", "33kk" ],
"direction": "ltr"
},
@@ -262,6 +275,13 @@
"direction": "rtl",
"modifier": "org.florisboard.layouts:persian2"
},
{
"id": "persian3",
"label": "Persian3",
"authors": [ "SaeID-Rz" ],
"direction": "rtl",
"modifier": "org.florisboard.layouts:persian3"
},
{
"id": "qwerty",
"label": "QWERTY",
@@ -438,6 +458,12 @@
"label": "Persian2",
"authors": [ "M-Koushan" ],
"direction": "rtl"
},
{
"id": "persian3",
"label": "Persian3",
"authors": [ "SaeID-Rz" ],
"direction": "rtl"
}
],
"extension": [

View File

@@ -1,47 +1,47 @@
[
[
{ "$": "variation_selector",
"default": { "$": "case_selector",
"lower": { "code": 39, "label": "'", "popup": {
{ "$": "shift_state_selector",
"shiftedManual": { "code": 34, "label": "\"", "popup": {
"relevant": [
{ "code": 33, "label": "!" },
{ "code": 39, "label": "'"}
]
} },
"default": { "$": "variation_selector",
"email": { "code": 64, "label": "@" },
"uri": { "code": 47, "label": "/" },
"default": { "code": 39, "label": "'", "popup": {
"relevant": [
{ "code": 33, "label": "!" },
{ "code": 34, "label": "\"" }
]
} },
"upper": { "code": 34, "label": "\"", "popup": {
"relevant": [
{ "code": 33, "label": "!" },
{ "code": 39, "label": "'"}
]
} }
},
"email": { "code": 64, "label": "@" },
"uri": { "code": 47, "label": "/" }
}
},
{ "$": "case_selector",
"lower": { "code": 44, "label": ",", "popup": {
"relevant": [
{ "code": 60, "label": "<" },
{ "code": 63, "label": "?" }
]
} },
"upper": { "code": 60, "label": "<", "popup": {
{ "$": "shift_state_selector",
"shiftedManual": { "code": 60, "label": "<", "popup": {
"relevant": [
{ "code": 44, "label": "," },
{ "code": 63, "label": "?" }
]
} },
"default": { "code": 44, "label": ",", "popup": {
"relevant": [
{ "code": 60, "label": "<" },
{ "code": 63, "label": "?" }
]
} }
},
{ "$": "case_selector",
"lower": { "code": 46, "label": ".", "popup": {
"relevant": [
{ "code": 62, "label": ">" }
]
} },
"upper": { "code": 62, "label": ">", "popup": {
{ "$": "shift_state_selector",
"shiftedManual": { "code": 62, "label": ">", "popup": {
"relevant": [
{ "code": 46, "label": "." }
]
} },
"default": { "code": 46, "label": ".", "popup": {
"relevant": [
{ "code": 62, "label": ">" }
]
} }
},
{ "$": "auto_text_key", "code": 112, "label": "p" },

View File

@@ -0,0 +1,78 @@
[
[
{ "$": "shift_state_selector",
"shiftedManual": { "code": 60, "label": "<", "popup": {
"relevant": [
{ "code": 44, "label": "," },
{ "code": 63, "label": "?" }
]
} },
"default": { "code": 44, "label": ",", "popup": {
"relevant": [
{ "code": 60, "label": "<" },
{ "code": 63, "label": "?" }
]
} }
},
{ "$": "shift_state_selector",
"shiftedManual": { "code": 62, "label": ">", "popup": {
"relevant": [
{ "code": 46, "label": "." }
]
} },
"default": { "code": 46, "label": ".", "popup": {
"relevant": [
{ "code": 62, "label": ">" }
]
} }
},
{ "$": "auto_text_key", "code": 241, "label": "ñ" },
{ "$": "auto_text_key", "code": 112, "label": "p" },
{ "$": "auto_text_key", "code": 121, "label": "y" },
{ "$": "auto_text_key", "code": 102, "label": "f" },
{ "$": "auto_text_key", "code": 103, "label": "g" },
{ "$": "auto_text_key", "code": 99, "label": "c" },
{ "$": "auto_text_key", "code": 104, "label": "h" },
{ "$": "auto_text_key", "code": 108, "label": "l" }
],
[
{ "$": "auto_text_key", "code": 97, "label": "a" },
{ "$": "auto_text_key", "code": 111, "label": "o" },
{ "$": "auto_text_key", "code": 101, "label": "e" },
{ "$": "auto_text_key", "code": 117, "label": "u" },
{ "$": "auto_text_key", "code": 105, "label": "i" },
{ "$": "auto_text_key", "code": 100, "label": "d" },
{ "$": "auto_text_key", "code": 114, "label": "r" },
{ "$": "auto_text_key", "code": 116, "label": "t" },
{ "$": "auto_text_key", "code": 110, "label": "n" },
{ "$": "auto_text_key", "code": 115, "label": "s" },
{ "$": "shift_state_selector",
"shiftedManual": { "code": 34, "label": "\"", "popup": {
"relevant": [
{ "code": 33, "label": "!" },
{ "code": 39, "label": "'"}
]
} },
"default": { "$": "variation_selector",
"email": { "code": 64, "label": "@" },
"uri": { "code": 47, "label": "/" },
"default": { "code": 39, "label": "'", "popup": {
"relevant": [
{ "code": 33, "label": "!" },
{ "code": 34, "label": "\"" }
]
} }
}
},
{ "$": "auto_text_key", "code": 231, "label": "ç" }
],
[
{ "$": "auto_text_key", "code": 106, "label": "j" },
{ "$": "auto_text_key", "code": 107, "label": "k" },
{ "$": "auto_text_key", "code": 120, "label": "x" },
{ "$": "auto_text_key", "code": 98, "label": "b" },
{ "$": "auto_text_key", "code": 109, "label": "m" },
{ "$": "auto_text_key", "code": 119, "label": "w" },
{ "$": "auto_text_key", "code": 118, "label": "v" }
]
]

View File

@@ -48,7 +48,9 @@
],
[
{ "code": 1600, "label": "kashida", "popup":
{ "main": { "code": 8204, "label": "half_space" }
} },
{ "code": 1574, "label": "ﺋ", "popup": {
"main": { "code": 1569, "label": "ء" }
} },
@@ -58,9 +60,14 @@
} },
{ "code": 1688, "label": "ژ" },
{ "code": 1605, "label": "م" },
{ "code": 1567, "label": "؟" },
{ "code": 1548, "label": "،" },
{ "code": 58, "label": ":" }
{ "code": 1567, "label": "؟", "popup": {
"main": { "code": 63, "label": "?" }
} },
{ "code": 1548, "label": "،", "popup": {
"main": { "code": 1643, "label": "٫" }
} },
{ "code": 58, "label": ":", "popup": {
"main": { "code": 1563, "label": "؛" }
} }
]
]

View File

@@ -0,0 +1,39 @@
[
[
{ "code": 1590, "label": "ض" },
{ "code": 1589, "label": "ص" },
{ "code": 1579, "label": "ث" },
{ "code": 1602, "label": "ق" },
{ "code": 1601, "label": "ف" },
{ "code": 1594, "label": "غ" },
{ "code": 1593, "label": "ع" },
{ "code": 1607, "label": "ه" },
{ "code": 1582, "label": "خ" },
{ "code": 1581, "label": "ح" },
{ "code": 1580, "label": "ج" }
],
[
{ "code": 1588, "label": "ش" },
{ "code": 1587, "label": "س" },
{ "code": 1740, "label": "ی" },
{ "code": 1576, "label": "ب" },
{ "code": 1604, "label": "ل" },
{ "code": 1575, "label": "ا" },
{ "code": 1578, "label": "ت" },
{ "code": 1606, "label": "ن" },
{ "code": 1605, "label": "م" },
{ "code": 1705, "label": "ک" },
{ "code": 1711, "label": "گ" }
],
[
{ "code": 1592, "label": "ظ" },
{ "code": 1591, "label": "ط" },
{ "code": 1586, "label": "ز" },
{ "code": 1585, "label": "ر" },
{ "code": 1584, "label": "ذ" },
{ "code": 1583, "label": "د" },
{ "code": 1662, "label": "پ" },
{ "code": 1608, "label": "و" },
{ "code": 1670, "label": "چ" }
]
]

View File

@@ -1,8 +1,5 @@
[
[
{ "code": 1600, "label": "kashida", "popup":
{ "main": { "code": 8204, "label": "half_space" }
} },
{ "code": 0, "type": "placeholder" },
{ "code": -7, "label": "delete", "type": "enter_editing" }
],

View File

@@ -0,0 +1,19 @@
[
[
{ "code": 0, "type": "placeholder" },
{ "code": -7, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "$": "variation_selector",
"default": { "code": 1548, "label": "،", "groupId": 1 },
"email": { "code": 64, "label": "@", "groupId": 1 },
"uri": { "code": 47, "label": "/", "groupId": 1 }
},
{ "code": -227, "label": "language_switch", "type": "system_gui" },
{ "code": -212, "label": "ime_ui_mode_media", "type": "system_gui" },
{ "code": 32, "label": "space" },
{ "code": 46, "label": ".", "groupId": 2 },
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]

View File

@@ -1,68 +1,200 @@
[
[
{ "code": 49, "label": "1", "type": "numeric", "popup": {
"main": { "code": 185, "label": "¹" },
"relevant": [
{ "code": 8537, "label": "" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "" }
]
} },
{ "code": 50, "label": "2", "type": "numeric", "popup": {
"main": { "code": 178, "label": "²" },
"relevant": [
{ "code": 8532, "label": "" },
{ "code": 8534, "label": "" }
]
} },
{ "code": 51, "label": "3", "type": "numeric", "popup": {
"main": { "code": 179, "label": "³" },
"relevant": [
{ "code": 8535, "label": "" },
{ "code": 190, "label": "¾" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 52, "label": "4", "type": "numeric", "popup": {
"main": { "code": 8308, "label": "" },
"relevant": [
{ "code": 8536, "label": "⅘" }
]
} },
{ "code": 53, "label": "5", "type": "numeric", "popup": {
"main": { "code": 8309, "label": "⁵" },
"relevant": [
{ "code": 8538, "label": "" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 54, "label": "6", "type": "numeric", "popup": {
"main": { "code": 8310, "label": "" }
} },
{ "code": 55, "label": "7", "type": "numeric", "popup": {
"main": { "code": 8311, "label": "" },
"relevant": [
{ "code": 8542, "label": "⅞" }
]
} },
{ "code": 56, "label": "8", "type": "numeric", "popup": {
"main": { "code": 8312, "label": "⁸" }
} },
{ "code": 57, "label": "9", "type": "numeric", "popup": {
"main": { "code": 8313, "label": "" }
} },
{ "code": 48, "label": "0", "type": "numeric", "popup": {
"main": { "code": 8304, "label": "" },
"relevant": [
{ "code": 8709, "label": "" },
{ "code": 8319, "label": "ⁿ" }
]
} }
{ "$": "shift_state_selector",
"shiftedManual": {
"code": 33, "label": "!", "type": "numeric", "popup": {
"main": { "code": 161, "label": "¡" }
}
},
"default": {
"code": 49, "label": "1", "type": "numeric", "popup": {
"main": { "code": 185, "label": "¹" },
"relevant": [
{ "code": 8537, "label": "" },
{ "code": 8528, "label": "" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "" }
]
}
}
},
{ "$": "shift_state_selector",
"shiftedManual": { "code": 64, "label": "@", "type": "numeric" },
"default": {
"code": 50, "label": "2", "type": "numeric", "popup": {
"main": { "code": 178, "label": "²" },
"relevant": [
{ "code": 8532, "label": "" },
{ "code": 8534, "label": "" }
]
}
}
},
{ "$": "shift_state_selector",
"shiftedManual": {
"code": 35, "label": "#", "type": "numeric", "popup": {
"main": { "code": 8470, "label": "" }
}
},
"default": {
"code": 51, "label": "3", "type": "numeric", "popup": {
"main": { "code": 179, "label": "³" },
"relevant": [
{ "code": 8535, "label": "" },
{ "code": 190, "label": "¾" },
{ "code": 8540, "label": "⅜" }
]
}
}
},
{ "$": "shift_state_selector",
"shiftedManual": {
"code": -801, "label": "currency_slot_1", "type": "numeric", "popup": {
"main": { "code": -802, "label": "currency_slot_2" },
"relevant": [
{ "code": -806, "label": "currency_slot_6" },
{ "code": -803, "label": "currency_slot_3" },
{ "code": -804, "label": "currency_slot_4" },
{ "code": -805, "label": "currency_slot_5" }
]
}
},
"default": {
"code": 52, "label": "4", "type": "numeric", "popup": {
"main": { "code": 8308, "label": "⁴" },
"relevant": [
{ "code": 8536, "label": "⅘" }
]
}
}
},
{ "$": "shift_state_selector",
"shiftedManual": {
"code": 37, "label": "%", "type": "numeric", "popup": {
"main": { "code": 8240, "label": "‰" },
"relevant": [
{ "code": 8453, "label": "℅" }
]
}
},
"default": {
"code": 53, "label": "5", "type": "numeric", "popup": {
"main": { "code": 8309, "label": "⁵" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8541, "label": "⅝" }
]
}
}
},
{ "$": "shift_state_selector",
"shiftedManual": {
"code": 94, "label": "^", "type": "numeric", "popup": {
"main": { "code": 8593, "label": "↑" },
"relevant": [
{ "code": 8592, "label": "←" },
{ "code": 8595, "label": "↓" },
{ "code": 8594, "label": "→" }
]
}
},
"default": {
"code": 54, "label": "6", "type": "numeric", "popup": {
"main": { "code": 8310, "label": "⁶" }
}
}
},
{ "$": "shift_state_selector",
"shiftedManual": { "code": 38, "label": "&", "type": "numeric" },
"default": {
"code": 55, "label": "7", "type": "numeric", "popup": {
"main": { "code": 8311, "label": "⁷" },
"relevant": [
{ "code": 8542, "label": "⅞" }
]
}
}
},
{ "$": "shift_state_selector",
"shiftedManual": {
"code": 42, "label": "*", "type": "numeric", "popup": {
"main": { "code": 8224, "label": "†" },
"relevant": [
{ "code": 9733, "label": "★" },
{ "code": 8225, "label": "‡" }
]
}
},
"default": {
"code": 56, "label": "8", "type": "numeric", "popup": {
"main": { "code": 8312, "label": "⁸" }
}
}
},
{ "$": "shift_state_selector",
"shiftedManual": {
"$": "layout_direction_selector",
"ltr": {
"code": 40, "label": "(", "type": "numeric", "popup": {
"main": { "code": 60, "label": "<" },
"relevant": [
{ "code": 91, "label": "[" },
{ "code": 123, "label": "{" }
]
}
},
"rtl": {
"code": 41, "label": "(", "type": "numeric", "popup": {
"main": { "code": 62, "label": "<" },
"relevant": [
{ "code": 93, "label": "[" },
{ "code": 125, "label": "{" }
]
}
}
},
"default": {
"code": 57, "label": "9", "type": "numeric", "popup": {
"main": { "code": 8313, "label": "⁹" }
}
}
},
{ "$": "shift_state_selector",
"shiftedManual": {
"$": "layout_direction_selector",
"ltr": {
"code": 41, "label": ")", "type": "numeric", "popup": {
"main": { "code": 62, "label": ">" },
"relevant": [
{ "code": 93, "label": "]" },
{ "code": 125, "label": "}" }
]
}
},
"rtl": {
"code": 40, "label": ")", "type": "numeric", "popup": {
"main": { "code": 60, "label": ">" },
"relevant": [
{ "code": 91, "label": "]" },
{ "code": 123, "label": "}" }
]
}
}
},
"default": {
"code": 48, "label": "0", "type": "numeric", "popup": {
"main": { "code": 8304, "label": "⁰" },
"relevant": [
{ "code": 8709, "label": "∅" },
{ "code": 8319, "label": "ⁿ" }
]
}
}
}
]
]

View File

@@ -2,9 +2,9 @@
"$": "ime.extension.keyboard",
"meta": {
"id": "org.florisboard.localization",
"version": "0.1.0",
"version": "0.2.0",
"title": "Default subtype presets / popup mappings",
"description": "Default subptye presets and popup mappings which are always available.",
"description": "Default subtype presets and popup mappings which are always available.",
"maintainers": [ "patrickgold <patrick@patrickgold.dev>" ],
"license": "apache-2.0"
},
@@ -13,188 +13,66 @@
"org.florisboard.currencysets",
"org.florisboard.layouts"
],
"popupMappings": [
"punctuationRules": [
{
"id": "default",
"authors": [ "patrickgold" ]
},
{
"id": "ar",
"authors": [ "HeiWiper" ]
},
{
"id": "bg",
"authors": [ "iorvethe" ]
},
{
"id": "ca",
"authors": [ "mikelloc" ]
},
{
"id": "ckb",
"authors": [ "GoRaN" ]
},
{
"id": "cs",
"authors": [ "stefan-misik" ]
},
{
"id": "da",
"authors": [ "patrickgold" ]
},
{
"id": "de",
"authors": [ "patrickgold" ]
},
{
"id": "de-DE-neobone",
"authors": [ "ostrya" ]
},
{
"id": "el",
"authors": [ "tsiflimagas" ]
},
{
"id": "en",
"authors": [ "patrickgold" ]
},
{
"id": "eo",
"authors": [ "jeremiah-miller" ]
},
{
"id": "es",
"authors": [ "patrickgold" ]
},
{
"id": "fa",
"authors": [ "PHELAT" ]
},
{
"id": "fa2",
"authors": [ "M-Koushan" ]
},
{
"id": "fi",
"authors": [ "patrickgold" ]
},
{
"id": "fo",
"authors": [ "BinFlush" ]
},
{
"id": "fr",
"authors": [ "patrickgold" ]
},
{
"id": "hr",
"authors": [ "hedidnothingwrong" ]
},
{
"id": "hu",
"authors": [ "zoli111, gabik65" ]
},
{
"id": "hy",
"authors": [ "PJTSearch" ]
},
{
"id": "is",
"authors": [ "patrickgold" ]
},
{
"id": "it",
"authors": [ "patrickgold" ]
},
{
"id": "iw",
"authors": [ "Antony" ]
},
{
"id": "ja-JP-jis",
"authors": [ "waelwindows" ]
},
{
"id": "ko",
"authors": [ "patrickgold", "Hayleia" ]
},
{
"id": "ku",
"authors": [ "GoRaN" ]
},
{
"id": "lt",
"authors": [ "patrickgold" ]
},
{
"id": "lv",
"authors": [ "patrickgold", "eandersons" ]
},
{
"id": "nb",
"authors": [ "patrickgold" ]
},
{
"id": "nn",
"authors": [ "patrickgold" ]
},
{
"id": "pl",
"authors": [ "Mikołaj Biel" ]
},
{
"id": "pt",
"authors": [ "patrickgold" ]
},
{
"id": "pt-BR",
"authors": [ "rickym7" ]
},
{
"id": "ro",
"authors": [ "bertin0" ]
},
{
"id": "ru",
"authors": [ "williamtheaker", "33kk" ]
},
{
"id": "rue",
"authors": [ "svvvst" ]
},
{
"id": "sk",
"authors": [ "stefan-misik", "majso" ]
},
{
"id": "sr",
"authors": [ "hedidnothingwrong", "GrbavaCigla" ]
},
{
"id": "sv",
"authors": [ "patrickgold" ]
},
{
"id": "tr",
"authors": [ "kisekinopureya", "patrickgold", "dvrnynr" ]
},
{
"id": "uk",
"authors": [ "williamtheaker", "33kk", "honsiorovskyi" ]
},
{
"id": "uk-cyr-ext",
"authors": [ "williamtheaker", "33kk", "honsiorovskyi" ]
},
{
"id": "ur-PK",
"authors": [ "mubashir-rehman", "mirfatif" ]
},
{
"id": "vi-VN",
"authors": [ "patrickgold", "Hayleia" ]
"label": "Default",
"symbolsPrecedingAutoSpace": ".,?‽!\"&%)]}»",
"symbolsFollowingAutoSpace": "",
"symbolsPrecedingPhantomSpace": ".,;:?‽!&%)]}»©®™",
"symbolsFollowingPhantomSpace": "¿⸘¡([{",
"symbolsTerminatingSentence": ".?‽!"
}
],
"popupMappings": [
{ "id": "default", "authors": [ "patrickgold" ] },
{ "id": "ar", "authors": [ "HeiWiper" ] },
{ "id": "bg", "authors": [ "iorvethe" ] },
{ "id": "ca", "authors": [ "mikelloc" ] },
{ "id": "ckb", "authors": [ "GoRaN" ] },
{ "id": "cs", "authors": [ "stefan-misik" ] },
{ "id": "da", "authors": [ "patrickgold" ] },
{ "id": "de", "authors": [ "patrickgold" ] },
{ "id": "de-DE-neobone", "authors": [ "ostrya" ] },
{ "id": "el", "authors": [ "tsiflimagas" ] },
{ "id": "en", "authors": [ "patrickgold" ] },
{ "id": "eo", "authors": [ "jeremiah-miller" ] },
{ "id": "es", "authors": [ "patrickgold" ] },
{ "id": "fa", "authors": [ "PHELAT" ] },
{ "id": "fa2", "authors": [ "M-Koushan" ] },
{ "id": "fa3", "authors": [ "SaeID-Rz" ] },
{ "id": "fi", "authors": [ "patrickgold" ] },
{ "id": "fo", "authors": [ "BinFlush" ] },
{ "id": "fr", "authors": [ "patrickgold" ] },
{ "id": "hr", "authors": [ "hedidnothingwrong" ] },
{ "id": "hu", "authors": [ "zoli111, gabik65" ] },
{ "id": "hy", "authors": [ "PJTSearch" ] },
{ "id": "id", "authors": [ "Linerly" ] },
{ "id": "is", "authors": [ "patrickgold" ] },
{ "id": "it", "authors": [ "patrickgold" ] },
{ "id": "iw", "authors": [ "Antony" ] },
{ "id": "ja-JP-jis", "authors": [ "waelwindows" ] },
{ "id": "ko", "authors": [ "patrickgold", "Hayleia" ] },
{ "id": "ku", "authors": [ "GoRaN" ] },
{ "id": "lt", "authors": [ "patrickgold" ] },
{ "id": "lv", "authors": [ "patrickgold", "eandersons" ] },
{ "id": "nb", "authors": [ "patrickgold" ] },
{ "id": "nn", "authors": [ "patrickgold" ] },
{ "id": "pl", "authors": [ "Mikołaj Biel" ] },
{ "id": "pt", "authors": [ "patrickgold" ] },
{ "id": "pt-BR", "authors": [ "rickym7" ] },
{ "id": "ro", "authors": [ "bertin0" ] },
{ "id": "ru", "authors": [ "williamtheaker", "33kk" ] },
{ "id": "rue", "authors": [ "svvvst" ] },
{ "id": "sk", "authors": [ "stefan-misik", "majso" ] },
{ "id": "sr", "authors": [ "hedidnothingwrong", "GrbavaCigla" ] },
{ "id": "sv", "authors": [ "patrickgold" ] },
{ "id": "tr", "authors": [ "kisekinopureya", "patrickgold", "dvrnynr" ] },
{ "id": "uk", "authors": [ "williamtheaker", "33kk", "honsiorovskyi" ] },
{ "id": "uk-cyr-ext", "authors": [ "williamtheaker", "33kk", "honsiorovskyi" ] },
{ "id": "ur-PK", "authors": [ "mubashir-rehman", "mirfatif" ] },
{ "id": "vi-VN", "authors": [ "patrickgold", "Hayleia" ] }
],
"subtypePresets": [
{
"languageTag": "en-US",
@@ -405,6 +283,15 @@
"characters": "org.florisboard.layouts:danish"
}
},
{
"languageTag": "id",
"composer": "org.florisboard.composers:appender",
"currencySet": "org.florisboard.currencysets:dollar",
"popupMapping": "org.florisboard.localization:id",
"preferred": {
"characters": "org.florisboard.layouts:qwerty"
}
},
{
"languageTag": "is-IS",
"composer": "org.florisboard.composers:appender",
@@ -447,6 +334,18 @@
"numericRow": "org.florisboard.layouts:persian"
}
},
{
"languageTag": "fa-FA",
"composer": "org.florisboard.composers:appender",
"currencySet": "org.florisboard.currencysets:rial",
"popupMapping": "org.florisboard.localization:fa3",
"preferred": {
"characters": "org.florisboard.layouts:persian3",
"symbols": "org.florisboard.layouts:persian",
"symbols2": "org.florisboard.layouts:persian",
"numericRow": "org.florisboard.layouts:western_arabic"
}
},
{
"languageTag": "ar",
"composer": "org.florisboard.composers:appender",
@@ -713,7 +612,7 @@
},
{
"languageTag": "vi-VN",
"composer": "org.florisboard.composers:basic-telex",
"composer": "org.florisboard.composers:telex",
"currencySet": "org.florisboard.currencysets:vietnamese_dong",
"popupMapping": "org.florisboard.localization:vi-VN",
"preferred": {

View File

@@ -11,16 +11,18 @@
]
},
"ی": {
"main": { "code": 1610, "label": "ي" },
"relevant": [
{ "code": 1746, "label": "ے" },
{ "code": 1741, "label": "ۍ" },
{ "code": 1744, "label": "ې" },
{ "code": 1610, "label": "ي" },
{ "code": 1597, "label": "ؽ" }
{ "code": 1744, "label": "ې" }
]
},
"ێ": {
"main": { "code": 1597, "label": "ؽ" }
},
"ﺋ": {
"relevant": [
{ "code": 65163, "label": "ﺋ" },
@@ -118,19 +120,23 @@
"~right": {
"main": { "code": 1567, "label": "؟" },
"relevant": [
{ "code": 64, "label": "@" },
{ "code": 35, "label": "#" },
{ "code": 40, "label": "(" },
{ "code": 41, "label": ")" },
{ "code": 171, "label": "«" },
{ "code": 187, "label": "»" },
{ "code": 60, "label": "<" },
{ "code": 62, "label": ">" },
{ "code": 1642, "label": "٪" },
{ "code": 35, "label": "#" },
{ "code": 42, "label": "*" },
{ "code": 1563, "label": "؛" },
{ "code": 59, "label": ";" },
{ "code": 58, "label": ":" },
{ "code": 44, "label": "," },
{ "code": 1549, "label": "؍" },
{ "code": 45, "label": "-" },
{ "code": 95, "label": "_" },
{ "code": 1600, "label": "" },
{ "code": 33, "label": "!" },
{ "code": 1548, "label": "،" }
{ "code": 1563, "label": "؛" },
{ "code": 58, "label": ":" },
{ "code": 1549, "label": "؍" },
{ "code": 1643, "label": "٫" },
{ "code": 1548, "label": "،" },
{ "code": 33, "label": "!" }
]
}
},

View File

@@ -2,41 +2,74 @@
"all": {
"α": {
"relevant": [
{ "$": "auto_text_key", "code": 940, "label": "ά" }
{ "$": "case_selector",
"lower": { "code": 940, "label": "ά" },
"upper": { "code": 902, "label": "Ά" }
}
]
},
"ε": {
"relevant": [
{ "$": "auto_text_key", "code": 941, "label": "έ" }
{ "$": "case_selector",
"lower": { "code": 941, "label": "έ" },
"upper": { "code": 904, "label": "Έ" }
}
]
},
"η": {
"relevant": [
{ "$": "auto_text_key", "code": 942, "label": "ή" }
{ "$": "case_selector",
"lower": { "code": 942, "label": "ή" },
"upper": { "code": 905, "label": "Ή" }
}
]
},
"ι": {
"main": { "$": "auto_text_key", "code": 943, "label": "ί" },
"main": { "$": "case_selector",
"lower": { "code": 943, "label": "ί" },
"upper": { "code": 906, "label": "Ί" }
},
"relevant": [
{ "$": "auto_text_key", "code": 912, "label": "ΐ" },
{ "$": "auto_text_key", "code": 970, "label": "ϊ" }
{ "$": "case_selector",
"lower": { "code": 912, "label": "ΐ" },
"upper": { "$": "multi_text_key", "codePoints": [921, 776, 769], "label": "Ϊ́" }
},
{ "$": "case_selector",
"lower": { "code": 970, "label": "ϊ" },
"upper": { "code": 938, "label": "Ϊ" }
}
]
},
"ο": {
"relevant": [
{ "$": "auto_text_key", "code": 972, "label": "ό" }
{ "$": "case_selector",
"lower": { "code": 972, "label": "ό" },
"upper": { "code": 908, "label": "Ό" }
}
]
},
"υ": {
"main": { "$": "auto_text_key", "code": 973, "label": "ύ" },
"main": { "$": "case_selector",
"lower": { "code": 973, "label": "ύ" },
"upper": { "code": 910, "label": "Ύ" }
},
"relevant": [
{ "$": "auto_text_key", "code": 944, "label": "ΰ" },
{ "$": "auto_text_key", "code": 971, "label": "ϋ" }
{ "$": "case_selector",
"lower": { "code": 944, "label": "ΰ" },
"upper": { "$": "multi_text_key", "codePoints": [933, 776, 769], "label": "Ϋ́" }
},
{ "$": "case_selector",
"lower": { "code": 971, "label": "ϋ" },
"upper": { "code": 939, "label": "Ϋ" }
}
]
},
"ω": {
"relevant": [
{ "$": "auto_text_key", "code": 974, "label": "ώ" }
{ "$": "case_selector",
"lower": { "code": 974, "label": "ώ" },
"upper": { "code": 911, "label": "Ώ" }
}
]
},
"~right": {

View File

@@ -0,0 +1,69 @@
{
"all": {
"ی": {
"main": { "code": 1574, "label": "ئ" },
"relevant": [
{ "code": 1610, "label": "ي" },
{ "code": 1746, "label": "ے" }
]
},
"ا": {
"main": { "code": 1570, "label": "آ" },
"relevant": [
{ "code": 1649, "label": "ٱ" },
{ "code": 1569, "label": "ء" },
{ "code": 1571, "label": "أ" },
{ "code": 1573, "label": "إ" }
]
},
"ه": {
"relevant": [
{ "code": 1729, "label": "ہ" },
{ "code": 1728, "label": "ۀ" },
{ "code": 1726, "label": "ھ" }
]
},
"ت": {
"relevant": [
{ "code": 1577, "label": "ة" }
]
},
"ک": {
"relevant": [
{ "code": 1706, "label": "ڪ"}
]
},
"ز": {
"relevant": [
{ "code": 1688, "label": "ژ" }
]
},
"و": {
"relevant": [
{ "code": 1572, "label": "ؤ" }
]
},
"~right": {
"main": { "code": 1611, "label": "ً" },
"relevant": [
{ "code": 1615, "label": "ُ" },
{ "code": 1616, "label": "ِ" },
{ "code": 1614, "label": "َ" },
{ "code": 1617, "label": "ّ" },
{ "code": 1620, "label": "ٔ" }
]
}
},
"uri": {
"~right": {
"main": { "code": -255, "label": ".ir"},
"relevant": [
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".net" }
]
}
}
}

View File

@@ -22,6 +22,11 @@
{ "$": "auto_text_key", "code": 337, "label": "ő" }
]
},
"ö": {
"relevant": [
{ "$": "auto_text_key", "code": 337, "label": "ő" }
]
},
"u": {
"relevant": [
{ "$": "auto_text_key", "code": 250, "label": "ú" },
@@ -29,6 +34,11 @@
{ "$": "auto_text_key", "code": 369, "label": "ű" }
]
},
"ü": {
"relevant": [
{ "$": "auto_text_key", "code": 369, "label": "ű" }
]
},
"~right": {
"main": { "code": 44, "label": "," },
"relevant": [
@@ -58,13 +68,13 @@
},
"uri": {
"~right": {
"main": { "code": -255, "label": ".com" },
"main": { "code": -255, "label": ".hu" },
"relevant": [
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".hu" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".net" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
{ "code": -255, "label": ".eu" },
{ "code": -255, "label": ".gov.hu" }
]
}
}

View File

@@ -0,0 +1,49 @@
{
"all": {
"e": {
"relevant": [
{ "$": "auto_text_key", "code": 234, "label": "ê" },
{ "$": "auto_text_key", "code": 233, "label": "é" },
{ "$": "auto_text_key", "code": 232, "label": "è" }
]
},
"~right": {
"main": { "code": 44, "label": "," },
"relevant": [
{ "code": 38, "label": "&" },
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
{ "code": 34, "label": "\"" },
{ "code": 45, "label": "-" },
{ "code": 58, "label": ":" },
{ "code": 39, "label": "'" },
{ "code": 64, "label": "@" },
{ "code": 59, "label": ";" },
{ "code": 47, "label": "/" },
{ "$": "layout_direction_selector",
"ltr": { "code": 40, "label": "(" },
"rtl": { "code": 41, "label": "(" }
},
{ "$": "layout_direction_selector",
"ltr": { "code": 41, "label": ")" },
"rtl": { "code": 40, "label": ")" }
},
{ "code": 35, "label": "#" },
{ "code": 33, "label": "!" },
{ "code": 63, "label": "?" }
]
}
},
"uri": {
"~right": {
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".id" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" },
{ "code": -255, "label": ".co.id"},
{ "code": -255, "label": ".go.id"}
]
}
}
}

View File

@@ -43,8 +43,8 @@
},
"z": {
"relevant": [
{ "$": "auto_text_key", "code": 378, "label": "ź" },
{ "$": "auto_text_key", "code": 380, "label": "ż" }
{ "$": "auto_text_key", "code": 380, "label": "ż" },
{ "$": "auto_text_key", "code": 378, "label": "ź" }
]
},
"~right": {

View File

@@ -1,5 +1,8 @@
{
"all": {
"a": {
"main": { "$": "auto_text_key", "code": 226, "label": "â" }
},
"c": {
"main": { "$": "auto_text_key", "code": 231, "label": "ç" }
},
@@ -10,13 +13,19 @@
"main": { "$": "case_selector",
"lower": { "code": 305, "label": "ı" },
"upper": { "code": 73, "label": "I" }
}
},
"relevant": [
{ "$": "auto_text_key", "code": 238, "label": "î" }
]
},
"ı": {
"main": { "$": "case_selector",
"lower": { "code": 105, "label": "i" },
"upper": { "code": 304, "label": "İ" }
}
},
"relevant": [
{ "$": "auto_text_key", "code": 238, "label": "î" }
]
},
"o": {
"main": { "$": "auto_text_key", "code": 246, "label": "ö" }
@@ -25,7 +34,10 @@
"main": { "$": "auto_text_key", "code": 351, "label": "ş" }
},
"u": {
"main": { "$": "auto_text_key", "code": 252, "label": "ü" }
"main": { "$": "auto_text_key", "code": 252, "label": "ü" },
"relevant": [
{ "$": "auto_text_key", "code": 251, "label": "û" }
]
},
"~right": {
"main": { "code": 44, "label": "," },

View File

@@ -1,15 +0,0 @@
[
")",
"}",
"]",
">",
"&",
".",
",",
"?",
"!",
";",
":"
]

View File

@@ -39,7 +39,7 @@
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][mode={m:capslock}]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {

View File

@@ -38,7 +38,7 @@
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][mode={m:capslock}]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {

View File

@@ -39,7 +39,7 @@
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][mode={m:capslock}]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {

View File

@@ -38,7 +38,7 @@
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][mode={m:capslock}]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {

View File

@@ -39,7 +39,7 @@
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][mode={m:capslock}]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {

View File

@@ -38,7 +38,7 @@
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][mode={m:capslock}]": {
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
{
"uniqueId": "google-material-icons",
"name": "Google Material Icons",
"developers": [
{
"name": "Google Inc.",
"organisationUrl": "https://github.com/google"
}
],
"website": "https://github.com/google/material-design-icons",
"licenses": [
"Apache-2.0"
]
}

View File

@@ -0,0 +1,14 @@
{
"uniqueId": "icu4c",
"name": "ICU4C Native C library",
"developers": [
{
"name": "The Unicode Consortium",
"organisationUrl": "https://github.com/unicode-org"
}
],
"website": "https://github.com/unicode-org/icu",
"licenses": [
"icu4c"
]
}

File diff suppressed because one or more lines are too long

View File

@@ -18,17 +18,12 @@ add_library(ICU::uc STATIC IMPORTED)
set_property(TARGET ICU::uc PROPERTY IMPORTED_LOCATION "${JNI_LIBS}/libicuuc.a")
### FlorisBoard ###
add_subdirectory(nuspell)
add_subdirectory(utils)
add_subdirectory(ime/nlp)
add_subdirectory(ime/spelling)
add_library(
florisboard-native
SHARED
dev_patrickgold_florisboard_FlorisApplication.cpp
dev_patrickgold_florisboard_ime_nlp_SuggestionList.cpp
dev_patrickgold_florisboard_ime_spelling_SpellingDict.cpp
)
target_compile_options(florisboard-native PRIVATE -ffunction-sections -fdata-sections -fexceptions)
@@ -41,8 +36,5 @@ target_link_libraries(
log
ICU::uc
ICU::data
Nuspell::nuspell
utils
ime-nlp
ime-spelling
)

View File

@@ -25,9 +25,8 @@
extern "C"
JNIEXPORT jint JNICALL
Java_dev_patrickgold_florisboard_FlorisApplication_00024Companion_nativeInitICUData(
JNIEnv *env,
jobject thiz,
jobject path) {
JNIEnv *env, jobject thiz, jobject path)
{
auto path_str = utils::j2std_string(env, path);
std::ifstream in_file(path_str, std::ios::in | std::ios::binary);
if (!in_file) {
@@ -42,6 +41,7 @@ Java_dev_patrickgold_florisboard_FlorisApplication_00024Companion_nativeInitICUD
char *icu_data = new char[size + 1];
in_file.read(icu_data, size);
if (!in_file) {
delete[] icu_data;
in_file.close();
return U_FILE_ACCESS_ERROR;
}

View File

@@ -1,123 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <jni.h>
#include "ime/nlp/suggestion_list.h"
#pragma ide diagnostic ignored "UnusedLocalVariable"
using namespace ime::nlp;
extern "C"
JNIEXPORT jlong JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeInitialize(
JNIEnv *env,
jobject thiz,
jint max_size) {
auto *suggestionList = new SuggestionList(max_size);
return reinterpret_cast<jlong>(suggestionList);
}
extern "C"
JNIEXPORT void JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeDispose(
JNIEnv *env,
jobject thiz,
jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
suggestionList->clear();
delete suggestionList;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeAdd(
JNIEnv *env,
jobject thiz,
jlong native_ptr,
jstring word,
jint freq) {
const char *cWord = env->GetStringUTFChars(word, nullptr);
word_t stdWord = word_t(cWord);
env->ReleaseStringUTFChars(word, cWord);
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->add(std::move(stdWord), freq);
}
extern "C"
JNIEXPORT void JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeClear(
JNIEnv *env,
jobject thiz,
jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
suggestionList->clear();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeContains(
JNIEnv *env,
jobject thiz,
jlong native_ptr,
jstring element) {
const char *cWord = env->GetStringUTFChars(element, nullptr);
const word_t stdWord = word_t(cWord);
env->ReleaseStringUTFChars(element, cWord);
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->containsWord(stdWord);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeGetOrNull(
JNIEnv *env,
jobject thiz,
jlong native_ptr,
jint index) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
auto weightedToken = suggestionList->get(index);
if (weightedToken == nullptr) {
return nullptr;
}
return env->NewStringUTF(weightedToken->data.c_str());
}
extern "C"
JNIEXPORT jint JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeSize(
JNIEnv *env,
jobject thiz,
jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->size();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeGetIsPrimaryTokenAutoInsert(
JNIEnv *env, jobject thiz, jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->isPrimaryTokenAutoInsert;
}
extern "C"
JNIEXPORT void JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeSetIsPrimaryTokenAutoInsert(
JNIEnv *env, jobject thiz, jlong native_ptr, jboolean v) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
suggestionList->isPrimaryTokenAutoInsert = v;
}

View File

@@ -1,90 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <jni.h>
#include <algorithm>
#include "ime/spelling/spellingdict.h"
#include "utils/jni_utils.h"
#pragma ide diagnostic ignored "UnusedLocalVariable"
using namespace ime::spellcheck;
extern "C"
JNIEXPORT jlong JNICALL
Java_dev_patrickgold_florisboard_ime_spelling_SpellingDict_00024Companion_nativeInitialize(
JNIEnv *env,
jobject thiz,
jobject base_path) {
auto strBasePath = utils::j2std_string(env, base_path);
auto *spellingDict = SpellingDict::load(strBasePath);
if (spellingDict == nullptr) {
return 0L;
} else {
return reinterpret_cast<jlong>(spellingDict);
}
}
extern "C"
JNIEXPORT void JNICALL
Java_dev_patrickgold_florisboard_ime_spelling_SpellingDict_00024Companion_nativeDispose(
JNIEnv *env,
jobject thiz,
jlong native_ptr) {
auto spellingDict = reinterpret_cast<SpellingDict *>(native_ptr);
delete spellingDict;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_dev_patrickgold_florisboard_ime_spelling_SpellingDict_00024Companion_nativeSpell(
JNIEnv *env,
jobject thiz,
jlong native_ptr,
jobject word) {
auto strWord = utils::j2std_string(env, word);
auto spellingDict = reinterpret_cast<SpellingDict *>(native_ptr);
auto result = spellingDict->spell(strWord);
return result;
}
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_dev_patrickgold_florisboard_ime_spelling_SpellingDict_00024Companion_nativeSuggest(
JNIEnv *env,
jobject thiz,
jlong native_ptr,
jobject word,
jint limit) {
auto strWord = utils::j2std_string(env, word);
auto spellingDict = reinterpret_cast<SpellingDict *>(native_ptr);
auto result = spellingDict->suggest(strWord);
auto retSize = std::min(result.size(), (size_t)std::max(0, limit));
jclass jByteArrayClass = env->FindClass("java/nio/ByteBuffer");
jobjectArray jSuggestions = env->NewObjectArray(retSize, jByteArrayClass, nullptr);
for (int n = 0; n < retSize; n++) {
env->SetObjectArrayElement(jSuggestions, n, utils::std2j_string(env, result[n]));
}
return jSuggestions;
}

View File

@@ -1,13 +0,0 @@
add_library(
# Name
ime-nlp
# Headers
nlp.h
token.h
suggestion_list.h
# Sources
token.cpp
suggestion_list.cpp
)

View File

@@ -1,98 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "suggestion_list.h"
#include <utility>
using namespace ime::nlp;
SuggestionList::SuggestionList(size_t _maxSize) :
maxSize(_maxSize), internalSize(0), tokens(_maxSize), isPrimaryTokenAutoInsert(false)
{ }
SuggestionList::~SuggestionList() = default;
bool SuggestionList::add(word_t &&word, freq_t &&freq) {
auto entryIndex = indexOfWord(word);
if (entryIndex.has_value()) {
// Word exists already
auto entry = tokens[entryIndex.value()];
if (entry.freq < freq) {
// Need to update freq
entry.freq = freq;
} else {
return false;
}
} else {
if (internalSize < maxSize) {
tokens[internalSize++] = WeightedToken(std::move(word), freq);
} else {
auto last = tokens[internalSize - 1];
if (last.freq < freq) {
tokens[internalSize - 1] = WeightedToken(std::move(word), freq);
} else {
return false;
}
}
}
std::sort(tokens.begin(), tokens.begin() + internalSize, std::greater<>());
return true;
}
void SuggestionList::clear() {
internalSize = 0;
isPrimaryTokenAutoInsert = false;
}
bool SuggestionList::contains(const WeightedToken &element) const {
return indexOf(element).has_value();
}
bool SuggestionList::containsWord(const word_t &word) const {
return indexOfWord(word).has_value();
}
const WeightedToken *SuggestionList::get(size_t index) const {
if (index < 0 || index >= internalSize) return nullptr;
return &tokens[index];
}
std::optional<size_t> SuggestionList::indexOf(const WeightedToken &element) const {
for (size_t n = 0; n < internalSize; n++) {
if (element == tokens[n]) {
return n;
}
}
return std::nullopt;
}
std::optional<size_t> SuggestionList::indexOfWord(const word_t &word) const {
for (size_t n = 0; n < internalSize; n++) {
if (word == tokens[n].data) {
return n;
}
}
return std::nullopt;
}
bool SuggestionList::isEmpty() const {
return internalSize == 0;
}
size_t SuggestionList::size() const {
return internalSize;
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FLORISBOARD_SUGGESTION_LIST_H
#define FLORISBOARD_SUGGESTION_LIST_H
#include <optional>
#include <vector>
#include "token.h"
namespace ime::nlp {
class SuggestionList {
public:
SuggestionList(size_t _maxSize);
~SuggestionList();
bool add(word_t &&word, freq_t &&freq);
void clear();
bool contains(const WeightedToken &element) const;
bool containsWord(const word_t &word) const;
const WeightedToken *get(size_t index) const;
std::optional<size_t> indexOf(const WeightedToken &element) const;
std::optional<size_t> indexOfWord(const word_t &word) const;
bool isEmpty() const;
size_t size() const;
bool isPrimaryTokenAutoInsert;
private:
std::vector<WeightedToken> tokens;
size_t internalSize;
size_t maxSize;
};
} // namespace ime::nlp
#endif // FLORISBOARD_SUGGESTION_LIST_H

View File

@@ -1,61 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "token.h"
#include <utility>
namespace ime::nlp {
Token::Token() : data() {}
Token::Token(word_t &&_data) : data(std::move(_data)) {}
bool operator==(const Token &t1, const Token &t2) {
return t1.data == t2.data;
}
bool operator!=(const Token &t1, const Token &t2) {
return !(t1 == t2);
}
WeightedToken::WeightedToken() : Token(), freq(0) {}
WeightedToken::WeightedToken(word_t &&_data, freq_t _freq) : Token(std::move(_data)), freq(_freq) {}
bool operator==(const WeightedToken &t1, const WeightedToken &t2) {
return t1.data == t2.data && t1.freq == t2.freq;
}
bool operator!=(const WeightedToken &t1, const WeightedToken &t2) {
return !(t1 == t2);
}
bool operator<(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq < t2.freq;
}
bool operator<=(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq <= t2.freq;
}
bool operator>(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq > t2.freq;
}
bool operator>=(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq >= t2.freq;
}
} // namespace ime::nlp

View File

@@ -1,51 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FLORISBOARD_TOKEN_H
#define FLORISBOARD_TOKEN_H
#include "nlp.h"
#include <string>
namespace ime::nlp {
class Token {
public:
word_t data;
Token();
Token(word_t &&_data);
friend bool operator==(const Token &t1, const Token &t2);
friend bool operator!=(const Token &t1, const Token &t2);
};
class WeightedToken : public Token {
public:
freq_t freq;
WeightedToken();
WeightedToken(word_t &&_data, freq_t _freq);
friend bool operator==(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator!=(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator<(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator<=(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator>(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator>=(const WeightedToken &t1, const WeightedToken &t2);
};
} // namespace ime::nlp
#endif // FLORISBOARD_TOKEN_H

View File

@@ -1,10 +0,0 @@
add_library(
# Name
ime-spelling
# Headers
spellingdict.h
# Sources
spellingdict.cpp
)

View File

@@ -1,51 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "spellingdict.h"
#include "utils/log.h"
using namespace ime::spellcheck;
SpellingDict::SpellingDict(const nuspell::Dictionary& dict) : dictionary(std::make_unique<nuspell::Dictionary>(dict))
{ }
SpellingDict::~SpellingDict() = default;
SpellingDict* SpellingDict::load(const std::string &basePath) {
utils::start_stdout_stderr_logger("spell-floris");
try {
auto temp = nuspell::Dictionary::load_from_path(basePath);
auto spellingDict = new SpellingDict(temp);
return spellingDict;
} catch (const nuspell::Dictionary_Loading_Error& e) {
utils::log_error("SpellingDict.load()", e.what());
return nullptr;
} catch (...) {
utils::log_error("SpellingDict.load()", "An unknown error occurred!");
return nullptr;
}
}
bool SpellingDict::spell(const std::string& word) {
bool result = dictionary->spell(word);
return result;
}
std::vector<std::string> SpellingDict::suggest(const std::string &word) {
auto result = std::vector<std::string>();
dictionary->suggest(word, result);
return result;
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FLORISBOARD_SPELLINGDICT_H
#define FLORISBOARD_SPELLINGDICT_H
#include "nuspell/dictionary.hxx"
#include <string>
#include <vector>
namespace ime::spellcheck {
class SpellingDict {
public:
SpellingDict(const nuspell::Dictionary& dict);
~SpellingDict();
static SpellingDict* load(const std::string& basePath);
bool spell(const std::string& word);
std::vector<std::string> suggest(const std::string& word);
private:
std::unique_ptr<nuspell::Dictionary> dictionary;
};
} // namespace ime::spellcheck
#endif // FLORISBOARD_SPELLINGDICT_H

View File

@@ -1,10 +0,0 @@
add_library(nuspell
aff_data.cxx aff_data.hxx
checker.cxx checker.hxx
suggester.cxx suggester.hxx
dictionary.cxx dictionary.hxx
unicode.hxx
utils.cxx utils.hxx
structures.hxx)
add_library(Nuspell::nuspell ALIAS nuspell)

View File

@@ -1,165 +0,0 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

File diff suppressed because it is too large Load Diff

View File

@@ -1,173 +0,0 @@
/* Copyright 2016-2021 Dimitrij Mijoski
*
* This file is part of Nuspell.
*
* Nuspell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Nuspell is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef NUSPELL_AFF_DATA_HXX
#define NUSPELL_AFF_DATA_HXX
#include "nuspell_export.h"
#include "structures.hxx"
#include <iosfwd>
#include <unicode/locid.h>
namespace nuspell {
inline namespace v5 {
class Encoding {
std::string name;
NUSPELL_EXPORT auto normalize_name() -> void;
public:
enum Enc_Type { SINGLEBYTE = false, UTF8 = true };
Encoding() = default;
explicit Encoding(const std::string& e) : name(e) { normalize_name(); }
explicit Encoding(std::string&& e) : name(move(e)) { normalize_name(); }
explicit Encoding(const char* e) : name(e) { normalize_name(); }
auto& operator=(const std::string& e)
{
name = e;
normalize_name();
return *this;
}
auto& operator=(std::string&& e)
{
name = move(e);
normalize_name();
return *this;
}
auto& operator=(const char* e)
{
name = e;
normalize_name();
return *this;
}
auto empty() const { return name.empty(); }
auto& value() const { return name; }
auto is_utf8() const { return name == "UTF-8"; }
auto value_or_default() const -> std::string
{
if (name.empty())
return "ISO8859-1";
else
return name;
}
operator Enc_Type() const { return is_utf8() ? UTF8 : SINGLEBYTE; }
};
enum class Flag_Type { SINGLE_CHAR, DOUBLE_CHAR, NUMBER, UTF8 };
/**
* @internal
* @brief Map between words and word_flags.
*
* Flags are stored as part of the container. Maybe for the future flags should
* be stored elsewhere (flag aliases) and this should store pointers.
*
* Does not store morphological data as is low priority feature and is out of
* scope.
*/
using Word_List = Hash_Multimap<std::string, Flag_Set>;
struct Aff_Data {
static constexpr auto HIDDEN_HOMONYM_FLAG = char16_t(-1);
static constexpr auto MAX_SUGGESTIONS = size_t(16);
// spell checking options
Word_List words;
Prefix_Table prefixes;
Suffix_Table suffixes;
bool complex_prefixes;
bool fullstrip;
bool checksharps;
bool forbid_warn;
char16_t compound_onlyin_flag;
char16_t circumfix_flag;
char16_t forbiddenword_flag;
char16_t keepcase_flag;
char16_t need_affix_flag;
char16_t warn_flag;
// compounding options
char16_t compound_flag;
char16_t compound_begin_flag;
char16_t compound_last_flag;
char16_t compound_middle_flag;
Compound_Rule_Table compound_rules;
// spell checking options
Break_Table break_table;
Substr_Replacer input_substr_replacer;
std::string ignored_chars;
icu::Locale icu_locale;
Substr_Replacer output_substr_replacer;
// suggestion options
Replacement_Table replacements;
std::vector<Similarity_Group> similarities;
std::string keyboard_closeness;
std::string try_chars;
// Phonetic_Table phonetic_table;
char16_t nosuggest_flag;
char16_t substandard_flag;
unsigned short max_compound_suggestions;
unsigned short max_ngram_suggestions;
unsigned short max_diff_factor;
bool only_max_diff;
bool no_split_suggestions;
bool suggest_with_dots;
// compounding options
unsigned short compound_min_length;
unsigned short compound_max_word_count;
char16_t compound_permit_flag;
char16_t compound_forbid_flag;
char16_t compound_root_flag;
char16_t compound_force_uppercase;
bool compound_more_suffixes;
bool compound_check_duplicate;
bool compound_check_rep;
bool compound_check_case;
bool compound_check_triple;
bool compound_simplified_triple;
bool compound_syllable_num;
unsigned short compound_syllable_max;
std::string compound_syllable_vowels;
std::vector<Compound_Pattern> compound_patterns;
// data members used only while parsing
Flag_Type flag_type;
Encoding encoding;
std::vector<Flag_Set> flag_aliases;
std::string wordchars; // deprecated?
auto parse_aff(std::istream& in) -> bool;
auto parse_dic(std::istream& in) -> bool;
auto parse_aff_dic(std::istream& aff, std::istream& dic)
{
if (parse_aff(aff))
return parse_dic(dic);
return false;
}
};
} // namespace v5
} // namespace nuspell
#endif // NUSPELL_AFF_DATA_HXX

File diff suppressed because it is too large Load Diff

View File

@@ -1,352 +0,0 @@
/* Copyright 2016-2021 Dimitrij Mijoski
*
* This file is part of Nuspell.
*
* Nuspell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Nuspell is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef NUSPELL_CHECKER_HXX
#define NUSPELL_CHECKER_HXX
#include "aff_data.hxx"
namespace nuspell {
inline namespace v5 {
enum Affixing_Mode {
FULL_WORD,
AT_COMPOUND_BEGIN,
AT_COMPOUND_END,
AT_COMPOUND_MIDDLE
};
struct Affixing_Result_Base {
Word_List::const_pointer root_word = {};
operator Word_List::const_pointer() const { return root_word; }
auto& operator*() const { return *root_word; }
auto operator->() const { return root_word; }
};
template <class T1 = void, class T2 = void>
struct Affixing_Result : Affixing_Result_Base {
const T1* a = {};
const T2* b = {};
Affixing_Result() = default;
Affixing_Result(Word_List::const_reference r, const T1& a, const T2& b)
: Affixing_Result_Base{&r}, a{&a}, b{&b}
{
}
};
template <class T1>
struct Affixing_Result<T1, void> : Affixing_Result_Base {
const T1* a = {};
Affixing_Result() = default;
Affixing_Result(Word_List::const_reference r, const T1& a)
: Affixing_Result_Base{&r}, a{&a}
{
}
};
template <>
struct Affixing_Result<void, void> : Affixing_Result_Base {
Affixing_Result() = default;
Affixing_Result(Word_List::const_reference r) : Affixing_Result_Base{&r}
{
}
};
struct Compounding_Result {
Word_List::const_pointer word_entry = {};
unsigned char num_words_modifier = {};
signed char num_syllable_modifier = {};
bool affixed_and_modified = {}; /**< non-zero affix */
operator Word_List::const_pointer() const { return word_entry; }
auto& operator*() const { return *word_entry; }
auto operator->() const { return word_entry; }
};
struct Checker : public Aff_Data {
enum Forceucase : bool {
FORBID_BAD_FORCEUCASE = false,
ALLOW_BAD_FORCEUCASE = true
};
enum Hidden_Homonym : bool {
ACCEPT_HIDDEN_HOMONYM = false,
SKIP_HIDDEN_HOMONYM = true
};
Checker()
: Aff_Data() // we explicity do value init so content is zeroed
{
}
auto spell_priv(std::string& s) const -> bool;
auto spell_break(std::string& s, size_t depth = 0) const -> bool;
auto spell_casing(std::string& s) const -> const Flag_Set*;
auto spell_casing_upper(std::string& s) const -> const Flag_Set*;
auto spell_casing_title(std::string& s) const -> const Flag_Set*;
auto spell_sharps(std::string& base, size_t n_pos = 0, size_t n = 0,
size_t rep = 0) const -> const Flag_Set*;
auto check_word(std::string& s, Forceucase allow_bad_forceucase = {},
Hidden_Homonym skip_hidden_homonym = {}) const
-> const Flag_Set*;
auto check_simple_word(std::string& word,
Hidden_Homonym skip_hidden_homonym = {}) const
-> const Flag_Set*;
template <Affixing_Mode m>
auto affix_NOT_valid(const Prefix& a) const;
template <Affixing_Mode m>
auto affix_NOT_valid(const Suffix& a) const;
template <Affixing_Mode m, class AffixT>
auto outer_affix_NOT_valid(const AffixT& a) const;
template <class AffixT>
auto is_circumfix(const AffixT& a) const;
template <Affixing_Mode m>
auto is_valid_inside_compound(const Flag_Set& flags) const;
template <Affixing_Mode m = FULL_WORD>
auto strip_prefix_only(std::string& s,
Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<Prefix>;
template <Affixing_Mode m = FULL_WORD>
auto strip_suffix_only(std::string& s,
Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<Suffix>;
template <Affixing_Mode m = FULL_WORD>
auto
strip_prefix_then_suffix(std::string& s,
Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<Suffix, Prefix>;
template <Affixing_Mode m>
auto strip_pfx_then_sfx_2(const Prefix& pe, std::string& s,
Hidden_Homonym skip_hidden_homonym) const
-> Affixing_Result<Suffix, Prefix>;
template <Affixing_Mode m = FULL_WORD>
auto
strip_suffix_then_prefix(std::string& s,
Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<Prefix, Suffix>;
template <Affixing_Mode m>
auto strip_sfx_then_pfx_2(const Suffix& se, std::string& s,
Hidden_Homonym skip_hidden_homonym) const
-> Affixing_Result<Prefix, Suffix>;
template <Affixing_Mode m = FULL_WORD>
auto strip_prefix_then_suffix_commutative(
std::string& word, Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<Suffix, Prefix>;
template <Affixing_Mode m = FULL_WORD>
auto strip_pfx_then_sfx_comm_2(const Prefix& pe, std::string& word,
Hidden_Homonym skip_hidden_homonym) const
-> Affixing_Result<Suffix, Prefix>;
template <Affixing_Mode m = FULL_WORD>
auto
strip_suffix_then_suffix(std::string& s,
Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<Suffix, Suffix>;
template <Affixing_Mode m>
auto strip_sfx_then_sfx_2(const Suffix& se1, std::string& s,
Hidden_Homonym skip_hidden_homonym) const
-> Affixing_Result<Suffix, Suffix>;
template <Affixing_Mode m = FULL_WORD>
auto
strip_prefix_then_prefix(std::string& s,
Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<Prefix, Prefix>;
template <Affixing_Mode m>
auto strip_pfx_then_pfx_2(const Prefix& pe1, std::string& s,
Hidden_Homonym skip_hidden_homonym) const
-> Affixing_Result<Prefix, Prefix>;
template <Affixing_Mode m = FULL_WORD>
auto strip_prefix_then_2_suffixes(
std::string& s, Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<>;
template <Affixing_Mode m>
auto strip_pfx_2_sfx_3(const Prefix& pe1, const Suffix& se1,
std::string& s,
Hidden_Homonym skip_hidden_homonym) const
-> Affixing_Result<>;
template <Affixing_Mode m = FULL_WORD>
auto strip_suffix_prefix_suffix(
std::string& s, Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<>;
template <Affixing_Mode m>
auto strip_s_p_s_3(const Suffix& se1, const Prefix& pe1,
std::string& word,
Hidden_Homonym skip_hidden_homonym) const
-> Affixing_Result<>;
template <Affixing_Mode m = FULL_WORD>
auto strip_2_suffixes_then_prefix(
std::string& s, Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<>;
template <Affixing_Mode m>
auto strip_2_sfx_pfx_3(const Suffix& se1, const Suffix& se2,
std::string& word,
Hidden_Homonym skip_hidden_homonym) const
-> Affixing_Result<>;
template <Affixing_Mode m = FULL_WORD>
auto strip_suffix_then_2_prefixes(
std::string& s, Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<>;
template <Affixing_Mode m>
auto strip_sfx_2_pfx_3(const Suffix& se1, const Prefix& pe1,
std::string& s,
Hidden_Homonym skip_hidden_homonym) const
-> Affixing_Result<>;
template <Affixing_Mode m = FULL_WORD>
auto strip_prefix_suffix_prefix(
std::string& word, Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<>;
template <Affixing_Mode m>
auto strip_p_s_p_3(const Prefix& pe1, const Suffix& se1,
std::string& word,
Hidden_Homonym skip_hidden_homonym) const
-> Affixing_Result<>;
template <Affixing_Mode m = FULL_WORD>
auto strip_2_prefixes_then_suffix(
std::string& word, Hidden_Homonym skip_hidden_homonym = {}) const
-> Affixing_Result<>;
template <Affixing_Mode m>
auto strip_2_pfx_sfx_3(const Prefix& pe1, const Prefix& pe2,
std::string& word,
Hidden_Homonym skip_hidden_homonym) const
-> Affixing_Result<>;
auto check_compound(std::string& word,
Forceucase allow_bad_forceucase) const
-> Compounding_Result;
template <Affixing_Mode m = AT_COMPOUND_BEGIN>
auto check_compound(std::string& word, size_t start_pos,
size_t num_part, std::string& part,
Forceucase allow_bad_forceucase) const
-> Compounding_Result;
template <Affixing_Mode m = AT_COMPOUND_BEGIN>
auto check_compound_classic(std::string& word, size_t start_pos,
size_t i, size_t num_part,
std::string& part,
Forceucase allow_bad_forceucase) const
-> Compounding_Result;
template <Affixing_Mode m = AT_COMPOUND_BEGIN>
auto check_compound_with_pattern_replacements(
std::string& word, size_t start_pos, size_t i, size_t num_part,
std::string& part, Forceucase allow_bad_forceucase) const
-> Compounding_Result;
template <Affixing_Mode m>
auto check_word_in_compound(std::string& s) const -> Compounding_Result;
auto calc_num_words_modifier(const Prefix& pfx) const -> unsigned char;
template <Affixing_Mode m>
auto calc_syllable_modifier(Word_List::const_reference we) const
-> signed char;
template <Affixing_Mode m>
auto calc_syllable_modifier(Word_List::const_reference we,
const Suffix& sfx) const -> signed char;
auto count_syllables(std::string_view word) const -> size_t;
auto check_compound_with_rules(std::string& word,
std::vector<const Flag_Set*>& words_data,
size_t start_pos, std::string& part,
Forceucase allow_bad_forceucase) const
-> Compounding_Result;
auto is_rep_similar(std::string& word) const -> bool;
};
template <Affixing_Mode m>
auto Checker::affix_NOT_valid(const Prefix& e) const
{
if (m == FULL_WORD && e.cont_flags.contains(compound_onlyin_flag))
return true;
if (m == AT_COMPOUND_END &&
!e.cont_flags.contains(compound_permit_flag))
return true;
if (m != FULL_WORD && e.cont_flags.contains(compound_forbid_flag))
return true;
return false;
}
template <Affixing_Mode m>
auto Checker::affix_NOT_valid(const Suffix& e) const
{
if (m == FULL_WORD && e.cont_flags.contains(compound_onlyin_flag))
return true;
if (m == AT_COMPOUND_BEGIN &&
!e.cont_flags.contains(compound_permit_flag))
return true;
if (m != FULL_WORD && e.cont_flags.contains(compound_forbid_flag))
return true;
return false;
}
template <Affixing_Mode m, class AffixT>
auto Checker::outer_affix_NOT_valid(const AffixT& e) const
{
if (affix_NOT_valid<m>(e))
return true;
if (e.cont_flags.contains(need_affix_flag))
return true;
return false;
}
template <class AffixT>
auto Checker::is_circumfix(const AffixT& a) const
{
return a.cont_flags.contains(circumfix_flag);
}
template <class AffixInner, class AffixOuter>
auto cross_valid_inner_outer(const AffixInner& inner, const AffixOuter& outer)
{
return inner.cont_flags.contains(outer.flag);
}
template <class Affix>
auto cross_valid_inner_outer(const Flag_Set& word_flags, const Affix& afx)
{
return word_flags.contains(afx.flag);
}
} // namespace v5
} // namespace nuspell
#endif // NUSPELL_CHECKER_HXX

View File

@@ -1 +0,0 @@
clang-format -style=file -i *.[ch]xx

View File

@@ -1,115 +0,0 @@
/* Copyright 2016-2021 Dimitrij Mijoski
*
* This file is part of Nuspell.
*
* Nuspell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Nuspell is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
*/
#include "dictionary.hxx"
#include "utils.hxx"
#include <fstream>
#include <iostream>
#include <stdexcept>
using namespace std;
namespace nuspell {
inline namespace v5 {
Dictionary::Dictionary(std::istream& aff, std::istream& dic)
{
if (!parse_aff_dic(aff, dic))
throw Dictionary_Loading_Error("error parsing");
}
Dictionary::Dictionary() = default;
/**
* @brief Create a dictionary from opened files as iostreams
*
* Prefer using load_from_path(). Use this if you have a specific use case,
* like when .aff and .dic are in-memory buffers istringstream.
*
* @param aff The iostream of the .aff file
* @param dic The iostream of the .dic file
* @return Dictionary object
* @throws Dictionary_Loading_Error on error
*/
auto Dictionary::load_from_aff_dic(std::istream& aff, std::istream& dic)
-> Dictionary
{
return Dictionary(aff, dic);
}
/**
* @brief Create a dictionary from files
* @param file_path_without_extension path *without* extensions (without .dic or
* .aff)
* @return Dictionary object
* @throws Dictionary_Loading_Error on error
*/
auto Dictionary::load_from_path(const std::string& file_path_without_extension)
-> Dictionary
{
auto path = file_path_without_extension;
path += ".aff";
std::ifstream aff_file(path);
if (aff_file.fail()) {
auto err = "Aff file " + path + " not found";
throw Dictionary_Loading_Error(err);
}
path.replace(path.size() - 3, 3, "dic");
std::ifstream dic_file(path);
if (dic_file.fail()) {
auto err = "Dic file " + path + " not found";
throw Dictionary_Loading_Error(err);
}
return load_from_aff_dic(aff_file, dic_file);
}
/**
* @brief Checks if a given word is correct
* @param word any word
* @return true if correct, false otherwise
*/
auto Dictionary::spell(std::string_view word) const -> bool
{
auto ok_enc = validate_utf8(word);
if (unlikely(word.size() > 360))
return false;
if (unlikely(!ok_enc))
return false;
auto word_buf = string(word);
return spell_priv(word_buf);
}
/**
* @brief Suggests correct words for a given incorrect word
* @param[in] word incorrect word
* @param[out] out this object will be populated with the suggestions
*/
auto Dictionary::suggest(std::string_view word,
std::vector<std::string>& out) const -> void
{
out.clear();
auto ok_enc = validate_utf8(word);
if (unlikely(word.size() > 360))
return;
if (unlikely(!ok_enc))
return;
suggest_priv(word, out);
}
} // namespace v5
} // namespace nuspell

View File

@@ -1,59 +0,0 @@
/* Copyright 2016-2021 Dimitrij Mijoski
*
* This file is part of Nuspell.
*
* Nuspell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Nuspell is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @file
* @brief Dictionary spelling.
*/
#ifndef NUSPELL_DICTIONARY_HXX
#define NUSPELL_DICTIONARY_HXX
#include "suggester.hxx"
namespace nuspell {
inline namespace v5 {
/**
* @brief The only important public exception
*/
class Dictionary_Loading_Error : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
/**
* @brief The only important public class
*/
class NUSPELL_EXPORT Dictionary : private Suggester {
Dictionary(std::istream& aff, std::istream& dic);
public:
Dictionary();
auto static load_from_aff_dic(std::istream& aff, std::istream& dic)
-> Dictionary;
auto static load_from_path(
const std::string& file_path_without_extension) -> Dictionary;
auto spell(std::string_view word) const -> bool;
auto suggest(std::string_view word, std::vector<std::string>& out) const
-> void;
};
} // namespace v5
} // namespace nuspell
#endif // NUSPELL_DICTIONARY_HXX

View File

@@ -1,18 +0,0 @@
#ifndef NUSPELL_EXPORT_H
#define NUSPELL_EXPORT_H
#ifdef NUSPELL_STATIC_DEFINE
# define NUSPELL_EXPORT
#elif defined(_WIN32) || defined(__CYGWIN__)
# ifdef nuspell_EXPORTS // Define this only when building Nuspell as DLL on Windows, not when using the DLL.
# define NUSPELL_EXPORT __declspec(dllexport)
# else
# define NUSPELL_EXPORT __declspec(dllimport)
# endif
#elif __GNUC__ >= 4
# define NUSPELL_EXPORT __attribute__((visibility("default")))
#else
# define NUSPELL_EXPORT
#endif
#endif /* NUSPELL_EXPORT_H */

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,97 +0,0 @@
/* Copyright 2016-2021 Dimitrij Mijoski
*
* This file is part of Nuspell.
*
* Nuspell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Nuspell is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef NUSPELL_SUGGESTER_HXX
#define NUSPELL_SUGGESTER_HXX
#include "checker.hxx"
namespace nuspell {
inline namespace v5 {
struct NUSPELL_EXPORT Suggester : public Checker {
enum High_Quality_Sugs : bool {
ALL_LOW_QUALITY_SUGS = false,
HAS_HIGH_QUALITY_SUGS = true
};
auto suggest_priv(std::string_view input_word, List_Strings& out) const
-> void;
auto suggest_low(std::string& word, List_Strings& out) const
-> High_Quality_Sugs;
auto add_sug_if_correct(std::string& word, List_Strings& out) const
-> bool;
auto uppercase_suggest(const std::string& word, List_Strings& out) const
-> void;
auto rep_suggest(std::string& word, List_Strings& out) const -> void;
auto try_rep_suggestion(std::string& word, List_Strings& out) const
-> void;
auto max_attempts_for_long_alogs(std::string_view word) const -> size_t;
auto map_suggest(std::string& word, List_Strings& out) const -> void;
auto map_suggest(std::string& word, List_Strings& out, size_t i,
size_t& remaining_attempts) const -> void;
auto adjacent_swap_suggest(std::string& word, List_Strings& out) const
-> void;
auto distant_swap_suggest(std::string& word, List_Strings& out) const
-> void;
auto keyboard_suggest(std::string& word, List_Strings& out) const
-> void;
auto extra_char_suggest(std::string& word, List_Strings& out) const
-> void;
auto forgotten_char_suggest(std::string& word, List_Strings& out) const
-> void;
auto move_char_suggest(std::string& word, List_Strings& out) const
-> void;
auto bad_char_suggest(std::string& word, List_Strings& out) const
-> void;
auto doubled_two_chars_suggest(std::string& word,
List_Strings& out) const -> void;
auto two_words_suggest(const std::string& word, List_Strings& out) const
-> void;
auto ngram_suggest(const std::string& word_u8, List_Strings& out) const
-> void;
auto expand_root_word_for_ngram(Word_List::const_reference root,
std::string_view wrong,
List_Strings& expanded_list,
std::vector<bool>& cross_affix) const
-> void;
};
} // namespace v5
} // namespace nuspell
#endif // NUSPELL_SUGGESTER_HXX

View File

@@ -1,383 +0,0 @@
/* Copyright 2021 Dimitrij Mijoski
*
* This file is part of Nuspell.
*
* Nuspell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Nuspell is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef NUSPELL_UNICODE_HXX
#define NUSPELL_UNICODE_HXX
#include <string>
#include <string_view>
#include <unicode/utf16.h>
#include <unicode/utf8.h>
namespace nuspell {
inline namespace v5 {
// UTF-8, work on malformed
inline constexpr auto u8_max_cp_length = U8_MAX_LENGTH;
auto inline u8_is_cp_error(int32_t cp) -> bool { return cp < 0; }
template <class Range>
auto u8_advance_cp(const Range& str, size_t& i, int32_t& cp) -> void
{
using std::size, std::data;
#if U_ICU_VERSION_MAJOR_NUM <= 60
auto s_ptr = data(str);
int32_t idx = i;
int32_t len = size(str);
U8_NEXT(s_ptr, idx, len, cp);
i = idx;
#else
auto len = size(str);
U8_NEXT(str, i, len, cp);
#endif
}
template <class Range>
auto u8_advance_index(const Range& str, size_t& i) -> void
{
using std::size;
auto len = size(str);
U8_FWD_1(str, i, len);
}
template <class Range>
auto u8_reverse_cp(const Range& str, size_t& i, int32_t& cp) -> void
{
using std::size, std::data;
auto ptr = data(str);
int32_t idx = i;
U8_PREV(ptr, 0, idx, cp);
i = idx;
}
template <class Range>
auto u8_reverse_index(const Range& str, size_t& i) -> void
{
using std::size, std::data;
auto ptr = data(str);
int32_t idx = i;
U8_BACK_1(ptr, 0, idx);
i = idx;
}
template <class Range>
auto u8_write_cp_and_advance(Range& buf, size_t& i, int32_t cp, bool& error)
-> void
{
using std::size, std::data;
#if U_ICU_VERSION_MAJOR_NUM <= 60
auto ptr = data(buf);
int32_t idx = i;
int32_t len = size(buf);
U8_APPEND(buf, idx, len, cp, error);
i = idx;
#else
auto len = size(buf);
U8_APPEND(buf, i, len, cp, error);
#endif
}
// UTF-8, valid
template <class Range>
auto valid_u8_advance_cp(const Range& str, size_t& i, char32_t& cp) -> void
{
U8_NEXT_UNSAFE(str, i, cp);
}
template <class Range>
auto valid_u8_advance_index(const Range& str, size_t& i) -> void
{
U8_FWD_1_UNSAFE(str, i);
}
template <class Range>
auto valid_u8_reverse_cp(const Range& str, size_t& i, char32_t& cp) -> void
{
U8_PREV_UNSAFE(str, i, cp);
}
template <class Range>
auto valid_u8_reverse_index(const Range& str, size_t& i) -> void
{
U8_BACK_1_UNSAFE(str, i);
}
template <class Range>
auto valid_u8_write_cp_and_advance(Range& buf, size_t& i, char32_t cp) -> void
{
U8_APPEND_UNSAFE(buf, i, cp);
}
// UTF-16, work on malformed
inline constexpr auto u16_max_cp_length = U16_MAX_LENGTH;
auto inline u16_is_cp_error(int32_t cp) -> bool { return U_IS_SURROGATE(cp); }
template <class Range>
auto u16_advance_cp(const Range& str, size_t& i, int32_t& cp) -> void
{
using std::size;
auto len = size(str);
U16_NEXT(str, i, len, cp);
}
template <class Range>
auto u16_advance_index(const Range& str, size_t& i) -> void
{
using std::size;
auto len = size(str);
U16_FWD_1(str, i, len);
}
template <class Range>
auto u16_reverse_cp(const Range& str, size_t& i, int32_t& cp) -> void
{
U16_PREV(str, 0, i, cp);
}
template <class Range>
auto u16_reverse_index(const Range& str, size_t& i) -> void
{
U16_BACK_1(str, 0, i);
}
template <class Range>
auto u16_write_cp_and_advance(Range& buf, size_t& i, int32_t cp, bool& error)
-> void
{
using std::size;
auto len = size(buf);
U16_APPEND(buf, i, len, cp, error);
}
// UTF-16, valid
template <class Range>
auto valid_u16_advance_cp(const Range& str, size_t& i, char32_t& cp) -> void
{
U16_NEXT_UNSAFE(str, i, cp);
}
template <class Range>
auto valid_u16_advance_index(const Range& str, size_t& i) -> void
{
U16_FWD_1_UNSAFE(str, i);
}
template <class Range>
auto valid_u16_reverse_cp(const Range& str, size_t& i, char32_t& cp) -> void
{
U16_PREV_UNSAFE(str, i, cp);
}
template <class Range>
auto valid_u16_reverse_index(const Range& str, size_t& i) -> void
{
U16_BACK_1_UNSAFE(str, i);
}
template <class Range>
auto valid_u16_write_cp_and_advance(Range& buf, size_t& i, char32_t cp) -> void
{
U16_APPEND_UNSAFE(buf, i, cp);
}
// higer level funcs
struct U8_CP_Pos {
size_t begin_i = 0;
size_t end_i = begin_i;
};
class U8_Encoded_CP {
char d[u8_max_cp_length];
int sz;
public:
explicit U8_Encoded_CP(std::string_view str, U8_CP_Pos pos)
: sz(pos.end_i - pos.begin_i)
{
auto i = sz;
auto j = pos.end_i;
auto max_len = 4;
do {
d[--i] = str[--j];
} while (i && --max_len);
}
U8_Encoded_CP(char32_t cp)
{
size_t z = 0;
valid_u8_write_cp_and_advance(d, z, cp);
sz = z;
}
auto size() const noexcept -> size_t { return sz; }
auto data() const noexcept -> const char* { return d; }
operator std::string_view() const noexcept
{
return std::string_view(data(), size());
}
auto copy_to(std::string& str, size_t j) const
{
auto i = sz;
j += sz;
auto max_len = 4;
do {
str[--j] = d[--i];
} while (i && --max_len);
}
};
auto inline u8_swap_adjacent_cp(std::string& str, size_t i1, size_t i2,
size_t i3) -> size_t
{
auto cp1 = U8_Encoded_CP(str, {i1, i2});
auto cp2 = U8_Encoded_CP(str, {i2, i3});
auto new_i2 = i1 + std::size(cp2);
cp1.copy_to(str, new_i2);
cp2.copy_to(str, i1);
return new_i2;
}
auto inline u8_swap_cp(std::string& str, U8_CP_Pos pos1, U8_CP_Pos pos2)
-> std::pair<size_t, size_t>
{
using std::size;
auto cp1 = U8_Encoded_CP(str, pos1);
auto cp2 = U8_Encoded_CP(str, pos2);
auto new_p1_end_i = pos1.begin_i + size(cp2);
auto new_p2_begin_i = pos2.end_i - size(cp1);
std::char_traits<char>::move(&str[new_p1_end_i], &str[pos1.end_i],
pos2.begin_i - pos1.end_i);
cp2.copy_to(str, pos1.begin_i);
cp1.copy_to(str, new_p2_begin_i);
return {new_p1_end_i, new_p2_begin_i};
}
// bellow go func without out-parametars
// UTF-8, can be malformed, no out-parametars
struct Idx_And_Next_CP {
size_t end_i;
int32_t cp;
};
struct Idx_And_Prev_CP {
size_t begin_i;
int32_t cp;
};
struct Write_CP_Idx_and_Error {
size_t end_i;
bool error;
};
template <class Range>
[[nodiscard]] auto u8_next_cp(const Range& str, size_t i) -> Idx_And_Next_CP
{
int32_t cp;
u8_advance_cp(str, i, cp);
return {i, cp};
}
template <class Range>
[[nodiscard]] auto u8_next_index(const Range& str, size_t i) -> size_t
{
u8_advance_index(str, i);
return i;
}
template <class Range>
[[nodiscard]] auto u8_prev_cp(const Range& str, size_t i) -> Idx_And_Prev_CP
{
int32_t cp;
u8_reverse_cp(str, i, cp);
return {i, cp};
}
template <class Range>
[[nodiscard]] auto u8_prev_index(const Range& str, size_t i) -> size_t
{
u8_reverse_index(str, i);
return i;
}
template <class Range>
[[nodiscard]] auto u8_write_cp(Range& buf, size_t i, int32_t cp)
-> Write_CP_Idx_and_Error
{
bool err;
u8_write_cp_and_advance(buf, i, cp, err);
return {i, err};
}
// UTF-8, valid, no out-parametars
struct Idx_And_Next_CP_Valid {
size_t end_i;
char32_t cp;
};
struct Idx_And_Prev_CP_Valid {
size_t begin_i;
char32_t cp;
};
template <class Range>
[[nodiscard]] auto valid_u8_next_cp(const Range& str, size_t i)
-> Idx_And_Next_CP_Valid
{
char32_t cp;
valid_u8_advance_cp(str, i, cp);
return {i, cp};
}
template <class Range>
[[nodiscard]] auto valid_u8_next_index(const Range& str, size_t i) -> size_t
{
valid_u8_advance_index(str, i);
return i;
}
template <class Range>
[[nodiscard]] auto valid_u8_prev_cp(const Range& str, size_t i)
-> Idx_And_Prev_CP_Valid
{
char32_t cp;
valid_u8_reverse_cp(str, i, cp);
return {i, cp};
}
template <class Range>
[[nodiscard]] auto valid_u8_prev_index(const Range& str, size_t i) -> size_t
{
valid_u8_reverse_index(str, i);
return i;
}
template <class Range>
[[nodiscard]] auto valid_u8_write_cp(Range& buf, size_t i, int32_t cp) -> size_t
{
valid_u8_write_cp_and_advance(buf, i, cp);
return i;
}
} // namespace v5
} // namespace nuspell
#endif // NUSPELL_UNICODE_HXX

View File

@@ -1,465 +0,0 @@
/* Copyright 2016-2021 Dimitrij Mijoski
*
* This file is part of Nuspell.
*
* Nuspell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Nuspell is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
*/
#include "utils.hxx"
#include "unicode.hxx"
#include <algorithm>
#include <limits>
#include <unicode/uchar.h>
#include <unicode/ucnv.h>
#include <unicode/unistr.h>
#include <unicode/ustring.h>
#if ' ' != 32 || '.' != 46 || 'A' != 65 || 'Z' != 90 || 'a' != 97 || 'z' != 122
#error "Basic execution character set is not ASCII"
#endif
using namespace std;
namespace nuspell {
inline namespace v5 {
template <class SepT>
static auto& split_on_any_of_low(std::string_view s, const SepT& sep,
std::vector<std::string>& out)
{
size_t i1 = 0;
size_t i2;
do {
i2 = s.find_first_of(sep, i1);
out.emplace_back(s.substr(i1, i2 - i1));
i1 = i2 + 1; // we can only add +1 if separator is single char.
// i2 gets s.npos after the last separator.
// Length of i2-i1 will always go past the end. That is defined.
} while (i2 != s.npos);
return out;
}
/**
* @internal
* @brief Splits string on single char seperator.
*
* Consecutive separators are treated as separate and will emit empty strings.
*
* @param s string to split.
* @param sep char that acts as separator to split on.
* @param out vector where separated strings are appended.
* @return @p out.
*/
auto split(std::string_view s, char sep, std::vector<std::string>& out)
-> std::vector<std::string>&
{
return split_on_any_of_low(s, sep, out);
}
/**
* @internal
* @brief Splits string on set of single char seperators.
*
* Consecutive separators are treated as separate and will emit empty strings.
*
* @param s string to split.
* @param sep seperator(s) to split on.
* @param out vector where separated strings are appended.
* @return @p out.
*/
auto split_on_any_of(std::string_view s, const char* sep,
std::vector<std::string>& out) -> std::vector<std::string>&
{
return split_on_any_of_low(s, sep, out);
}
auto utf32_to_utf8(std::u32string_view in, std::string& out) -> void
{
out.clear();
for (size_t i = 0; i != size(in); ++i) {
auto cp = in[i];
auto enc_cp = U8_Encoded_CP(cp);
out += enc_cp;
}
}
auto utf32_to_utf8(std::u32string_view in) -> std::string
{
auto out = string();
utf32_to_utf8(in, out);
return out;
}
auto valid_utf8_to_32(std::string_view in, std::u32string& out) -> void
{
out.clear();
for (size_t i = 0; i != size(in);) {
char32_t cp;
valid_u8_advance_cp(in, i, cp);
out.push_back(cp);
}
}
auto valid_utf8_to_32(std::string_view in) -> std::u32string
{
auto out = u32string();
valid_utf8_to_32(in, out);
return out;
}
auto utf8_to_16(std::string_view in) -> std::u16string
{
auto out = u16string();
utf8_to_16(in, out);
return out;
}
bool utf8_to_16(std::string_view in, std::u16string& out)
{
int32_t len;
auto err = U_ZERO_ERROR;
u_strFromUTF8(data(out), size(out), &len, data(in), size(in), &err);
out.resize(len);
if (err == U_BUFFER_OVERFLOW_ERROR) {
err = U_ZERO_ERROR;
u_strFromUTF8(data(out), size(out), &len, data(in), size(in),
&err);
}
if (U_SUCCESS(err))
return true;
out.clear();
return false;
}
bool validate_utf8(string_view s)
{
auto err = U_ZERO_ERROR;
u_strFromUTF8(nullptr, 0, nullptr, data(s), size(s), &err);
if (err == U_INVALID_CHAR_FOUND)
return false;
return err == U_BUFFER_OVERFLOW_ERROR || U_SUCCESS(err);
}
auto static is_ascii(char c) -> bool
{
return static_cast<unsigned char>(c) <= 127;
}
auto is_all_ascii(std::string_view s) -> bool
{
return all_of(begin(s), end(s), is_ascii);
}
auto static widen_latin1(char c) -> char16_t
{
return static_cast<unsigned char>(c);
}
auto latin1_to_ucs2(std::string_view s) -> std::u16string
{
u16string ret;
latin1_to_ucs2(s, ret);
return ret;
}
auto latin1_to_ucs2(std::string_view s, std::u16string& out) -> void
{
out.resize(s.size());
transform(begin(s), end(s), begin(out), widen_latin1);
}
auto static is_surrogate_pair(char16_t c) -> bool
{
return 0xD800 <= c && c <= 0xDFFF;
}
auto is_all_bmp(std::u16string_view s) -> bool
{
return none_of(begin(s), end(s), is_surrogate_pair);
}
auto to_upper_ascii(std::string& s) -> void
{
auto& char_type = use_facet<ctype<char>>(locale::classic());
char_type.toupper(begin_ptr(s), end_ptr(s));
}
auto static utf32_to_icu(u32string_view in) -> icu::UnicodeString
{
static_assert(sizeof(UChar32) == sizeof(char32_t));
return icu::UnicodeString::fromUTF32(
reinterpret_cast<const UChar32*>(in.data()), in.size());
}
auto static icu_to_utf32(const icu::UnicodeString& in, std::u32string& out)
-> bool
{
out.resize(in.length());
auto err = U_ZERO_ERROR;
auto len =
in.toUTF32(reinterpret_cast<UChar32*>(out.data()), out.size(), err);
if (U_SUCCESS(err)) {
out.erase(len);
return true;
}
out.clear();
return false;
}
auto to_upper(std::string_view in, const icu::Locale& loc) -> std::string
{
auto out = std::string();
to_upper(in, loc, out);
return out;
}
auto to_title(std::string_view in, const icu::Locale& loc) -> std::string
{
auto out = std::string();
to_title(in, loc, out);
return out;
}
auto to_lower(std::string_view in, const icu::Locale& loc) -> std::string
{
auto out = std::string();
to_lower(in, loc, out);
return out;
}
auto to_upper(string_view in, const icu::Locale& loc, string& out) -> void
{
auto sp = icu::StringPiece(data(in), size(in));
auto us = icu::UnicodeString::fromUTF8(sp);
us.toUpper(loc);
out.clear();
us.toUTF8String(out);
}
auto to_title(string_view in, const icu::Locale& loc, string& out) -> void
{
auto sp = icu::StringPiece(data(in), size(in));
auto us = icu::UnicodeString::fromUTF8(sp);
us.toTitle(nullptr, loc);
out.clear();
us.toUTF8String(out);
}
auto to_lower(u32string_view in, const icu::Locale& loc, u32string& out) -> void
{
auto us = utf32_to_icu(in);
us.toLower(loc);
icu_to_utf32(us, out);
}
auto to_lower(string_view in, const icu::Locale& loc, string& out) -> void
{
auto sp = icu::StringPiece(data(in), size(in));
auto us = icu::UnicodeString::fromUTF8(sp);
us.toLower(loc);
out.clear();
us.toUTF8String(out);
}
auto to_lower_char_at(std::string& s, size_t i, const icu::Locale& loc) -> void
{
auto cp = valid_u8_next_cp(s, i);
auto us = icu::UnicodeString(UChar32(cp.cp));
us.toLower(loc);
auto u8_low = string();
us.toUTF8String(u8_low);
s.replace(i, cp.end_i - i, u8_low);
}
auto to_title_char_at(std::string& s, size_t i, const icu::Locale& loc) -> void
{
auto cp = valid_u8_next_cp(s, i);
auto us = icu::UnicodeString(UChar32(cp.cp));
us.toTitle(nullptr, loc);
auto u8_title = string();
us.toUTF8String(u8_title);
s.replace(i, cp.end_i - i, u8_title);
}
/**
* @internal
* @brief Determines casing (capitalization) type for a word.
*
* Casing is sometimes referred to as capitalization.
*
* @param s word.
* @return The casing type.
*/
auto classify_casing(string_view s) -> Casing
{
size_t upper = 0;
size_t lower = 0;
for (size_t i = 0; i != size(s);) {
char32_t c;
valid_u8_advance_cp(s, i, c);
if (u_isupper(c))
upper++;
else if (u_islower(c))
lower++;
// else neutral
}
if (upper == 0) // all lowercase, maybe with some neutral
return Casing::SMALL; // most common case
auto first_cp = valid_u8_next_cp(s, 0);
auto first_capital = u_isupper(first_cp.cp);
if (first_capital && upper == 1)
return Casing::INIT_CAPITAL; // second most common
if (lower == 0)
return Casing::ALL_CAPITAL;
if (first_capital)
return Casing::PASCAL;
else
return Casing::CAMEL;
}
/**
* @internal
* @brief Check if word[i] or word[i-1] are uppercase
*
* Check if the two chars are alphabetic and at least one of them is in
* uppercase.
*
* @return true if at least one is uppercase, false otherwise.
*/
auto has_uppercase_at_compound_word_boundary(string_view word, size_t i) -> bool
{
auto cp = valid_u8_next_cp(word, i);
auto cp_prev = valid_u8_prev_cp(word, i);
if (u_isupper(cp.cp)) {
if (u_isalpha(cp_prev.cp))
return true;
}
else if (u_isupper(cp_prev.cp) && u_isalpha(cp.cp))
return true;
return false;
}
Encoding_Converter::Encoding_Converter(const char* enc)
{
auto err = UErrorCode();
cnv = ucnv_open(enc, &err);
}
Encoding_Converter::~Encoding_Converter()
{
if (cnv)
ucnv_close(cnv);
}
Encoding_Converter::Encoding_Converter(const Encoding_Converter& other)
{
auto err = UErrorCode();
cnv = ucnv_safeClone(other.cnv, nullptr, nullptr, &err);
}
auto Encoding_Converter::operator=(const Encoding_Converter& other)
-> Encoding_Converter&
{
this->~Encoding_Converter();
auto err = UErrorCode();
cnv = ucnv_safeClone(other.cnv, nullptr, nullptr, &err);
return *this;
}
auto Encoding_Converter::to_utf8(string_view in, string& out) -> bool
{
if (ucnv_getType(cnv) == UCNV_UTF8) {
if (validate_utf8(in)) {
out = in;
return true;
}
else {
out.clear();
return false;
}
}
auto err = U_ZERO_ERROR;
auto len = ucnv_toAlgorithmic(UCNV_UTF8, cnv, out.data(), out.size(),
in.data(), in.size(), &err);
out.resize(len);
if (err == U_BUFFER_OVERFLOW_ERROR) {
err = U_ZERO_ERROR;
ucnv_toAlgorithmic(UCNV_UTF8, cnv, out.data(), out.size(),
in.data(), in.size(), &err);
}
return U_SUCCESS(err);
}
auto replace_ascii_char(string& s, char from, char to) -> void
{
for (auto i = s.find(from); i != s.npos; i = s.find(from, i + 1)) {
s[i] = to;
}
}
auto erase_chars(string& s, string_view erase_chars) -> void
{
if (erase_chars.empty())
return;
for (size_t i = 0, next_i = 0; i != size(s); i = next_i) {
valid_u8_advance_index(s, next_i);
auto enc_cp = string_view(&s[i], next_i - i);
if (erase_chars.find(enc_cp) != erase_chars.npos) {
s.erase(i, next_i - i);
next_i = i;
}
}
return;
}
/**
* @internal
* @brief Tests if word is a number.
*
* Allow numbers with dot ".", dash "-" or comma "," inbetween the digits, but
* forbids double separators such as "..", "--" and ".,".
*/
auto is_number(string_view s) -> bool
{
if (s.empty())
return false;
auto it = begin(s);
if (s[0] == '-')
++it;
while (it != end(s)) {
auto next = std::find_if(
it, end(s), [](auto c) { return c < '0' || c > '9'; });
if (next == it)
return false;
if (next == end(s))
return true;
it = next;
auto c = *it;
if (c == '.' || c == ',' || c == '-')
++it;
else
return false;
}
return false;
}
auto count_appereances_of(string_view haystack, string_view needles) -> size_t
{
auto ret = size_t(0);
for (size_t i = 0, next_i = 0; i != size(haystack); i = next_i) {
valid_u8_advance_index(haystack, next_i);
auto enc_cp = string_view(&haystack[i], next_i - i);
ret += needles.find(enc_cp) != needles.npos;
}
return ret;
}
} // namespace v5
} // namespace nuspell

View File

@@ -1,228 +0,0 @@
/* Copyright 2016-2021 Dimitrij Mijoski
*
* This file is part of Nuspell.
*
* Nuspell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Nuspell is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef NUSPELL_UTILS_HXX
#define NUSPELL_UTILS_HXX
#include "nuspell_export.h"
#include <clocale>
#include <locale>
#include <string>
#include <string_view>
#include <vector>
#if !defined(_WIN32) && \
(defined(__unix__) || defined(__unix) || \
(defined(__APPLE__) && defined(__MACH__)) || defined(__HAIKU__))
#include <unistd.h>
#endif
#include <unicode/locid.h>
#ifdef __GNUC__
#define likely(expr) __builtin_expect(!!(expr), 1)
#define unlikely(expr) __builtin_expect(!!(expr), 0)
#else
#define likely(expr) (expr)
#define unlikely(expr) (expr)
#endif
struct UConverter; // unicode/ucnv.h
namespace nuspell {
inline namespace v5 {
auto split(std::string_view s, char sep, std::vector<std::string>& out)
-> std::vector<std::string>&;
NUSPELL_EXPORT auto split_on_any_of(std::string_view s, const char* sep,
std::vector<std::string>& out)
-> std::vector<std::string>&;
NUSPELL_EXPORT auto utf32_to_utf8(std::u32string_view in, std::string& out)
-> void;
NUSPELL_EXPORT auto utf32_to_utf8(std::u32string_view in) -> std::string;
auto valid_utf8_to_32(std::string_view in, std::u32string& out) -> void;
auto valid_utf8_to_32(std::string_view in) -> std::u32string;
auto utf8_to_16(std::string_view in) -> std::u16string;
auto utf8_to_16(std::string_view in, std::u16string& out) -> bool;
auto validate_utf8(std::string_view s) -> bool;
NUSPELL_EXPORT auto is_all_ascii(std::string_view s) -> bool;
NUSPELL_EXPORT auto latin1_to_ucs2(std::string_view s) -> std::u16string;
auto latin1_to_ucs2(std::string_view s, std::u16string& out) -> void;
NUSPELL_EXPORT auto is_all_bmp(std::u16string_view s) -> bool;
auto to_upper_ascii(std::string& s) -> void;
[[nodiscard]] NUSPELL_EXPORT auto to_upper(std::string_view in,
const icu::Locale& loc)
-> std::string;
[[nodiscard]] NUSPELL_EXPORT auto to_title(std::string_view in,
const icu::Locale& loc)
-> std::string;
[[nodiscard]] NUSPELL_EXPORT auto to_lower(std::string_view in,
const icu::Locale& loc)
-> std::string;
auto to_upper(std::string_view in, const icu::Locale& loc, std::string& out)
-> void;
auto to_title(std::string_view in, const icu::Locale& loc, std::string& out)
-> void;
auto to_lower(std::u32string_view in, const icu::Locale& loc,
std::u32string& out) -> void;
auto to_lower(std::string_view in, const icu::Locale& loc, std::string& out)
-> void;
auto to_lower_char_at(std::string& s, size_t i, const icu::Locale& loc) -> void;
auto to_title_char_at(std::string& s, size_t i, const icu::Locale& loc) -> void;
/**
* @internal
* @brief Enum that identifies the casing type of a word.
*
* Neutral characters like numbers are ignored, so "abc" and "abc123abc" are
* both classified as small.
*/
enum class Casing : char {
SMALL,
INIT_CAPITAL,
ALL_CAPITAL,
CAMEL /**< @internal camelCase i.e. mixed case with first small */,
PASCAL /**< @internal PascalCase i.e. mixed case with first capital */
};
NUSPELL_EXPORT auto classify_casing(std::string_view s) -> Casing;
auto has_uppercase_at_compound_word_boundary(std::string_view word, size_t i)
-> bool;
class Encoding_Converter {
UConverter* cnv = nullptr;
public:
Encoding_Converter() = default;
explicit Encoding_Converter(const char* enc);
explicit Encoding_Converter(const std::string& enc)
: Encoding_Converter(enc.c_str())
{
}
~Encoding_Converter();
Encoding_Converter(const Encoding_Converter& other);
Encoding_Converter(Encoding_Converter&& other) noexcept
{
cnv = other.cnv;
cnv = nullptr;
}
auto operator=(const Encoding_Converter& other) -> Encoding_Converter&;
auto operator=(Encoding_Converter&& other) noexcept
-> Encoding_Converter&
{
std::swap(cnv, other.cnv);
return *this;
}
auto to_utf8(std::string_view in, std::string& out) -> bool;
auto valid() -> bool { return cnv != nullptr; }
};
//#if _POSIX_VERSION >= 200809L
#if defined(_POSIX_VERSION) && !defined(__NetBSD__) && !defined(__HAIKU__)
class Setlocale_To_C_In_Scope {
locale_t old_loc = nullptr;
public:
Setlocale_To_C_In_Scope()
: old_loc{uselocale(newlocale(0, "C", nullptr))}
{
}
~Setlocale_To_C_In_Scope()
{
auto new_loc = uselocale(old_loc);
if (new_loc != old_loc)
freelocale(new_loc);
}
Setlocale_To_C_In_Scope(const Setlocale_To_C_In_Scope&) = delete;
};
#else
class Setlocale_To_C_In_Scope {
std::string old_name;
#ifdef _WIN32
int old_per_thread;
#endif
public:
Setlocale_To_C_In_Scope() : old_name(setlocale(LC_ALL, nullptr))
{
#ifdef _WIN32
old_per_thread = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
#endif
auto x = setlocale(LC_ALL, "C");
if (!x)
old_name.clear();
}
~Setlocale_To_C_In_Scope()
{
#ifdef _WIN32
_configthreadlocale(old_per_thread);
if (old_per_thread == _ENABLE_PER_THREAD_LOCALE)
#endif
{
if (!old_name.empty())
setlocale(LC_ALL, old_name.c_str());
}
}
Setlocale_To_C_In_Scope(const Setlocale_To_C_In_Scope&) = delete;
};
#endif
auto replace_ascii_char(std::string& s, char from, char to) -> void;
auto erase_chars(std::string& s, std::string_view erase_chars) -> void;
NUSPELL_EXPORT auto is_number(std::string_view s) -> bool;
auto count_appereances_of(std::string_view haystack, std::string_view needles)
-> size_t;
auto inline begins_with(std::string_view haystack, std::string_view needle)
-> bool
{
return haystack.compare(0, needle.size(), needle) == 0;
}
auto inline ends_with(std::string_view haystack, std::string_view needle)
-> bool
{
return haystack.size() >= needle.size() &&
haystack.compare(haystack.size() - needle.size(), needle.size(),
needle) == 0;
}
template <class T>
auto begin_ptr(T& x)
{
return x.data();
}
template <class T>
auto end_ptr(T& x)
{
return x.data() + x.size();
}
} // namespace v5
} // namespace nuspell
#endif // NUSPELL_UTILS_HXX

View File

@@ -21,12 +21,12 @@ std::string utils::j2std_string(JNIEnv *env, jobject jStr) {
auto cStr = reinterpret_cast<const char *>(env->GetDirectBufferAddress(jStr));
auto size = env->GetDirectBufferCapacity(jStr);
std::string stdStr(cStr, size);
log_debug("spell j2s", stdStr);
utils::log(ANDROID_LOG_DEBUG, "spell j2s", stdStr);
return stdStr;
}
jobject utils::std2j_string(JNIEnv *env, const std::string& stdStr) {
log_debug("spell s2j", stdStr);
utils::log(ANDROID_LOG_DEBUG, "spell s2j", stdStr);
size_t byteCount = stdStr.length();
auto cStr = stdStr.c_str();
auto buffer = env->NewDirectByteBuffer((void *) cStr, byteCount);

View File

@@ -15,67 +15,61 @@
*/
#include <android/log.h>
#include <cerrno>
#include <cstring>
#include <fstream>
#include <iostream>
#include <thread>
#include <unistd.h>
#include "log.h"
void utils::log_debug(const std::string &tag, const std::string &msg) {
__android_log_print(ANDROID_LOG_DEBUG, tag.c_str(), "%s", msg.c_str());
}
void utils::log_info(const std::string &tag, const std::string &msg) {
__android_log_print(ANDROID_LOG_INFO, tag.c_str(), "%s", msg.c_str());
}
void utils::log_warning(const std::string &tag, const std::string &msg) {
__android_log_print(ANDROID_LOG_WARN, tag.c_str(), "%s", msg.c_str());
}
void utils::log_error(const std::string &tag, const std::string &msg) {
__android_log_print(ANDROID_LOG_ERROR, tag.c_str(), "%s", msg.c_str());
}
void utils::log_wtf(const std::string &tag, const std::string &msg) {
__android_log_print(ANDROID_LOG_FATAL, tag.c_str(), "%s", msg.c_str());
void utils::log(int log_priority, const std::string &tag, const std::string &msg) {
__android_log_print(log_priority, tag.c_str(), "%s", msg.c_str());
}
/**
* Code below taken from here:
* Code below based on:
* https://codelab.wordpress.com/2014/11/03/how-to-use-standard-output-streams-for-logging-in-android-apps/
*/
static int pfd[2];
static pthread_t thr;
static const char *tag = "myapp";
static bool already_started = false;
int utils::start_stdout_stderr_logger(const std::string &app_name) {
static bool already_started = false;
if (already_started)
return 0;
static void *thread_func(void*) {
ssize_t rdsz;
char buf[2048];
while ((rdsz = read(pfd[0], buf, sizeof buf - 1)) > 0) {
if (buf[rdsz - 1] == '\n') --rdsz;
buf[rdsz] = 0; /* add null-terminator */
__android_log_write(ANDROID_LOG_DEBUG, tag, buf);
int piperw[2];
if (pipe(piperw) < 0) {
std::string msg = "pipe(): ";
msg += strerror(errno);
utils::log(ANDROID_LOG_ERROR, "stdout/stderr logger", std::ref(msg));
return 1;
}
return nullptr;
}
int utils::start_stdout_stderr_logger(const char *app_name) {
if (already_started) return 0;
already_started = true;
tag = app_name;
/* make stdout line-buffered and stderr unbuffered */
setvbuf(stdout, nullptr, _IOLBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
/* create the pipe and redirect stdout and stderr */
pipe(pfd);
dup2(pfd[1], 1);
dup2(pfd[1], 2);
dup2(piperw[0], STDIN_FILENO);
dup2(piperw[1], STDOUT_FILENO);
dup2(piperw[1], STDERR_FILENO);
close(piperw[0]);
close(piperw[1]);
auto f = [](const std::string &tag) {
std::string buf;
while (std::getline(std::cin, buf)) {
char &back = buf.back();
if (back == '\n')
back = '\0';
utils::log(ANDROID_LOG_DEBUG, tag, std::ref(buf));
}
};
/* spawn the logging thread */
if (pthread_create(&thr, nullptr, thread_func, nullptr) != 0) {
return -1;
}
pthread_detach(thr);
std::thread thr(f, app_name);
thr.detach();
already_started = true;
return 0;
}

View File

@@ -17,17 +17,14 @@
#ifndef FLORISBOARD_LOG_H
#define FLORISBOARD_LOG_H
#include <android/log.h>
#include <string>
namespace utils {
void log_debug(const std::string& tag, const std::string& msg);
void log_info(const std::string& tag, const std::string& msg);
void log_warning(const std::string& tag, const std::string& msg);
void log_error(const std::string& tag, const std::string& msg);
void log_wtf(const std::string& tag, const std::string& msg);
void log(int log_priority, const std::string &tag, const std::string &msg);
int start_stdout_stderr_logger(const char *app_name);
int start_stdout_stderr_logger(const std::string &app_name);
} // namespace utils

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,112 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.ui.settings.about
import android.webkit.URLUtil
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.res.stringRes
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
import dev.patrickgold.florisboard.app.ui.components.florisScrollbar
import dev.patrickgold.florisboard.common.android.launchUrl
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
data class Library(val name: String, val licenseText: String)
@Composable
fun ThirdPartyLicensesScreen() = FlorisScreen {
title = stringRes(R.string.about__third_party_licenses__title)
scrollable = false
iconSpaceReserved = false
val context = LocalContext.current
var dialogLibraryToShow by rememberSaveable {
mutableStateOf<Library?>(null)
}
val libraries = remember {
val list = mutableListOf<Library>()
val licensesData = context.resources
.openRawResource(R.raw.third_party_licenses)
.readBytes()
val licensesMetaDataReader = context.resources
.openRawResource(R.raw.third_party_license_metadata)
.bufferedReader()
licensesMetaDataReader.use { it.readLines() }.map { line ->
val (section, name) = line.split(" ", limit = 2)
val (startOffset, length) = section.split(":", limit = 2).map { it.toInt() }
val licenseData = licensesData.sliceArray(startOffset until startOffset + length)
val licenseText = licenseData.toString(Charsets.UTF_8)
Library(name, licenseText)
}.all { list.add(it) }
list.add(
Library("ICU4C Native C library", "https://github.com/unicode-org/icu/blob/main/icu4c/LICENSE")
)
list.add(
Library("Google Material Icons", "https://www.apache.org/licenses/LICENSE-2.0.txt")
)
list.sortedBy { it.name.lowercase() }.toList()
}
content {
val state = rememberLazyListState()
LazyColumn(
modifier = Modifier
.fillMaxSize()
.florisScrollbar(state = state, isVertical = true),
state = state,
) {
items(libraries) { library ->
val isUrl = URLUtil.isValidUrl(library.licenseText)
Preference(
title = library.name,
onClick = {
if (isUrl) {
context.launchUrl(library.licenseText)
} else {
dialogLibraryToShow = library
}
},
)
}
}
if (dialogLibraryToShow != null) {
JetPrefAlertDialog(
title = dialogLibraryToShow?.name ?: "",
dismissLabel = stringRes(android.R.string.ok),
onDismiss = { dialogLibraryToShow = null },
) {
Text(dialogLibraryToShow?.licenseText ?: "")
}
}
}
}

View File

@@ -1,257 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.ui.settings.spelling
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.res.stringRes
import dev.patrickgold.florisboard.app.ui.components.FlorisDropdownMenu
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
import dev.patrickgold.florisboard.app.ui.components.FlorisStep
import dev.patrickgold.florisboard.app.ui.components.FlorisStepLayout
import dev.patrickgold.florisboard.app.ui.components.FlorisStepState
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.spelling.SpellingExtensionEditor
import dev.patrickgold.florisboard.ime.spelling.SpellingManager
import dev.patrickgold.florisboard.spellingManager
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
private object Step {
const val SelectSource: Int = 1
const val ImportArchive: Int = 2
const val VerifyImport: Int = 3
}
@Composable
fun ImportSpellingArchiveScreen() = FlorisScreen {
title = stringRes(R.string.settings__spelling__import__title)
scrollable = false
val navController = LocalNavController.current
val context = LocalContext.current
val extensionManager by context.extensionManager()
val spellingManager by context.spellingManager()
val sources = remember { listOf("-") + SpellingManager.Config.importSources.map { it.label } }
var sourceExpanded by remember { mutableStateOf(false) }
var sourceSelectedIndex by rememberSaveable { mutableStateOf(0) }
var importArchiveUri by remember { mutableStateOf<Uri?>(null) }
var importArchiveEditor by remember { mutableStateOf<SpellingExtensionEditor?>(null) }
var importArchiveError by remember { mutableStateOf<Throwable?>(null) }
var writeExtError by remember { mutableStateOf<Throwable?>(null) }
var errorDialogVisible by remember { mutableStateOf(false) }
val importArchiveLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(),
onResult = { uri ->
// If uri is null it indicates that the selection activity
// was cancelled (mostly by pressing the back button), so
// we don't display an error message here.
if (uri == null) return@rememberLauncherForActivityResult
val importSource = SpellingManager.Config.importSources[sourceSelectedIndex - 1]
spellingManager.prepareImport(importSource.id, uri).fold(
onSuccess = {
importArchiveUri = uri
importArchiveEditor = it
importArchiveError = null
writeExtError = null
},
onFailure = {
importArchiveUri = null
importArchiveEditor = null
importArchiveError = it
writeExtError = null
},
)
},
)
val stepState = rememberSaveable(saver = FlorisStepState.Saver) {
FlorisStepState.new(init = Step.SelectSource)
}
content {
LaunchedEffect(sourceSelectedIndex, importArchiveEditor) {
stepState.setCurrentAuto(when {
sourceSelectedIndex <= 0 -> Step.SelectSource
importArchiveEditor == null -> Step.ImportArchive
else -> Step.VerifyImport
})
}
FlorisStepLayout(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
stepState = stepState,
steps = listOf(
FlorisStep(
id = Step.SelectSource,
title = if (stepState.getCurrent().value > Step.SelectSource) {
sources.getOrElse(sourceSelectedIndex) { "undefined" }
} else {
stringRes(R.string.settings__spelling__import_archive_s1__title)
},
) {
StepText(
modifier = Modifier.padding(bottom = 16.dp),
text = stringRes(R.string.settings__spelling__import_archive_s1__p1),
)
FlorisDropdownMenu(
items = sources,
expanded = sourceExpanded,
selectedIndex = sourceSelectedIndex,
onSelectItem = { sourceSelectedIndex = it },
onExpandRequest = { sourceExpanded = true },
onDismissRequest = { sourceExpanded = false },
)
},
FlorisStep(
id = Step.ImportArchive,
title = if (stepState.getCurrent().value > Step.ImportArchive && importArchiveEditor != null) {
importArchiveEditor?.meta?.title ?: "undefined"
} else {
stringRes(R.string.settings__spelling__import_archive_s2__title)
},
) {
StepText(
modifier = Modifier.padding(bottom = 16.dp),
text = stringRes(R.string.settings__spelling__import_archive_s2__p1),
)
StepText(
modifier = Modifier.padding(bottom = 16.dp),
text = importArchiveUri?.toString() ?: "No file selected.",
fontStyle = FontStyle.Italic,
)
if (importArchiveError != null) {
ErrorCard(
onActionClick = { errorDialogVisible = true }
)
}
StepButton(
onClick = { importArchiveLauncher.launch("*/*") },
label = stringRes(R.string.action__select_file),
)
},
FlorisStep(
id = Step.VerifyImport,
title = stringRes(R.string.settings__spelling__import_any_s3__title),
) {
StepText(
modifier = Modifier.padding(bottom = 16.dp),
text = stringRes(R.string.settings__spelling__import_any_s3__p1),
)
StepText(
modifier = Modifier.padding(bottom = 16.dp),
// TODO: add verify view
text = "TODO: add verify view",
fontStyle = FontStyle.Italic,
)
if (writeExtError != null) {
ErrorCard(
onActionClick = { errorDialogVisible = true }
)
}
StepButton(
onClick = {
runCatching {
extensionManager.import(importArchiveEditor!!.build())
}.fold(
onSuccess = {
navController.popBackStack()
},
onFailure = {
writeExtError = it
},
)
},
label = stringRes(R.string.action__import),
)
},
),
)
if (errorDialogVisible) {
JetPrefAlertDialog(
title = "Detailed crash log",
onDismiss = { errorDialogVisible = false },
) {
if (importArchiveError != null) {
Text(
text = importArchiveError.toString(),
style = MaterialTheme.typography.body2,
)
}
if (writeExtError != null) {
Text(
text = writeExtError.toString(),
style = MaterialTheme.typography.body2,
)
}
}
}
}
}
@Composable
private fun ErrorCard(
onActionClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Card(modifier = modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(start = 16.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.weight(1.0f)
.padding(end = 8.dp),
text = "Something went wrong!",
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
TextButton(onClick = { onActionClick() }) {
Text(text = "Details")
}
}
}
}

View File

@@ -1,81 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.ui.settings.spelling
import androidx.compose.foundation.layout.padding
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.res.stringRes
import dev.patrickgold.florisboard.app.ui.Routes
import dev.patrickgold.florisboard.app.ui.components.FlorisInfoCard
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
import dev.patrickgold.florisboard.app.ui.ext.ExtensionList
import dev.patrickgold.florisboard.extensionManager
@Composable
fun ManageSpellingDictsScreen() = FlorisScreen {
title = stringRes(R.string.settings__spelling__manage_dicts__title)
previewFieldVisible = true
val navController = LocalNavController.current
val context = LocalContext.current
val extensionManager by context.extensionManager()
floatingActionButton {
FloatingActionButton(
onClick = { navController.navigate(Routes.Settings.ImportSpellingArchive) },
) {
Icon(
painter = painterResource(R.drawable.ic_add),
contentDescription = "Add dictionary",
)
}
}
content {
FlorisInfoCard(
modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 16.dp),
text = stringRes(R.string.settings__spelling__dict_sources_info__title),
onClick = { navController.navigate(Routes.Settings.SpellingInfo) },
)
val spellingDicts by extensionManager.spellingDicts.observeAsState()
if (spellingDicts != null && spellingDicts!!.isNotEmpty()) {
ExtensionList(
extList = spellingDicts!!,
summaryProvider = { ext ->
"${ext.spelling.locale.languageTag()} | ${ext.meta.version} | ${ext.spelling.originalSourceId}"
},
)
} else {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringRes(R.string.settings__spelling__manage_dicts__no_dicts_installed),
)
}
}
}

View File

@@ -1,58 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.ui.settings.spelling
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.res.stringRes
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
import dev.patrickgold.florisboard.common.android.launchUrl
import dev.patrickgold.florisboard.ime.spelling.SpellingManager
import dev.patrickgold.jetpref.datastore.ui.Preference
@Composable
fun SpellingInfoScreen() = FlorisScreen {
title = stringRes(R.string.settings__spelling__dict_sources_info__title)
iconSpaceReserved = false
val context = LocalContext.current
content {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringRes(R.string.settings__spelling__dict_sources_info__intro),
)
for (source in SpellingManager.Config.importSources) {
Preference(
title = source.label,
summary = source.url,
onClick = {
source.url?.let { context.launchUrl(it) }
},
)
}
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringRes(R.string.settings__spelling__dict_sources_info__other),
)
}
}

View File

@@ -1,193 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.ui.settings.spelling
import android.content.ComponentName
import android.content.Intent
import android.graphics.drawable.Drawable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.res.stringRes
import dev.patrickgold.florisboard.app.ui.Routes
import dev.patrickgold.florisboard.app.ui.components.FlorisCanvasIcon
import dev.patrickgold.florisboard.app.ui.components.FlorisErrorCard
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
import dev.patrickgold.florisboard.app.ui.components.FlorisSimpleCard
import dev.patrickgold.florisboard.app.ui.components.FlorisWarningCard
import dev.patrickgold.florisboard.common.android.AndroidSettings
import dev.patrickgold.florisboard.common.android.launchActivity
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.spelling.SpellingLanguageMode
import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
@Composable
fun SpellingScreen() = FlorisScreen {
title = stringRes(R.string.settings__spelling__title)
previewFieldVisible = true
val navController = LocalNavController.current
val context = LocalContext.current
val extensionManager by context.extensionManager()
val systemSpellCheckerId by AndroidSettings.Secure.observeAsState(
key = "selected_spell_checker",
foregroundOnly = true,
)
val systemSpellCheckerEnabled by AndroidSettings.Secure.observeAsState(
key = "spell_checker_enabled",
foregroundOnly = true,
)
val systemSpellCheckerSubtypeIndex by AndroidSettings.Secure.observeAsState(
key = "selected_spell_checker_subtype",
foregroundOnly = true,
)
val systemSpellCheckerPkgName = remember(systemSpellCheckerId) {
runCatching {
ComponentName.unflattenFromString(systemSpellCheckerId!!)!!.packageName
}.getOrDefault("null")
}
val openSystemSpellCheckerSettings = {
val componentToLaunch = ComponentName(
"com.android.settings",
"com.android.settings.Settings\$SpellCheckersSettingsActivity",
)
context.launchActivity {
it.addCategory(Intent.CATEGORY_DEFAULT)
it.component = componentToLaunch
}
}
val florisSpellCheckerEnabled =
systemSpellCheckerEnabled == "1" &&
systemSpellCheckerPkgName == context.packageName &&
systemSpellCheckerSubtypeIndex != "0"
content {
PreferenceGroup(title = stringRes(R.string.pref__spelling__active_spellchecker__label)) {
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
if (systemSpellCheckerEnabled == "1") {
if (systemSpellCheckerId == null) {
FlorisWarningCard(
text = stringRes(R.string.pref__spelling__active_spellchecker__summary_none),
onClick = openSystemSpellCheckerSettings,
)
} else {
var spellCheckerIcon: Drawable?
var spellCheckerLabel = "Unknown"
try {
val pm = context.packageManager
val remoteAppInfo = pm.getApplicationInfo(systemSpellCheckerPkgName, 0)
spellCheckerIcon = pm.getApplicationIcon(remoteAppInfo)
spellCheckerLabel = pm.getApplicationLabel(remoteAppInfo).toString()
} catch (e: Exception) {
spellCheckerIcon = null
}
FlorisSimpleCard(
icon = {
if (spellCheckerIcon != null) {
FlorisCanvasIcon(
modifier = Modifier
.padding(end = 8.dp)
.requiredSize(32.dp),
drawable = spellCheckerIcon,
)
} else {
Icon(
modifier = Modifier
.padding(end = 8.dp)
.requiredSize(32.dp),
painter = painterResource(R.drawable.ic_help_outline),
contentDescription = null,
)
}
},
text = spellCheckerLabel,
secondaryText = systemSpellCheckerPkgName,
contentPadding = PaddingValues(all = 8.dp),
onClick = openSystemSpellCheckerSettings,
)
if (systemSpellCheckerPkgName == context.packageName && systemSpellCheckerSubtypeIndex == "0") {
FlorisWarningCard(
modifier = Modifier.padding(top = 8.dp),
text = stringRes(
R.string.pref__spelling__active_spellchecker__summary_use_sys_lang_set,
"use_floris_config" to stringRes(R.string.settings__spelling__use_floris_config),
),
onClick = openSystemSpellCheckerSettings,
)
}
}
} else {
FlorisErrorCard(
text = stringRes(R.string.pref__spelling__active_spellchecker__summary_disabled),
onClick = openSystemSpellCheckerSettings,
)
}
}
}
val spellingDicts by extensionManager.spellingDicts.observeAsState()
Preference(
iconId = R.drawable.ic_library_books,
title = stringRes(R.string.settings__spelling__manage_dicts__title),
summary = stringRes(
R.string.settings__spelling__manage_dicts__n_installed,
"n" to (spellingDicts?.size ?: 0).toString(),
),
onClick = { navController.navigate(Routes.Settings.ManageSpellingDicts) },
enabledIf = { florisSpellCheckerEnabled },
)
PreferenceGroup(title = stringRes(R.string.pref__spelling__group_spellchecker_config__title)) {
ListPreference(
prefs.spelling.languageMode,
iconId = R.drawable.ic_language,
title = stringRes(R.string.pref__spelling__language_mode__label),
entries = SpellingLanguageMode.listEntries(),
enabledIf = { florisSpellCheckerEnabled },
)
SwitchPreference(
prefs.spelling.useContacts,
iconId = R.drawable.ic_contacts,
title = stringRes(R.string.pref__spelling__use_contacts__label),
summary = stringRes(R.string.pref__spelling__use_contacts__summary),
enabledIf = { florisSpellCheckerEnabled },
)
SwitchPreference(
prefs.spelling.useUdmEntries,
iconId = R.drawable.ic_library_books,
title = stringRes(R.string.pref__spelling__use_udm_entries__label),
summary = stringRes(R.string.pref__spelling__use_udm_entries__summary),
enabledIf = { florisSpellCheckerEnabled },
)
}
}
}

View File

@@ -1,106 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.ui.settings.typing
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.res.stringRes
import dev.patrickgold.florisboard.app.ui.components.FlorisErrorCard
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
import dev.patrickgold.florisboard.common.android.AndroidVersion
import dev.patrickgold.florisboard.ime.text.smartbar.CandidatesDisplayMode
import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
@OptIn(ExperimentalJetPrefDatastoreUi::class)
@Composable
fun TypingScreen() = FlorisScreen {
title = stringRes(R.string.settings__typing__title)
previewFieldVisible = true
content {
PreferenceGroup(title = stringRes(R.string.pref__suggestion__title)) {
SwitchPreference(
prefs.suggestion.api30InlineSuggestionsEnabled,
title = stringRes(R.string.pref__suggestion__api30_inline_suggestions_enabled__label),
summary = stringRes(R.string.pref__suggestion__api30_inline_suggestions_enabled__summary),
visibleIf = { AndroidVersion.ATLEAST_API30_R },
)
// This card is temporary and is therefore not using a string resource
FlorisErrorCard(
modifier = Modifier.padding(8.dp),
text = if (AndroidVersion.ATLEAST_API30_R) {
"Suggestions (except autofill) are not available in this release"
} else {
"Suggestions are not available in this release"
},
)
SwitchPreference(
prefs.suggestion.enabled,
title = stringRes(R.string.pref__suggestion__enabled__label),
//summary = stringRes(R.string.pref__suggestion__enabled__summary),
)
ListPreference(
prefs.suggestion.displayMode,
title = stringRes(R.string.pref__suggestion__display_mode__label),
entries = CandidatesDisplayMode.listEntries(),
enabledIf = { prefs.suggestion.enabled isEqualTo true },
)
SwitchPreference(
prefs.suggestion.clipboardContentEnabled,
title = stringRes(R.string.pref__suggestion__clipboard_content_enabled__label),
summary = stringRes(R.string.pref__suggestion__clipboard_content_enabled__summary),
enabledIf = { prefs.suggestion.enabled isEqualTo true },
)
DialogSliderPreference(
prefs.suggestion.clipboardContentTimeout,
title = stringRes(R.string.pref__suggestion__clipboard_content_timeout__label),
unit = stringRes(R.string.unit__seconds__symbol),
min = 30,
max = 300,
stepIncrement = 5,
enabledIf = {
(prefs.suggestion.enabled isEqualTo true) && (prefs.suggestion.clipboardContentEnabled isEqualTo true)
}
)
}
PreferenceGroup(title = stringRes(R.string.pref__correction__title)) {
SwitchPreference(
prefs.correction.autoCapitalization,
title = stringRes(R.string.pref__correction__auto_capitalization__label),
summary = stringRes(R.string.pref__correction__auto_capitalization__summary),
)
SwitchPreference(
prefs.correction.rememberCapsLockState,
title = stringRes(R.string.pref__correction__remember_caps_lock_state__label),
summary = stringRes(R.string.pref__correction__remember_caps_lock_state__summary),
)
SwitchPreference(
prefs.correction.doubleSpacePeriod,
title = stringRes(R.string.pref__correction__double_space_period__label),
summary = stringRes(R.string.pref__correction__double_space_period__summary),
)
}
}
}

View File

@@ -1,67 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.ui.splash
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
import dev.patrickgold.florisboard.app.ui.Routes
import dev.patrickgold.florisboard.app.ui.components.FlorisCanvasIcon
import dev.patrickgold.florisboard.app.ui.components.LocalPreviewFieldController
import dev.patrickgold.jetpref.datastore.model.observeAsState
@Composable
fun SplashScreen() = Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
val prefs by florisPreferenceModel()
val isModelLoaded by prefs.datastoreReadyStatus.observeAsState()
val isImeSetUp by prefs.internal.isImeSetUp.observeAsState()
val navController = LocalNavController.current
val previewFieldController = LocalPreviewFieldController.current
SideEffect {
previewFieldController?.isVisible = false
}
LaunchedEffect(isModelLoaded) {
if (isModelLoaded) {
navController.navigate(if (isImeSetUp) Routes.Settings.Home else Routes.Setup.Screen) {
popUpTo(Routes.Splash.Screen) {
inclusive = true
}
}
}
}
FlorisCanvasIcon(
modifier = Modifier.requiredSize(92.dp),
iconId = R.mipmap.floris_app_icon,
contentDescription = "FlorisBoard app icon",
)
}

View File

@@ -1,19 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.common.android
typealias AndroidAssetsManager = android.content.res.AssetManager

View File

@@ -1,368 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.core
import android.os.SystemClock
import androidx.collection.SparseArrayCompat
import androidx.collection.forEach
import androidx.collection.set
import dev.patrickgold.florisboard.debug.LogTopic
import dev.patrickgold.florisboard.debug.flogDebug
import dev.patrickgold.florisboard.ime.keyboard.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
/**
* The main logic point of processing input events and delegating them to the registered event receivers. Currently,
* only [InputKeyEvent]s are supported, but in the future this class is thought to be the single point where input
* events can be dispatched.
*/
class InputEventDispatcher private constructor(
channelCapacity: Int,
private val mainDispatcher: CoroutineDispatcher,
private val defaultDispatcher: CoroutineDispatcher,
private val repeatableKeyCodes: IntArray
) : InputKeyEventSender {
private val channel: Channel<InputKeyEvent> = Channel(channelCapacity)
private val mainScope: CoroutineScope = CoroutineScope(mainDispatcher + SupervisorJob())
private val defaultScope: CoroutineScope = CoroutineScope(defaultDispatcher + SupervisorJob())
private val pressedKeys: SparseArrayCompat<PressedKeyInfo> = SparseArrayCompat()
var lastKeyEventDown: InputKeyEvent? = null
private set
var lastKeyEventUp: InputKeyEvent? = null
private set
/**
* The input key event register. If null, the dispatcher will still process input, but won't dispatch them to an
* event receiver.
*/
var keyEventReceiver: InputKeyEventReceiver? = null
companion object {
/**
* The default input event channel capacity to be used in [new].
*/
private const val DEFAULT_CHANNEL_CAPACITY: Int = 64
/**
* Creates a new [InputEventDispatcher] instance from given arguments and returns it.
*
* @param channelCapacity The capacity of this input channel, defaults to [DEFAULT_CHANNEL_CAPACITY].
* @param mainDispatcher The main dispatcher used to switch the context to call the receiver callbacks.
* Defaults to [Dispatchers.Main].
* @param defaultDispatcher The default dispatcher used to switch the context to call the receiver callbacks.
* Defaults to [Dispatchers.Default].
* @param repeatableKeyCodes An int array of all key codes which are repeatable while being pressed down.
*
* @return A new [InputEventDispatcher] instance initialized with given arguments.
*/
fun new(
channelCapacity: Int = DEFAULT_CHANNEL_CAPACITY,
mainDispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
repeatableKeyCodes: IntArray = intArrayOf()
): InputEventDispatcher = InputEventDispatcher(
channelCapacity, mainDispatcher, defaultDispatcher, repeatableKeyCodes.clone()
)
private fun <T> SparseArrayCompat<T>.removeAndReturn(key: Int): T? {
val elem = get(key)
return if (elem == null) {
null
} else {
remove(key)
elem
}
}
}
init {
defaultScope.launch {
for (ev in channel) {
if (!isActive) break
val startTime = System.nanoTime()
flogDebug(LogTopic.KEY_EVENTS) { ev.toString() }
when (ev.action) {
InputKeyEvent.Action.DOWN -> {
if (pressedKeys.indexOfKey(ev.data.code) >= 0) continue
pressedKeys[ev.data.code] = PressedKeyInfo(
eventTimeDown = ev.eventTime,
repeatKeyPressJob = if (!repeatableKeyCodes.contains(ev.data.code)) { null } else {
defaultScope.launch {
delay(600)
while (isActive) {
channel.send(InputKeyEvent.repeat(ev.data))
delay(50)
}
}
}
)
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyDown(ev)
}
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
lastKeyEventDown = ev
}
}
InputKeyEvent.Action.DOWN_UP -> {
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyDown(ev)
keyEventReceiver?.onInputKeyUp(ev)
}
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
lastKeyEventDown = ev
lastKeyEventUp = ev
}
}
InputKeyEvent.Action.UP -> {
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyUp(ev)
}
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
lastKeyEventUp = ev
}
}
InputKeyEvent.Action.REPEAT -> {
if (pressedKeys.indexOfKey(ev.data.code) >= 0) {
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyRepeat(ev)
}
}
}
InputKeyEvent.Action.CANCEL -> {
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyCancel(ev)
}
}
}
flogDebug(LogTopic.KEY_EVENTS) { "Time elapsed: ${(System.nanoTime() - startTime) / 1_000_000}" }
}
pressedKeys.forEach { _, value -> value.repeatKeyPressJob?.cancel() }
pressedKeys.clear()
}
}
override fun send(ev: InputKeyEvent) {
channel.trySend(ev)
}
/**
* Checks if there's currently a key down with given [code].
*
* @param code The key code to check for.
*
* @return True if the given [code] is currently down, false otherwise.
*/
fun isPressed(code: Int): Boolean {
return pressedKeys.indexOfKey(code) >= 0
}
/**
* Closes this dispatcher and cancels the local coroutine scope.
*/
fun close() {
keyEventReceiver = null
mainScope.cancel()
defaultScope.cancel()
}
data class PressedKeyInfo(
val eventTimeDown: Long,
val repeatKeyPressJob: Job?
)
}
/**
* Data class representing a single input key event.
*
* @property eventTime The exact event time when this event occurred, measured in milliseconds since a static point in
* the past. The exact point is irrelevant, but while this input dispatcher is active, the point must not change in
* order for difference time calculation to succeed.
* @property action The action of this event.
* @property data The data of this event.
* @property count The count how often this event occurred. Is only respected by other methods if the [action] of this
* event is [Action.DOWN_UP] or [Action.REPEAT], else always 1 is assumed.
*/
data class InputKeyEvent(
val eventTime: Long,
val action: Action,
val data: KeyData,
val count: Int
) {
companion object {
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.DOWN].
*
* @param keyData The key data of the input key event event to create.
*
* @return The created input key event.
*/
fun down(keyData: KeyData): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.DOWN,
data = keyData,
count = 1
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.DOWN_UP].
*
* @param keyData The key data of the input key event event to create.
* @param count How often this event occurred. Must be grater or equal to 1, defaults to 1.
*
* @return The created input key event.
*/
fun downUp(keyData: KeyData, count: Int = 1): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.DOWN_UP,
data = keyData,
count = count
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.UP].
*
* @param keyData The key data of the input key event event to create.
*
* @return The created input key event.
*/
fun up(keyData: KeyData): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.UP,
data = keyData,
count = 1
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.REPEAT].
*
* @param keyData The key data of the input key event event to create.
* @param count How often this event occurred. Must be grater or equal to 1, defaults to 1.
*
* @return The created input key event.
*/
fun repeat(keyData: KeyData, count: Int = 1): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.REPEAT,
data = keyData,
count = count
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.CANCEL].
*
* @param keyData The key data of the input key event event to create.
*
* @return The created input key event.
*/
fun cancel(keyData: KeyData): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.CANCEL,
data = keyData,
count = 1
)
}
}
/**
* Checks if the [other] input key event is a consecutive event while respecting [maxEventTimeDiff].
*
* @param other The other input key event to compare with this one.
* @param maxEventTimeDiff The maximum event time diff between this event and [other], in milliseconds.
*
* @return True if this event is a consecutive event of [other], false otherwise.
*/
fun isConsecutiveEventOf(other: InputKeyEvent?, maxEventTimeDiff: Long): Boolean {
return other != null && data.code == other.data.code && eventTime - other.eventTime <= maxEventTimeDiff
}
/**
* Returns a string representation of this input key event.
*/
override fun toString(): String {
return "FlorisKeyEvent { eventTime=${eventTime}ms, action=$action, data=$data, count=$count }"
}
/**
* The action of an input key event.
*/
enum class Action {
DOWN,
DOWN_UP,
UP,
REPEAT,
CANCEL,
}
}
/**
* Interface which represents an input key event sender.
*/
interface InputKeyEventSender {
/**
* Sends given input key event [ev] to the underlying input channel, awaiting to be processed.
*
* @param ev The input key event to send.
*/
fun send(ev: InputKeyEvent)
}
/**
* Interface which represents an input key event receiver.
*/
interface InputKeyEventReceiver {
/**
* Event method which gets called when a key went down.
*
* @param ev The associated input key event.
*/
fun onInputKeyDown(ev: InputKeyEvent)
/**
* Event method which gets called when a key went up.
*
* @param ev The associated input key event.
*/
fun onInputKeyUp(ev: InputKeyEvent)
/**
* Event method which gets called when a key is called repeatedly while being pressed down.
*
* @param ev The associated input key event.
*/
fun onInputKeyRepeat(ev: InputKeyEvent)
/**
* Event method which gets called when a key press is cancelled.
*
* @param ev The associated input key event.
*/
fun onInputKeyCancel(ev: InputKeyEvent)
}

View File

@@ -1,54 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.dictionary
import dev.patrickgold.florisboard.res.Asset
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
import dev.patrickgold.florisboard.ime.nlp.Word
/**
* Standardized dictionary interface for interacting with dictionaries.
*/
interface Dictionary : Asset {
/**
* Gets token predictions based on the given [precedingTokens] and the [currentToken]. The
* length of the returned list is limited to [maxSuggestionCount]. Note that the returned list
* may at any time give back less items than [maxSuggestionCount] indicates.
*/
fun getTokenPredictions(
precedingTokens: List<Word>,
currentToken: Word?,
maxSuggestionCount: Int,
allowPossiblyOffensive: Boolean,
destSuggestionList: SuggestionList
)
fun getDate(): Long
fun getVersion(): Int
}
interface MutableDictionary : Dictionary {
fun trainTokenPredictions(
precedingTokens: List<Word>,
lastToken: Word
)
fun setDate(date: Int)
fun setVersion(version: Int)
}

View File

@@ -1,426 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.dictionary
import android.content.Context
import dev.patrickgold.florisboard.ime.nlp.*
import dev.patrickgold.florisboard.res.FlorisRef
import java.io.InputStream
/**
* Class Flictionary which takes care of loading the binary asset as well as providing words for
* queries.
*
* This class accepts binary dictionary files of the type "flict" as defined in here:
* https://github.com/florisboard/dictionary-tools/blob/main/flictionary.md
*/
class Flictionary private constructor(
override val name: String,
override val label: String,
override val authors: List<String>,
private val date: Long,
private val version: Int,
private val headerStr: String,
private val languageModel: FlorisLanguageModel
) : Dictionary {
companion object {
private const val VERSION_0 = 0x0
private const val MASK_BEGIN_PTREE_NODE = 0x80
private const val CMDB_BEGIN_PTREE_NODE = 0x00
private const val ATTR_PTREE_NODE_ORDER = 0x70
private const val ATTR_PTREE_NODE_TYPE = 0x0C
private const val ATTR_PTREE_NODE_TYPE_CHAR = 0
private const val ATTR_PTREE_NODE_TYPE_WORD_FILLER = 1
private const val ATTR_PTREE_NODE_TYPE_WORD = 2
private const val ATTR_PTREE_NODE_TYPE_SHORTCUT = 3
private const val ATTR_PTREE_NODE_SIZE = 0x03
private const val MASK_END = 0xC0
private const val CMDB_END = 0x80
private const val ATTR_END_COUNT = 0x3F
private const val MASK_BEGIN_HEADER = 0xE0
private const val CMDB_BEGIN_HEADER = 0xC0
private const val ATTR_HEADER_VERSION = 0x1F
private const val MASK_DEFINE_SHORTCUT = 0xF0
private const val CMDB_DEFINE_SHORTCUT = 0xE0
/**
* Loads a Flictionary binary asset from given [assetRef] and returns a result containing
* either the parsed dictionary or an exception giving information about the error which
* occurred.
*/
fun load(context: Context, assetRef: FlorisRef): Result<Flictionary> {
val buffer = ByteArray(5000) { 0 }
val inputStream: InputStream
if (assetRef.isAssets) {
inputStream = context.assets.open(assetRef.relativePath)
} else {
return Result.failure(Exception("Only AssetSource.Assets is currently supported!"))
}
var headerStr: String? = null
var date: Long = 0
var version = 0
val ngramTree = NgramTree()
var pos = 0
val ngramOrderStack = mutableListOf<Int>()
val ngramTreeStack = mutableListOf<NgramNode>()
while (true) {
if (inputStream.readNext(buffer, 0, 1) <= 0) {
break
}
val cmd = buffer[0].toInt() and 0xFF
when {
(cmd and MASK_BEGIN_PTREE_NODE) == CMDB_BEGIN_PTREE_NODE -> {
if (pos == 0) {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_PTREE_NODE,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
val order = ((cmd and ATTR_PTREE_NODE_ORDER) shr 4) + 1
val type = ((cmd and ATTR_PTREE_NODE_TYPE) shr 2)
val size = (cmd and ATTR_PTREE_NODE_SIZE) + 1
val freq: Int
val freqSize: Int
when (type) {
ATTR_PTREE_NODE_TYPE_CHAR -> {
freq = NgramNode.FREQ_CHARACTER
freqSize = 0
}
ATTR_PTREE_NODE_TYPE_WORD_FILLER -> {
freq = NgramNode.FREQ_WORD_FILLER
freqSize = 0
}
ATTR_PTREE_NODE_TYPE_WORD -> {
if (inputStream.readNext(buffer, 1, 1) > 0) {
freq = buffer[1].toInt() and 0xFF
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
freqSize = 1
}
else -> return Result.failure(Exception("TODO: shortcut not supported"))
}
if (inputStream.readNext(buffer, freqSize + 1, size) > 0) {
val char = String(buffer, freqSize + 1, size, Charsets.UTF_8)[0]
val node = NgramNode(order, char, freq)
val lastOrder = ngramOrderStack.lastOrNull()
if (lastOrder == null) {
ngramTree.higherOrderChildren.add(node)
} else {
if (lastOrder == order) {
ngramTreeStack.last().sameOrderChildren.add(node)
} else {
ngramTreeStack.last().higherOrderChildren.add(node)
}
}
ngramOrderStack.add(order)
ngramTreeStack.add(node)
pos += (freqSize + 1 + size)
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
}
(cmd and MASK_BEGIN_HEADER) == CMDB_BEGIN_HEADER -> {
if (pos != 0) {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_HEADER,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
version = cmd and ATTR_HEADER_VERSION
if (version != VERSION_0) {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNSUPPORTED_FLICTIONARY_VERSION,
address = pos,
cmdByte = cmd.toByte(),
absoluteDepth = ngramTreeStack.size
)
)
}
if (inputStream.readNext(buffer, 1, 9) > 0) {
val size = (buffer[1].toInt() and 0xFF)
date =
((buffer[2].toInt() and 0xFF).toLong() shl 56) +
((buffer[3].toInt() and 0xFF).toLong() shl 48) +
((buffer[4].toInt() and 0xFF).toLong() shl 40) +
((buffer[5].toInt() and 0xFF).toLong() shl 32) +
((buffer[6].toInt() and 0xFF).toLong() shl 24) +
((buffer[7].toInt() and 0xFF).toLong() shl 16) +
((buffer[8].toInt() and 0xFF).toLong() shl 8) +
((buffer[9].toInt() and 0xFF).toLong() shl 0)
if (inputStream.readNext(buffer, 10, size) > 0) {
headerStr = String(buffer, 10, size, Charsets.UTF_8)
ngramOrderStack.add(-1)
ngramTreeStack.add(NgramTree())
pos += (10 + size)
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
}
(cmd and MASK_END) == CMDB_END -> {
if (pos == 0) {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
val n = (cmd and ATTR_END_COUNT)
if (n > 0) {
if (n <= ngramTreeStack.size) {
for (c in 0 until n) {
ngramOrderStack.removeLast()
ngramTreeStack.removeLast()
}
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size - n
)
)
}
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END_ZERO_VALUE,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
pos += 1
}
else -> {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.INVALID_CMD_BYTE_PROVIDED,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
}
}
inputStream.close()
if (ngramTreeStack.size != 0) {
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF,
address = pos, cmdByte = 0x00.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
return Result.success(
Flictionary(
name = "flict",
label = "flict",
authors = listOf(),
headerStr = headerStr ?: "",
date = date,
version = version,
languageModel = FlorisLanguageModel(ngramTree)
)
)
}
}
override fun getDate(): Long = date
override fun getVersion(): Int = version
// TODO: preceding tokens are currently ignored
override fun getTokenPredictions(
precedingTokens: List<Word>,
currentToken: Word?,
maxSuggestionCount: Int,
allowPossiblyOffensive: Boolean,
destSuggestionList: SuggestionList
) {
currentToken ?: return
if (currentToken.isNotBlank()) {
val retList = languageModel.matchAllNgrams(
ngram = Ngram(
_tokens = listOf(Token(currentToken.lowercase())),
_freq = -1
),
maxEditDistance = 2,
maxTokenCount = maxSuggestionCount,
allowPossiblyOffensive = allowPossiblyOffensive
)
retList.forEach { destSuggestionList.add(it.data, 128) }
}
}
/**
* A parse exception to be used by [Flictionary] to indicate where the parsing of a binary file
* failed, while also providing some additional information.
*/
class ParseException(
private val errorType: ErrorType,
private val address: Int,
private val cmdByte: Byte,
private val absoluteDepth: Int
) : Exception() {
enum class ErrorType {
UNSUPPORTED_FLICTIONARY_VERSION,
UNEXPECTED_CMD_BEGIN_HEADER,
UNEXPECTED_CMD_BEGIN_PTREE_NODE,
UNEXPECTED_CMD_DEFINE_SHORTCUT,
UNEXPECTED_CMD_END,
UNEXPECTED_CMD_END_ZERO_VALUE,
UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO,
UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF,
UNEXPECTED_EOF,
INVALID_CMD_BYTE_PROVIDED,
}
override val message: String
get() = toString()
override fun toString(): String {
return StringBuilder().run {
append(
when (errorType) {
ErrorType.UNSUPPORTED_FLICTIONARY_VERSION -> {
"Unexpected Flictionary version: ${(cmdByte.toInt() and 0xFF) and ATTR_HEADER_VERSION}"
}
ErrorType.UNEXPECTED_CMD_BEGIN_HEADER -> {
"Unexpected command: BEGIN_HEADER"
}
ErrorType.UNEXPECTED_CMD_BEGIN_PTREE_NODE -> {
"Unexpected command: BEGIN_PTREE_NODE"
}
ErrorType.UNEXPECTED_CMD_DEFINE_SHORTCUT -> {
"Unexpected command: DEFINE_SHORTCUT"
}
ErrorType.UNEXPECTED_CMD_END -> {
"Unexpected command: END"
}
ErrorType.UNEXPECTED_CMD_END_ZERO_VALUE -> {
"Unexpected zero value provided for command END"
}
ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO -> {
"Unexpected decrease in absolute depth: cannot go below zero"
}
ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF -> {
"Unexpected non-zero value in absolute depth at end of file"
}
ErrorType.UNEXPECTED_EOF -> {
"Unexpected end of file while try to do look-ahead"
}
ErrorType.INVALID_CMD_BYTE_PROVIDED -> {
"Invalid command byte provided"
}
}
)
append(
String.format(
"\n at address 0x%08X where cmd_byte=0x%02X and section_depth=%d",
address,
cmdByte,
absoluteDepth
)
)
toString()
}
}
}
}
/**
* Reads the next [len] bytes from the input stream into the given byte array [b]. This method guarantees to either
* read the full length requested or if an EOF file is encountered, -1 is returned. The first byte written is at
* `b[off]`, the second byte at `b[off+1]` and so on.
*
* @param b The byte array to read the next [len] bytes into.
* @param off The offset of the first byte written in the byte array [b]. Must be non-negative.
* @param len The number of bytes to read. Must be non-negative.
*
* @return The number of bytes read, always matching [len] or -1 if EOF was encountered.
*
* @throws IndexOutOfBoundsException if either [off] or [len] is negative or the byte array has insufficient space to
* write the request [len] bytes into it.
*/
@Throws(IndexOutOfBoundsException::class)
fun InputStream.readNext(b: ByteArray, off: Int, len: Int): Int {
if (off < 0 || len < 0 || len > b.size - off) {
throw IndexOutOfBoundsException()
} else if (len == 0) {
return 0
}
var lenRead = 0
while (lenRead < len) {
val c = read()
if (c == -1) {
return -1
} else {
b[off + lenRead++] = c.toByte()
}
}
return lenRead
}

View File

@@ -1,441 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.dictionary
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
/*
* ====================== IMPORTANT ========================
*
* All code in this file is only temporary added back in so the stable track has suggestions again.
* In 0.3.15 a renewed suggestion algorithm will be built and this mess will be removed!
*
* ==========================================================
*/
/**
* Abstract interface representing a n-gram of tokens. Each n-gram instance can be assigned a
* unique frequency [freq].
*/
open class Ngram<T : Any, F : Comparable<F>>(_tokens: List<Token<T>>, _freq: F) {
companion object {
/** Constant order value for unigrams. */
const val ORDER_UNIGRAM: Int = 1
/** Constant order value for bigrams. */
const val ORDER_BIGRAM: Int = 2
/** Constant order value for trigrams. */
const val ORDER_TRIGRAM: Int = 3
}
init {
if (_tokens.size < ORDER_UNIGRAM) {
throw Exception("A n-gram must contain at least 1 token!")
}
}
/**
* A list of tokens for this n-gram. The length of this list is guaranteed to be matching
* [order].
*/
val tokens: List<Token<T>> = _tokens
/**
* The frequency value of this n-gram.
*/
val freq: F = _freq
/**
* The order of this n-gram (1, 2, 3, ...).
*/
val order: Int
get() = tokens.size
}
/**
* Abstract interface representing a token used in [Ngram].
*/
open class Token<T : Any>(_data: T) {
/**
* The data of this token.
*/
val data: T = _data
override fun toString(): String {
return "Token(\"$data\")"
}
override fun hashCode(): Int {
return data.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Token<*>
if (data != other.data) return false
return true
}
}
/**
* Converts a list of tokens carrying [CharSequence] data to a list of [CharSequence].
*/
fun List<Token<CharSequence>>.toCharSequenceList(): List<CharSequence> {
return this.map { it.data }
}
/**
* Converts a list of tokens carrying [String] data to a list of [String].
*/
fun List<Token<String>>.toStringList(): List<String> {
return this.map { it.data }
}
/**
* Abstract interface for a language model. Can house any n-grams with a minimum order of one.
*/
interface LanguageModel<T : Any, F : Comparable<F>> {
/**
* Tries to get the n-gram for the passed [tokens]. Throws a NPE if no match could be found.
*/
@Throws(NullPointerException::class)
fun getNgram(vararg tokens: T): Ngram<T, F>
/**
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
* searching. Throws a NPE if no match could be found.
*/
@Throws(NullPointerException::class)
fun getNgram(ngram: Ngram<T, F>): Ngram<T, F>
/**
* Tries to get the n-gram for the passed [tokens]. Returns null if no match could be found.
*/
fun getNgramOrNull(vararg tokens: T): Ngram<T, F>?
/**
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
* searching. Returns null if no match could be found.
*/
fun getNgramOrNull(ngram: Ngram<T, F>): Ngram<T, F>?
/**
* Checks if a given [ngram] exists within this model. If [doMatchFreq] is set to true, the
* frequency is also matched.
*/
fun hasNgram(ngram: Ngram<T, F>, doMatchFreq: Boolean = false): Boolean
/**
* Matches all n-grams which match the given [ngram], whereas the last item in the n-gram is
* is used to search for predictions.
*/
fun matchAllNgrams(
ngram: Ngram<T, F>,
maxEditDistance: Int,
maxTokenCount: Int,
allowPossiblyOffensive: Boolean
): List<Token<T>>
}
/**
* Mutable version of [LanguageModel].
*/
interface MutableLanguageModel<T : Any, F : Comparable<F>> : LanguageModel<T, F> {
fun deleteNgram(ngram: Ngram<T, F>)
fun insertNgram(ngram: Ngram<T, F>)
fun updateNgram(ngram: Ngram<T, F>)
}
/**
* Represents the root node to a n-gram tree.
*/
open class NgramTree(
sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
higherOrderChildren: MutableList<NgramNode> = mutableListOf()
) : NgramNode(0, '?', -1, sameOrderChildren, higherOrderChildren)
/**
* A node of a n-gram tree, which holds the character it represents, the corresponding frequency,
* a pre-computed string representing all parent characters and the current one as well as child
* nodes, one for the same order n-gram nodes and one for the higher order n-gram nodes.
*/
open class NgramNode(
val order: Int,
val char: Char,
val freq: Int,
val sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
val higherOrderChildren: MutableList<NgramNode> = mutableListOf()
) {
companion object {
const val FREQ_CHARACTER = -1
const val FREQ_WORD_MIN = 0
const val FREQ_WORD_MAX = 255
const val FREQ_WORD_FILLER = -2
const val FREQ_IS_POSSIBLY_OFFENSIVE = 0
}
val isCharacter: Boolean
get() = freq == FREQ_CHARACTER
val isWord: Boolean
get() = freq in FREQ_WORD_MIN..FREQ_WORD_MAX
val isWordFiller: Boolean
get() = freq == FREQ_WORD_FILLER
val isPossiblyOffensive: Boolean
get() = freq == FREQ_IS_POSSIBLY_OFFENSIVE
fun findWord(word: String): NgramNode? {
var currentNode = this
for ((pos, char) in word.withIndex()) {
val childNode = if (pos == 0) {
currentNode.higherOrderChildren.find { it.char == char }
} else {
currentNode.sameOrderChildren.find { it.char == char }
}
if (childNode != null) {
currentNode = childNode
} else {
return null
}
}
return if (currentNode.isWord || currentNode.isWordFiller) {
currentNode
} else {
null
}
}
/**
* This function allows to search for a given [input] word with a given [maxEditDistance] and
* adds all matches in the trie to the [list].
*/
fun listSimilarWords(
input: String,
list: SuggestionList,
word: StringBuilder,
allowPossiblyOffensive: Boolean,
maxEditDistance: Int,
deletionCost: Int = 0,
insertionCost: Int = 0,
substitutionCost: Int = 0,
pos: Int = -1
) {
if (pos > -1) {
word.append(char)
}
val costSum = deletionCost + insertionCost + substitutionCost
if (pos > -1 && (pos + 1 == input.length) && isWord && ((isPossiblyOffensive && allowPossiblyOffensive)
|| !isPossiblyOffensive)) {
// Using shift right instead of divide by 2^(costSum) as it is mathematically the
// same but faster.
list.add(word.toString(), freq shr costSum)
}
if (pos <= -1) {
for (childNode in higherOrderChildren) {
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance, 0, 0, 0, 0
)
}
} else if (maxEditDistance == costSum) {
if (pos + 1 < input.length) {
sameOrderChildren.find { it.char == input[pos + 1] }?.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost, substitutionCost, pos + 1
)
}
} else {
// Delete
if (pos + 2 < input.length) {
sameOrderChildren.find { it.char == input[pos + 2] }?.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost + 1, insertionCost, substitutionCost, pos + 2
)
}
for (childNode in sameOrderChildren) {
if (pos + 1 < input.length && childNode.char == input[pos + 1]) {
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost, substitutionCost, pos + 1
)
} else {
// Insert
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost + 1, substitutionCost, pos
)
if (pos + 1 < input.length) {
// Substitute
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost, substitutionCost + 1, pos + 1
)
}
}
}
}
if (pos > -1) {
word.deleteAt(word.lastIndex)
}
}
fun listAllSameOrderWords(list: SuggestionList, word: StringBuilder, allowPossiblyOffensive: Boolean) {
word.append(char)
if (isWord && ((isPossiblyOffensive && allowPossiblyOffensive) || !isPossiblyOffensive)) {
list.add(word.toString(), freq)
}
for (childNode in sameOrderChildren) {
childNode.listAllSameOrderWords(list, word, allowPossiblyOffensive)
}
word.deleteAt(word.lastIndex)
}
}
open class FlorisLanguageModel(
initTreeObj: NgramTree? = null
) : LanguageModel<String, Int> {
protected val ngramTree: NgramTree = initTreeObj ?: NgramTree()
override fun getNgram(vararg tokens: String): Ngram<String, Int> {
val ngramOut = getNgramOrNull(*tokens)
if (ngramOut != null) {
return ngramOut
} else {
throw NullPointerException("No n-gram found matching the given tokens: $tokens")
}
}
override fun getNgram(ngram: Ngram<String, Int>): Ngram<String, Int> {
val ngramOut = getNgramOrNull(ngram)
if (ngramOut != null) {
return ngramOut
} else {
throw NullPointerException("No n-gram found matching the given ngram: $ngram")
}
}
override fun getNgramOrNull(vararg tokens: String): Ngram<String, Int>? {
var currentNode: NgramNode = ngramTree
for (token in tokens) {
val childNode = currentNode.findWord(token)
if (childNode != null) {
currentNode = childNode
} else {
return null
}
}
return Ngram(tokens.toList().map { Token(it) }, currentNode.freq)
}
override fun getNgramOrNull(ngram: Ngram<String, Int>): Ngram<String, Int>? {
return getNgramOrNull(*ngram.tokens.toStringList().toTypedArray())
}
override fun hasNgram(ngram: Ngram<String, Int>, doMatchFreq: Boolean): Boolean {
val result = getNgramOrNull(ngram)
return if (result != null) {
if (doMatchFreq) {
ngram.freq == result.freq
} else {
true
}
} else {
false
}
}
override fun matchAllNgrams(
ngram: Ngram<String, Int>,
maxEditDistance: Int,
maxTokenCount: Int,
allowPossiblyOffensive: Boolean
): List<Token<String>> {
val ngramList = mutableListOf<Token<String>>()
var currentNode: NgramNode = ngramTree
for ((t, token) in ngram.tokens.withIndex()) {
val word = token.data
if (t + 1 >= ngram.tokens.size) {
if (word.isNotEmpty()) {
// The last word is not complete, so find all possible words and sort
val splitWord = mutableListOf<Char>()
var splitNode: NgramNode? = currentNode
for ((pos, char) in word.withIndex()) {
val node = if (pos == 0) {
splitNode?.higherOrderChildren?.find { it.char == char }
} else {
splitNode?.sameOrderChildren?.find { it.char == char }
}
splitWord.add(char)
splitNode = node
if (node == null) {
break
}
}
if (splitNode != null) {
// Input thus far is valid
val wordNodes = SuggestionList.new(maxTokenCount)
val strBuilder = StringBuilder().append(word.substring(0, word.length - 1))
splitNode.listAllSameOrderWords(wordNodes, strBuilder, allowPossiblyOffensive)
ngramList.addAll(wordNodes.map { Token(it) })
}
if (ngramList.size < maxTokenCount) {
val wordNodes = SuggestionList.new(maxTokenCount)
val strBuilder = StringBuilder()
currentNode.listSimilarWords(word, wordNodes, strBuilder, allowPossiblyOffensive, maxEditDistance)
ngramList.addAll(wordNodes.map { Token(it) })
}
}
} else {
val node = currentNode.findWord(word)
if (node == null) {
return ngramList
} else {
currentNode = node
}
}
}
return ngramList
}
fun toFlorisMutableLanguageModel(): FlorisMutableLanguageModel = FlorisMutableLanguageModel(ngramTree)
}
open class FlorisMutableLanguageModel(
initTreeObj: NgramTree? = null
) : MutableLanguageModel<String, Int>, FlorisLanguageModel(initTreeObj) {
override fun deleteNgram(ngram: Ngram<String, Int>) {
TODO("Not yet implemented")
}
override fun insertNgram(ngram: Ngram<String, Int>) {
TODO("Not yet implemented")
}
override fun updateNgram(ngram: Ngram<String, Int>) {
TODO("Not yet implemented")
}
fun toFlorisLanguageModel(): FlorisLanguageModel = FlorisLanguageModel(ngramTree)
}

View File

@@ -1,108 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package dev.patrickgold.florisboard.ime.keyboard
import android.view.inputmethod.EditorInfo
import dev.patrickgold.florisboard.common.android.AndroidVersion
/**
* Class which holds the same information as an [EditorInfo.imeOptions] int but more accessible and
* readable.
*/
@JvmInline
value class ImeOptions(val state: KeyboardState) {
companion object {
const val M_IME_OPTIONS: ULong = 0x0F_FFu
const val O_IME_OPTIONS: Int = 32
const val M_ENTER_ACTION: ULong = 0x0Fu
const val O_ENTER_ACTION: Int = 32
const val F_FORCE_ASCII: ULong = 0x00_00_00_10_00_00_00_00u
const val F_NAVIGATE_NEXT: ULong = 0x00_00_00_20_00_00_00_00u
const val F_NAVIGATE_PREVIOUS: ULong = 0x00_00_00_40_00_00_00_00u
const val F_NO_ACCESSORY_ACTION: ULong = 0x00_00_00_80_00_00_00_00u
const val F_NO_ENTER_ACTION: ULong = 0x00_00_01_00_00_00_00_00u
const val F_NO_EXTRACT_UI: ULong = 0x00_00_02_00_00_00_00_00u
const val F_NO_FULLSCREEN: ULong = 0x00_00_04_00_00_00_00_00u
const val F_NO_PERSONALIZED_LEARNING: ULong = 0x00_00_08_00_00_00_00_00u
}
var enterAction: EnterAction
get() = EnterAction.fromInt(state.getRegion(M_ENTER_ACTION, O_ENTER_ACTION))
private set(v) = state.setRegion(M_ENTER_ACTION, O_ENTER_ACTION, v.toInt())
var flagForceAscii: Boolean
get() = state.getFlag(F_FORCE_ASCII)
private set(v) = state.setFlag(F_FORCE_ASCII, v)
var flagNavigateNext: Boolean
get() = state.getFlag(F_NAVIGATE_NEXT)
private set(v) = state.setFlag(F_NAVIGATE_NEXT, v)
var flagNavigatePrevious: Boolean
get() = state.getFlag(F_NAVIGATE_PREVIOUS)
private set(v) = state.setFlag(F_NAVIGATE_PREVIOUS, v)
var flagNoAccessoryAction: Boolean
get() = state.getFlag(F_NO_ACCESSORY_ACTION)
private set(v) = state.setFlag(F_NO_ACCESSORY_ACTION, v)
var flagNoEnterAction: Boolean
get() = state.getFlag(F_NO_ENTER_ACTION)
private set(v) = state.setFlag(F_NO_ENTER_ACTION, v)
var flagNoExtractUi: Boolean
get() = state.getFlag(F_NO_EXTRACT_UI)
private set(v) = state.setFlag(F_NO_EXTRACT_UI, v)
var flagNoFullscreen: Boolean
get() = state.getFlag(F_NO_FULLSCREEN)
private set(v) = state.setFlag(F_NO_FULLSCREEN, v)
var flagNoPersonalizedLearning: Boolean
get() = state.getFlag(F_NO_PERSONALIZED_LEARNING)
private set(v) = state.setFlag(F_NO_PERSONALIZED_LEARNING, v)
fun update(editorInfo: EditorInfo) {
val imeOptionsRaw = editorInfo.imeOptions
state.setRegion(M_IME_OPTIONS, O_IME_OPTIONS, 0) // reset imeOptions region
enterAction = EnterAction.fromInt(imeOptionsRaw and EditorInfo.IME_MASK_ACTION)
flagForceAscii = imeOptionsRaw and EditorInfo.IME_FLAG_FORCE_ASCII != 0
flagNavigateNext = imeOptionsRaw and EditorInfo.IME_FLAG_NAVIGATE_NEXT != 0
flagNavigatePrevious = imeOptionsRaw and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS != 0
flagNoAccessoryAction = imeOptionsRaw and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION != 0
flagNoEnterAction = imeOptionsRaw and EditorInfo.IME_FLAG_NO_ENTER_ACTION != 0
flagNoExtractUi = imeOptionsRaw and EditorInfo.IME_FLAG_NO_EXTRACT_UI != 0
flagNoFullscreen = imeOptionsRaw and EditorInfo.IME_FLAG_NO_FULLSCREEN != 0
if (AndroidVersion.ATLEAST_API26_O) {
flagNoPersonalizedLearning = imeOptionsRaw and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING != 0
}
}
enum class EnterAction(val value: Int) {
UNSPECIFIED(EditorInfo.IME_ACTION_UNSPECIFIED),
DONE(EditorInfo.IME_ACTION_DONE),
GO(EditorInfo.IME_ACTION_GO),
NEXT(EditorInfo.IME_ACTION_NEXT),
NONE(EditorInfo.IME_ACTION_NONE),
PREVIOUS(EditorInfo.IME_ACTION_PREVIOUS),
SEARCH(EditorInfo.IME_ACTION_SEARCH),
SEND(EditorInfo.IME_ACTION_SEND);
companion object {
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NONE
}
fun toInt() = value
}
}

View File

@@ -1,224 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package dev.patrickgold.florisboard.ime.keyboard
import android.text.InputType
import android.view.inputmethod.EditorInfo
/**
* Class which holds the same information as an [EditorInfo.inputType] int but more accessible and
* readable.
*/
@JvmInline
value class InputAttributes(val state: KeyboardState) {
companion object {
const val M_INPUT_ATTRIBUTES: ULong = 0x0F_FF_FFu
const val O_INPUT_ATTRIBUTES: Int = 44
const val M_TYPE: ULong = 0x07u
const val O_TYPE: Int = 44
const val M_VARIATION: ULong = 0x1Fu
const val O_VARIATION: Int = 47
const val M_CAPS_MODE: ULong = 0x03u
const val O_CAPS_MODE: Int = 52
const val F_NUMBER_DECIMAL: ULong = 0x00_40_00_00_00_00_00_00u
const val F_NUMBER_SIGNED: ULong = 0x00_80_00_00_00_00_00_00u
const val F_TEXT_AUTO_COMPLETE: ULong = 0x01_00_00_00_00_00_00_00u
const val F_TEXT_AUTO_CORRECT: ULong = 0x02_00_00_00_00_00_00_00u
const val F_TEXT_IME_MULTILINE: ULong = 0x04_00_00_00_00_00_00_00u
const val F_TEXT_MULTILINE: ULong = 0x08_00_00_00_00_00_00_00u
const val F_TEXT_NO_SUGGESTIONS: ULong = 0x10_00_00_00_00_00_00_00u
}
var type: Type
get() = Type.fromInt(state.getRegion(M_TYPE, O_TYPE))
private set(v) = state.setRegion(M_TYPE, O_TYPE, v.toInt())
var variation: Variation
get() = Variation.fromInt(state.getRegion(M_VARIATION, O_VARIATION))
private set(v) = state.setRegion(M_VARIATION, O_VARIATION, v.toInt())
var capsMode: CapsMode
get() = CapsMode.fromInt(state.getRegion(M_CAPS_MODE, O_CAPS_MODE))
private set(v) = state.setRegion(M_CAPS_MODE, O_CAPS_MODE, v.toInt())
var flagNumberDecimal: Boolean
get() = state.getFlag(F_NUMBER_DECIMAL)
private set(v) = state.setFlag(F_NUMBER_DECIMAL, v)
var flagNumberSigned: Boolean
get() = state.getFlag(F_NUMBER_SIGNED)
private set(v) = state.setFlag(F_NUMBER_SIGNED, v)
var flagTextAutoComplete: Boolean
get() = state.getFlag(F_TEXT_AUTO_COMPLETE)
private set(v) = state.setFlag(F_TEXT_AUTO_COMPLETE, v)
var flagTextAutoCorrect: Boolean
get() = state.getFlag(F_TEXT_AUTO_CORRECT)
private set(v) = state.setFlag(F_TEXT_AUTO_CORRECT, v)
var flagTextImeMultiLine: Boolean
get() = state.getFlag(F_TEXT_IME_MULTILINE)
private set(v) = state.setFlag(F_TEXT_IME_MULTILINE, v)
var flagTextMultiLine: Boolean
get() = state.getFlag(F_TEXT_MULTILINE)
private set(v) = state.setFlag(F_TEXT_MULTILINE, v)
var flagTextNoSuggestions: Boolean
get() = state.getFlag(F_TEXT_NO_SUGGESTIONS)
private set(v) = state.setFlag(F_TEXT_NO_SUGGESTIONS, v)
fun update(editorInfo: EditorInfo) {
val inputAttrsRaw = editorInfo.inputType
state.setRegion(M_INPUT_ATTRIBUTES, O_INPUT_ATTRIBUTES, 0) // reset inputAttributes region
when (inputAttrsRaw and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_NULL -> {
type = Type.NULL
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_DATETIME -> {
type = Type.DATETIME
variation = when (inputAttrsRaw and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_DATETIME_VARIATION_DATE -> Variation.DATE
InputType.TYPE_DATETIME_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_DATETIME_VARIATION_TIME -> Variation.TIME
else -> Variation.NORMAL
}
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_NUMBER -> {
type = Type.NUMBER
variation = when (inputAttrsRaw and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_NUMBER_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> Variation.PASSWORD
else -> Variation.NORMAL
}
capsMode = CapsMode.NONE
flagNumberDecimal = inputAttrsRaw and InputType.TYPE_NUMBER_FLAG_DECIMAL != 0
flagNumberSigned = inputAttrsRaw and InputType.TYPE_NUMBER_FLAG_SIGNED != 0
}
InputType.TYPE_CLASS_PHONE -> {
type = Type.PHONE
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_TEXT -> {
type = Type.TEXT
variation = when (inputAttrsRaw and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> Variation.EMAIL_ADDRESS
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> Variation.EMAIL_SUBJECT
InputType.TYPE_TEXT_VARIATION_FILTER -> Variation.FILTER
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> Variation.LONG_MESSAGE
InputType.TYPE_TEXT_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_TEXT_VARIATION_PASSWORD -> Variation.PASSWORD
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> Variation.PERSON_NAME
InputType.TYPE_TEXT_VARIATION_PHONETIC -> Variation.PHONETIC
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> Variation.POSTAL_ADDRESS
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> Variation.SHORT_MESSAGE
InputType.TYPE_TEXT_VARIATION_URI -> Variation.URI
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> Variation.VISIBLE_PASSWORD
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> Variation.WEB_EDIT_TEXT
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> Variation.WEB_EMAIL_ADDRESS
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> Variation.WEB_PASSWORD
else -> Variation.NORMAL
}
capsMode = CapsMode.fromFlags(inputAttrsRaw)
flagTextAutoComplete = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE != 0
flagTextAutoCorrect = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT != 0
flagTextImeMultiLine = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE != 0
flagTextMultiLine = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_MULTI_LINE != 0
flagTextNoSuggestions = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS != 0
}
else -> {
type = Type.TEXT
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
}
}
enum class Type(val value: Int) {
NULL(EditorInfo.TYPE_NULL),
DATETIME(EditorInfo.TYPE_CLASS_DATETIME),
NUMBER(EditorInfo.TYPE_CLASS_NUMBER),
PHONE(EditorInfo.TYPE_CLASS_PHONE),
TEXT(EditorInfo.TYPE_CLASS_TEXT);
companion object {
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NULL
}
fun toInt() = value
}
enum class Variation(val value: Int) {
NORMAL(0),
DATE(1),
EMAIL_ADDRESS(2),
EMAIL_SUBJECT(3),
FILTER(4),
LONG_MESSAGE(5),
PASSWORD(6),
PERSON_NAME(7),
PHONETIC(8),
POSTAL_ADDRESS(9),
SHORT_MESSAGE(10),
TIME(11),
URI(12),
VISIBLE_PASSWORD(13),
WEB_EDIT_TEXT(14),
WEB_EMAIL_ADDRESS(15),
WEB_PASSWORD(16);
companion object {
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NORMAL
}
fun toInt() = value
}
enum class CapsMode(val value: Int) {
NONE(0),
ALL(1),
SENTENCES(2),
WORDS(3);
companion object {
fun fromFlags(flags: Int): CapsMode {
return when {
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS != 0 -> ALL
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES != 0 -> SENTENCES
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS != 0 -> WORDS
else -> NONE
}
}
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NONE
}
fun toFlags(): Int {
return when (this) {
ALL -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
SENTENCES -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
WORDS -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
else -> 0
}
}
fun toInt() = value
}
}

View File

@@ -1,194 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.nlp
import android.content.Context
import android.os.Build
import android.util.Size
import android.view.inputmethod.InlineSuggestion
import android.widget.inline.InlineContentView
import androidx.annotation.RequiresApi
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import dev.patrickgold.florisboard.FlorisImeService
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
import dev.patrickgold.florisboard.clipboardManager
import dev.patrickgold.florisboard.debug.flogError
import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardItem
import java.util.*
class NlpManager(context: Context) {
private val prefs by florisPreferenceModel()
private val clipboardManager by context.clipboardManager()
private val _suggestions = MutableLiveData<SuggestionList2?>(null)
val suggestions: LiveData<SuggestionList2?> get() = _suggestions
private val _candidates = MutableLiveData<List<Candidate>>(emptyList())
val candidates: LiveData<List<Candidate>> get() = _candidates
private val inlineContentViews = Collections.synchronizedMap<InlineSuggestion, InlineContentView>(hashMapOf())
private val _inlineSuggestions = MutableLiveData<List<InlineSuggestion>>(emptyList())
val inlineSuggestions: LiveData<List<InlineSuggestion>> get() = _inlineSuggestions
init {
clipboardManager.primaryClip.observeForever {
assembleCandidates()
}
suggestions.observeForever {
assembleCandidates()
}
prefs.suggestion.clipboardContentEnabled.observeForever {
assembleCandidates()
}
}
fun suggest(
currentWord: String,
precedingWords: List<String>,
) {
if (currentWord.isBlank() && (precedingWords.isEmpty() || precedingWords.all { it.isBlank() })) {
clearSuggestions()
return
}
val allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive.get()
val maxSuggestionCount = 16 // TODO: make customizable in prefs
val suggestions = buildList {
for (n in 0..6) {
add("$currentWord$n")
}
}
_suggestions.postValue(SuggestionList2(suggestions, false))
}
fun suggestDirectly(suggestions: List<String>) {
_suggestions.postValue(SuggestionList2(suggestions, false))
}
fun clearSuggestions() {
_suggestions.postValue(null)
}
private fun assembleCandidates() {
val candidates = buildList {
if (prefs.smartbar.enabled.get() && prefs.suggestion.enabled.get()) {
if (prefs.suggestion.clipboardContentEnabled.get()) {
val now = System.currentTimeMillis()
clipboardManager.primaryClip()?.let { item ->
if ((now - item.creationTimestampMs) < prefs.suggestion.clipboardContentTimeout.get() * 1000) {
add(Candidate.Clip(item))
}
}
}
suggestions.value?.let { suggestionList ->
suggestionList.forEachIndexed { n, word ->
add(Candidate.Word(word, isAutoInsert = n == 0 && suggestionList.isPrimaryTokenAutoInsert))
}
}
}
}
_candidates.postValue(candidates)
autoExpandCollapseSmartbarActions(candidates, inlineSuggestions.value)
}
/**
* Inflates the given inline suggestions. Once all provided views are ready, the suggestions
* strip is updated and the Smartbar update cycle is triggered.
*
* @param inlineSuggestions A collection of inline suggestions to be inflated and shown.
*/
fun showInlineSuggestions(inlineSuggestions: List<InlineSuggestion>) {
inlineContentViews.clear()
_inlineSuggestions.postValue(inlineSuggestions)
autoExpandCollapseSmartbarActions(candidates.value, inlineSuggestions)
}
/**
* Clears the inline suggestions and triggers the Smartbar update cycle.
*/
fun clearInlineSuggestions() {
inlineContentViews.clear()
_inlineSuggestions.postValue(emptyList())
autoExpandCollapseSmartbarActions(candidates.value, null)
}
@RequiresApi(Build.VERSION_CODES.R)
fun inflateOrGet(context: Context, size: Size, inlineSuggestion: InlineSuggestion, callback: (InlineContentView) -> Unit) {
val view = inlineContentViews[inlineSuggestion]
if (view != null) {
callback(view)
} else {
try {
inlineSuggestion.inflate(context, size, context.mainExecutor) { inflatedView ->
if (inflatedView != null) {
inlineContentViews[inlineSuggestion] = inflatedView
callback(inflatedView)
}
}
} catch (e: Exception) {
flogError { e.toString() }
}
}
}
private fun autoExpandCollapseSmartbarActions(list1: List<*>?, list2: List<*>?) {
if (prefs.smartbar.enabled.get() && prefs.smartbar.primaryActionsAutoExpandCollapse.get()) {
val isSelection = FlorisImeService.activeEditorInstance()?.selection?.isSelectionMode ?: true
val isExpanded = list1.isNullOrEmpty() && list2.isNullOrEmpty() || isSelection
prefs.smartbar.primaryActionsExpandWithAnimation.set(false)
prefs.smartbar.primaryActionsExpanded.set(isExpanded)
}
}
class SuggestionList2(
candidates: List<String>,
val isPrimaryTokenAutoInsert: Boolean,
) : List<String> by candidates
/**
* Data class describing a computed candidate item.
*/
sealed class Candidate {
fun text(): String {
return when (this) {
is Clip -> clipboardItem.stringRepresentation()
is Word -> word
}
}
fun isAutoInsertWord(): Boolean {
return this is Word && this.isAutoInsert
}
/**
* Computed word candidate, used for suggestions provided by the NLP algorithm.
*
* @property word The word this computed candidate item represents. Used in the callback to provide which word
* should be filled out.
* @property isAutoInsert If pressing space bar auto corrects/inserts this word.
*/
data class Word(val word: String, val isAutoInsert: Boolean) : Candidate()
/**
* Computed word candidate, used for clipboard paste suggestions.
*
* @property clipboardItem The clipboard item this computed candidate item represents. Used in the callback to
* provide which item should be pasted.
*/
data class Clip(val clipboardItem: ClipboardItem) : Candidate()
}
}

View File

@@ -1,104 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.nlp
import dev.patrickgold.florisboard.common.NativeInstanceWrapper
import dev.patrickgold.florisboard.common.NativePtr
@JvmInline
value class SuggestionList private constructor(
private val _nativePtr: NativePtr
) : Collection<String>, NativeInstanceWrapper {
companion object {
fun new(maxSize: Int): SuggestionList {
val nativePtr = nativeInitialize(maxSize)
return SuggestionList(nativePtr)
}
external fun nativeInitialize(maxSize: Int): NativePtr
external fun nativeDispose(nativePtr: NativePtr)
external fun nativeAdd(nativePtr: NativePtr, word: Word, freq: Freq): Boolean
external fun nativeClear(nativePtr: NativePtr)
external fun nativeContains(nativePtr: NativePtr, element: Word): Boolean
external fun nativeGetOrNull(nativePtr: NativePtr, index: Int): Word?
external fun nativeGetIsPrimaryTokenAutoInsert(nativePtr: NativePtr): Boolean
external fun nativeSetIsPrimaryTokenAutoInsert(nativePtr: NativePtr, v: Boolean)
external fun nativeSize(nativePtr: NativePtr): Int
}
override val size: Int
get() = nativeSize(_nativePtr)
fun add(word: Word, freq: Freq): Boolean {
return nativeAdd(_nativePtr, word, freq)
}
fun clear() {
nativeClear(_nativePtr)
}
override fun contains(element: Word): Boolean {
return nativeContains(_nativePtr, element)
}
override fun containsAll(elements: Collection<Word>): Boolean {
elements.forEach { if (!contains(it)) return false }
return true
}
@Throws(IndexOutOfBoundsException::class)
operator fun get(index: Int): Word {
val element = getOrNull(index)
if (element == null) {
throw IndexOutOfBoundsException("The specified index $index is not within the bounds of this list!")
} else {
return element
}
}
fun getOrNull(index: Int): Word? {
return nativeGetOrNull(_nativePtr, index)
}
override fun isEmpty(): Boolean = size <= 0
val isPrimaryTokenAutoInsert: Boolean
get() = nativeGetIsPrimaryTokenAutoInsert(_nativePtr)
override fun iterator(): Iterator<Word> {
return SuggestionListIterator(this)
}
override fun nativePtr(): NativePtr {
return _nativePtr
}
override fun dispose() {
nativeDispose(_nativePtr)
}
class SuggestionListIterator internal constructor (
private val suggestionList: SuggestionList
) : Iterator<Word> {
var index = 0
override fun next(): Word = suggestionList[index++]
override fun hasNext(): Boolean = suggestionList.getOrNull(index) != null
}
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.nlp
import dev.patrickgold.florisboard.common.FlorisLocale
@Suppress("RegExpRedundantEscape")
object TextProcessor {
private val LATIN_BASIC_WORD_REGEX = """[_]*(([\p{L}\d\']+[_-]*[\p{L}\d\']+)|[\p{L}\d\']+)[_]*""".toRegex()
private fun wordRegexFor(locale: FlorisLocale): Regex {
return when (locale) {
else -> LATIN_BASIC_WORD_REGEX
}
}
fun detectWords(text: CharSequence, locale: FlorisLocale): Sequence<IntRange> {
val regex = wordRegexFor(locale)
return regex.findAll(text).map { it.range }
}
fun detectWords(text: CharSequence, start: Int, end: Int, locale: FlorisLocale): Sequence<IntRange> {
val regex = wordRegexFor(locale)
val tStart = start.coerceAtLeast(0)
val tEnd = end.coerceAtMost(text.length)
return regex.findAll(text.slice(tStart..tEnd)).map { it.range }
}
fun isWord(text: CharSequence, locale: FlorisLocale): Boolean {
val regex = wordRegexFor(locale)
return regex.matches(text)
}
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.spelling
data class SpellingConfig(
val basePath: String,
val importSources: List<ImportSource>
) {
companion object {
fun default() = SpellingConfig("ime/spelling", listOf())
}
data class ImportSource(
val id: String,
val label: String,
val url: String? = null,
val format: ImportFormat
)
sealed interface ImportFormat {
data class Archive(
val file: FileInput,
) : ImportFormat
data class Raw(
val affFile: FileInput,
val dicFile: FileInput,
) : ImportFormat
}
data class FileInput(
val fileNameRegex: Regex,
val isRequired: Boolean,
)
}

View File

@@ -1,69 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.spelling
import dev.patrickgold.florisboard.common.NATIVE_NULLPTR
import dev.patrickgold.florisboard.common.NativeInstanceWrapper
import dev.patrickgold.florisboard.common.NativePtr
import dev.patrickgold.florisboard.common.NativeStr
import dev.patrickgold.florisboard.common.toJavaString
import dev.patrickgold.florisboard.common.toNativeStr
import dev.patrickgold.florisboard.ime.nlp.Word
@JvmInline
value class SpellingDict private constructor(
private val _nativePtr: NativePtr
) : NativeInstanceWrapper {
companion object {
const val LICENSE_FILE_NAME = "LICENSE.txt"
const val README_FILE_NAME = "README.txt"
fun new(path: String, ext: SpellingExtension): SpellingDict? {
val baseName = ext.spelling.affFile.removeSuffix(".aff")
val nativePtr = nativeInitialize("$path/$baseName".toNativeStr())
return if (nativePtr == NATIVE_NULLPTR) {
null
} else {
SpellingDict(nativePtr)
}
}
external fun nativeInitialize(basePath: NativeStr): NativePtr
external fun nativeDispose(nativePtr: NativePtr)
external fun nativeSpell(nativePtr: NativePtr, word: NativeStr): Boolean
external fun nativeSuggest(nativePtr: NativePtr, word: NativeStr, limit: Int): Array<NativeStr>
}
override fun nativePtr(): NativePtr {
return _nativePtr
}
override fun dispose() {
nativeDispose(_nativePtr)
}
fun spell(word: Word): Boolean {
return nativeSpell(_nativePtr, word.toNativeStr())
}
fun suggest(word: Word, limit: Int): Array<out String> {
val nativeSuggestions = nativeSuggest(_nativePtr, word.toNativeStr(), limit)
return Array(nativeSuggestions.size) { i -> nativeSuggestions[i].toJavaString() }
}
}

View File

@@ -1,114 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.spelling
import android.content.Context
import dev.patrickgold.florisboard.common.FlorisLocale
import dev.patrickgold.florisboard.res.ext.Extension
import dev.patrickgold.florisboard.res.ext.ExtensionComponent
import dev.patrickgold.florisboard.res.ext.ExtensionEditor
import dev.patrickgold.florisboard.res.ext.ExtensionMeta
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.io.File
@SerialName(SpellingExtension.SERIAL_TYPE)
@Serializable
data class SpellingExtension(
override val meta: ExtensionMeta,
override val dependencies: List<String>? = null,
val spelling: SpellingExtensionConfig,
) : Extension() {
companion object {
const val SERIAL_TYPE = "ime.extension.spelling"
}
@Transient var dict: SpellingDict? = null
override fun serialType() = SERIAL_TYPE
override fun components(): List<ExtensionComponent> {
return emptyList()
}
override fun onAfterLoad(context: Context, cacheDir: File) {
dict = SpellingDict.new(cacheDir.absolutePath, this)
}
override fun onBeforeUnload(context: Context, cacheDir: File) {
dict?.dispose()
dict = null
}
override fun edit() = SpellingExtensionEditor(
meta = meta,
dependencies?.toMutableList() ?: mutableListOf(),
workingDir,
spelling.edit(),
)
}
data class SpellingExtensionEditor(
override var meta: ExtensionMeta,
override val dependencies: MutableList<String>,
var workingDir: File?,
val spelling: SpellingExtensionConfigEditor,
) : ExtensionEditor {
override fun build(): SpellingExtension {
return SpellingExtension(
meta = meta,
spelling = spelling.build(),
).also {
it.workingDir = workingDir
}
}
}
@Serializable
data class SpellingExtensionConfig(
@SerialName("language")
@Serializable(with = FlorisLocale.Serializer::class)
val locale: FlorisLocale,
val originalSourceId: String? = null,
val affFile: String,
val dicFile: String,
) {
fun edit() = SpellingExtensionConfigEditor(
locale.languageTag(), originalSourceId ?: "", affFile, dicFile
)
}
data class SpellingExtensionConfigEditor(
var locale: String = "",
var originalSourceId: String = "",
var affFile: String = "",
var dicFile: String = "",
) {
fun build(): SpellingExtensionConfig {
val config = SpellingExtensionConfig(
locale.trim().let { FlorisLocale.from(it) },
originalSourceId.trim().ifBlank { null },
affFile.trim(),
dicFile.trim(),
)
check(config.affFile.isNotBlank()) { "Spelling extension aff file path cannot be blank" }
check(config.dicFile.isNotBlank()) { "Spelling extension dic file path cannot be blank" }
return config
}
}

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