├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ ├── pre-release.yml
│ ├── release.yml
│ └── sonar-analysis.yml
├── .gitignore
├── DEVELOPMENT.md
├── LICENSE
├── README.md
├── assets
├── mobile-search.png
└── preview.gif
├── esbuild.config.mjs
├── jest.config.js
├── manifest.json
├── package-lock.json
├── package.json
├── resources
└── save-data-schema.json
├── sonar-project.properties
├── src
├── libraries
│ ├── comparison
│ │ ├── compareCharacterMatches.ts
│ │ ├── compareCharacters.ts
│ │ ├── compareCodepoints.ts
│ │ ├── compareDates.ts
│ │ ├── compareFavoriteCharacters.ts
│ │ ├── compareFavoriteInfo.ts
│ │ ├── compareNullable.ts
│ │ ├── compareNumbers.ts
│ │ ├── compareSearchMatchScores.ts
│ │ ├── compareSearchMatches.ts
│ │ ├── compareUsageInfo.ts
│ │ ├── compareUsedCharacters.ts
│ │ ├── fillNullCharacterMatchScores.ts
│ │ └── fillNullSearchMatchScores.ts
│ ├── data
│ │ ├── characterCategory.ts
│ │ ├── characterCategoryGroup.ts
│ │ ├── unicodeCharacterCategories.ts
│ │ ├── unicodePlaneNumber.ts
│ │ └── unicodePlanes.ts
│ ├── helpers
│ │ ├── asHexadecimal.ts
│ │ ├── averageUseCount.ts
│ │ ├── codePointIn.ts
│ │ ├── getRandomItem.ts
│ │ ├── intervalWithin.ts
│ │ ├── intervalsEqual.ts
│ │ ├── isFavoriteCharacter.ts
│ │ ├── isTypeSaveData.ts
│ │ ├── isUsedCharacter.ts
│ │ ├── matchedNameOrCodepoint.ts
│ │ ├── mergeIntervals.ts
│ │ ├── mostRecentUses.ts
│ │ ├── parseUsageInfo.ts
│ │ ├── serializeFavoriteInfo.ts
│ │ ├── serializeUsageInfo.ts
│ │ ├── toHexadecimal.ts
│ │ ├── toNullMatch.ts
│ │ └── toSearchQueryMatch.ts
│ ├── order
│ │ ├── inverse.ts
│ │ └── order.ts
│ └── types
│ │ ├── codepoint
│ │ ├── character.ts
│ │ ├── codepointInterval.ts
│ │ ├── extension.ts
│ │ └── unicode.ts
│ │ ├── maybe.ts
│ │ ├── persistCache.ts
│ │ ├── readCache.ts
│ │ ├── savedata
│ │ ├── dataFragment.ts
│ │ ├── favoriteInfo.ts
│ │ ├── favoritesFragment.ts
│ │ ├── filterFragment.ts
│ │ ├── initialSaveData.ts
│ │ ├── metaFragment.ts
│ │ ├── saveData.ts
│ │ ├── unicodeFilter.ts
│ │ ├── unicodeFragment.ts
│ │ ├── usageFragment.ts
│ │ ├── usageInfo.ts
│ │ └── version.ts
│ │ ├── unicode
│ │ ├── unicodeBlock.ts
│ │ ├── unicodeGeneralCategory.ts
│ │ ├── unicodeGeneralCategoryGroup.ts
│ │ └── unicodePlane.ts
│ │ └── usageDisplayStatistics.ts
└── unicode-search
│ ├── components
│ ├── characterSearch.ts
│ ├── characterSearchAttributes.ts
│ ├── fuzzySearchModal.ts
│ ├── insertCharacterModal.ts
│ ├── pickCharacterModal.ts
│ ├── settingTab.ts
│ └── visualElements.ts
│ ├── errors
│ └── unicodeSearchError.ts
│ ├── main.ts
│ ├── service
│ ├── characterDownloader.ts
│ ├── characterService.ts
│ ├── codePointStore.ts
│ ├── codepointFavoritesStorage.ts
│ ├── codepointStorage.ts
│ ├── codepointUsageStorage.ts
│ ├── commander.ts
│ ├── dataFragmentManager.ts
│ ├── dataManager.ts
│ ├── favoritesDataManager.ts
│ ├── favoritesStore.ts
│ ├── filterDataManager.ts
│ ├── filterStorage.ts
│ ├── filterStore.ts
│ ├── metaDataManager.ts
│ ├── metaStorage.ts
│ ├── rootDataManager.ts
│ ├── rootDataStore.ts
│ ├── rootPluginDataStorage.ts
│ ├── ucdUserFilterDownloader.ts
│ ├── unicodeDataManager.ts
│ ├── usageDataManager.ts
│ ├── usageStore.ts
│ └── userCharacterService.ts
│ └── styles.scss
├── tests
└── libraries
│ ├── comparison
│ ├── compareCodepoints.spec.ts
│ ├── compareDates.test.ts
│ ├── compareFavoriteCharacters.spec.ts
│ ├── compareFavoriteInfo.spec.ts
│ ├── compareNullable.spec.ts
│ ├── compareNumbers.spec.ts
│ ├── compareUsageInfo.spec.ts
│ └── compareUsedCharacters.spec.ts
│ └── helpers
│ ├── averageUseCount.spec.ts
│ ├── codePointIn.spec.ts
│ ├── hexadecimal.characters.spec.ts
│ ├── intervalWithin.spec.ts
│ ├── intervalsEqual.spec.ts
│ └── mergeIntervals.spec.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{yml,yaml,json}]
12 | indent_size = 2
13 |
14 | [package-lock.json]
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Normalize line endings to LF.
2 | * text eol=lf
3 | # Ensure the following are treated as binary.
4 | *.gif filter=lfs diff=lfs merge=lfs -text
5 | *.jar binary
6 | *.jpeg binary
7 | *.jpg binary
8 | *.png filter=lfs diff=lfs merge=lfs -text
9 | *.vsd binary
10 |
--------------------------------------------------------------------------------
/.github/workflows/pre-release.yml:
--------------------------------------------------------------------------------
1 | name: Pre-release
2 |
3 | on:
4 | push:
5 | branches:
6 | - "release/[0-9]+.[0-9]+.[0-9]+"
7 |
8 | env:
9 | PLUGIN_NAME: "unicode-search"
10 | BUILD_DIR: "./dist"
11 | RELEASE_VERSION: ${{ github.ref_name }}
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 | - name: Use node.js
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: "18"
22 |
23 | - name: NPM Install and build
24 | run: |
25 | npm clean-install
26 | npm run build --if-present
27 |
28 | - name: ZIP build output
29 | working-directory: ${{ env.BUILD_DIR }}
30 | run: |
31 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
32 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "[0-9]+.[0-9]+.[0-9]+"
7 |
8 | env:
9 | PLUGIN_NAME: "unicode-search"
10 | BUILD_DIR: "./dist"
11 | RELEASE_VERSION: ${{ github.ref_name }}
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 | - name: Use node.js
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: "18"
22 |
23 | - name: NPM Install and build
24 | run: |
25 | npm clean-install
26 | npm run build --if-present
27 |
28 | - name: ZIP build output
29 | working-directory: ${{ env.BUILD_DIR }}
30 | run: |
31 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
32 |
33 | - name: Create Release
34 | id: create_release
35 | uses: actions/create-release@v1
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 | with:
39 | tag_name: ${{ env.RELEASE_VERSION }}
40 | release_name: ${{ env.RELEASE_VERSION }}
41 | draft: false
42 | prerelease: false
43 |
44 | - name: Upload manifest
45 | uses: actions/upload-release-asset@v1
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | with:
49 | upload_url: ${{ steps.create_release.outputs.upload_url }}
50 | asset_path: ./${{ env.BUILD_DIR }}/${{ env.PLUGIN_NAME }}/manifest.json
51 | asset_name: manifest.json
52 | asset_content_type: application/json
53 |
54 | - name: Upload script
55 | uses: actions/upload-release-asset@v1
56 | env:
57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58 | with:
59 | upload_url: ${{ steps.create_release.outputs.upload_url }}
60 | asset_path: ./${{ env.BUILD_DIR }}/${{ env.PLUGIN_NAME }}/main.js
61 | asset_name: main.js
62 | asset_content_type: text/javascript
63 |
64 | - name: Upload stylesheet
65 | uses: actions/upload-release-asset@v1
66 | env:
67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
68 | with:
69 | upload_url: ${{ steps.create_release.outputs.upload_url }}
70 | asset_path: ./${{ env.BUILD_DIR }}/${{ env.PLUGIN_NAME }}/styles.css
71 | asset_name: styles.css
72 | asset_content_type: text/css
73 |
74 | - name: Upload archive
75 | uses: actions/upload-release-asset@v1
76 | env:
77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
78 | with:
79 | upload_url: ${{ steps.create_release.outputs.upload_url }}
80 | asset_path: ./${{ env.BUILD_DIR }}/${{ env.PLUGIN_NAME }}.zip
81 | asset_name: ${{ env.PLUGIN_NAME }}-${{ env.RELEASE_VERSION }}.zip
82 | asset_content_type: application/zip
83 |
--------------------------------------------------------------------------------
/.github/workflows/sonar-analysis.yml:
--------------------------------------------------------------------------------
1 | name: Sonar Analysis
2 |
3 | on:
4 | push:
5 |
6 | jobs:
7 |
8 | sonarcloud:
9 | name: SonarCloud
10 | runs-on: ubuntu-latest
11 | steps:
12 |
13 | - uses: actions/checkout@v3
14 | with:
15 | # Shallow clones should be disabled for a better relevancy of analysis
16 | fetch-depth: 0
17 |
18 | - name: Use node.js
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: "18"
22 |
23 | - name: NPM Install
24 | run: |
25 | npm clean-install
26 |
27 | - name: Test with coverage
28 | run: |
29 | npx jest --coverage
30 |
31 | - name: SonarCloud Scan
32 | uses: SonarSource/sonarqube-scan-action@master
33 | env:
34 | # Needed to get PR information, if any
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IntelliJ Idea
2 | /.idea/
3 |
4 | # Visual Studio Code
5 | /.vscode/
6 |
7 | # NPM
8 | /node_modules/
9 |
10 | # Build output
11 | /dist/
12 |
13 | # Exclude sourcemaps
14 | *.map
15 |
16 | # Obsidian
17 | /data.json
18 |
19 | # Exclude macOS Finder (System Explorer) View States
20 | .DS_Store
21 |
22 | /.ignore/
23 |
24 | /coverage/
25 |
--------------------------------------------------------------------------------
/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 | # Unicode Search: Development
2 |
3 | These are my notes on the development of the plugin.
4 |
5 | ## Release: How To
6 |
7 | 1. From the `develop` branch, create a release branch `release/X.Y.Z`
8 | 2. Go to [package.json](./package.json) and overwrite `X.Y.Z-NEXT` with new version throughout the whole project (double check if save data version needs to be updated)
9 | 3. Update the [package-lock](./package-lock.json) file: `npm install`
10 | 4. Export save data schema: `npm run export-schema`
11 | 5. Run tests: `npm run test`
12 | 6. Run build and test out the app: `npm run build`
13 | 7. Commit as "Version `X.Y.Z`"
14 | 8. Push, and fix if build fails, otherwise merge to the `main` branch
15 | 9. Create a tag with the label `X.Y.Z`
16 | 10. GitHub Actions will create a [release](https://github.com/BambusControl/obsidian-unicode-search/releases)
17 | 11. Add release notes to the release
18 | 12. Fast-forward the `develop` branch
19 | 13. Go to [package.json](./package.json) and overwrite `X.Y.Z` with _next_ version `X.Y.Z-NEXT` throughout the whole project
20 | 14. Update the [package-lock](./package-lock.json) file: `npm install`
21 | 15. Commit as "Set version as `X.Y.Z-NEXT`" to the `develop` branch
22 |
23 | ## Development Diary
24 |
25 | ### 22. April 2025
26 |
27 | The save data rework is kind-of done.
28 | Still, there is overlap between fetching and initializing data which I want to look at later.
29 |
30 | ### 23. December 2024
31 |
32 | Before releasing the new feature, I have to fix the user save data implementation.
33 | Now it deletes their data when a new version of the plugin is loaded.
34 | So, it would forget the favorite characters after an update.
35 |
36 | ### 19. December 2024
37 |
38 | This small plugin became a bit too elaborately written.
39 | I added the "_favorite characters_," and I'm almost done with it.
40 | It works alright, but adding new settings is tedious, so I'm thinking of adding Svelte as the next step (to make the UI modifications easier.)
41 |
42 | Also, I want to simplify the code after, because I believe the codebase exploded for no good reason.
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2024 Mojmír Majer
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Obsidian Unicode Search
2 |
3 | 
4 |
5 | > See [what's new](https://github.com/BambusControl/obsidian-unicode-search/releases)!
6 |
7 | Search the [Unicode Character Database](https://www.unicode.org/ucd/) index
8 | and insert any character into your editor.
9 | Mobile is also supported!
10 |
11 | > *This is a plugin for [Obsidian: unicode-search](https://obsidian.md/plugins?id=unicode-search)*.
12 |
13 |

18 |
19 | ## Usage
20 |
21 | The plugin adds a command for searching unicode characters.
22 | Make sure to add a hotkey, like Ctrl + Shift + O for the command in the settings for Obsidian.
23 |
24 | Just describe the character you're searching for
25 | and press ↵ to insert it into the editor.
26 | You can also search by Unicode codepoints!
27 |
28 | 
33 |
34 | ## Features
35 |
36 | - **Fuzzy Search**: Find characters by name, codepoint, or keywords.
37 | - **Direct Insert**: Insert characters directly into your editor.
38 | - **Favorites & Hotkeys**: Mark frequently used characters as favorites and assign custom hotkeys for quick insertion.
39 | - **Persistent Filters**: Configure and save filters for planes, categories, and custom sets to refine your searches.
40 | - **Usage Statistics**: Quickly access frequently used symbols.
41 | - **Configurable**: All settings are managed within Obsidian's settings pane.
42 |
43 | ## Using the Plugin
44 |
45 | To begin using the plugin, launch the search by executing the **Search Unicode characters** command. For more convenient access, consider assigning a hotkey:
46 |
47 | - Navigate to **Settings → Hotkeys**.
48 | - Find **Search Unicode characters** and assign your preferred hotkey (e.g., `Ctrl+Shift+O`).
49 |
50 | ### Searching for Characters
51 |
52 | Once the search modal is open, you can find characters by typing your query. This can be a descriptive term like `arrow` or `heart`, or a specific codepoint such as `269`.
53 |
54 | - Use the `↑` and `↓` arrow keys to navigate through the search results.
55 | - Press `Enter` to insert the selected character directly into your document.
56 |
57 | ### Managing Favorites
58 |
59 | You can manage your favorite characters from the search modal by adding them in the plugin settings.
60 |
61 | - View and manage all your favorites in **Settings → Unicode Search → Favorites**.
62 | - Assign a hotkey to any favorite character via the plugin's settings tab. This creates a new command, "Insert '\'", which can then have a hotkey assigned under **Settings → Hotkeys → Insert '\'**.
63 |
64 | ### Filtering Characters
65 |
66 | Character filters help you refine which characters appear in your search results. These are configured in **Settings → Unicode Search**.
67 |
68 | - Toggle filters for various Unicode planes and categories.
69 | - Define custom filters to tailor the search to your needs.
70 | - Be aware that a default set of filters is active. If you're unable to find a specific character, it might be excluded by the current filter settings.
71 | - Search for your character to find out it's plane/block/category, if it's missing here:
72 |
--------------------------------------------------------------------------------
/assets/mobile-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/4d434fc42c5f18dd5d123ab361ca55e1515c9d75/assets/mobile-search.png
--------------------------------------------------------------------------------
/assets/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/4d434fc42c5f18dd5d123ab361ca55e1515c9d75/assets/preview.gif
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import process from "process";
3 | import builtins from "builtin-modules";
4 | import {sassPlugin} from "esbuild-sass-plugin";
5 | import * as fs from "fs/promises";
6 |
7 | const banner =
8 | "/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD see https://github.com/BambusControl/obsidian-unicode-search for the source */";
9 |
10 | const prodBuild = (process.argv[2] === "production");
11 |
12 | const sourceDir = "./src/unicode-search";
13 | const outputDir = "./dist/unicode-search";
14 |
15 | /* Copy the manifest for working*/
16 | await fs.mkdir(outputDir, {recursive: true});
17 | await fs.copyFile("manifest.json", `${outputDir}/manifest.json`);
18 |
19 | const buildOptions = {
20 | banner: {
21 | js: banner,
22 | },
23 | entryPoints: [
24 | `${sourceDir}/main.ts`,
25 | `${sourceDir}/styles.scss`,
26 | ],
27 | entryNames: "[name]",
28 | outdir: outputDir,
29 | bundle: true,
30 | external: [
31 | "obsidian",
32 | ...builtins,
33 | ],
34 | format: "cjs",
35 | target: "ES6",
36 | logLevel: "info",
37 | sourcemap: prodBuild ? false : "inline",
38 | treeShaking: true,
39 | minify: prodBuild,
40 | plugins: [
41 | sassPlugin({
42 | syntax: "scss",
43 | style: prodBuild ? "compressed" : "expanded",
44 | }),
45 | ],
46 | drop: prodBuild ? ["console"] : []
47 | };
48 |
49 | try {
50 | if (prodBuild) {
51 | await esbuild.build(buildOptions);
52 | } else {
53 | await (await esbuild.context(buildOptions)).watch();
54 | }
55 |
56 | } catch {
57 | process.exit(1);
58 | }
59 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'node',
5 | };
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "unicode-search",
3 | "name": "Unicode Search",
4 | "version": "0.7.1",
5 | "minAppVersion": "1.8.7",
6 | "description": "Search and insert Unicode characters into your editor",
7 | "author": "BambusControl",
8 | "authorUrl": "https://github.com/BambusControl",
9 | "isDesktopOnly": false
10 | }
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-unicode-search",
3 | "version": "0.7.1",
4 | "description": "Obsidian plugin for searching Unicode characters.",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
9 | "export-schema": "typescript-json-schema --required --strictNullChecks --validationKeywords --ignoreErrors --refs --id https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json --out ./resources/save-data-schema.json tsconfig.json SaveData",
10 | "test": "jest"
11 | },
12 | "keywords": [
13 | "obsidian"
14 | ],
15 | "private": true,
16 | "repository": "https://github.com/BambusControl/obsidian-unicode-search",
17 | "author": {
18 | "name": "BambusControl",
19 | "url": "https://github.com/BambusControl"
20 | },
21 | "license": "Apache-2.0",
22 | "dependencies": {
23 | "papaparse": "^5.5.1"
24 | },
25 | "devDependencies": {
26 | "@types/jest": "^29",
27 | "@types/node": "^18",
28 | "@types/papaparse": "^5",
29 | "@typescript-eslint/eslint-plugin": "^8",
30 | "@typescript-eslint/parser": "^8",
31 | "builtin-modules": "^4",
32 | "esbuild": "^0",
33 | "esbuild-sass-plugin": "^3",
34 | "jest": "^29",
35 | "obsidian": "1.8.7",
36 | "ts-jest": "^29",
37 | "tslib": "^2",
38 | "typescript": "^5",
39 | "typescript-json-schema": "^0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/resources/save-data-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$id": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json",
3 | "$schema": "http://json-schema.org/draft-07/schema#",
4 | "definitions": {
5 | "BlockFilter": {
6 | "allOf": [
7 | {
8 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/CodepointInterval"
9 | },
10 | {
11 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/InclusionFlag"
12 | }
13 | ],
14 | "description": "Block of Unicode Codepoints"
15 | },
16 | "CategoryFilter": {
17 | "description": "Filter for a single category.",
18 | "properties": {
19 | "abbreviation": {
20 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/CharacterCategoryType",
21 | "description": "Two letter abbreviation of the category."
22 | },
23 | "included": {
24 | "description": "Indicates whether a character is included in search or not",
25 | "type": "boolean"
26 | }
27 | },
28 | "required": [
29 | "abbreviation",
30 | "included"
31 | ],
32 | "type": "object"
33 | },
34 | "CategoryGroupFilter": {
35 | "description": "Filters for a group of categories.",
36 | "properties": {
37 | "abbreviation": {
38 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/CharacterCategoryGroupType",
39 | "description": "Single letter abbreviation of the category group."
40 | },
41 | "categories": {
42 | "description": "Categories which belong to the group.",
43 | "items": {
44 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/CategoryFilter"
45 | },
46 | "type": "array"
47 | }
48 | },
49 | "required": [
50 | "abbreviation",
51 | "categories"
52 | ],
53 | "type": "object"
54 | },
55 | "CharacterCategoryGroupType": {
56 | "enum": [
57 | "C",
58 | "L",
59 | "M",
60 | "N",
61 | "P",
62 | "S",
63 | "Z"
64 | ],
65 | "type": "string"
66 | },
67 | "CharacterCategoryType": {
68 | "enum": [
69 | "Cc",
70 | "Cf",
71 | "Cn",
72 | "Co",
73 | "Cs",
74 | "Ll",
75 | "Lm",
76 | "Lo",
77 | "Lt",
78 | "Lu",
79 | "Mc",
80 | "Me",
81 | "Mn",
82 | "Nd",
83 | "Nl",
84 | "No",
85 | "Pc",
86 | "Pd",
87 | "Pe",
88 | "Pf",
89 | "Pi",
90 | "Po",
91 | "Ps",
92 | "Sc",
93 | "Sk",
94 | "Sm",
95 | "So",
96 | "Zl",
97 | "Zp",
98 | "Zs"
99 | ],
100 | "type": "string"
101 | },
102 | "CharacterUseFragment": {
103 | "description": "User generated usage data",
104 | "properties": {
105 | "codepoints": {
106 | "description": "Statistics of the individual codepoint usage",
107 | "items": {
108 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/RawCodepointUse"
109 | },
110 | "type": "array"
111 | },
112 | "initialized": {
113 | "type": "boolean"
114 | },
115 | "version": {
116 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/SaveDataVersion"
117 | }
118 | },
119 | "required": [
120 | "codepoints",
121 | "initialized",
122 | "version"
123 | ],
124 | "type": "object"
125 | },
126 | "CodepointAttribute": {
127 | "description": "General attributes of a Unicode codepoint",
128 | "properties": {
129 | "category": {
130 | "description": "Unicode category of the character",
131 | "type": "string"
132 | },
133 | "name": {
134 | "description": "Unicode provided description of the character",
135 | "type": "string"
136 | }
137 | },
138 | "required": [
139 | "category",
140 | "name"
141 | ],
142 | "type": "object"
143 | },
144 | "CodepointInterval": {
145 | "description": "Represents a closed interval/range of Unicode Code Points",
146 | "properties": {
147 | "end": {
148 | "type": "number"
149 | },
150 | "start": {
151 | "type": "number"
152 | }
153 | },
154 | "required": [
155 | "end",
156 | "start"
157 | ],
158 | "type": "object"
159 | },
160 | "CodepointKey": {
161 | "description": "Universally used key for a Unicode codepoint",
162 | "properties": {
163 | "codepoint": {
164 | "description": "A single character defined by a Unicode code point",
165 | "maxLength": 1,
166 | "minLength": 1,
167 | "type": "string"
168 | }
169 | },
170 | "required": [
171 | "codepoint"
172 | ],
173 | "type": "object"
174 | },
175 | "FavoriteInfo": {
176 | "description": "Raw favorite information, as stored in save data",
177 | "properties": {
178 | "added": {
179 | "description": "Date when the codepoint was added to favorites",
180 | "type": "string"
181 | },
182 | "hotkey": {
183 | "description": "Whether the codepoint has a hotkey command",
184 | "type": "boolean"
185 | }
186 | },
187 | "required": [
188 | "added",
189 | "hotkey"
190 | ],
191 | "type": "object"
192 | },
193 | "FavoritesFragment": {
194 | "description": "Users favorite codepoints",
195 | "properties": {
196 | "codepoints": {
197 | "description": "List of favorite codepoints",
198 | "items": {
199 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/RawCodepointFavorite"
200 | },
201 | "type": "array"
202 | },
203 | "initialized": {
204 | "type": "boolean"
205 | },
206 | "version": {
207 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/SaveDataVersion"
208 | }
209 | },
210 | "required": [
211 | "codepoints",
212 | "initialized",
213 | "version"
214 | ],
215 | "type": "object"
216 | },
217 | "FilterFragment": {
218 | "description": "User saved character filters",
219 | "properties": {
220 | "initialized": {
221 | "type": "boolean"
222 | },
223 | "unicode": {
224 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/UnicodeFilter",
225 | "description": "Filter criteria for Unicode characters"
226 | },
227 | "version": {
228 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/SaveDataVersion"
229 | }
230 | },
231 | "required": [
232 | "initialized",
233 | "unicode",
234 | "version"
235 | ],
236 | "type": "object"
237 | },
238 | "InclusionFlag": {
239 | "description": "A flag indicating whether a character is included in search or not",
240 | "properties": {
241 | "included": {
242 | "description": "Indicates whether a character is included in search or not",
243 | "type": "boolean"
244 | }
245 | },
246 | "required": [
247 | "included"
248 | ],
249 | "type": "object"
250 | },
251 | "MetaFragment": {
252 | "description": "Meta information for the datastore",
253 | "properties": {
254 | "events": {
255 | "description": "A collection of unique data events.",
256 | "items": {
257 | "const": "download_characters",
258 | "type": "string"
259 | },
260 | "type": "array"
261 | },
262 | "initialized": {
263 | "type": "boolean"
264 | },
265 | "pluginVersion": {
266 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/PluginVersion",
267 | "description": "Latest used version of the plugin which used this data"
268 | },
269 | "version": {
270 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/SaveDataVersion"
271 | }
272 | },
273 | "required": [
274 | "events",
275 | "initialized",
276 | "pluginVersion",
277 | "version"
278 | ],
279 | "type": "object"
280 | },
281 | "PlaneFilter": {
282 | "description": "Unicode Plane of Unicode Blocks",
283 | "properties": {
284 | "blocks": {
285 | "description": "Filter criteria for blocks of Unicode characters",
286 | "items": {
287 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/BlockFilter"
288 | },
289 | "type": "array"
290 | },
291 | "end": {
292 | "type": "number"
293 | },
294 | "start": {
295 | "type": "number"
296 | }
297 | },
298 | "required": [
299 | "blocks",
300 | "end",
301 | "start"
302 | ],
303 | "type": "object"
304 | },
305 | "PluginVersion": {
306 | "description": "Version of the plugin.\n\nMust comply with RegEx:\n```^[0-9]+\\\\.[0-9]+\\\\.[0-9]+(?:-[A-Z]+)?$```",
307 | "enum": [
308 | "0.2.0",
309 | "0.2.1",
310 | "0.2.2",
311 | "0.2.3",
312 | "0.3.0",
313 | "0.4.0",
314 | "0.4.1",
315 | "0.5.0",
316 | "0.6.0",
317 | "0.6.1",
318 | "0.7.0",
319 | "0.7.1"
320 | ],
321 | "type": "string"
322 | },
323 | "RawCodepointFavorite": {
324 | "allOf": [
325 | {
326 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/CodepointKey"
327 | },
328 | {
329 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/FavoriteInfo"
330 | }
331 | ],
332 | "description": "Favorite infor of a specific codepoint as stored in save data"
333 | },
334 | "RawCodepointUse": {
335 | "allOf": [
336 | {
337 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/CodepointKey"
338 | },
339 | {
340 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/RawUsageInfo"
341 | }
342 | ],
343 | "description": "Usage information of a specific codepoint as stored in save data"
344 | },
345 | "RawUsageInfo": {
346 | "description": "Raw usage information, as stored in save data",
347 | "properties": {
348 | "firstUsed": {
349 | "description": "Alias to hint that a string is a date string",
350 | "type": "string"
351 | },
352 | "lastUsed": {
353 | "description": "Alias to hint that a string is a date string",
354 | "type": "string"
355 | },
356 | "useCount": {
357 | "type": "number"
358 | }
359 | },
360 | "required": [
361 | "firstUsed",
362 | "lastUsed",
363 | "useCount"
364 | ],
365 | "type": "object"
366 | },
367 | "SaveDataVersion": {
368 | "description": "Version of the save data schema.\n\nMust comply with RegEx:\n```^[0-9]+\\\\.[0-9]+\\\\.[0-9]+(?:-[A-Z]+)?$```\n\nThe version of the save data schema is independent of the plugin version",
369 | "enum": [
370 | "0.4.0",
371 | "0.5.0",
372 | "0.6.0",
373 | "0.7.0"
374 | ],
375 | "type": "string"
376 | },
377 | "UnicodeCodepoint": {
378 | "allOf": [
379 | {
380 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/CodepointKey"
381 | },
382 | {
383 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/CodepointAttribute"
384 | }
385 | ],
386 | "description": "Unicode codepoint representation throughout the plugin"
387 | },
388 | "UnicodeFilter": {
389 | "description": "User set filter data for Unicode characters.",
390 | "properties": {
391 | "categoryGroups": {
392 | "description": "Filter criteria for category groups of Unicode characters",
393 | "items": {
394 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/CategoryGroupFilter"
395 | },
396 | "type": "array"
397 | },
398 | "planes": {
399 | "description": "Filter criteria for planes of Unicode characters",
400 | "items": {
401 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/PlaneFilter"
402 | },
403 | "type": "array"
404 | }
405 | },
406 | "required": [
407 | "categoryGroups",
408 | "planes"
409 | ],
410 | "type": "object"
411 | },
412 | "UnicodeFragment": {
413 | "description": "Downloaded Unicode Character Database",
414 | "properties": {
415 | "codepoints": {
416 | "description": "Codepoints downloaded from the Unicode Character Database",
417 | "items": {
418 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/UnicodeCodepoint"
419 | },
420 | "type": "array"
421 | },
422 | "initialized": {
423 | "type": "boolean"
424 | },
425 | "version": {
426 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/SaveDataVersion"
427 | }
428 | },
429 | "required": [
430 | "codepoints",
431 | "initialized",
432 | "version"
433 | ],
434 | "type": "object"
435 | }
436 | },
437 | "description": "Structure of `data.json`, where each fragment is a self-managed data fragment",
438 | "properties": {
439 | "characters": {
440 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/UnicodeFragment",
441 | "description": "Local character database"
442 | },
443 | "favorites": {
444 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/FavoritesFragment",
445 | "description": "Favorites saved manually by the user"
446 | },
447 | "filter": {
448 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/FilterFragment",
449 | "description": "Filtering of downloaded/displayed codepoints"
450 | },
451 | "meta": {
452 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/MetaFragment",
453 | "description": "Metadata information about the save data itself"
454 | },
455 | "usage": {
456 | "$ref": "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/0.7.1/resources/save-data-schema.json#/definitions/CharacterUseFragment",
457 | "description": "Usage information generated by the user"
458 | }
459 | },
460 | "required": [
461 | "characters",
462 | "favorites",
463 | "filter",
464 | "meta",
465 | "usage"
466 | ],
467 | "type": "object"
468 | }
469 |
470 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=BambusControl_obsidian-unicode-search
2 | sonar.organization=bambuscontrol
3 |
4 | # This is the name and version displayed in the SonarCloud UI.
5 | sonar.projectVersion=0.7.1
6 |
7 |
8 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
9 | sonar.sources=./src
10 | sonar.tests=./tests
11 |
12 | # Encoding of the source code. Default is default system encoding
13 | sonar.sourceEncoding=UTF-8
14 |
15 | # Path for coverage reports
16 | sonar.javascript.lcov.reportPaths=./coverage/lcov.info
17 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareCharacterMatches.ts:
--------------------------------------------------------------------------------
1 | import {MaybeMetaCharacterSearchResult} from "../../unicode-search/components/characterSearch";
2 | import {Order} from "../order/order";
3 | import {compareSearchMatches} from "./compareSearchMatches";
4 | import {compareCharacters} from "./compareCharacters";
5 |
6 | export function compareCharacterMatches(
7 | left: MaybeMetaCharacterSearchResult,
8 | right: MaybeMetaCharacterSearchResult,
9 | recencyCutoff: Date,
10 | ): Order {
11 | const matchComparison = compareSearchMatches(left.match, right.match);
12 |
13 | if (matchComparison !== Order.Equal) {
14 | return matchComparison;
15 | }
16 |
17 | return compareCharacters(left.character, right.character, recencyCutoff);
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareCharacters.ts:
--------------------------------------------------------------------------------
1 | import {MetadataCharacter} from "../types/codepoint/character";
2 | import {Order} from "../order/order";
3 | import {compareFavoriteCharacters} from "./compareFavoriteCharacters";
4 | import {compareUsedCharacters} from "./compareUsedCharacters";
5 | import {compareCodepoints} from "./compareCodepoints";
6 |
7 | export function compareCharacters(
8 | left: MetadataCharacter,
9 | right: MetadataCharacter,
10 | recencyCutoff: Date,
11 | ): Order {
12 | const usedComparison = compareUsedCharacters(left, right, recencyCutoff);
13 |
14 | if (usedComparison !== Order.Equal) {
15 | return usedComparison;
16 | }
17 |
18 | const favoriteComparison = compareFavoriteCharacters(left, right);
19 |
20 | if (favoriteComparison !== Order.Equal) {
21 | return favoriteComparison;
22 | }
23 |
24 | return compareCodepoints(left, right);
25 | }
26 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareCodepoints.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeCodepoint} from "../types/codepoint/unicode";
2 | import {Order} from "../order/order";
3 | import {compareNumbers} from "./compareNumbers";
4 |
5 | export function compareCodepoints(
6 | left: UnicodeCodepoint,
7 | right: UnicodeCodepoint,
8 | ): Order {
9 | return compareNumbers(left.codepoint.codePointAt(0)!, right.codepoint.codePointAt(0)!)
10 | }
11 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareDates.ts:
--------------------------------------------------------------------------------
1 | import {Order} from "../order/order";
2 |
3 | export function compareDates(left: Date, right: Date): Order {
4 | if (left < right) {
5 | return Order.Smaller;
6 | }
7 |
8 | if (left > right) {
9 | return Order.Greater;
10 | }
11 |
12 | return Order.Equal;
13 | }
14 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareFavoriteCharacters.ts:
--------------------------------------------------------------------------------
1 | import {FavoriteCharacter, MaybeFavoriteCharacter, MaybeUsedCharacter} from "../types/codepoint/character";
2 | import {isFavoriteCharacter} from "../helpers/isFavoriteCharacter";
3 | import {Order} from "../order/order";
4 | import {compareNullable} from "./compareNullable";
5 | import {compareFavoriteInfo} from "./compareFavoriteInfo";
6 |
7 | function toFavoriteCharacter(character: MaybeUsedCharacter): FavoriteCharacter | null {
8 | return isFavoriteCharacter(character) ? character : null;
9 | }
10 |
11 | export function compareFavoriteCharacters(
12 | left: MaybeFavoriteCharacter,
13 | right: MaybeFavoriteCharacter,
14 | ): Order {
15 | return compareNullable(
16 | toFavoriteCharacter(left),
17 | toFavoriteCharacter(right),
18 | (l, r) => compareFavoriteInfo(l, r),
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareFavoriteInfo.ts:
--------------------------------------------------------------------------------
1 | import {Order} from "../order/order";
2 | import {inverse} from "../order/inverse";
3 | import {compareDates} from "./compareDates";
4 | import {ParsedFavoriteInfo} from "../types/savedata/favoriteInfo";
5 |
6 | export function compareFavoriteInfo(
7 | left: ParsedFavoriteInfo,
8 | right: ParsedFavoriteInfo,
9 | ): Order {
10 | // We want the most recently added to be first.
11 | return inverse(compareDates(left.added, right.added));
12 | }
13 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareNullable.ts:
--------------------------------------------------------------------------------
1 | import {Order} from "../order/order";
2 |
3 | export function compareNullable(
4 | left: T | null | undefined,
5 | right: T | null | undefined,
6 | compareFn: (a: T, b: T) => Order,
7 | ): Order {
8 | if (left == null && right == null) {
9 | return Order.Equal;
10 | }
11 |
12 | if (left != null && right != null) {
13 | return compareFn(left, right);
14 | }
15 |
16 | return left == null
17 | ? Order.After
18 | : Order.Before;
19 | }
20 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareNumbers.ts:
--------------------------------------------------------------------------------
1 | import {Order} from "../order/order";
2 |
3 | export function compareNumbers(left: number, right: number): Order {
4 | if (left === right) {
5 | return Order.Equal;
6 | }
7 |
8 | return left < right
9 | ? Order.Smaller
10 | : Order.Greater;
11 | }
12 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareSearchMatchScores.ts:
--------------------------------------------------------------------------------
1 | import {SearchMatchAttributes} from "../../unicode-search/components/characterSearch";
2 | import {Order} from "../order/order";
3 |
4 | export function compareSearchMatchScores(left: SearchMatchAttributes, right: SearchMatchAttributes): Order {
5 | /* Matches are scored with negative values up to 0, with 0 meaning full match for fuzzy search */
6 | const codepointScore = right.codepoint.score - left.codepoint.score;
7 | const nameScore = right.name.score - left.name.score;
8 | const value = codepointScore + nameScore;
9 | const nValue = value / Math.abs(value);
10 |
11 | return nValue as Order;
12 | }
13 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareSearchMatches.ts:
--------------------------------------------------------------------------------
1 | import {MaybeSearchMatchAttributes} from "../../unicode-search/components/characterSearch";
2 | import {Order} from "../order/order";
3 | import {compareSearchMatchScores} from "./compareSearchMatchScores";
4 | import {compareNullable} from "./compareNullable";
5 | import {fillNullSearchMatchScores} from "./fillNullSearchMatchScores";
6 |
7 | export function compareSearchMatches(left: MaybeSearchMatchAttributes, right: MaybeSearchMatchAttributes): Order {
8 | const leftNull = left.codepoint == null && left.name == null;
9 | const rightNull = right.codepoint == null && right.name == null;
10 |
11 | return compareNullable(
12 | leftNull ? null : fillNullSearchMatchScores(left),
13 | rightNull ? null : fillNullSearchMatchScores(right),
14 | (l, r) => compareSearchMatchScores(l, r)
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareUsageInfo.ts:
--------------------------------------------------------------------------------
1 | import {Order} from "../order/order";
2 | import {inverse} from "../order/inverse";
3 | import {compareNumbers} from "./compareNumbers";
4 |
5 | import {compareDates} from "./compareDates";
6 | import {compareNullable} from "./compareNullable";
7 |
8 |
9 | import {UsageInfo} from "../types/savedata/usageInfo";
10 |
11 | export function compareUsageInfo(
12 | left: UsageInfo,
13 | right: UsageInfo,
14 | recencyCutoff: Date,
15 | ): Order {
16 | // We want the most recently used to be first.
17 | const lastUsedComparison = compareNullable(
18 | left.lastUsed < recencyCutoff ? null : left.lastUsed,
19 | right.lastUsed < recencyCutoff ? null : right.lastUsed,
20 | (l, r) => inverse(compareDates(l, r))
21 | );
22 |
23 | if (lastUsedComparison !== Order.Equal) {
24 | return lastUsedComparison;
25 | }
26 |
27 | // We want the most used to be before the less used.
28 | return inverse(compareNumbers(left.useCount, right.useCount));
29 | }
30 |
--------------------------------------------------------------------------------
/src/libraries/comparison/compareUsedCharacters.ts:
--------------------------------------------------------------------------------
1 | import {Order} from "../order/order";
2 | import {compareNullable} from "./compareNullable";
3 | import {compareUsageInfo} from "./compareUsageInfo";
4 | import {MaybeUsedCharacter, UsedCharacter} from "../types/codepoint/character";
5 | import {isUsedCharacter} from "../helpers/isUsedCharacter";
6 |
7 | function toUsedCharacter(character: MaybeUsedCharacter): UsedCharacter | null {
8 | return isUsedCharacter(character) ? character : null;
9 | }
10 |
11 | export function compareUsedCharacters(
12 | left: MaybeUsedCharacter,
13 | right: MaybeUsedCharacter,
14 | recencyCutoff: Date,
15 | ): Order {
16 | return compareNullable(
17 | toUsedCharacter(left),
18 | toUsedCharacter(right),
19 | (l, r) => compareUsageInfo(l, r, recencyCutoff),
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/libraries/comparison/fillNullCharacterMatchScores.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MaybeMetaCharacterSearchResult,
3 | MetaCharacterSearchResult
4 | } from "../../unicode-search/components/characterSearch";
5 | import {fillNullSearchMatchScores} from "./fillNullSearchMatchScores";
6 |
7 | export function fillNullCharacterMatchScores(characterMatch: MaybeMetaCharacterSearchResult): MetaCharacterSearchResult {
8 | return {
9 | ...characterMatch,
10 | match: fillNullSearchMatchScores(characterMatch.match),
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/src/libraries/comparison/fillNullSearchMatchScores.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MaybeSearchMatchAttributes,
3 | NONE_RESULT,
4 | SearchMatchAttributes
5 | } from "../../unicode-search/components/characterSearch";
6 |
7 | export function fillNullSearchMatchScores(match: MaybeSearchMatchAttributes): SearchMatchAttributes {
8 | return {
9 | name: match.name ?? NONE_RESULT,
10 | codepoint: match.codepoint ?? NONE_RESULT
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/libraries/data/characterCategory.ts:
--------------------------------------------------------------------------------
1 | //noinspection JSUnusedGlobalSymbols
2 |
3 | export enum CharacterCategoryLetter {
4 | Lowercase = "Ll",
5 | Modifier = "Lm",
6 | Titlecase = "Lt",
7 | Uppercase = "Lu",
8 | Other = "Lo",
9 | }
10 |
11 | export enum CharacterCategoryMark {
12 | SpacingCombining = "Mc",
13 | Enclosing = "Me",
14 | NonSpacing = "Mn",
15 | }
16 |
17 | export enum CharacterCategoryNumber {
18 | DecimalDigit = "Nd",
19 | Letter = "Nl",
20 | Other = "No",
21 | }
22 |
23 | export enum CharacterCategoryPunctuation {
24 | Connector = "Pc",
25 | Dash = "Pd",
26 | InitialQuote = "Pi",
27 | FinalQuote = "Pf",
28 | Open = "Ps",
29 | Close = "Pe",
30 | Other = "Po",
31 | }
32 |
33 | export enum CharacterCategorySymbol {
34 | Currency = "Sc",
35 | Modifier = "Sk",
36 | Math = "Sm",
37 | Other = "So",
38 | }
39 |
40 | export enum CharacterCategorySeparator {
41 | Line = "Zl",
42 | Paragraph = "Zp",
43 | Space = "Zs",
44 | }
45 |
46 | export enum CharacterCategoryOther {
47 | Control = "Cc",
48 | Format = "Cf",
49 | NotAssigned = "Cn",
50 | PrivateUse = "Co",
51 | Surrogate = "Cs",
52 | }
53 |
54 | export type CharacterCategory
55 | = CharacterCategoryLetter
56 | | CharacterCategoryMark
57 | | CharacterCategoryNumber
58 | | CharacterCategoryPunctuation
59 | | CharacterCategorySymbol
60 | | CharacterCategorySeparator
61 | | CharacterCategoryOther
62 |
63 | export type CharacterCategoryType =
64 | "Ll" | "Lm" | "Lt" | "Lu" | "Lo"
65 | | "Mc" | "Me" | "Mn"
66 | | "Nd" | "Nl" | "No"
67 | | "Pc" | "Pd" | "Pi" | "Pf" | "Ps" | "Pe" | "Po"
68 | | "Sc" | "Sk" | "Sm" | "So"
69 | | "Zl" | "Zp" | "Zs"
70 | | "Cc" | "Cf" | "Cn" | "Co" | "Cs"
71 |
--------------------------------------------------------------------------------
/src/libraries/data/characterCategoryGroup.ts:
--------------------------------------------------------------------------------
1 | //noinspection JSUnusedGlobalSymbols
2 |
3 | export enum CharacterCategoryGroup {
4 | Letter = "L",
5 | Mark = "M",
6 | Number = "N",
7 | Punctuation = "P",
8 | Symbol = "S",
9 | Separator = "Z",
10 | Other = "C",
11 | }
12 |
13 | export type CharacterCategoryGroupType = "L" | "M" | "N" | "P" | "S" | "Z" | "C";
14 |
--------------------------------------------------------------------------------
/src/libraries/data/unicodeCharacterCategories.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeGeneralCategoryGroup} from "../types/unicode/unicodeGeneralCategoryGroup";
2 |
3 | export const UNICODE_CHARACTER_CATEGORIES: UnicodeGeneralCategoryGroup[] = [
4 | {
5 | abbreviation: "L",
6 | name: "Letter",
7 | categories: [
8 | {
9 | abbreviation: "Lu",
10 | name: "Uppercase Letter",
11 | description: "an uppercase letter",
12 | },
13 |
14 | {
15 | abbreviation: "Ll",
16 | name: "Lowercase Letter",
17 | description: "a lowercase letter",
18 | },
19 |
20 | {
21 | abbreviation: "Lt",
22 | name: "Titlecase Letter",
23 | description: "a digraph encoded as a single character, with first part uppercase",
24 | },
25 |
26 | {
27 | abbreviation: "Lm",
28 | name: "Modifier Letter",
29 | description: "a modifier letter",
30 | },
31 |
32 | {
33 | abbreviation: "Lo",
34 | name: "Other Letter",
35 | description: "other letters, including syllables and ideographs",
36 | },
37 | ]
38 | },
39 | {
40 | abbreviation: "M",
41 | name: "Mark",
42 | categories: [
43 | {
44 | abbreviation: "Mn",
45 | name: "Nonspacing Mark",
46 | description: "a nonspacing combining mark (zero advance width)",
47 | },
48 |
49 | {
50 | abbreviation: "Mc",
51 | name: "Spacing Mark",
52 | description: "a spacing combining mark (positive advance width)",
53 | },
54 |
55 | {
56 | abbreviation: "Me",
57 | name: "Enclosing Mark",
58 | description: "an enclosing combining mark",
59 | },
60 | ],
61 | },
62 |
63 | {
64 | abbreviation: "N",
65 | name: "Number",
66 | categories: [
67 | {
68 | abbreviation: "Nd",
69 | name: "Decimal Number",
70 | description: "a decimal digit",
71 | },
72 |
73 | {
74 | abbreviation: "Nl",
75 | name: "Letter Number",
76 | description: "a letterlike numeric character",
77 | },
78 |
79 | {
80 | abbreviation: "No",
81 | name: "Other Number",
82 | description: "a numeric character of other type",
83 | },
84 | ]
85 | },
86 | {
87 | abbreviation: "P",
88 | name: "Punctuation",
89 | categories: [
90 | {
91 | abbreviation: "Pc",
92 | name: "Connector Punctuation",
93 | description: "a connecting punctuation mark, like a tie",
94 | },
95 |
96 | {
97 | abbreviation: "Pd",
98 | name: "Dash Punctuation",
99 | description: "a dash or hyphen punctuation mark",
100 | },
101 |
102 | {
103 | abbreviation: "Ps",
104 | name: "Open Punctuation",
105 | description: "an opening punctuation mark (of a pair)",
106 | },
107 |
108 | {
109 | abbreviation: "Pe",
110 | name: "Close Punctuation",
111 | description: "a closing punctuation mark (of a pair)",
112 | },
113 |
114 | {
115 | abbreviation: "Pi",
116 | name: "Initial Punctuation",
117 | description: "an initial quotation mark",
118 | },
119 |
120 | {
121 | abbreviation: "Pf",
122 | name: "Final Punctuation",
123 | description: "a final quotation mark",
124 | },
125 |
126 | {
127 | abbreviation: "Po",
128 | name: "Other Punctuation",
129 | description: "a punctuation mark of other type",
130 | },
131 | ]
132 | },
133 | {
134 | abbreviation: "S",
135 | name: "Symbol",
136 | categories: [
137 | {
138 | abbreviation: "Sm",
139 | name: "Math Symbol",
140 | description: "a symbol of mathematical use",
141 | },
142 |
143 | {
144 | abbreviation: "Sc",
145 | name: "Currency Symbol",
146 | description: "a currency sign",
147 | },
148 |
149 | {
150 | abbreviation: "Sk",
151 | name: "Modifier Symbol",
152 | description: "a non-letterlike modifier symbol",
153 | },
154 | {
155 | abbreviation: "So",
156 | name: "Other Symbol",
157 | description: "a symbol of other type",
158 | },
159 | ]
160 | },
161 |
162 | {
163 | abbreviation: "Z",
164 | name: "Separator",
165 | categories: [
166 | {
167 | abbreviation: "Zs",
168 | name: "Space Separator",
169 | description: "a space character (of various non-zero widths)",
170 | },
171 |
172 | {
173 | abbreviation: "Zl",
174 | name: "Line Separator",
175 | description: "U+2028 LINE SEPARATOR only",
176 | },
177 |
178 | {
179 | abbreviation: "Zp",
180 | name: "Paragraph Separator",
181 | description: "U+2029 PARAGRAPH SEPARATOR only",
182 | },
183 | ]
184 | },
185 | {
186 | abbreviation: "C",
187 | name: "Other",
188 | categories: [
189 | {
190 | abbreviation: "Cc",
191 | name: "Control",
192 | description: "a C0 or C1 control code",
193 | },
194 |
195 | {
196 | abbreviation: "Cf",
197 | name: "Format",
198 | description: "a format control character",
199 | },
200 |
201 | {
202 | abbreviation: "Cs",
203 | name: "Surrogate",
204 | description: "a surrogate code point",
205 | },
206 |
207 | {
208 | abbreviation: "Co",
209 | name: "Private Use",
210 | description: "a private-use character",
211 | },
212 |
213 | {
214 | abbreviation: "Cn",
215 | name: "Unassigned",
216 | description: "a reserved unassigned code point or a noncharacter",
217 | },
218 | ]
219 | },
220 | ];
221 |
--------------------------------------------------------------------------------
/src/libraries/data/unicodePlaneNumber.ts:
--------------------------------------------------------------------------------
1 | export type UnicodePlaneNumber = 0 | 1 | 2 | 3 | 14 | 15 | 16;
2 |
--------------------------------------------------------------------------------
/src/libraries/helpers/asHexadecimal.ts:
--------------------------------------------------------------------------------
1 | export function asHexadecimal(n: number): string {
2 | return n.toString(16).padStart(4, "0");
3 | }
4 |
--------------------------------------------------------------------------------
/src/libraries/helpers/averageUseCount.ts:
--------------------------------------------------------------------------------
1 | import {UsageCount} from "../types/savedata/usageInfo";
2 |
3 | export function averageUseCount(items: UsageCount[]): number {
4 | const result = items.reduce(
5 | (acc, item) => ({
6 | totalUses: acc.totalUses + item.useCount,
7 | itemCount: acc.itemCount + 1
8 | }),
9 | {totalUses: 0, itemCount: 0}
10 | );
11 |
12 | const {totalUses, itemCount} = result;
13 | return itemCount === 0 ? 0 : totalUses / itemCount;
14 | }
15 |
--------------------------------------------------------------------------------
/src/libraries/helpers/codePointIn.ts:
--------------------------------------------------------------------------------
1 | import {Codepoint} from "../types/codepoint/unicode";
2 | import {CodepointInterval} from "../types/codepoint/codepointInterval";
3 |
4 | export function codepointIn(codepoint: Codepoint, interval: CodepointInterval): boolean {
5 | return codepoint >= interval.start
6 | && codepoint <= interval.end;
7 | }
8 |
--------------------------------------------------------------------------------
/src/libraries/helpers/getRandomItem.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeSearchError} from "../../unicode-search/errors/unicodeSearchError";
2 |
3 | export function getRandomItem(items: T[]): T {
4 | if (items.length < 1) {
5 | throw new UnicodeSearchError("Cannot get a random item from an empty array")
6 | }
7 |
8 | return items[Math.floor(Math.random() * items.length)];
9 | }
10 |
--------------------------------------------------------------------------------
/src/libraries/helpers/intervalWithin.ts:
--------------------------------------------------------------------------------
1 | import {CodepointInterval} from "../types/codepoint/codepointInterval";
2 |
3 | export function intervalWithin(outer: CodepointInterval, inner: CodepointInterval): boolean {
4 | return outer.start <= inner.start
5 | && outer.end >= inner.end;
6 | }
7 |
--------------------------------------------------------------------------------
/src/libraries/helpers/intervalsEqual.ts:
--------------------------------------------------------------------------------
1 | import {CodepointInterval} from "../types/codepoint/codepointInterval";
2 |
3 | export function intervalsEqual(left: CodepointInterval, right: CodepointInterval): boolean {
4 | return left.start === right.start
5 | && left.end === right.end;
6 | }
7 |
--------------------------------------------------------------------------------
/src/libraries/helpers/isFavoriteCharacter.ts:
--------------------------------------------------------------------------------
1 | import {FavoriteCharacter, MaybeFavoriteCharacter} from "../types/codepoint/character";
2 |
3 | export function isFavoriteCharacter(character: MaybeFavoriteCharacter): character is FavoriteCharacter {
4 | return character != null
5 | && "added" in character
6 | && "hotkey" in character
7 | ;
8 | }
9 |
--------------------------------------------------------------------------------
/src/libraries/helpers/isTypeSaveData.ts:
--------------------------------------------------------------------------------
1 | import {DataFragment} from "../types/savedata/dataFragment";
2 | import {Char, CodepointKey} from "../types/codepoint/unicode";
3 |
4 |
5 | export function isTypeDataFragment(object: any): object is DataFragment {
6 | return object != null
7 | && "initialized" in object
8 | && "version" in object
9 | ;
10 | }
11 |
12 | export function isCodepointKey(object: any): object is CodepointKey {
13 | return object != null
14 | && "codepoint" in object
15 | && isChar(object.codepoint);
16 |
17 | }
18 | export function isChar(object: any): object is Char {
19 | return object != null
20 | && typeof object === "string"
21 | && object.length === 1
22 | }
23 |
--------------------------------------------------------------------------------
/src/libraries/helpers/isUsedCharacter.ts:
--------------------------------------------------------------------------------
1 | import {MaybeUsedCharacter, UsedCharacter} from "../types/codepoint/character";
2 |
3 | export function isUsedCharacter(character: MaybeUsedCharacter): character is UsedCharacter {
4 | return character != null
5 | && "useCount" in character
6 | && "firstUsed" in character
7 | && "lastUsed" in character
8 | ;
9 | }
10 |
--------------------------------------------------------------------------------
/src/libraries/helpers/matchedNameOrCodepoint.ts:
--------------------------------------------------------------------------------
1 | import {MaybeMetaCharacterSearchResult, MetaCharacterSearchResult} from "../../unicode-search/components/characterSearch";
2 |
3 | export function matchedNameOrCodepoint(match: MetaCharacterSearchResult | MaybeMetaCharacterSearchResult) {
4 | return match.match.name != null || match.match.codepoint != null;
5 | }
6 |
--------------------------------------------------------------------------------
/src/libraries/helpers/mergeIntervals.ts:
--------------------------------------------------------------------------------
1 | import {CodepointInterval} from "../types/codepoint/codepointInterval";
2 |
3 | /**
4 | * @param codepointIntervals
5 | * @see https://www.geeksforgeeks.org/merging-intervals/
6 | */
7 | export function mergeIntervals(codepointIntervals: CodepointInterval[]): CodepointInterval[] {
8 | const intervals = Array.from(codepointIntervals);
9 |
10 | /* Sort intervals in increasing order of start time */
11 | intervals.sort((a, b) => a.start - b.start);
12 |
13 | /* Stores index of last element in output array (modified intervals[]) */
14 | let p = 0;
15 |
16 | for (let c = 1; c < intervals.length; c++) {
17 | const previous = intervals[p];
18 | const current = intervals[c];
19 |
20 | const overlaps = previous.end >= current.start;
21 |
22 | if (overlaps) {
23 | previous.end = Math.max(previous.end, current.end);
24 | } else {
25 | p++;
26 | intervals[p] = current;
27 | }
28 | }
29 |
30 | /* intervals[0 .. p - 1] stores the merged intervals */
31 | return intervals.slice(0, p + 1);
32 | }
33 |
--------------------------------------------------------------------------------
/src/libraries/helpers/mostRecentUses.ts:
--------------------------------------------------------------------------------
1 | import {compareDates} from "../comparison/compareDates";
2 | import {inverse} from "../order/inverse";
3 |
4 |
5 | import {UsageDate} from "../types/savedata/usageInfo";
6 |
7 | export function mostRecentUses(items: UsageDate[]): Date[] {
8 | return items.slice()
9 | .map(value => value.lastUsed)
10 | .sort((l, r) => inverse(compareDates(l, r)));
11 | }
12 |
--------------------------------------------------------------------------------
/src/libraries/helpers/parseUsageInfo.ts:
--------------------------------------------------------------------------------
1 | import {UsageInfo, RawUsageInfo} from "../types/savedata/usageInfo";
2 |
3 | export function parseUsageInfo(value: T & RawUsageInfo): T & UsageInfo {
4 | return {
5 | ...value,
6 | lastUsed: new Date(value.lastUsed),
7 | firstUsed: new Date(value.firstUsed),
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/libraries/helpers/serializeFavoriteInfo.ts:
--------------------------------------------------------------------------------
1 | import {FavoriteInfo, ParsedFavoriteInfo} from "../types/savedata/favoriteInfo";
2 |
3 | export function serializeFavoriteInfo(value: T & ParsedFavoriteInfo): T & FavoriteInfo {
4 | return {
5 | ...value,
6 | added: value.added.toJSON(),
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/libraries/helpers/serializeUsageInfo.ts:
--------------------------------------------------------------------------------
1 | import {UsageInfo, RawUsageInfo} from "../types/savedata/usageInfo";
2 |
3 | export function serializeUsageInfo(value: T & UsageInfo): T & RawUsageInfo {
4 | return {
5 | ...value,
6 | lastUsed: value.lastUsed.toJSON(),
7 | firstUsed: value.firstUsed.toJSON(),
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/libraries/helpers/toHexadecimal.ts:
--------------------------------------------------------------------------------
1 | import {asHexadecimal} from "./asHexadecimal";
2 |
3 | import {CodepointKey} from "../types/codepoint/unicode";
4 |
5 | export function toHexadecimal(character: CodepointKey): string {
6 | /* Characters are expected to always have a single character */
7 | return asHexadecimal(character.codepoint.codePointAt(0)!);
8 | }
9 |
--------------------------------------------------------------------------------
/src/libraries/helpers/toNullMatch.ts:
--------------------------------------------------------------------------------
1 | import {MaybeUsedCharacter} from "../types/codepoint/character";
2 | import {MaybeMetaCharacterSearchResult} from "../../unicode-search/components/characterSearch";
3 |
4 | export function toNullMatch(character: MaybeUsedCharacter): MaybeMetaCharacterSearchResult {
5 | return {
6 | character: character,
7 | match: {
8 | codepoint: null,
9 | name: null
10 | }
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/src/libraries/helpers/toSearchQueryMatch.ts:
--------------------------------------------------------------------------------
1 | import {prepareFuzzySearch, prepareSimpleSearch} from "obsidian";
2 | import {MaybeUsedCharacter} from "../types/codepoint/character";
3 | import {MaybeMetaCharacterSearchResult} from "../../unicode-search/components/characterSearch";
4 | import {toHexadecimal} from "./toHexadecimal";
5 |
6 | export function toSearchQueryMatch(query: string) {
7 | const isHexSafe = query.length <= 4 && !query.contains(" ");
8 |
9 | const codepointSearch = isHexSafe ? prepareSimpleSearch(query) : ((_: string) => null);
10 | const fuzzyNameSearch = prepareFuzzySearch(query);
11 |
12 | return (character: MaybeUsedCharacter): MaybeMetaCharacterSearchResult => ({
13 | character: character,
14 | match: {
15 | codepoint: codepointSearch(toHexadecimal(character)),
16 | name: fuzzyNameSearch(character.name)
17 | }
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/libraries/order/inverse.ts:
--------------------------------------------------------------------------------
1 | import {Order} from "./order";
2 |
3 | export function inverse(order: Order): Order {
4 | switch (order) {
5 | case Order.Before:
6 | return Order.After;
7 | case Order.After:
8 | return Order.Before;
9 | default:
10 | return Order.Equal;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/libraries/order/order.ts:
--------------------------------------------------------------------------------
1 | export enum Order {
2 |
3 | /**
4 | * **First** argument is **smaller than** the **second** argument.
5 | */
6 | Before = -1,
7 | Smaller = Before,
8 |
9 | /**
10 | * **First** argument is **equal to** the **second** argument.
11 | */
12 | Equal = 0,
13 |
14 | /**
15 | * **First** argument is **greater than** the **second** argument.
16 | */
17 | After = 1,
18 | Greater = After,
19 | }
20 |
--------------------------------------------------------------------------------
/src/libraries/types/codepoint/character.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeCodepoint} from "./unicode";
2 |
3 |
4 |
5 | import {ParsedFavoriteInfo} from "../savedata/favoriteInfo";
6 | import {UsageInfo} from "../savedata/usageInfo";
7 |
8 | /**
9 | * Base representation of a character
10 | */
11 | export type Character = UnicodeCodepoint;
12 |
13 | export type UsedCharacter = Character & UsageInfo;
14 | export type MaybeUsedCharacter = Character | UsedCharacter;
15 |
16 | export type FavoriteCharacter = Character & ParsedFavoriteInfo;
17 | export type MaybeFavoriteCharacter = Character | FavoriteCharacter;
18 |
19 | /**
20 | * Character with attached metadata for use throughout the plugin
21 | */
22 | export type MetadataCharacter = Character | UsedCharacter | FavoriteCharacter;
23 |
24 | export type CharacterKey = Character["codepoint"];
25 |
--------------------------------------------------------------------------------
/src/libraries/types/codepoint/codepointInterval.ts:
--------------------------------------------------------------------------------
1 | import {Codepoint} from "./unicode";
2 |
3 | /**
4 | * Represents a closed interval/range of Unicode Code Points
5 | */
6 | export interface CodepointInterval {
7 | start: Codepoint,
8 | end: Codepoint,
9 | }
10 |
--------------------------------------------------------------------------------
/src/libraries/types/codepoint/extension.ts:
--------------------------------------------------------------------------------
1 | import {FavoriteInfo as RawFavoriteInfo, ParsedFavoriteInfo as FavoriteInfo} from "../savedata/favoriteInfo";
2 | import {UsageInfo as UseInfo, RawUsageInfo as RawUseInfo} from "../savedata/usageInfo";
3 | import {CodepointKey} from "./unicode";
4 |
5 | /**
6 | * Usage information of a specific codepoint as stored in save data
7 | * @see {@link CodepointUse} for parsed version
8 | */
9 | export type RawCodepointUse = CodepointKey & RawUseInfo;
10 |
11 | /**
12 | * Usage information of a specific codepoint, parsed for use in the plugin
13 | * @see {@link RawCodepointUse} for raw version
14 | */
15 | export type CodepointUse = CodepointKey & UseInfo;
16 |
17 | /**
18 | * Favorite infor of a specific codepoint as stored in save data
19 | * @see {@link CodepointFavorite} for parsed version
20 | */
21 | export type RawCodepointFavorite = CodepointKey & RawFavoriteInfo;
22 |
23 | /**
24 | * Favorite information of a specific codepoint, parsed for use in the plugin
25 | * @see {@link RawCodepointFavorite} for raw version
26 | */
27 | export type CodepointFavorite = CodepointKey & FavoriteInfo;
28 |
--------------------------------------------------------------------------------
/src/libraries/types/codepoint/unicode.ts:
--------------------------------------------------------------------------------
1 | export type Char = string
2 | export type Codepoint = number;
3 |
4 | /**
5 | * Universally used key for a Unicode codepoint
6 | */
7 | export interface CodepointKey {
8 | /**
9 | * A single character defined by a Unicode code point
10 | * @maxLength 1
11 | * @minLength 1
12 | */
13 | codepoint: Char;
14 | }
15 |
16 | /**
17 | * General attributes of a Unicode codepoint
18 | */
19 | export interface CodepointAttribute {
20 | /**
21 | * Unicode provided description of the character
22 | */
23 | name: string;
24 |
25 | /**
26 | * Unicode category of the character
27 | */
28 | category: string;
29 | }
30 |
31 | /**
32 | * Unicode codepoint representation throughout the plugin
33 | */
34 | export type UnicodeCodepoint = CodepointKey & CodepointAttribute;
35 |
--------------------------------------------------------------------------------
/src/libraries/types/maybe.ts:
--------------------------------------------------------------------------------
1 | export type Maybe = T | null | undefined;
2 |
--------------------------------------------------------------------------------
/src/libraries/types/persistCache.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeSearchError} from "../../unicode-search/errors/unicodeSearchError";
2 |
3 | export class PersistCache {
4 | private value?: T;
5 |
6 | constructor(
7 | private readonly getCallback: () => Promise,
8 | private readonly persistCallback: (value: T) => Promise,
9 | initialValue?: T
10 | ) {
11 | this.value = initialValue;
12 | }
13 |
14 | async get(): Promise {
15 | if (this.value == null) {
16 | this.value = await this.getCallback();
17 | }
18 |
19 | return this.value;
20 | }
21 |
22 | set(value: T) {
23 | this.value = value
24 | }
25 |
26 | async persist(): Promise {
27 | if (this.value == null) {
28 | throw new UnicodeSearchError("Refuse to persist a null value");
29 | }
30 |
31 | await this.persistCallback(this.value);
32 | return this.value;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/libraries/types/readCache.ts:
--------------------------------------------------------------------------------
1 | export class ReadCache {
2 | private value?: T;
3 |
4 | constructor(
5 | private readonly getCallback: () => Promise,
6 | initialValue?: T
7 | ) {
8 | this.value = initialValue;
9 | }
10 |
11 | async get(): Promise {
12 | if (this.value == null) {
13 | this.value = await this.getCallback();
14 | }
15 |
16 | return this.value;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/dataFragment.ts:
--------------------------------------------------------------------------------
1 | import {SaveDataVersion} from "./version";
2 |
3 | /**
4 | * Top level fragment of self-standing save data
5 | */
6 | export interface DataFragment {
7 | initialized: boolean;
8 | version: SaveDataVersion;
9 | }
10 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/favoriteInfo.ts:
--------------------------------------------------------------------------------
1 | import {DateString} from "./usageInfo";
2 |
3 | /**
4 | * Raw favorite information, as stored in save data
5 | */
6 | export interface FavoriteInfo {
7 | /**
8 | * Date when the codepoint was added to favorites
9 | */
10 | added: DateString;
11 |
12 | /**
13 | * Whether the codepoint has a hotkey command
14 | */
15 | hotkey: boolean;
16 | }
17 |
18 | /**
19 | * Parsed favorite information for use in the plugin
20 | */
21 | export interface ParsedFavoriteInfo {
22 | /**
23 | * Date when the codepoint was added to favorites
24 | */
25 | added: Date;
26 |
27 | /**
28 | * Whether the codepoint has a hotkey command
29 | */
30 | hotkey: boolean;
31 | }
32 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/favoritesFragment.ts:
--------------------------------------------------------------------------------
1 | import {DataFragment} from "./dataFragment";
2 | import {RawCodepointFavorite} from "../codepoint/extension";
3 |
4 | /**
5 | * Users favorite codepoints
6 | */
7 | export interface FavoritesFragment extends DataFragment {
8 | /**
9 | * List of favorite codepoints
10 | */
11 | codepoints: RawCodepointFavorite[]
12 | }
13 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/filterFragment.ts:
--------------------------------------------------------------------------------
1 | import {DataFragment} from "./dataFragment";
2 | import {UnicodeFilter} from "./unicodeFilter";
3 |
4 | /**
5 | * User saved character filters
6 | */
7 | export interface FilterFragment extends DataFragment {
8 | /**
9 | * Filter criteria for Unicode characters
10 | */
11 | unicode: UnicodeFilter;
12 | }
13 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/initialSaveData.ts:
--------------------------------------------------------------------------------
1 | import {SaveDataVersion} from "./version";
2 |
3 | export interface InitialSaveData {
4 | initialized: boolean;
5 | version: SaveDataVersion & ( "0.4.0" | "0.5.0" | "0.6.0" );
6 |
7 | settings: {
8 | initialized: boolean;
9 | modified: boolean;
10 | filter: {
11 | planes: Array<{
12 | start: number
13 | end: number
14 | blocks: Array<{
15 | start: number
16 | end: number
17 | included: boolean
18 | }>
19 | }>;
20 | categoryGroups: Array<{
21 | abbreviation: "L" | "M" | "N" | "P" | "S" | "Z" | "C";
22 | categories: Array<{
23 | abbreviation: "Ll" | "Lm" | "Lt" | "Lu" | "Lo"
24 | | "Mc" | "Me" | "Mn"
25 | | "Nd" | "Nl" | "No"
26 | | "Pc" | "Pd" | "Pi" | "Pf" | "Ps" | "Pe" | "Po"
27 | | "Sc" | "Sk" | "Sm" | "So"
28 | | "Zl" | "Zp" | "Zs"
29 | | "Cc" | "Cf" | "Cn" | "Co" | "Cs";
30 | included: boolean;
31 | }>;
32 | }>;
33 | };
34 | };
35 | unicode: {
36 | initialized: boolean;
37 | codepoints: Array<{
38 | codepoint: string;
39 | name: string;
40 | category: string;
41 | }>
42 | };
43 | usage: {
44 | initialized: boolean;
45 | codepoints: Array<{
46 | codepoint: string
47 | firstUsed: string;
48 | lastUsed: string;
49 | useCount: number;
50 | }>
51 | };
52 | }
53 |
54 | export function isInitialSaveData(object: Partial): object is InitialSaveData {
55 | return object != null
56 | && "initialized" in object
57 | && "version" in object
58 |
59 | && "settings" in object
60 | && object.settings != null
61 | && "initialized" in object.settings
62 | && "modified" in object.settings
63 | && "filter" in object.settings
64 |
65 | && "unicode" in object
66 | && object.unicode != null
67 | && "initialized" in object.unicode
68 | && "codepoints" in object.unicode
69 |
70 | && "usage" in object
71 | && object.usage != null
72 | && "initialized" in object.usage
73 | && "codepoints" in object.usage
74 | ;
75 | }
76 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/metaFragment.ts:
--------------------------------------------------------------------------------
1 | import {DataFragment} from "./dataFragment";
2 | import { PluginVersion } from "./version";
3 |
4 | /**
5 | * Meta information for the datastore
6 | */
7 | export interface MetaFragment extends DataFragment {
8 | /**
9 | * Latest used version of the plugin which used this data
10 | */
11 | pluginVersion: PluginVersion;
12 |
13 | /**
14 | * A collection of unique data events.
15 | */
16 | events: DataEvent[];
17 | }
18 |
19 | /**
20 | * An event, which communicates information between data parts
21 | */
22 | export enum DataEvent {
23 | DownloadCharacters = "download_characters",
24 | }
25 |
26 | /**
27 | * Checks if the given object is a valid DataEvent.
28 | * @param object The object to check.
29 | * @returns True if the object is a valid DataEvent, false otherwise.
30 | */
31 | export function isDataEvent(object: any): object is DataEvent {
32 | return Object.values(DataEvent).includes(object);
33 | }
34 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/saveData.ts:
--------------------------------------------------------------------------------
1 | import {FilterFragment} from "./filterFragment";
2 | import {UnicodeFragment} from "./unicodeFragment";
3 | import {CharacterUseFragment as UsageFragment} from "./usageFragment";
4 | import {FavoritesFragment} from "./favoritesFragment";
5 | import {MetaFragment} from "./metaFragment";
6 | import { DataFragment } from "./dataFragment";
7 |
8 | /**
9 | * Generic structure of `data.json`
10 | */
11 | export interface SaveDataOf {
12 | /**
13 | * Metadata information about the save data itself
14 | */
15 | meta: T;
16 |
17 | /**
18 | * Filtering of downloaded/displayed codepoints
19 | */
20 | filter: T;
21 |
22 | /**
23 | * Local character database
24 | */
25 | characters: T;
26 |
27 | /**
28 | * Usage information generated by the user
29 | */
30 | usage: T;
31 |
32 | /**
33 | * Favorites saved manually by the user
34 | */
35 | favorites: T;
36 | }
37 |
38 | /**
39 | * Structure of `data.json`, where each fragment is a self-managed data fragment
40 | */
41 | export interface SaveData extends SaveDataOf {
42 | meta: MetaFragment;
43 | filter: FilterFragment;
44 | characters: UnicodeFragment;
45 | usage: UsageFragment;
46 | favorites: FavoritesFragment;
47 | }
48 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/unicodeFilter.ts:
--------------------------------------------------------------------------------
1 | import {CodepointInterval} from "../codepoint/codepointInterval";
2 | import {CharacterCategoryType} from "../../data/characterCategory";
3 | import {CharacterCategoryGroupType} from "../../data/characterCategoryGroup";
4 |
5 | /**
6 | * User set filter data for Unicode characters.
7 | */
8 | export interface UnicodeFilter {
9 | /**
10 | * Filter criteria for planes of Unicode characters
11 | */
12 | planes: PlaneFilter[];
13 |
14 | /**
15 | * Filter criteria for category groups of Unicode characters
16 | */
17 | categoryGroups: CategoryGroupFilter[];
18 | }
19 |
20 | /**
21 | * Unicode Plane of Unicode Blocks
22 | */
23 | export interface PlaneFilter extends CodepointInterval {
24 | /**
25 | * Filter criteria for blocks of Unicode characters
26 | */
27 | blocks: BlockFilter[];
28 | }
29 |
30 | /**
31 | * A flag indicating whether a character is included in search or not
32 | */
33 | export interface InclusionFlag {
34 | /**
35 | * Indicates whether a character is included in search or not
36 | */
37 | included: boolean;
38 | }
39 |
40 | /**
41 | * Block of Unicode Codepoints
42 | */
43 | export type BlockFilter = CodepointInterval & InclusionFlag;
44 |
45 | /**
46 | * Filters for a group of categories.
47 | */
48 | export interface CategoryGroupFilter {
49 | /**
50 | * Single letter abbreviation of the category group.
51 | * @maxLength 1
52 | * @minLength 1
53 | */
54 | abbreviation: CharacterCategoryGroupType;
55 |
56 | /**
57 | * Categories which belong to the group.
58 | */
59 | categories: CategoryFilter[];
60 | }
61 |
62 | /**
63 | * Filter for a single category.
64 | */
65 | export interface CategoryFilter extends InclusionFlag {
66 | /**
67 | * Two letter abbreviation of the category.
68 | * @maxLength 2
69 | * @minLength 2
70 | */
71 | abbreviation: CharacterCategoryType;
72 | }
73 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/unicodeFragment.ts:
--------------------------------------------------------------------------------
1 | import {DataFragment} from "./dataFragment";
2 | import {UnicodeCodepoint} from "../codepoint/unicode";
3 |
4 | /**
5 | * Downloaded Unicode Character Database
6 | */
7 | export interface UnicodeFragment extends DataFragment {
8 | /**
9 | * Codepoints downloaded from the Unicode Character Database
10 | */
11 | codepoints: UnicodeCodepoint[]
12 | }
13 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/usageFragment.ts:
--------------------------------------------------------------------------------
1 | import {DataFragment} from "./dataFragment";
2 | import {RawCodepointUse} from "../codepoint/extension";
3 |
4 | /**
5 | * User generated usage data
6 | */
7 | export interface CharacterUseFragment extends DataFragment {
8 | /**
9 | * Statistics of the individual codepoint usage
10 | */
11 | codepoints: RawCodepointUse[]
12 | }
13 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/usageInfo.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Alias to hint that a string is a date string
3 | * @example "2023-10-01T12:00:00Z"
4 | */
5 | export type DateString = string;
6 |
7 | /**
8 | * Raw usage information, as stored in save data
9 | */
10 | export interface RawUsageInfo {
11 | firstUsed: DateString;
12 | lastUsed: DateString;
13 | useCount: number;
14 | }
15 |
16 | /**
17 | * Parsed usage information for use in the plugin
18 | */
19 | export type UsageInfo = UsageCount & UsageDate;
20 |
21 | /**
22 | * Usage count for statistics
23 | */
24 | export interface UsageCount {
25 | useCount: number;
26 | }
27 |
28 | /**
29 | * Usage date for statistics
30 | */
31 | export interface UsageDate {
32 | firstUsed: Date;
33 | lastUsed: Date;
34 | }
35 |
--------------------------------------------------------------------------------
/src/libraries/types/savedata/version.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Version of the plugin.
3 | *
4 | * Must comply with RegEx:
5 | * ```^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[A-Z]+)?$```
6 | */
7 | export type PluginVersion
8 | = "0.2.0"
9 | | "0.2.1"
10 | | "0.2.2"
11 | | "0.2.3"
12 | | "0.3.0"
13 | | "0.4.0"
14 | | "0.4.1"
15 | | "0.5.0"
16 | | "0.6.0"
17 | | "0.6.1"
18 | | "0.7.0"
19 | | "0.7.1"
20 | // Update every release
21 | ;
22 |
23 | export type CurrentPluginVersion = "0.7.1" & PluginVersion;
24 | export const CURRENT_PLUGIN_VERSION: CurrentPluginVersion = "0.7.1";
25 |
26 | /**
27 | * Version of the save data schema.
28 | *
29 | * Must comply with RegEx:
30 | * ```^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[A-Z]+)?$```
31 | *
32 | * The version of the save data schema is independent of the plugin version
33 | * @see {@link PluginVersion}
34 | */
35 | export type SaveDataVersion = PluginVersion &
36 | ( "0.4.0"
37 | | "0.5.0"
38 | | "0.6.0"
39 | | "0.7.0"
40 | // Update only if save data schema changed
41 | );
42 |
43 | export type CurrentSaveDataVersion = "0.7.0" & SaveDataVersion;
44 | export const CURRENT_DATA_VERSION: SaveDataVersion = "0.7.0";
45 |
--------------------------------------------------------------------------------
/src/libraries/types/unicode/unicodeBlock.ts:
--------------------------------------------------------------------------------
1 | import {UnicodePlaneNumber} from "../../data/unicodePlaneNumber";
2 |
3 | import {CodepointInterval} from "../codepoint/codepointInterval";
4 |
5 | export interface UnicodeBlock {
6 | interval: CodepointInterval,
7 | description: string,
8 | plane: UnicodePlaneNumber
9 | }
10 |
--------------------------------------------------------------------------------
/src/libraries/types/unicode/unicodeGeneralCategory.ts:
--------------------------------------------------------------------------------
1 | import {CharacterCategoryType} from "../../data/characterCategory";
2 |
3 | export interface UnicodeGeneralCategory {
4 | abbreviation: CharacterCategoryType;
5 | name: string;
6 | description: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/libraries/types/unicode/unicodeGeneralCategoryGroup.ts:
--------------------------------------------------------------------------------
1 | import {CharacterCategoryGroupType} from "../../data/characterCategoryGroup";
2 | import {UnicodeGeneralCategory} from "./unicodeGeneralCategory";
3 |
4 | export interface UnicodeGeneralCategoryGroup {
5 | abbreviation: CharacterCategoryGroupType;
6 | name: string;
7 | categories: UnicodeGeneralCategory[];
8 | }
9 |
--------------------------------------------------------------------------------
/src/libraries/types/unicode/unicodePlane.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeBlock} from "./unicodeBlock";
2 | import {UnicodePlaneNumber} from "../../data/unicodePlaneNumber";
3 | import {CodepointInterval} from "../codepoint/codepointInterval";
4 |
5 | export interface UnicodePlane {
6 | planeNumber: UnicodePlaneNumber,
7 | description: string,
8 | abbreviation: string,
9 | interval: CodepointInterval,
10 | blocks: UnicodeBlock[],
11 | }
12 |
--------------------------------------------------------------------------------
/src/libraries/types/usageDisplayStatistics.ts:
--------------------------------------------------------------------------------
1 | export interface UsageDisplayStatistics {
2 | topThirdRecentlyUsed: Date;
3 | averageUseCount: number;
4 | }
5 |
--------------------------------------------------------------------------------
/src/unicode-search/components/characterSearch.ts:
--------------------------------------------------------------------------------
1 | import {SearchResult} from "obsidian";
2 | import {Character, MetadataCharacter} from "../../libraries/types/codepoint/character";
3 | import {CharacterSearchAttributes} from "./characterSearchAttributes";
4 | import {Maybe} from "../../libraries/types/maybe";
5 |
6 | /**
7 | * Evaluation of the strength of a match from a search
8 | */
9 | export type SearchMatchResult = SearchResult;
10 |
11 | export const NONE_RESULT: SearchMatchResult = {
12 | score: 0,
13 | matches: []
14 | }
15 |
16 | /**
17 | * Search result of a single character match
18 | */
19 | export type CharacterSearchResult = {
20 | character: Character & CharacterType,
21 | match: CharacterSearchAttributes,
22 | }
23 |
24 | export type MetaCharacterSearchResult = CharacterSearchResult;
25 | export type MaybeMetaCharacterSearchResult = CharacterSearchResult>;
26 | export type MaybeSearchMatchAttributes = CharacterSearchAttributes>;
27 | export type SearchMatchAttributes = CharacterSearchAttributes;
28 |
--------------------------------------------------------------------------------
/src/unicode-search/components/characterSearchAttributes.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Attributes which are used for searching characters
3 | */
4 | export type CharacterSearchAttributes = {
5 |
6 | /**
7 | * Literal value identifying the codepoint
8 | *
9 | * @see Codepoint
10 | */
11 | codepoint: T,
12 |
13 |
14 | /**
15 | * The main name representing the character
16 | *
17 | * @see CodepointAttribute.name
18 | */
19 | name: T,
20 | }
21 |
--------------------------------------------------------------------------------
/src/unicode-search/components/fuzzySearchModal.ts:
--------------------------------------------------------------------------------
1 | import {App, Instruction, renderMatches, SuggestModal} from "obsidian";
2 | import {MetaCharacterSearchResult} from "./characterSearch";
3 | import {CharacterService} from "../service/characterService";
4 | import {
5 | ELEMENT_FAVORITE,
6 | ELEMENT_FREQUENT,
7 | ELEMENT_RECENT,
8 | INSTRUCTION_DISMISS,
9 | NAVIGATE_INSTRUCTION
10 | } from "./visualElements";
11 | import {toHexadecimal} from "../../libraries/helpers/toHexadecimal";
12 | import {getRandomItem} from "../../libraries/helpers/getRandomItem";
13 | import {fillNullCharacterMatchScores} from "../../libraries/comparison/fillNullCharacterMatchScores";
14 | import {compareCharacterMatches} from "../../libraries/comparison/compareCharacterMatches";
15 | import {ReadCache} from "../../libraries/types/readCache";
16 | import {mostRecentUses} from "../../libraries/helpers/mostRecentUses";
17 | import {averageUseCount} from "../../libraries/helpers/averageUseCount";
18 | import {UsageDisplayStatistics} from "../../libraries/types/usageDisplayStatistics";
19 | import {toNullMatch} from "../../libraries/helpers/toNullMatch";
20 | import {toSearchQueryMatch} from "../../libraries/helpers/toSearchQueryMatch";
21 | import {matchedNameOrCodepoint} from "../../libraries/helpers/matchedNameOrCodepoint";
22 |
23 | import {isFavoriteCharacter} from "../../libraries/helpers/isFavoriteCharacter";
24 | import {UsageInfo} from "../../libraries/types/savedata/usageInfo";
25 |
26 | export abstract class FuzzySearchModal extends SuggestModal {
27 | /* TODO [non-func]: Extract the functionalities needed for inserting/picking characters
28 | * Picking characters needs to have a filter for chars too.
29 | * The inheritance used here is very messy, use composition instead.
30 | */
31 |
32 | private readonly usageStatistics: ReadCache;
33 |
34 | protected constructor(
35 | app: App,
36 | protected readonly characterService: CharacterService,
37 | chooseCharacter: Instruction,
38 |
39 | ) {
40 | super(app);
41 |
42 | super.setInstructions([
43 | NAVIGATE_INSTRUCTION,
44 | chooseCharacter,
45 | INSTRUCTION_DISMISS,
46 | ]);
47 |
48 | // Purposefully ignored result
49 | this.setRandomPlaceholder().then();
50 |
51 | this.usageStatistics = new ReadCache(async () => {
52 | const usedCharacters = await characterService.getUsed();
53 | return {
54 | topThirdRecentlyUsed: mostRecentUses(usedCharacters).slice(0, 3).last() ?? new Date(0),
55 | averageUseCount: averageUseCount(usedCharacters),
56 | } as UsageDisplayStatistics;
57 | });
58 | }
59 |
60 | public override async getSuggestions(query: string): Promise {
61 | const allCharacters = (await this.characterService.getAll());
62 | const queryEmpty = query == null || query.length < 1;
63 |
64 | const prepared = queryEmpty
65 | ? allCharacters
66 | .map(toNullMatch)
67 | : allCharacters
68 | .map(toSearchQueryMatch(query))
69 | .filter(matchedNameOrCodepoint);
70 |
71 | const recencyCutoff = (await this.usageStatistics.get()).topThirdRecentlyUsed;
72 |
73 | return prepared
74 | .sort((l, r) => compareCharacterMatches(l, r, recencyCutoff))
75 | .slice(0, this.limit)
76 | .map(fillNullCharacterMatchScores);
77 | }
78 |
79 | public override async renderSuggestion(search: MetaCharacterSearchResult, container: HTMLElement): Promise {
80 | const char = search.character;
81 |
82 | container.addClass("plugin", "unicode-search", "result-item");
83 |
84 | container.createDiv({
85 | cls: "character-preview",
86 | }).createSpan({
87 | text: char.codepoint,
88 | });
89 |
90 | const matches = container.createDiv({
91 | cls: "character-match",
92 | });
93 |
94 | const text = matches.createDiv({
95 | cls: "character-name",
96 | });
97 |
98 | renderMatches(text, char.name, search.match.name.matches);
99 |
100 | /* TODO [ui][?]: We can show the character category in search results */
101 | /* const category = matches.createDiv({
102 | cls: "character-category",
103 | }).createSpan({
104 | text: char.category,
105 | }); */
106 |
107 | const codepoint = matches.createDiv({
108 | cls: "character-codepoint",
109 | });
110 |
111 | renderMatches(codepoint, toHexadecimal(char), search.match.codepoint.matches);
112 |
113 | const detail = container.createDiv({
114 | cls: "detail",
115 | });
116 |
117 | const attributes = detail.createDiv({
118 | cls: "attributes",
119 | });
120 |
121 | if (isFavoriteCharacter(char)) {
122 | attributes.createDiv(ELEMENT_FAVORITE);
123 | } else {
124 | const usageStats = await this.usageStatistics.get();
125 |
126 | /* The type hinting doesn't work, and shows as an error in the IDE (or the type is wrong) */
127 | const maybeUsedChar = char as Partial
128 | const showLastUsed = maybeUsedChar.lastUsed != null && maybeUsedChar.lastUsed >= usageStats.topThirdRecentlyUsed;
129 | const showUseCount = maybeUsedChar.useCount != null && maybeUsedChar.useCount >= usageStats.averageUseCount;
130 |
131 | if (showLastUsed) {
132 | attributes.createDiv(ELEMENT_RECENT);
133 | }
134 |
135 | if (showUseCount) {
136 | attributes.createDiv(ELEMENT_FREQUENT);
137 | }
138 | }
139 | }
140 |
141 | public override async onNoSuggestion(): Promise {
142 | await this.setRandomPlaceholder();
143 | }
144 |
145 | private async setRandomPlaceholder(): Promise {
146 | const randomCharacterName = getRandomItem(await this.characterService.getAllCharacters()).name;
147 | super.setPlaceholder(`Unicode search: ${randomCharacterName}`);
148 | }
149 |
150 | }
151 |
--------------------------------------------------------------------------------
/src/unicode-search/components/insertCharacterModal.ts:
--------------------------------------------------------------------------------
1 | import {App, Editor} from "obsidian";
2 | import {MetaCharacterSearchResult} from "./characterSearch";
3 | import {CharacterService} from "../service/characterService";
4 | import {INSERT_CHAR_INSTRUCTION} from "./visualElements";
5 | import {FuzzySearchModal} from "./fuzzySearchModal";
6 |
7 | export class InsertCharacterModal extends FuzzySearchModal {
8 | public constructor(
9 | app: App,
10 | characterService: CharacterService,
11 | private readonly editor: Editor,
12 | ) {
13 | super(app, characterService, INSERT_CHAR_INSTRUCTION);
14 | }
15 |
16 | public override async onChooseSuggestion(search: MetaCharacterSearchResult, _: MouseEvent | KeyboardEvent): Promise {
17 | this.editor.replaceSelection(search.character.codepoint);
18 |
19 | try {
20 | /* super.characterService here throws an undefined exception (super is undefined) */
21 | await this.characterService.recordUsage(search.character.codepoint);
22 | } catch (error) {
23 | console.error("Failed to record character usage", {err: error});
24 | }
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/src/unicode-search/components/pickCharacterModal.ts:
--------------------------------------------------------------------------------
1 | import {FuzzySearchModal} from "./fuzzySearchModal";
2 | import {App} from "obsidian";
3 | import {CharacterService} from "../service/characterService";
4 | import {Character} from "../../libraries/types/codepoint/character";
5 | import {INSERT_CHAR_INSTRUCTION} from "./visualElements";
6 | import {MetaCharacterSearchResult} from "./characterSearch";
7 | import {Maybe} from "../../libraries/types/maybe";
8 |
9 | export class PickCharacterModal extends FuzzySearchModal {
10 | private constructor(
11 | app: App,
12 | characterService: CharacterService,
13 | private readonly resolve: (value: (PromiseLike> | Maybe)) => void,
14 | ) {
15 | super(app, characterService, INSERT_CHAR_INSTRUCTION);
16 | }
17 |
18 | /**
19 | * Open the modal and resolve the promise with the chosen character
20 | */
21 | static open(app: App, characterService: CharacterService): Promise> {
22 | return new Promise>(resolve => {
23 | const modal = new PickCharacterModal(app, characterService, resolve);
24 | modal.open();
25 | })
26 | }
27 |
28 | override onChooseSuggestion(_: MetaCharacterSearchResult, __: MouseEvent | KeyboardEvent) {
29 | /*
30 | * Intentionally left blank
31 | * The `onClose` fires before `onChooseSuggestion`, so we have to override the `selectSuggestion` which calls both of them
32 | */
33 | }
34 |
35 | override onClose() {
36 | this.resolve(null)
37 | }
38 |
39 | override selectSuggestion(search: MetaCharacterSearchResult, evt: MouseEvent | KeyboardEvent) {
40 | this.resolve(search.character);
41 | super.selectSuggestion(search, evt);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/unicode-search/components/settingTab.ts:
--------------------------------------------------------------------------------
1 | import {App, Plugin, PluginSettingTab, Setting} from "obsidian";
2 | import {UNICODE_PLANES_ALL} from "../../libraries/data/unicodePlanes";
3 | import {UnicodeBlock} from "../../libraries/types/unicode/unicodeBlock";
4 |
5 | import {asHexadecimal} from "../../libraries/helpers/asHexadecimal";
6 | import {CharacterService} from "../service/characterService";
7 | import {FilterStore} from "../service/filterStore";
8 | import {CodepointInterval} from "../../libraries/types/codepoint/codepointInterval";
9 | import {UnicodePlane} from "../../libraries/types/unicode/unicodePlane";
10 | import {UNICODE_CHARACTER_CATEGORIES} from "../../libraries/data/unicodeCharacterCategories";
11 | import {UnicodeGeneralCategoryGroup} from "../../libraries/types/unicode/unicodeGeneralCategoryGroup";
12 | import {UnicodeGeneralCategory} from "../../libraries/types/unicode/unicodeGeneralCategory";
13 | import {DataManager} from "../service/dataManager";
14 | import {FavoritesStore} from "../service/favoritesStore";
15 | import {toHexadecimal} from "../../libraries/helpers/toHexadecimal";
16 | import {Character, FavoriteCharacter} from "../../libraries/types/codepoint/character";
17 | import {PickCharacterModal} from "./pickCharacterModal";
18 |
19 | export class SettingTab extends PluginSettingTab {
20 | /* TODO [non-func]: Make settings code easier to comprehend
21 | * Try using svelte for nicer UI component code.
22 | * Also, the naming is confusing.
23 | * Don't forget about the CSS styles too.
24 | */
25 |
26 | private rendered = false;
27 |
28 | constructor(
29 | app: App,
30 | private readonly plugin: Plugin,
31 | private readonly characterService: CharacterService,
32 | private readonly favoritesStore: FavoritesStore,
33 | private readonly settingsStore: FilterStore,
34 | private readonly initializer: DataManager,
35 | ) {
36 | super(app, plugin);
37 | this.containerEl.addClass("plugin", "unicode-search", "setting-tab");
38 | }
39 |
40 | override async display(): Promise {
41 | if (this.rendered) {
42 | return;
43 | }
44 |
45 | await this.displayFilterSettings(this.containerEl);
46 | await this.displayFavoritesSettings(this.containerEl);
47 |
48 | this.rendered = true;
49 | }
50 |
51 | override async hide(): Promise {
52 | await this.initializer.initializeData();
53 | this.containerEl.empty();
54 | this.rendered = false;
55 | }
56 |
57 | private async displayFavoritesSettings(container: HTMLElement) {
58 | new Setting(container)
59 | .setHeading()
60 | .setName("Favorite Characters")
61 | .setDesc(
62 | "Manage your favorite characters which will be displayed in the plugin's search results. " +
63 | "You can also enable them as a hotkey, making them available as a command in Obsidian."
64 | )
65 | .setClass("group-control")
66 | .addToggle(toggle => toggle
67 | .setValue(false)
68 | .onChange(visible => manageFavoritesContainer.toggleClass("hidden", !visible))
69 | )
70 | ;
71 |
72 | const favorites = await this.characterService.getFavorites();
73 |
74 | const manageFavoritesContainer = container.createDiv({cls: ["group-container", "hidden"]});
75 | const itemContainer = manageFavoritesContainer.createDiv({cls: ["item-container"]});
76 | const newCharacterList = itemContainer.createDiv({cls: ["character-list", "new"]});
77 | const characterList = itemContainer.createDiv({cls: ["character-list", "no-first"]});
78 |
79 | new Setting(newCharacterList)
80 | .setName("")
81 | .setDesc("Add a new favorite character")
82 | .addButton(btn => {
83 | btn.setIcon("plus")
84 | btn.onClick(async _ => {
85 | const char = await PickCharacterModal.open(this.plugin.app, this.characterService);
86 |
87 | if (char == null) {
88 | return;
89 | }
90 |
91 | const isAlreadyFavorite = favorites.some(fav => fav.codepoint === char.codepoint);
92 | if (isAlreadyFavorite) {
93 | return;
94 | }
95 |
96 | const favorite = await this.favoritesStore.addFavorite(char.codepoint);
97 | const favoriteChar = {...favorite, ...char};
98 | this.displayFavoriteChar(newCharacterList, favoriteChar)
99 | })
100 | })
101 | ;
102 |
103 | for (const character of favorites) {
104 | this.displayFavoriteChar(characterList, character);
105 | }
106 | }
107 |
108 | private displayFavoriteChar(container: HTMLElement, character: FavoriteCharacter) {
109 | const setting = new Setting(container);
110 |
111 | setting
112 | .setClass("favorite-control")
113 | .setName(character.codepoint)
114 | .setDesc(character.name)
115 | .addToggle(toggle => toggle
116 | .setTooltip("Add insert command to Obsidian")
117 | .setValue(character.hotkey)
118 | .onChange(enabled => this.toggleHotkeyCommand(character, enabled))
119 | )
120 | .addButton(button => button
121 | .setIcon("trash")
122 | .setTooltip("Remove from favorites")
123 | .onClick(() => {
124 | setting.settingEl.hide()
125 | return this.favoritesStore.removeFavorite(character.codepoint);
126 | })
127 | )
128 | ;
129 | }
130 |
131 | private async toggleHotkeyCommand(character: Character, enabled: boolean): Promise {
132 | const insertCharId = `insert-${toHexadecimal(character)}`;
133 |
134 | if (enabled) {
135 | this.plugin.addCommand({
136 | id: insertCharId,
137 | name: `Insert '${character.codepoint}'`,
138 | editorCallback: editor => {
139 | editor.replaceSelection(character.codepoint);
140 | return true;
141 | },
142 | })
143 | } else {
144 | this.plugin.removeCommand(insertCharId);
145 | }
146 |
147 | await this.favoritesStore.update(character.codepoint, () => ({hotkey: enabled}));
148 | }
149 |
150 | private async displayFilterSettings(container: HTMLElement) {
151 | new Setting(container)
152 | .setHeading()
153 | .setName("Unicode Character Filters")
154 | .setDesc(
155 | "Here you can set which characters would you like to be included " +
156 | "or excluded from the plugins search results. " +
157 | "Toggle the headings to display the options."
158 | )
159 | ;
160 |
161 | new Setting(container)
162 | .setName("General Categories")
163 | .setClass("group-control")
164 | .addToggle(toggle => toggle
165 | .setValue(false)
166 | .onChange(visible => categoryFilterDiv.toggleClass("hidden", !visible))
167 | )
168 | ;
169 |
170 | const categoryFilterDiv = container.createDiv({cls: ["group-container", "hidden"]});
171 |
172 | for (const category of UNICODE_CHARACTER_CATEGORIES) {
173 | await this.addCharacterCategoryFilter(categoryFilterDiv, category);
174 | }
175 |
176 | new Setting(container)
177 | .setName("Planes and Blocks")
178 | .setClass("group-control")
179 | .addToggle(toggle => toggle
180 | .setValue(false)
181 | .onChange(visible => planesFilterDiv.toggleClass("hidden", !visible))
182 | )
183 | ;
184 |
185 | const planesFilterDiv = container.createDiv({cls: ["group-container", "hidden"]});
186 |
187 | for (const plane of UNICODE_PLANES_ALL) {
188 | await this.addCharacterPlaneFilters(planesFilterDiv, plane);
189 | }
190 | }
191 |
192 | private async addCharacterCategoryFilter(container: HTMLElement, categoryGroup: UnicodeGeneralCategoryGroup) {
193 | const categoryGroupContainer = container.createDiv({cls: "item-container"});
194 |
195 | new Setting(categoryGroupContainer)
196 | .setHeading()
197 | .setName(categoryGroup.name)
198 | ;
199 |
200 | const categoryContainer = categoryGroupContainer.createDiv({cls: "items-list"});
201 |
202 | for (const category of categoryGroup.categories) {
203 | await SettingTab.addCharacterCategoryFilterToggle(categoryContainer, this.settingsStore, category);
204 | }
205 | }
206 |
207 | private async addCharacterPlaneFilters(container: HTMLElement, plane: UnicodePlane) {
208 | const planeContainer = container.createDiv({cls: "item-container"});
209 |
210 | new Setting(planeContainer)
211 | .setHeading()
212 | .setClass("codepoint-interval")
213 | .setName(createFragment(fragment => {
214 | fragment.createSpan().appendText(plane.description);
215 | SettingTab.codepointFragment(fragment, plane.interval)
216 | }))
217 | ;
218 |
219 | const blocksContainer = planeContainer.createDiv({cls: "blocks-list"});
220 |
221 | for (const block of plane.blocks) {
222 | await SettingTab.addCharacterBlockFilterToggle(blocksContainer, this.settingsStore, block);
223 | }
224 | }
225 |
226 | private static async addCharacterBlockFilterToggle(
227 | container: HTMLElement,
228 | options: FilterStore,
229 | block: UnicodeBlock
230 | ) {
231 | /* Low: try to redo more effectively, we always get a plane worth of blocks */
232 | const blockIncluded = await options.getCharacterBlock(block.interval);
233 |
234 | new Setting(container)
235 | .setName(block.description)
236 | .setDesc(createFragment(fragment => SettingTab.codepointFragment(fragment, block.interval)))
237 | .addToggle(input => input
238 | .setValue(blockIncluded)
239 | .onChange((value) => options.setCharacterBlock(block.interval, value))
240 | );
241 | }
242 |
243 | private static async addCharacterCategoryFilterToggle(
244 | container: HTMLElement,
245 | options: FilterStore,
246 | category: UnicodeGeneralCategory
247 | ) {
248 | /* Low: try to redo more effectively, we always get a plane worth of blocks */
249 | const blockIncluded = await options.getCharacterCategory(category.abbreviation);
250 |
251 | new Setting(container)
252 | .setName(category.name)
253 | .setDesc(category.description)
254 | .addToggle(input => input
255 | .setValue(blockIncluded)
256 | .onChange((value) => options.setCharacterCategory(category.abbreviation, value))
257 | );
258 | }
259 |
260 | private static codepointFragment(parent: DocumentFragment, interval: CodepointInterval): DocumentFragment {
261 | parent
262 | .createSpan({cls: ["character-codepoint", "monospace"],})
263 | .setText(`${asHexadecimal(interval.start)}-${asHexadecimal(interval.end)}`);
264 |
265 | return parent;
266 | }
267 |
268 | }
269 |
--------------------------------------------------------------------------------
/src/unicode-search/components/visualElements.ts:
--------------------------------------------------------------------------------
1 | import {Instruction} from "obsidian";
2 |
3 | export const NAVIGATE_INSTRUCTION: Instruction = {
4 | command: "⮁",
5 | purpose: "to navigate",
6 | };
7 |
8 | export const INSERT_CHAR_INSTRUCTION: Instruction = {
9 | command: "↵",
10 | purpose: "to insert selected character",
11 | };
12 |
13 | export const INSTRUCTION_DISMISS: Instruction = {
14 | command: "esc",
15 | purpose: "to dismiss",
16 | };
17 |
18 | export const ELEMENT_RECENT: DomElementInfo = {
19 | cls: "icon inline-description recent",
20 | text: "↩",
21 | title: "used recently",
22 | };
23 |
24 | export const ELEMENT_FREQUENT: DomElementInfo = {
25 | cls: "icon inline-description frequent",
26 | text: "↺",
27 | title: "used frequently",
28 | };
29 |
30 | export const ELEMENT_FAVORITE: DomElementInfo = {
31 | cls: "icon inline-description favorite",
32 | text: "☆",
33 | title: "favorite",
34 | };
35 |
--------------------------------------------------------------------------------
/src/unicode-search/errors/unicodeSearchError.ts:
--------------------------------------------------------------------------------
1 | export class UnicodeSearchError extends Error {
2 |
3 | public constructor(message: string) {
4 | super(message);
5 | }
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/unicode-search/main.ts:
--------------------------------------------------------------------------------
1 | import {App, Plugin, PluginManifest} from "obsidian";
2 | import {UcdUserFilterDownloader} from "./service/ucdUserFilterDownloader";
3 | import {SettingTab} from "./components/settingTab";
4 | import {UserCharacterService} from "./service/userCharacterService";
5 |
6 | import {Commander} from "./service/commander";
7 | import {RootDataManager} from "./service/rootDataManager";
8 | import {FilterDataManager} from "./service/filterDataManager";
9 | import {UnicodeDataManager} from "./service/unicodeDataManager";
10 | import {UsageDataManager} from "./service/usageDataManager";
11 | import {FavoritesDataManager} from "./service/favoritesDataManager";
12 | import {PersistCache} from "../libraries/types/persistCache";
13 | import {RootPluginDataStorage} from "./service/rootPluginDataStorage";
14 | import {CodepointStorage} from "./service/codepointStorage";
15 | import {CodepointUsageStorage} from "./service/codepointUsageStorage";
16 | import {CodepointFavoritesStorage} from "./service/codepointFavoritesStorage";
17 | import {FilterStorage} from "./service/filterStorage";
18 | import {MetaDataManager} from "./service/metaDataManager";
19 | import {MetaStorage} from "./service/metaStorage";
20 |
21 | /* Used by Obsidian */
22 | // noinspection JSUnusedGlobalSymbols
23 | export default class UnicodeSearchPlugin extends Plugin {
24 | /* TODO [non-func]: Cleanup the codebase -- make it intuitive
25 | * There's a bunch of unnecessary classes and extraneous generalizations.
26 | * Add docs to the necessary parts.
27 | */
28 |
29 | public constructor(
30 | app: App,
31 | manifest: PluginManifest,
32 | ) {
33 | super(app, manifest);
34 | }
35 |
36 | public override async onload(): Promise {
37 | console.group("Loading Unicode Search plugin");
38 | console.time("Unicode Search load time");
39 |
40 | console.info("Creating services");
41 |
42 | const dataLoader = new PersistCache(
43 | () => this.loadData(),
44 | (data) => this.saveData(data)
45 | );
46 |
47 | /* TODO [rework]: Data stores duplicate access to data */
48 | const dataStore = new RootPluginDataStorage(dataLoader);
49 | const metaStore = new MetaStorage(dataStore);
50 | const codepointStore = new CodepointStorage(dataStore);
51 | const usageStore = new CodepointUsageStorage(dataStore);
52 | const favoritesStore = new CodepointFavoritesStorage(dataStore);
53 | const characterService = new UserCharacterService(codepointStore, usageStore, favoritesStore);
54 | const filterStore = new FilterStorage(dataStore, metaStore);
55 |
56 | /* TODO [rework]: Downloader needs filter data, but is before update of char mng. */
57 | const downloader = new UcdUserFilterDownloader(filterStore);
58 |
59 | const metaDm = new MetaDataManager();
60 | const filterDm = new FilterDataManager();
61 | const unicodeDm = new UnicodeDataManager(downloader);
62 | const usageDm = new UsageDataManager();
63 | const favoritesDm = new FavoritesDataManager();
64 |
65 | const dataManager = new RootDataManager(
66 | dataLoader,
67 | metaDm,
68 | filterDm,
69 | unicodeDm,
70 | usageDm,
71 | favoritesDm,
72 | );
73 |
74 | await dataManager.initializeData();
75 |
76 | console.info("Adding UI elements");
77 |
78 | const commandAdder = new Commander(this);
79 | commandAdder.addModal(characterService)
80 | await commandAdder.addFavorites(favoritesStore);
81 |
82 | this.addSettingTab(new SettingTab(
83 | this.app,
84 | this,
85 | characterService,
86 | favoritesStore,
87 | filterStore,
88 | dataManager,
89 | ));
90 |
91 | console.timeEnd("Unicode Search load time");
92 | console.groupEnd();
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/src/unicode-search/service/characterDownloader.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeCodepoint} from "../../libraries/types/codepoint/unicode";
2 |
3 | export interface CharacterDownloader {
4 | download(): Promise;
5 | }
6 |
--------------------------------------------------------------------------------
/src/unicode-search/service/characterService.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Character,
3 | CharacterKey,
4 | FavoriteCharacter,
5 | MaybeUsedCharacter,
6 | UsedCharacter
7 | } from "../../libraries/types/codepoint/character";
8 |
9 |
10 | import {UsageInfo} from "../../libraries/types/savedata/usageInfo";
11 |
12 | export interface CharacterService {
13 | getOne(key: CharacterKey): Promise;
14 | getAllCharacters(): Promise;
15 | getUsed(): Promise;
16 | getFavorites(): Promise;
17 | getAll(): Promise;
18 | recordUsage(key: CharacterKey): Promise;
19 | }
20 |
--------------------------------------------------------------------------------
/src/unicode-search/service/codePointStore.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeCodepoint} from "../../libraries/types/codepoint/unicode";
2 |
3 | export interface CodepointStore {
4 |
5 | /**
6 | * Retrieve all characters.
7 | */
8 | getCodepoints(): Promise;
9 | }
10 |
--------------------------------------------------------------------------------
/src/unicode-search/service/codepointFavoritesStorage.ts:
--------------------------------------------------------------------------------
1 | import {FavoritesStore} from "./favoritesStore";
2 | import {CharacterKey} from "../../libraries/types/codepoint/character";
3 | import {serializeFavoriteInfo} from "../../libraries/helpers/serializeFavoriteInfo";
4 | import {UnicodeSearchError} from "../errors/unicodeSearchError";
5 | import {RootDataStore} from "./rootDataStore";
6 | import {FavoritesFragment} from "../../libraries/types/savedata/favoritesFragment";
7 |
8 | import {CodepointFavorite} from "../../libraries/types/codepoint/extension";
9 | import {ParsedFavoriteInfo} from "../../libraries/types/savedata/favoriteInfo";
10 |
11 | export class CodepointFavoritesStorage implements FavoritesStore {
12 |
13 | constructor(
14 | private readonly store: RootDataStore,
15 | ) {
16 | }
17 |
18 | async upsert(
19 | key: CharacterKey,
20 | apply: (char?: ParsedFavoriteInfo) => ParsedFavoriteInfo
21 | ): Promise {
22 | const data = await this.getFavorites();
23 |
24 | const foundIndex = data.findIndex(ch => ch.codepoint === key);
25 | const found = foundIndex >= 0;
26 | const index = found ? foundIndex : 0;
27 |
28 | const modified = {
29 | ...apply(found ? {...data[index]} : undefined),
30 | codepoint: key,
31 | };
32 |
33 | if (found) {
34 | data[foundIndex] = modified;
35 | } else {
36 | data.unshift(modified);
37 | }
38 |
39 | await this.overwriteFavoritesData(data);
40 |
41 | return data[index];
42 | }
43 |
44 | async update(
45 | key: CharacterKey,
46 | apply: (char: ParsedFavoriteInfo) => Partial
47 | ): Promise {
48 | const data = await this.getFavorites();
49 |
50 | const foundIndex = data.findIndex(ch => ch.codepoint === key);
51 |
52 | if (foundIndex < 0) {
53 | throw new UnicodeSearchError(`No character '${key}' exists in favorites.`);
54 | }
55 |
56 | data[foundIndex] = {
57 | ...data[foundIndex],
58 | ...apply({...data[foundIndex]}),
59 | codepoint: key,
60 | } as CodepointFavorite;
61 |
62 | await this.overwriteFavoritesData(data);
63 |
64 | return data[foundIndex];
65 | }
66 |
67 | async addFavorite(key: CharacterKey): Promise {
68 | const favorites = await this.getFavorites();
69 | const foundFavorite = favorites.find(fav => fav.codepoint === key);
70 |
71 | if (foundFavorite != null) {
72 | return foundFavorite;
73 | }
74 |
75 | const newFavorite: CodepointFavorite = {
76 | codepoint: key,
77 | added: new Date(),
78 | hotkey: false
79 | };
80 |
81 | favorites.unshift(newFavorite);
82 | await this.overwriteFavoritesData(favorites);
83 |
84 | return newFavorite;
85 | }
86 |
87 | async removeFavorite(key: CharacterKey): Promise {
88 | console.log(`Removing favorite ${key}`);
89 |
90 | let favorites = await this.getFavorites();
91 | favorites = favorites.filter(fav => fav.codepoint !== key);
92 | await this.overwriteFavoritesData(favorites);
93 | }
94 |
95 | async getFavorites(): Promise {
96 | return (await this.store.getFavorites()).codepoints.map(fav => ({
97 | ...fav,
98 | added: new Date(fav.added),
99 | }));
100 | }
101 |
102 | private async overwriteFavoritesData(data: CodepointFavorite[]): Promise {
103 | const newData = await this.mergeFavorites({
104 | codepoints: data.map(serializeFavoriteInfo),
105 | });
106 |
107 | await this.store.overwriteFavorites(newData);
108 | }
109 |
110 | private async mergeFavorites(data: Partial): Promise {
111 | const storedData = await this.store.getFavorites();
112 |
113 | const newData = {
114 | ...storedData,
115 | ...data
116 | };
117 |
118 | return await this.store.overwriteFavorites(newData);
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/unicode-search/service/codepointStorage.ts:
--------------------------------------------------------------------------------
1 | import {CodepointStore} from "./codePointStore";
2 | import {UnicodeCodepoint} from "../../libraries/types/codepoint/unicode";
3 | import {RootDataStore} from "./rootDataStore";
4 |
5 | export class CodepointStorage implements CodepointStore {
6 |
7 | constructor(private readonly store: RootDataStore) {
8 | }
9 |
10 | async getCodepoints(): Promise {
11 | return (await this.store.getUnicode()).codepoints;
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/unicode-search/service/codepointUsageStorage.ts:
--------------------------------------------------------------------------------
1 | import {UsageStore} from "./usageStore";
2 | import {CharacterKey} from "../../libraries/types/codepoint/character";
3 | import {parseUsageInfo} from "../../libraries/helpers/parseUsageInfo";
4 | import {serializeUsageInfo} from "../../libraries/helpers/serializeUsageInfo";
5 | import {RootDataStore} from "./rootDataStore";
6 | import {CharacterUseFragment} from "../../libraries/types/savedata/usageFragment";
7 |
8 | import {CodepointUse} from "../../libraries/types/codepoint/extension";
9 | import {UsageInfo} from "../../libraries/types/savedata/usageInfo";
10 |
11 | export class CodepointUsageStorage implements UsageStore {
12 |
13 | constructor(
14 | private readonly store: RootDataStore,
15 | ) {
16 | }
17 |
18 | async upsert(
19 | key: CharacterKey,
20 | apply: (char?: UsageInfo) => UsageInfo
21 | ): Promise
22 | {
23 | const data = await this.getUsed();
24 |
25 | const foundIndex = data.findIndex(ch => ch.codepoint === key);
26 | const found = foundIndex >= 0;
27 | const index = found ? foundIndex : 0;
28 |
29 | const modified = {
30 | ...apply(found ? {...data[index]} : undefined),
31 | codepoint: key,
32 | };
33 |
34 | if (found) {
35 | data[foundIndex] = modified
36 | } else {
37 | data.unshift(modified)
38 | }
39 |
40 | await this.overwriteUsageData(data)
41 |
42 | return data[index];
43 | }
44 |
45 | async getUsed(): Promise {
46 | return (await this.store.getUsage())
47 | .codepoints
48 | .map(parseUsageInfo)
49 | }
50 |
51 | private async overwriteUsageData(data: CodepointUse[]): Promise {
52 | const newData = await this.mergeUsage({
53 | codepoints: data.map(serializeUsageInfo),
54 | });
55 |
56 | return newData.codepoints.map(parseUsageInfo)
57 | }
58 |
59 | private async mergeUsage(data: Partial): Promise {
60 | const storedData = await this.store.getUsage();
61 |
62 | const newData = {
63 | ...storedData,
64 | ...data
65 | };
66 |
67 | return await this.store.overwriteUsage(newData);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/unicode-search/service/commander.ts:
--------------------------------------------------------------------------------
1 | import {Plugin} from "obsidian";
2 | import {FavoritesStore} from "./favoritesStore";
3 | import {toHexadecimal} from "../../libraries/helpers/toHexadecimal";
4 | import {CharacterService} from "./characterService";
5 | import {InsertCharacterModal} from "../components/insertCharacterModal";
6 | import {CodepointKey} from "../../libraries/types/codepoint/unicode";
7 |
8 | export class Commander {
9 | constructor(
10 | private readonly plugin: Plugin,
11 | ) {
12 | }
13 |
14 | addModal(characters: CharacterService) {
15 | this.plugin.addCommand({
16 | id: "search-unicode-chars",
17 | name: "Search Unicode characters",
18 |
19 | editorCallback: editor => {
20 | const modal = new InsertCharacterModal(
21 | this.plugin.app,
22 | characters,
23 | editor,
24 | );
25 | modal.open();
26 | return true;
27 | },
28 | });
29 | }
30 |
31 | async addFavorites(favorites: FavoritesStore) {
32 | const hotkeys = (await favorites.getFavorites())
33 | .filter(favorite => favorite.hotkey);
34 |
35 | for (const character of hotkeys) {
36 | this.addCommandFor(character);
37 | }
38 | }
39 |
40 | private addCommandFor(character: CodepointKey) {
41 | this.plugin.addCommand({
42 | id: `insert-${toHexadecimal(character)}`,
43 | name: `Insert '${character.codepoint}'`,
44 | repeatable: true,
45 |
46 | editorCallback: editor => {
47 | editor.replaceSelection(character.codepoint);
48 | },
49 | })
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/unicode-search/service/dataFragmentManager.ts:
--------------------------------------------------------------------------------
1 | import {DataFragment} from "../../libraries/types/savedata/dataFragment";
2 | import {DataEvent} from "../../libraries/types/savedata/metaFragment";
3 |
4 | /**
5 | * Manages the lifecycle of a structured data segment, typically stored as JSON.
6 | * Responsible for initialization and updates of data parts.
7 | */
8 | export interface DataFragmentManager {
9 | /**
10 | * Initializes the data structure with default/empty values.
11 | * @param fragment to be populated with default values
12 | * @returns the initialized data with defaults applied
13 | */
14 | initData(fragment: DataFragment): Fragment;
15 |
16 | /**
17 | * Migrates data between different versions when the data structure changes.
18 | * Handles both structural changes and data transformations.
19 | * @param fragment to be updated to the current version
20 | * @param events to be handled by the updater
21 | * @returns the updated data
22 | */
23 | updateData(fragment: Fragment, events: Set): Promise;
24 | }
25 |
--------------------------------------------------------------------------------
/src/unicode-search/service/dataManager.ts:
--------------------------------------------------------------------------------
1 | export interface DataManager {
2 | initializeData(): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/unicode-search/service/favoritesDataManager.ts:
--------------------------------------------------------------------------------
1 | import {DataFragmentManager} from "./dataFragmentManager";
2 | import {FavoritesFragment} from "../../libraries/types/savedata/favoritesFragment";
3 | import {SaveDataVersion} from "../../libraries/types/savedata/version";
4 | import {DataEvent} from "../../libraries/types/savedata/metaFragment";
5 | import {DataFragment} from "../../libraries/types/savedata/dataFragment";
6 |
7 | export class FavoritesDataManager implements DataFragmentManager {
8 | initData(fragment: DataFragment): FavoritesFragment {
9 | if (fragment.initialized && isFavoritesFragment(fragment)) {
10 | return fragment;
11 | }
12 |
13 | console.info("Initializing favorites");
14 |
15 | return {
16 | ...fragment,
17 | initialized: true,
18 | codepoints: [],
19 | };
20 | }
21 |
22 | async updateData(fragment: FavoritesFragment, _: Set): Promise {
23 | /* No-op yet */
24 | return fragment;
25 | }
26 |
27 | }
28 |
29 | function isFavoritesFragment(fragment: DataFragment): fragment is FavoritesFragment {
30 | return "codepoints" in fragment
31 | && Array.isArray(fragment.codepoints)
32 | ;
33 | }
34 |
--------------------------------------------------------------------------------
/src/unicode-search/service/favoritesStore.ts:
--------------------------------------------------------------------------------
1 | import { CharacterKey } from "../../libraries/types/codepoint/character";
2 |
3 | import {CodepointFavorite} from "../../libraries/types/codepoint/extension";
4 | import {ParsedFavoriteInfo} from "../../libraries/types/savedata/favoriteInfo";
5 |
6 | export interface FavoritesStore {
7 | update(key: CharacterKey, apply: (char: ParsedFavoriteInfo) => Partial): Promise;
8 | getFavorites(): Promise;
9 | /* Maybe replace with upsert*/
10 | addFavorite(key: CharacterKey): Promise;
11 | removeFavorite(key: CharacterKey): Promise;
12 |
13 |
14 | /* Unused so far */
15 | upsert(key: CharacterKey, apply: (char?: ParsedFavoriteInfo) => ParsedFavoriteInfo): Promise;
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/unicode-search/service/filterDataManager.ts:
--------------------------------------------------------------------------------
1 | import {DataFragmentManager} from "./dataFragmentManager";
2 | import {UNICODE_PLANES_ALL} from "../../libraries/data/unicodePlanes";
3 | import {UNICODE_CHARACTER_CATEGORIES} from "../../libraries/data/unicodeCharacterCategories";
4 | import {UnicodePlaneNumber} from "../../libraries/data/unicodePlaneNumber";
5 | import {CharacterCategoryGroupType} from "../../libraries/data/characterCategoryGroup";
6 | import {SaveDataVersion} from "../../libraries/types/savedata/version";
7 | import {FilterFragment} from "../../libraries/types/savedata/filterFragment";
8 | import {DataEvent} from "../../libraries/types/savedata/metaFragment";
9 | import {DataFragment} from "../../libraries/types/savedata/dataFragment";
10 | import {UnicodeFilter} from "../../libraries/types/savedata/unicodeFilter";
11 |
12 | export class FilterDataManager implements DataFragmentManager {
13 |
14 | initData(fragment: DataFragment): FilterFragment {
15 | if (fragment.initialized && isFilterFragment(fragment)) {
16 | return fragment;
17 | }
18 |
19 | console.info("Initializing filter");
20 |
21 | return {
22 | ...fragment,
23 | initialized: true,
24 | unicode: {
25 | planes: UNICODE_PLANES_ALL.map(plane => ({
26 | ...plane.interval,
27 | blocks: plane.blocks.map(block => ({
28 | ...block.interval,
29 | included: DATA_DEFAULTS.planes.includes(plane.planeNumber),
30 | }))
31 | })),
32 | categoryGroups: UNICODE_CHARACTER_CATEGORIES.map(group => ({
33 | abbreviation: group.abbreviation,
34 | categories: group.categories.map(category => ({
35 | abbreviation: category.abbreviation,
36 | included: DATA_DEFAULTS.categories.includes(group.abbreviation),
37 | }))
38 | })),
39 | }
40 | }
41 | }
42 |
43 | async updateData(fragment: FilterFragment, _: Set): Promise {
44 | /* No-op yet */
45 | return fragment;
46 | }
47 |
48 | }
49 |
50 | type InclusionDefaults = {
51 | planes: UnicodePlaneNumber[],
52 | categories: CharacterCategoryGroupType[],
53 | };
54 |
55 | const DATA_DEFAULTS: InclusionDefaults = {
56 | planes: [
57 | 0,
58 | // 1,
59 | // 2,
60 | // 3,
61 | // 14,
62 | // 15,
63 | // 16
64 | ],
65 | categories: [
66 | "L",
67 | // "M",
68 | "N",
69 | "P",
70 | "S",
71 | // "Z",
72 | // "C",
73 | ],
74 | }
75 |
76 | function isFilterFragment(object: DataFragment): object is FilterFragment {
77 | return "unicode" in object
78 | && isUnicodeFilter(object.unicode)
79 | ;
80 | }
81 |
82 | function isUnicodeFilter(object: any): object is UnicodeFilter {
83 | return "planes" in object
84 | && Array.isArray(object.planes)
85 | && "categoryGroups" in object
86 | && Array.isArray(object.categoryGroups)
87 | ;
88 | }
89 |
--------------------------------------------------------------------------------
/src/unicode-search/service/filterStorage.ts:
--------------------------------------------------------------------------------
1 | import {FilterStore} from "./filterStore";
2 | import {BlockFilter, CategoryFilter, UnicodeFilter} from "../../libraries/types/savedata/unicodeFilter";
3 | import {UnicodeSearchError} from "../errors/unicodeSearchError";
4 | import {CodepointInterval} from "../../libraries/types/codepoint/codepointInterval";
5 | import {intervalsEqual} from "../../libraries/helpers/intervalsEqual";
6 | import {intervalWithin} from "../../libraries/helpers/intervalWithin";
7 | import {CharacterCategoryType} from "../../libraries/data/characterCategory";
8 | import {RootDataStore} from "./rootDataStore";
9 | import {MetaStorage} from "./metaStorage";
10 | import {DataEvent} from "../../libraries/types/savedata/metaFragment";
11 |
12 | export class FilterStorage implements FilterStore {
13 |
14 | constructor(
15 | private readonly store: RootDataStore,
16 | private readonly meta: MetaStorage,
17 | ) {
18 | }
19 |
20 | async getFilter(): Promise {
21 | return (await this.store.getFilter()).unicode
22 | }
23 |
24 | async getCharacterBlock(block: CodepointInterval): Promise {
25 | return (await this.getBlockFilters())
26 | .some(blockFilter => intervalsEqual(blockFilter, block) && blockFilter.included);
27 | }
28 |
29 | async setCharacterBlock(block: CodepointInterval, set: boolean): Promise {
30 | const filter = await this.store.getFilter();
31 | const planeIndex = filter.unicode.planes.findIndex(pf => intervalWithin(pf, block));
32 |
33 | if (planeIndex < 0) {
34 | throw new UnicodeSearchError(`Block doesn't belong to any codepoint plane. ${block}`);
35 | }
36 |
37 | const blockIndex = filter.unicode.planes[planeIndex].blocks.findIndex(bf => intervalsEqual(bf, block));
38 |
39 | if (blockIndex < 0) {
40 | throw new UnicodeSearchError(`Block doesn't exist within a plane. ${block}`);
41 | }
42 |
43 | filter.unicode.planes[planeIndex].blocks[blockIndex].included = set;
44 | await this.meta.request(DataEvent.DownloadCharacters);
45 |
46 | await this.store.overwriteFilter(filter);
47 | }
48 |
49 | async getCharacterCategory(category: CharacterCategoryType): Promise {
50 | return (await this.getCategoryFilters())
51 | .some(filter => filter.abbreviation === category && filter.included);
52 | }
53 |
54 | async setCharacterCategory(category: CharacterCategoryType, set: boolean): Promise {
55 | const filter = await this.store.getFilter();
56 | const groupIndex = filter.unicode.categoryGroups.findIndex(gf => gf.abbreviation === category[0]);
57 |
58 | if (groupIndex < 0) {
59 | throw new UnicodeSearchError(`Codepoint category group doesn't exist. ${category}: ${category[0]}`);
60 | }
61 |
62 | const categoryIndex = filter.unicode.categoryGroups[groupIndex].categories.findIndex(cf => cf.abbreviation === category);
63 |
64 | if (categoryIndex < 0) {
65 | throw new UnicodeSearchError(`Block doesn't exist within a plane. ${category}`);
66 | }
67 |
68 | filter.unicode.categoryGroups[groupIndex].categories[categoryIndex].included = set;
69 | await this.meta.request(DataEvent.DownloadCharacters);
70 |
71 | await this.store.overwriteFilter(filter);
72 | }
73 |
74 | private async getBlockFilters(): Promise {
75 | return (await this.getFilter()).planes
76 | .flatMap(plane => plane.blocks);
77 | }
78 |
79 | private async getCategoryFilters(): Promise {
80 | return (await this.getFilter()).categoryGroups
81 | .flatMap(group => group.categories);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/unicode-search/service/filterStore.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeFilter} from "../../libraries/types/savedata/unicodeFilter";
2 | import {CodepointInterval} from "../../libraries/types/codepoint/codepointInterval";
3 | import {CharacterCategoryType} from "../../libraries/data/characterCategory";
4 |
5 | export interface FilterStore {
6 | getFilter(): Promise
7 |
8 | getCharacterBlock(block: CodepointInterval): Promise
9 | setCharacterBlock(block: CodepointInterval, set: boolean): Promise
10 |
11 | getCharacterCategory(category: CharacterCategoryType): Promise
12 | setCharacterCategory(category: CharacterCategoryType, set: boolean): Promise
13 | }
14 |
--------------------------------------------------------------------------------
/src/unicode-search/service/metaDataManager.ts:
--------------------------------------------------------------------------------
1 | import {DataFragmentManager} from "./dataFragmentManager";
2 | import {DataEvent, isDataEvent, MetaFragment} from "../../libraries/types/savedata/metaFragment";
3 | import {CURRENT_PLUGIN_VERSION, SaveDataVersion} from "../../libraries/types/savedata/version";
4 | import {DataFragment} from "../../libraries/types/savedata/dataFragment";
5 |
6 | export class MetaDataManager implements DataFragmentManager {
7 | initData(fragment: DataFragment): MetaFragment {
8 | if (fragment.initialized && isMetaFragment(fragment)) {
9 | return fragment;
10 | }
11 |
12 | console.info("Initializing metadata");
13 |
14 | return {
15 | ...fragment,
16 | pluginVersion: CURRENT_PLUGIN_VERSION,
17 | initialized: true,
18 | events: [],
19 | };
20 | }
21 |
22 | async updateData(fragment: MetaFragment, _: Set): Promise {
23 | /* No-op yet */
24 | return fragment;
25 | }
26 |
27 | }
28 |
29 | function isMetaFragment(fragment: DataFragment): fragment is MetaFragment {
30 | return "events" in fragment
31 | && fragment.events != null
32 | && Array.isArray(fragment.events)
33 | && fragment.events.every(e => isDataEvent(e))
34 | ;
35 | }
36 |
--------------------------------------------------------------------------------
/src/unicode-search/service/metaStorage.ts:
--------------------------------------------------------------------------------
1 | import {RootDataStore} from "./rootDataStore";
2 | import {DataEvent} from "../../libraries/types/savedata/metaFragment";
3 |
4 | export class MetaStorage {
5 |
6 | constructor(private readonly store: RootDataStore) {
7 | }
8 |
9 | async request(event: DataEvent): Promise {
10 | const meta = await this.store.getMeta();
11 | meta.events.push(event);
12 | await this.store.overwriteMeta(meta)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/unicode-search/service/rootDataManager.ts:
--------------------------------------------------------------------------------
1 | import {FilterDataManager} from "./filterDataManager";
2 | import {PersistCache} from "../../libraries/types/persistCache";
3 | import {
4 | SaveData,
5 | SaveDataOf
6 | } from "../../libraries/types/savedata/saveData";
7 | import {UnicodeDataManager} from "./unicodeDataManager";
8 | import {UsageDataManager} from "./usageDataManager";
9 | import {FavoritesDataManager} from "./favoritesDataManager";
10 | import {isTypeDataFragment} from "../../libraries/helpers/isTypeSaveData";
11 | import {DataManager} from "./dataManager";
12 | import {MetaDataManager} from "./metaDataManager";
13 | import {DataFragment} from "../../libraries/types/savedata/dataFragment";
14 | import {CURRENT_DATA_VERSION} from "../../libraries/types/savedata/version";
15 | import {MetaFragment} from "../../libraries/types/savedata/metaFragment";
16 | import {isInitialSaveData} from "../../libraries/types/savedata/initialSaveData";
17 |
18 | type MetaSaveDataFragments = Omit, "meta"> & { meta: MetaFragment };
19 |
20 | export class RootDataManager implements DataManager {
21 | constructor(
22 | private readonly storedData: PersistCache,
23 | private readonly metaDm: MetaDataManager,
24 | private readonly filterDm: FilterDataManager,
25 | private readonly unicodeDm: UnicodeDataManager,
26 | private readonly usageDm: UsageDataManager,
27 | private readonly favoritesDm: FavoritesDataManager,
28 | ) {
29 | }
30 |
31 | async initializeData(): Promise {
32 | console.group("Save data initialization");
33 | /* We don't know what we will load */
34 | const loadedData: any = (await this.storedData.get()) ?? {};
35 |
36 | /* First, migrate data from initial release */
37 | const migratedData = RootDataManager.initialMigration(loadedData);
38 |
39 | /* Make sure the data is well-shaped */
40 | const shapedData = RootDataManager.shapeLoadedData(migratedData);
41 |
42 | /* Full initialization of meta-data, because other fragments need to process its events */
43 | const loadedDataWithMeta = await this.initMeta(shapedData);
44 |
45 | /* Now we let each data fragment handle initialization of its data if needed */
46 | console.group("Initializing data");
47 | const initializedData = this.initData(loadedDataWithMeta);
48 | console.groupEnd();
49 |
50 | /* After this, each data fragment manager can request fragments data */
51 | this.storedData.set(initializedData);
52 |
53 | /* Each data fragment can update its data if needed */
54 | console.group("Updating data");
55 | const upToDateData = await this.updateData(initializedData);
56 | console.groupEnd();
57 |
58 | /* Finally, persist the data */
59 | console.info("Saving initialized data");
60 | this.storedData.set(upToDateData);
61 | await this.storedData.persist();
62 | console.groupEnd();
63 | }
64 |
65 | private async initMeta(fragments: SaveDataOf): Promise {
66 | /* Does the skeleton have data? */
67 | const initializedMeta = this.metaDm.initData(fragments.meta);
68 |
69 | /* Is the data up to date with the latest data version? */
70 | const upToDateMeta = await this.metaDm.updateData(initializedMeta, new Set([]));
71 |
72 | /* Check and create the shape of save-data if missing */
73 | return {
74 | ...fragments,
75 | meta: upToDateMeta,
76 | };
77 | }
78 |
79 | private initData(fragments: MetaSaveDataFragments): SaveData {
80 | const filterData = this.filterDm.initData(fragments.filter);
81 | const unicodeData = this.unicodeDm.initData(fragments.characters);
82 | const usageData = this.usageDm.initData(fragments.usage);
83 | const favoritesData = this.favoritesDm.initData(fragments.favorites);
84 |
85 | return {
86 | ...fragments,
87 | filter: filterData,
88 | characters: unicodeData,
89 | usage: usageData,
90 | favorites: favoritesData,
91 | };
92 | }
93 |
94 | private async updateData(initializedData: SaveData): Promise {
95 | /* We load the meta-data first, to be able to process events like re-downloading of characters etc. */
96 | const metaData = await this.metaDm.updateData(initializedData.meta, new Set([]));
97 | const events = new Set(metaData.events);
98 | console.info("Events to process", events);
99 |
100 | /* All the other updates see the events, and handle them accordingly */
101 | const filterData = await this.filterDm.updateData(initializedData.filter, events);
102 | const unicodeData = await this.unicodeDm.updateData(initializedData.characters, events);
103 | const usageData = await this.usageDm.updateData(initializedData.usage, events);
104 | const favoritesData = await this.favoritesDm.updateData(initializedData.favorites, events);
105 |
106 | console.info("Unprocessed events", events);
107 | metaData.events = Array.from(events);
108 |
109 | return {
110 | meta: metaData,
111 | filter: filterData,
112 | characters: unicodeData,
113 | usage: usageData,
114 | favorites: favoritesData,
115 | };
116 | }
117 |
118 | private static shapeLoadedData(loadedData: any): SaveDataOf {
119 | /* Check and create the shape of save-data if missing
120 | * Removing any element will remove it from save data
121 | */
122 | return {
123 | meta: RootDataManager.createFragment(loadedData.meta),
124 | filter: RootDataManager.createFragment(loadedData.filter),
125 | usage: RootDataManager.createFragment(loadedData.usage),
126 | characters: RootDataManager.createFragment(loadedData.characters),
127 | favorites: RootDataManager.createFragment(loadedData.favorites),
128 | };
129 | }
130 |
131 | private static createFragment(dataPart: any): DataFragment {
132 | return isTypeDataFragment(dataPart)
133 | ? dataPart
134 | : {
135 | initialized: false,
136 | version: CURRENT_DATA_VERSION,
137 | };
138 | }
139 |
140 | /**
141 | * Migration of data created before the save-data update
142 | * Only data version of "0.6.0" is migrated
143 | */
144 | private static initialMigration(loadedData: any): Pick {
145 | const shouldMigrate = isInitialSaveData(loadedData)
146 | && loadedData.initialized
147 | && loadedData.version === "0.6.0";
148 |
149 | if (!shouldMigrate) {
150 | return loadedData;
151 | }
152 |
153 | console.info("Migrating from data version 0.6.0");
154 |
155 | return {
156 | filter: {
157 | version: loadedData.version,
158 | ...loadedData.settings,
159 | unicode: loadedData.settings.filter,
160 | },
161 | usage: {
162 | version: loadedData.version,
163 | ...loadedData.usage
164 | },
165 | characters: {
166 | version: loadedData.version,
167 | ...loadedData.unicode
168 | },
169 | }
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/unicode-search/service/rootDataStore.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeFragment} from "../../libraries/types/savedata/unicodeFragment";
2 | import {FilterFragment} from "../../libraries/types/savedata/filterFragment";
3 | import {CharacterUseFragment} from "../../libraries/types/savedata/usageFragment";
4 | import {FavoritesFragment} from "../../libraries/types/savedata/favoritesFragment";
5 | import {MetaFragment} from "../../libraries/types/savedata/metaFragment";
6 |
7 | export interface RootDataStore {
8 | getMeta(): Promise
9 | overwriteMeta(data: MetaFragment): Promise
10 |
11 | getUnicode(): Promise
12 | overwriteUnicode(data: UnicodeFragment): Promise
13 |
14 | getFilter(): Promise
15 | overwriteFilter(settings: FilterFragment): Promise
16 |
17 | getUsage(): Promise
18 | overwriteUsage(usage: CharacterUseFragment): Promise
19 |
20 | getFavorites(): Promise
21 | overwriteFavorites(favorites: FavoritesFragment): Promise
22 | }
23 |
--------------------------------------------------------------------------------
/src/unicode-search/service/rootPluginDataStorage.ts:
--------------------------------------------------------------------------------
1 | import {PersistCache} from "../../libraries/types/persistCache";
2 | import {SaveData} from "../../libraries/types/savedata/saveData";
3 | import {FilterFragment} from "../../libraries/types/savedata/filterFragment";
4 | import {UnicodeFragment} from "../../libraries/types/savedata/unicodeFragment";
5 | import {CharacterUseFragment} from "../../libraries/types/savedata/usageFragment";
6 | import {FavoritesFragment} from "../../libraries/types/savedata/favoritesFragment";
7 | import {RootDataStore} from "./rootDataStore";
8 | import {MetaFragment} from "../../libraries/types/savedata/metaFragment";
9 |
10 | export class RootPluginDataStorage implements RootDataStore {
11 |
12 | constructor(
13 | private readonly storedData: PersistCache,
14 | ) {
15 | }
16 |
17 | async getMeta(): Promise {
18 | return (await this.storedData.get()).meta;
19 | }
20 |
21 | async overwriteMeta(data: MetaFragment): Promise {
22 | const mergedData = await this.mergeData({
23 | meta: data,
24 | });
25 |
26 | return mergedData.meta;
27 | }
28 |
29 | async getUnicode(): Promise {
30 | return (await this.storedData.get()).characters;
31 | }
32 |
33 | async overwriteUnicode(data: UnicodeFragment): Promise {
34 | const mergedData = await this.mergeData({
35 | characters: data,
36 | });
37 |
38 | return mergedData.characters;
39 | }
40 |
41 | async getFilter(): Promise {
42 | return (await this.storedData.get()).filter
43 | }
44 |
45 | async overwriteFilter(filter: FilterFragment): Promise {
46 | const mergedData = await this.mergeData({
47 | filter: filter,
48 | });
49 |
50 | return mergedData.filter;
51 | }
52 |
53 | async getUsage(): Promise {
54 | return (await this.storedData.get()).usage;
55 | }
56 |
57 | async overwriteUsage(usage: CharacterUseFragment): Promise {
58 | const mergedData = await this.mergeData({
59 | usage: usage,
60 | });
61 |
62 | return mergedData.usage;
63 | }
64 |
65 | async getFavorites(): Promise {
66 | return (await this.storedData.get()).favorites;
67 | }
68 |
69 | async overwriteFavorites(favorites: FavoritesFragment): Promise {
70 | const mergedData = await this.mergeData({
71 | favorites: favorites,
72 | });
73 |
74 | return mergedData.favorites;
75 | }
76 |
77 | private async mergeData(data: Partial): Promise {
78 | const storedData = await this.storedData.get();
79 |
80 | const newData: SaveData = {
81 | ...storedData,
82 | ...data,
83 | };
84 |
85 | this.storedData.set(newData);
86 | return await this.storedData.persist();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/unicode-search/service/ucdUserFilterDownloader.ts:
--------------------------------------------------------------------------------
1 | import {getIcon, Notice, request, requestUrl} from "obsidian";
2 | import {parse, ParseConfig, ParseResult, ParseWorkerConfig} from "papaparse";
3 | import {UnicodeSearchError} from "../errors/unicodeSearchError";
4 | import {UnicodeCodepoint} from "../../libraries/types/codepoint/unicode";
5 | import {CharacterDownloader} from "./characterDownloader";
6 | import {FilterStore} from "./filterStore";
7 | import {mergeIntervals} from "../../libraries/helpers/mergeIntervals";
8 | import {codepointIn} from "../../libraries/helpers/codePointIn";
9 | import {CharacterCategoryType} from "../../libraries/data/characterCategory";
10 | import {CodepointInterval} from "../../libraries/types/codepoint/codepointInterval";
11 |
12 | export class UcdUserFilterDownloader implements CharacterDownloader {
13 |
14 | private readonly config: ParseConfig = {
15 | delimiter: ";",
16 | header: false,
17 | transformHeader: undefined,
18 | dynamicTyping: false,
19 | fastMode: true,
20 | };
21 |
22 | public constructor(
23 | private readonly filterStore: FilterStore,
24 | ) {
25 | }
26 |
27 | public async download(): Promise {
28 | /* NOTE: You must also push a GIT mirror of the UCD version to the `ucd-mirror` branch */
29 | const unicodeVersion = "14.0.0";
30 |
31 | let info = "Unicode Search: Character Database Update";
32 | info += `\nUCD version ${unicodeVersion}`
33 | const notice = new Notice(info, 0);
34 |
35 | const sourceUcd = "https://www.unicode.org";
36 | const sourceGit = "https://raw.githubusercontent.com/BambusControl/obsidian-unicode-search/refs/heads/ucd-mirror"
37 | const path = `Public/${unicodeVersion}/ucd/UnicodeData.txt`
38 |
39 | let response = await requestUrl({url: `${sourceUcd}/${path}`, throw: false});
40 |
41 | if (response.status != 200) {
42 | info += `\n✗ Failed to download from Unicode: HTTP ${response.status}`;
43 | notice.setMessage(info)
44 |
45 | response = await requestUrl({url: `${sourceGit}/${path}`, throw: false});
46 |
47 | if (response.status != 200) {
48 | info += `\n✗ Failed to download from GIT: HTTP ${response.status}`;
49 | notice.setMessage(info)
50 | return [];
51 | }
52 | }
53 |
54 | info = info + `\n✓ Successfully downloaded characters`;
55 |
56 | const parsed = await this.transformToCharacters(response.text);
57 | const filtered = await this.filterCharacters(parsed);
58 | const unicode = filtered.map(intoUnicodeCodepoint);
59 |
60 | info += `\n✱ Filtered ${unicode.length} out of ${parsed.length} total characters`;
61 | notice.setMessage(info);
62 | setTimeout(() => notice.hide(), 6 * 1000);
63 |
64 | return unicode;
65 | }
66 |
67 | private async filterCharacters(parsed: ParsedCharacter[]): Promise {
68 | const filter = await this.filterStore.getFilter();
69 |
70 | const includedBlocks = mergeIntervals(filter.planes
71 | .flatMap(p => p.blocks)
72 | .filter(b => b.included));
73 |
74 | const includedCategories = filter.categoryGroups
75 | .flatMap(p => p.categories)
76 | .filter(c => c.included)
77 | .map(c => c.abbreviation);
78 |
79 | return parsed.filter(char =>
80 | !containsNullValues(char)
81 | && includedInBlocks(char, includedBlocks)
82 | && categoryIncluded(char, includedCategories)
83 | );
84 | }
85 |
86 | private transformToCharacters(csvString: string): Promise {
87 | return new Promise((resolve, reject) => {
88 | const completeFn = (results: ParseResult): void => {
89 | if (results.errors.length !== 0) {
90 | reject(new UnicodeSearchError("Error while parsing data from Unicode Character Database"));
91 | }
92 |
93 | const parsedCharacters = results.data
94 | .map((row): ParsedCharacter => ({
95 | codepoint: parseInt(row[0], 16),
96 | name: row[1],
97 | category: row[2],
98 | }));
99 |
100 | resolve(parsedCharacters);
101 | };
102 |
103 | const configuration: ParseWorkerConfig = {
104 | ...this.config,
105 | worker: true,
106 | complete: results => completeFn(results),
107 | };
108 |
109 | parse(csvString, configuration);
110 | });
111 | }
112 |
113 | }
114 |
115 | type ParsedData = string[];
116 |
117 | type ParsedCharacter = {
118 | codepoint: number;
119 | name: string;
120 | category: string;
121 | };
122 |
123 | function containsNullValues(char: Partial): boolean {
124 | return char == null
125 | || char.name == null
126 | || char.codepoint == null
127 | || char.category == null
128 | }
129 |
130 | function includedInBlocks(character: Pick, includedBlocks: CodepointInterval[]): boolean {
131 | return includedBlocks.some(
132 | (block) => codepointIn(character.codepoint, block)
133 | );
134 | }
135 |
136 | function categoryIncluded(character: Pick, includedCategories: CharacterCategoryType[]): boolean {
137 | return includedCategories.some(
138 | (category) => character.category === category
139 | );
140 | }
141 |
142 | function intoUnicodeCodepoint(char: ParsedCharacter): UnicodeCodepoint {
143 | return {
144 | codepoint: String.fromCodePoint(char.codepoint),
145 | name: char.name.toLowerCase(),
146 | category: char.category
147 | };
148 | }
149 |
--------------------------------------------------------------------------------
/src/unicode-search/service/unicodeDataManager.ts:
--------------------------------------------------------------------------------
1 | import {DataFragmentManager} from "./dataFragmentManager";
2 | import {UnicodeFragment} from "../../libraries/types/savedata/unicodeFragment";
3 | import {SaveDataVersion} from "../../libraries/types/savedata/version";
4 | import {isCodepointKey} from "../../libraries/helpers/isTypeSaveData";
5 | import {CharacterDownloader} from "./characterDownloader";
6 | import {DataEvent} from "../../libraries/types/savedata/metaFragment";
7 | import {DataFragment} from "../../libraries/types/savedata/dataFragment";
8 | import {UnicodeCodepoint} from "../../libraries/types/codepoint/unicode";
9 |
10 | export class UnicodeDataManager implements DataFragmentManager {
11 | constructor(
12 | private readonly ucdService: CharacterDownloader,
13 | ) {
14 | }
15 |
16 | initData(fragment: DataFragment): UnicodeFragment {
17 | if (fragment.initialized && isUnicodeFragment(fragment)) {
18 | return fragment;
19 | }
20 |
21 | console.info("Initializing characters");
22 |
23 | return {
24 | ...fragment,
25 | initialized: true,
26 | codepoints: [],
27 | };
28 | }
29 |
30 | async updateData(fragment: UnicodeFragment, events: Set): Promise {
31 | const updatedData = this.updateByVersion(fragment);
32 | const downloadRequested = events.has(DataEvent.DownloadCharacters);
33 | const emptyCharacterSet = fragment.codepoints.length < 1;
34 |
35 | if (!(downloadRequested || emptyCharacterSet)) {
36 | return updatedData;
37 | }
38 |
39 | console.info("Downloading character database");
40 | const codepoints = await this.ucdService.download();
41 |
42 | /* Yeah, we modify the input parameter */
43 | events.delete(DataEvent.DownloadCharacters);
44 |
45 | return {
46 | ...updatedData,
47 | codepoints: codepoints,
48 | };
49 | }
50 |
51 | private updateByVersion(fragment: UnicodeFragment): UnicodeFragment {
52 | /* No-op yet */
53 | return fragment;
54 | }
55 | }
56 |
57 | export function isUnicodeFragment(fragment: DataFragment): fragment is UnicodeFragment {
58 | return "codepoints" in fragment
59 | && fragment.codepoints != null
60 | && Array.isArray(fragment.codepoints)
61 | && fragment.codepoints.every(isUnicodeCodepoint)
62 | ;
63 | }
64 |
65 | function isUnicodeCodepoint(object: any): object is UnicodeCodepoint {
66 | return isCodepointKey(object)
67 |
68 | && "name" in object
69 | && object.name != null
70 | && typeof object.name === "string"
71 |
72 | && "category" in object
73 | && object.category != null
74 | && typeof object.category === "string"
75 | ;
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/src/unicode-search/service/usageDataManager.ts:
--------------------------------------------------------------------------------
1 | import {DataFragmentManager} from "./dataFragmentManager";
2 | import {CharacterUseFragment} from "../../libraries/types/savedata/usageFragment";
3 | import {SaveDataVersion} from "../../libraries/types/savedata/version";
4 | import {isCodepointKey} from "../../libraries/helpers/isTypeSaveData";
5 | import {DataEvent} from "../../libraries/types/savedata/metaFragment";
6 | import {DataFragment} from "../../libraries/types/savedata/dataFragment";
7 |
8 |
9 | import {RawCodepointUse} from "../../libraries/types/codepoint/extension";
10 |
11 | export class UsageDataManager implements DataFragmentManager {
12 | initData(fragment: DataFragment): CharacterUseFragment {
13 | if (fragment.initialized && isUsageFragment(fragment)) {
14 | return fragment;
15 | }
16 |
17 | console.info("Initializing usage");
18 |
19 | return {
20 | ...fragment,
21 | initialized: true,
22 | codepoints: [],
23 | };
24 | }
25 |
26 | async updateData(fragment: CharacterUseFragment, _: Set): Promise {
27 | /* No-op yet */
28 | return fragment;
29 | }
30 |
31 | }
32 |
33 | function isUsageFragment(fragment: DataFragment): fragment is CharacterUseFragment {
34 | return "codepoints" in fragment
35 | && fragment.codepoints != null
36 | && Array.isArray(fragment.codepoints)
37 | && fragment.codepoints.every(isCodepointUsage)
38 | ;
39 | }
40 |
41 | function isCodepointUsage(object: any): object is RawCodepointUse {
42 | return isCodepointKey(object)
43 |
44 | && "firstUsed" in object
45 | && object.firstUsed != null
46 | && typeof object.firstUsed === "string"
47 |
48 | && "lastUsed" in object
49 | && object.lastUsed != null
50 | && typeof object.lastUsed === "string"
51 |
52 | && "useCount" in object
53 | && object.useCount != null
54 | && typeof object.useCount === "number"
55 | }
56 |
--------------------------------------------------------------------------------
/src/unicode-search/service/usageStore.ts:
--------------------------------------------------------------------------------
1 | import {CharacterKey} from "../../libraries/types/codepoint/character";
2 |
3 |
4 | import {CodepointUse} from "../../libraries/types/codepoint/extension";
5 | import {UsageInfo} from "../../libraries/types/savedata/usageInfo";
6 |
7 | export interface UsageStore {
8 | upsert(key: CharacterKey, apply: (char?: UsageInfo) => UsageInfo): Promise;
9 | getUsed(): Promise;
10 | }
11 |
--------------------------------------------------------------------------------
/src/unicode-search/service/userCharacterService.ts:
--------------------------------------------------------------------------------
1 | import {UnicodeSearchError} from "../errors/unicodeSearchError";
2 | import {
3 | Character,
4 | CharacterKey, FavoriteCharacter, MaybeUsedCharacter,
5 | UsedCharacter
6 | } from "../../libraries/types/codepoint/character";
7 | import {CodepointStore} from "./codePointStore";
8 | import {CharacterService} from "./characterService";
9 | import {UsageStore} from "./usageStore";
10 |
11 |
12 | import {FavoritesStore} from "./favoritesStore";
13 | import {UsageInfo} from "../../libraries/types/savedata/usageInfo";
14 |
15 | export class UserCharacterService implements CharacterService {
16 |
17 | public constructor(
18 | private readonly codepointStore: CodepointStore,
19 | private readonly usageStore: UsageStore,
20 | private readonly favoritesStore: FavoritesStore,
21 | ) {
22 | }
23 |
24 | public async getOne(key: CharacterKey): Promise {
25 | const characters = await this.getAllCharacters();
26 | const char = characters.find(char => char.codepoint === key);
27 |
28 | if (char == null) {
29 | throw new UnicodeSearchError(`No character '${key}' exists.`);
30 | }
31 |
32 | return char;
33 | }
34 |
35 | public getAllCharacters(): Promise {
36 | return this.codepointStore.getCodepoints();
37 | }
38 |
39 | public async getUsed(): Promise {
40 | const allCharacters = await this.getAllCharacters();
41 | const usedCharacters = await this.usageStore.getUsed();
42 | const usedKeys = usedCharacters.map(ch => ch.codepoint);
43 |
44 | return allCharacters
45 | .filter(ch => usedKeys.contains(ch.codepoint))
46 | .map(character => ({
47 | ...usedCharacters.find(usage => usage.codepoint === character.codepoint)!,
48 | ...character,
49 | }));
50 | }
51 |
52 | public async getFavorites(): Promise {
53 | const allCharacters = await this.getAllCharacters();
54 | const favorite = await this.favoritesStore.getFavorites();
55 | const favoriteKeys = favorite.map(ch => ch.codepoint);
56 |
57 | return allCharacters
58 | .filter(ch => favoriteKeys.contains(ch.codepoint))
59 | .map(character => ({
60 | ...favorite.find(usage => usage.codepoint === character.codepoint)!,
61 | ...character,
62 | }));
63 | }
64 |
65 | public async getAll(): Promise {
66 | const allCharacters = await this.getAllCharacters();
67 | const favoriteCharacters = await this.favoritesStore.getFavorites();
68 | const usedCharacters = await this.usageStore.getUsed();
69 |
70 | return allCharacters.map(character => ({
71 | ...favoriteCharacters.find(fav => fav.codepoint === character.codepoint),
72 | ...usedCharacters.find(usage => usage.codepoint === character.codepoint),
73 | ...character,
74 | }));
75 | }
76 |
77 | public recordUsage(key: CharacterKey): Promise {
78 | const timestamp = new Date();
79 |
80 | return this.usageStore.upsert(key, (current) => ({
81 | ...current,
82 | firstUsed: current?.firstUsed ?? timestamp,
83 | lastUsed: timestamp,
84 | useCount: (current?.useCount ?? 0) + 1,
85 | }))
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/unicode-search/styles.scss:
--------------------------------------------------------------------------------
1 | .plugin.unicode-search {
2 | --custom-checkbox-radius-modifier: 1;
3 |
4 | &.result-item {
5 | display: flex;
6 | gap: 1em;
7 | justify-content: flex-start;
8 | align-items: center;
9 |
10 | & > .character-preview {
11 | font-size: 1.5em;
12 | width: 1.75em;
13 |
14 | display: flex;
15 | justify-content: center;
16 | align-items: center;
17 | }
18 |
19 | & > .character-match {
20 | flex-grow: 1;
21 |
22 | display: flex;
23 | flex-direction: column;
24 | justify-content: center;
25 | align-items: flex-start;
26 |
27 | column-gap: 1em;
28 | row-gap: 0.05em;
29 |
30 | min-height: 2.5em;
31 |
32 | @media (min-width: 40em) {
33 | flex-direction: row;
34 | justify-content: space-between;
35 | align-items: center;
36 | }
37 |
38 | & > .character-name {
39 | letter-spacing: 0.025em;
40 | text-transform: capitalize;
41 | }
42 |
43 | & > .character-codepoint {
44 | color: var(--icon-color);
45 | font-family: var(--font-monospace), monospace;
46 | font-size: 0.7em;
47 | letter-spacing: 0.1em;
48 | text-transform: uppercase;
49 |
50 | &::before {
51 | content: "U+";
52 | letter-spacing: 0.15em;
53 | }
54 |
55 | & > .suggestion-highlight {
56 | color: var(--text-normal);
57 | }
58 | }
59 | }
60 |
61 | & > .detail {
62 | display: flex;
63 | flex-direction: row;
64 | justify-content: flex-end;
65 | align-items: center;
66 |
67 | & > .attributes {
68 | display: flex;
69 | flex-direction: column;
70 | flex-wrap: wrap;
71 | justify-content: center;
72 | align-items: center;
73 | align-content: center;
74 | font-size: var(--font-smallest);
75 | width: 1.25em;
76 |
77 | & > .favorite {
78 | font-size: 1.25em;
79 | }
80 | }
81 | }
82 | }
83 |
84 | & .icon {
85 | color: var(--icon-color);
86 | opacity: var(--icon-opacity);
87 |
88 | &:hover {
89 | color: var(--icon-color-hover);
90 | opacity: var(--icon-opacity-hover);
91 |
92 | &.interactive {
93 | color: var(--interactive-accent-hover);
94 | }
95 | }
96 | }
97 |
98 | &.setting-tab {
99 | .group-control {
100 | .checkbox-container {
101 | font-size: calc(var(--font-smallest) * 0.9);
102 |
103 | --toggle-thumb-height: 1.5em;
104 | --toggle-thumb-width: var(--toggle-thumb-height);
105 | --toggle-width: calc(4em + var(--toggle-thumb-height));
106 |
107 | border-radius: calc(var(--checkbox-radius) * var(--custom-checkbox-radius-modifier));
108 | background-color: var(--background-modifier-border-hover);
109 |
110 | transition: background-color 0.15s ease-in-out, outline 0.15s ease-in-out, border 0.15s ease-in-out, opacity 0.15s ease-in-out,
111 | color 0.15s ease-in-out, outline 0.15s ease-in-out, border 0.15s ease-in-out, opacity 0.15s ease-in-out;
112 | ;
113 |
114 | &:before {
115 | opacity: 100%;
116 | color: var(--text-muted);
117 | padding: 0 .5em;
118 | line-height: 1.85em;
119 | text-transform: uppercase;
120 |
121 | content: "show";
122 | text-align: end;
123 | }
124 |
125 | &:after {
126 | border-radius: calc(var(--checkbox-radius) * var(--custom-checkbox-radius-modifier));
127 | background-color: var(--text-muted);
128 | }
129 |
130 | &.is-enabled {
131 | background-color: var(--background-modifier-border-hover);
132 |
133 | &:before {
134 | content: "hide";
135 | text-align: start;
136 | }
137 |
138 | &:after {
139 | background-color: var(--interactive-accent);
140 | }
141 | }
142 | }
143 | }
144 |
145 | .favorite-control {
146 | .checkbox-container {
147 | font-size: calc(var(--font-smallest) * 0.9);
148 |
149 | --toggle-thumb-height: 1.5em;
150 | --toggle-thumb-width: var(--toggle-thumb-height);
151 | --toggle-width: calc(4em + var(--toggle-thumb-height));
152 |
153 | border-radius: calc(var(--checkbox-radius) * var(--custom-checkbox-radius-modifier));
154 | background-color: var(--background-modifier-border-hover);
155 |
156 | transition: background-color 0.15s ease-in-out, outline 0.15s ease-in-out, border 0.15s ease-in-out, opacity 0.15s ease-in-out,
157 | color 0.15s ease-in-out, outline 0.15s ease-in-out, border 0.15s ease-in-out, opacity 0.15s ease-in-out;
158 | ;
159 |
160 | &:before {
161 | opacity: 100%;
162 | color: var(--text-muted);
163 | padding: 0 .5em;
164 | line-height: 1.85em;
165 | text-transform: uppercase;
166 |
167 | content: "INSERT";
168 | text-align: end;
169 | }
170 |
171 | &:after {
172 | border-radius: calc(var(--checkbox-radius) * var(--custom-checkbox-radius-modifier));
173 | background-color: var(--text-muted);
174 | }
175 |
176 | &.is-enabled {
177 | background-color: var(--background-modifier-border-hover);
178 |
179 | &:before {
180 | content: "DEL";
181 | text-align: start;
182 | }
183 |
184 | &:after {
185 | background-color: var(--interactive-accent);
186 | }
187 | }
188 | }
189 | }
190 |
191 | .group-container {
192 | gap: 1em;
193 | flex-direction: column;
194 | align-items: stretch;
195 | align-content: stretch;
196 | flex-wrap: wrap;
197 | justify-content: flex-start;
198 |
199 | &:not(.hidden) {
200 | display: flex;
201 | }
202 |
203 | &.hidden {
204 | display: none;
205 | }
206 | }
207 |
208 | .item-container {
209 | border: 1px solid var(--background-modifier-border);
210 | border-radius: var(--radius-s);
211 | padding: .5em .75em;
212 | background-color: var(--background-primary-alt);
213 |
214 | .setting-item.setting-item-heading {
215 |
216 | &:first-child {
217 | padding-top: 0;
218 | }
219 |
220 | @media (min-width: 40em) {
221 | margin: .75em 0;
222 | }
223 |
224 | & > .setting-item-info {
225 |
226 | & > .setting-item-name {
227 | display: flex;
228 | flex-direction: row;
229 | flex-wrap: wrap;
230 | justify-content: space-between;
231 | align-items: center;
232 | }
233 | }
234 | }
235 | }
236 |
237 | @media (min-width: 40em) {
238 | .blocks-list .setting-item {
239 | & > .setting-item-info {
240 | display: inline-flex;
241 | flex-direction: row;
242 | flex-wrap: wrap;
243 | justify-content: space-between;
244 | align-items: center;
245 |
246 |
247 | & > .setting-item-name {
248 | font-size: 90%;
249 | }
250 |
251 | & > .setting-item-description {
252 | padding-top: 0;
253 | }
254 | }
255 |
256 | & > .setting-item-control {
257 | flex-grow: 0;
258 | }
259 | }
260 | }
261 |
262 | .character-list.no-first {
263 | & > .setting-item {
264 | padding: 0.75em 0;
265 | border-top: 1px solid var(--background-modifier-border);
266 | }
267 | }
268 |
269 | .character-list.new {
270 | flex-direction: column-reverse;
271 | }
272 |
273 | @media (min-width: 40em) {
274 | .character-list .setting-item {
275 |
276 | & > .setting-item-info {
277 | display: inline-flex;
278 | flex-direction: row;
279 | flex-wrap: wrap;
280 | justify-content: start;
281 | align-items: center;
282 | gap: 1em;
283 |
284 | & > .setting-item-name {
285 | font-size: 1.5em;
286 | width: 1.75em;
287 |
288 | display: flex;
289 | justify-content: center;
290 | align-items: center;
291 | }
292 |
293 | & > .setting-item-description {
294 | padding-top: 0;
295 | letter-spacing: 0.025em;
296 | text-transform: capitalize;
297 | }
298 | }
299 |
300 | & > .setting-item-control {
301 | flex-grow: 0;
302 | }
303 | }
304 | }
305 |
306 | .monospace {
307 | color: var(--code-normal);
308 | font-family: var(--font-monospace);
309 | }
310 |
311 | .character-codepoint {
312 | letter-spacing: 0.055em;
313 | text-transform: uppercase;
314 | font-size: var(--code-size);
315 |
316 | @media (min-width: 40em) {
317 | border-radius: var(--radius-s);
318 | background-color: var(--code-background);
319 | padding: 0.1em 0.3em;
320 | }
321 |
322 | & > .suggestion-highlight {
323 | color: var(--text-normal);
324 | }
325 | }
326 |
327 | .setting-item {
328 | &.focus-control {
329 |
330 | & > .setting-item-info {
331 | width: 0;
332 | }
333 |
334 | & > .setting-item-control {
335 | flex-grow: 1;
336 |
337 | & > input:only-child {
338 | width: 100%;
339 | }
340 |
341 | }
342 |
343 | }
344 |
345 | &.codepoint-interval {
346 |
347 | & > .setting-item-info {
348 | flex-grow: 1;
349 | }
350 |
351 | & > .setting-item-control {
352 | flex-grow: 0;
353 | }
354 |
355 | }
356 |
357 | &.prompt-input {
358 | & input {
359 | width: 100%;
360 | padding: var(--size-4-3);
361 | padding-inline-end: var(--size-4-6);
362 | background-color: var(--background-primary);
363 | font-size: var(--font-ui-medium);
364 | border: none;
365 | height: var(--prompt-input-height);
366 | border-radius: 0;
367 | border-bottom: 1px solid var(--background-secondary);
368 | }
369 | }
370 | }
371 |
372 | .favorite-settings .setting-item-control {
373 | button:not(.clickable-icon) {
374 | width: revert;
375 | }
376 | }
377 | }
378 |
379 |
380 | }
381 |
--------------------------------------------------------------------------------
/tests/libraries/comparison/compareCodepoints.spec.ts:
--------------------------------------------------------------------------------
1 | import {compareCodepoints} from "../../../src/libraries/comparison/compareCodepoints";
2 |
3 | test(
4 | "character with codepoint `a` is before character with codepoint `b`",
5 | () => {
6 | expect(compareCodepoints(
7 | {
8 | codepoint: "a",
9 | name: "",
10 | category: "Ll",
11 | },
12 | {
13 | codepoint: "b",
14 | name: "",
15 | category: "Ll",
16 | },
17 | )).toBe(-1);
18 | }
19 | );
20 |
21 | test(
22 | "character with codepoint `b` is after character with codepoint `a`",
23 | () => {
24 | expect(compareCodepoints(
25 | {
26 | codepoint: "b",
27 | name: "",
28 | category: "Ll",
29 | },
30 | {
31 | codepoint: "a",
32 | name: "",
33 | category: "Ll",
34 | },
35 | )).toBe(1);
36 | }
37 | );
38 |
39 | test(
40 | "character with codepoint `a` is equal to character with codepoint `a`",
41 | () => {
42 | expect(compareCodepoints(
43 | {
44 | codepoint: "a",
45 | name: "",
46 | category: "Ll",
47 | },
48 | {
49 | codepoint: "a",
50 | name: "",
51 | category: "Ll",
52 | },
53 | )).toBe(0);
54 | }
55 | );
56 |
57 | test(
58 | "character with codepoint `A` is before character with codepoint `a`",
59 | () => {
60 | expect(compareCodepoints(
61 | {
62 | codepoint: "A",
63 | name: "",
64 | category: "Lu",
65 | },
66 | {
67 | codepoint: "a",
68 | name: "",
69 | category: "Ll",
70 | },
71 | )).toBe(-1);
72 | }
73 | );
74 |
75 | test(
76 | "character with codepoint `z` is after character with codepoint `y`",
77 | () => {
78 | expect(compareCodepoints(
79 | {
80 | codepoint: "z",
81 | name: "",
82 | category: "Ll",
83 | },
84 | {
85 | codepoint: "y",
86 | name: "",
87 | category: "Ll",
88 | },
89 | )).toBe(1);
90 | }
91 | );
92 |
--------------------------------------------------------------------------------
/tests/libraries/comparison/compareDates.test.ts:
--------------------------------------------------------------------------------
1 | import { compareDates } from "../../../src/libraries/comparison/compareDates";
2 | import { Order } from "../../../src/libraries/order/order";
3 |
4 | describe("compareDates", () => {
5 | it("should return Order.Smaller when the left date is earlier than the right date", () => {
6 | const left = new Date("2023-01-01");
7 | const right = new Date("2023-01-02");
8 | expect(compareDates(left, right)).toBe(Order.Smaller);
9 | });
10 |
11 | it("should return Order.Greater when the left date is later than the right date", () => {
12 | const left = new Date("2023-01-02");
13 | const right = new Date("2023-01-01");
14 | expect(compareDates(left, right)).toBe(Order.Greater);
15 | });
16 |
17 | it("should return Order.Equal when the left date is the same as the right date", () => {
18 | const left = new Date("2023-01-01");
19 | const right = new Date("2023-01-01");
20 | expect(compareDates(left, right)).toBe(Order.Equal);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/tests/libraries/comparison/compareFavoriteCharacters.spec.ts:
--------------------------------------------------------------------------------
1 | import {compareFavoriteCharacters} from "../../../src/libraries/comparison/compareFavoriteCharacters";
2 |
3 | test(
4 | "character which is `favorite` is before character which is not",
5 | () => {
6 | expect(compareFavoriteCharacters(
7 | {
8 | codepoint: " ",
9 | name: "favorite",
10 | category: "Ll",
11 | added: new Date(1),
12 | hotkey: false,
13 | },
14 | {
15 | codepoint: " ",
16 | name: "not-favorite",
17 | category: "Ll",
18 | },
19 | )).toBe(-1)
20 | }
21 | )
22 |
--------------------------------------------------------------------------------
/tests/libraries/comparison/compareFavoriteInfo.spec.ts:
--------------------------------------------------------------------------------
1 | import {compareFavoriteInfo} from "../../../src/libraries/comparison/compareFavoriteInfo";
2 |
3 | test(
4 | "later added is before sooner added",
5 | () => {
6 | expect(compareFavoriteInfo(
7 | {
8 | added: new Date(1),
9 | hotkey: false,
10 | },
11 | {
12 | added: new Date(0),
13 | hotkey: false,
14 | },
15 | )).toBe(-1)
16 | }
17 | )
18 |
19 | test(
20 | "sooner added is after later added",
21 | () => {
22 | expect(compareFavoriteInfo(
23 | {
24 | added: new Date(0),
25 | hotkey: false,
26 | },
27 | {
28 | added: new Date(1),
29 | hotkey: false,
30 | },
31 | )).toBe(1)
32 | }
33 | )
34 |
--------------------------------------------------------------------------------
/tests/libraries/comparison/compareNullable.spec.ts:
--------------------------------------------------------------------------------
1 | import {expect, test} from "@jest/globals";
2 | import {compareNullable} from "../../../src/libraries/comparison/compareNullable";
3 | import {compareNumbers} from "../../../src/libraries/comparison/compareNumbers";
4 |
5 | test(
6 | "null equals null",
7 | () => {
8 | expect(compareNullable(null, null, compareNumbers)).toBe(0);
9 | }
10 | )
11 |
12 | test(
13 | "non-null is less than null",
14 | () => {
15 | expect(compareNullable(0, null, compareNumbers)).toBe(-1);
16 | }
17 | )
18 |
19 | test(
20 | "null is more than non-null",
21 | () => {
22 | expect(compareNullable(null, 0, compareNumbers)).toBe(1);
23 | }
24 | )
25 |
--------------------------------------------------------------------------------
/tests/libraries/comparison/compareNumbers.spec.ts:
--------------------------------------------------------------------------------
1 | import {expect, test} from "@jest/globals";
2 | import {compareNumbers} from "../../../src/libraries/comparison/compareNumbers";
3 |
4 | test(
5 | "one equals one",
6 | () => {
7 | expect(compareNumbers(1, 1)).toBe(0);
8 | }
9 | )
10 |
11 | test(
12 | "zero is less than one",
13 | () => {
14 | expect(compareNumbers(0, 1)).toBe(-1);
15 | }
16 | )
17 |
18 | test(
19 | "two is more than one",
20 | () => {
21 | expect(compareNumbers(2, 1)).toBe(1);
22 | }
23 | )
24 |
--------------------------------------------------------------------------------
/tests/libraries/comparison/compareUsageInfo.spec.ts:
--------------------------------------------------------------------------------
1 | import {compareUsageInfo} from "../../../src/libraries/comparison/compareUsageInfo";
2 |
3 | test(
4 | "later use is before sooner use",
5 | () => {
6 | expect(compareUsageInfo(
7 | {
8 | firstUsed: new Date(0),
9 | lastUsed: new Date(2),
10 | useCount: 1,
11 | },
12 | {
13 | firstUsed: new Date(0),
14 | lastUsed: new Date(1),
15 | useCount: 1,
16 | },
17 | new Date(0),
18 | )).toBe(-1)
19 | }
20 | )
21 |
22 | test(
23 | "sooner use is after later use",
24 | () => {
25 | expect(compareUsageInfo(
26 | {
27 | firstUsed: new Date(0),
28 | lastUsed: new Date(1),
29 | useCount: 1,
30 | },
31 | {
32 | firstUsed: new Date(0),
33 | lastUsed: new Date(2),
34 | useCount: 1,
35 | },
36 | new Date(0),
37 | )).toBe(1)
38 | }
39 | )
40 |
41 | test(
42 | "one use is before zero uses",
43 | () => {
44 | expect(compareUsageInfo(
45 | {
46 | useCount: 1,
47 | firstUsed: new Date(0),
48 | lastUsed: new Date(0),
49 | },
50 | {
51 | useCount: 0,
52 | firstUsed: new Date(0),
53 | lastUsed: new Date(0),
54 | },
55 | new Date(0),
56 | )).toBe(-1)
57 | }
58 | )
59 |
60 | test(
61 | "zero uses is after one use",
62 | () => {
63 | expect(compareUsageInfo(
64 | {
65 | useCount: 0,
66 | firstUsed: new Date(0),
67 | lastUsed: new Date(0),
68 | },
69 | {
70 | useCount: 1,
71 | firstUsed: new Date(0),
72 | lastUsed: new Date(0),
73 | },
74 | new Date(0),
75 | )).toBe(1)
76 | }
77 | )
78 |
--------------------------------------------------------------------------------
/tests/libraries/comparison/compareUsedCharacters.spec.ts:
--------------------------------------------------------------------------------
1 | import {compareUsedCharacters} from "../../../src/libraries/comparison/compareUsedCharacters";
2 |
3 | test(
4 | "character with `use` is before character without",
5 | () => {
6 | expect(compareUsedCharacters(
7 | {
8 | codepoint: " ",
9 | name: "b",
10 | category: "Ll",
11 | lastUsed: new Date(2),
12 | firstUsed: new Date(1),
13 | useCount: 1
14 | },
15 | {
16 | codepoint: " ",
17 | name: "a",
18 | category: "Ll",
19 | },
20 | new Date(0),
21 | )).toBe(-1)
22 | }
23 | )
24 |
25 | test(
26 | "characters with same `name`, `use`, and `favorite` are equal",
27 | () => {
28 | expect(compareUsedCharacters(
29 | {
30 | codepoint: " ",
31 | name: "name",
32 | category: "Ll",
33 | firstUsed: new Date(1),
34 | lastUsed: new Date(1),
35 | useCount: 1,
36 | },
37 | {
38 | codepoint: " ",
39 | name: "name",
40 | category: "Ll",
41 | firstUsed: new Date(1),
42 | lastUsed: new Date(1),
43 | useCount: 1
44 | },
45 | new Date(0)
46 | )).toBe(0)
47 | }
48 | )
49 |
--------------------------------------------------------------------------------
/tests/libraries/helpers/averageUseCount.spec.ts:
--------------------------------------------------------------------------------
1 | import { averageUseCount } from "../../../src/libraries/helpers/averageUseCount";
2 |
3 | describe("averageUseCount", () => {
4 | it("should return 0 when the input array is empty", () => {
5 | const result = averageUseCount([]);
6 | expect(result).toBe(0);
7 | });
8 |
9 | it("should return the correct average when the input array has one item", () => {
10 | const result = averageUseCount([{ useCount: 5 }]);
11 | expect(result).toBe(5);
12 | });
13 |
14 | it("should return the correct average when the input array has multiple items", () => {
15 | const result = averageUseCount([{ useCount: 5 }, { useCount: 10 }, { useCount: 15 }]);
16 | expect(result).toBe(10);
17 | });
18 |
19 | it("should handle an array with all zero useCounts", () => {
20 | const result = averageUseCount([{ useCount: 0 }, { useCount: 0 }, { useCount: 0 }]);
21 | expect(result).toBe(0);
22 | });
23 |
24 | it("should handle an array with mixed positive and zero useCounts", () => {
25 | const result = averageUseCount([{ useCount: 0 }, { useCount: 10 }, { useCount: 20 }]);
26 | expect(result).toBe(10);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/tests/libraries/helpers/codePointIn.spec.ts:
--------------------------------------------------------------------------------
1 | import { codepointIn } from "../../../src/libraries/helpers/codePointIn";
2 | import { Codepoint } from "../../../src/libraries/types/codepoint/unicode";
3 | import { CodepointInterval } from "../../../src/libraries/types/codepoint/codepointInterval";
4 |
5 | describe("codepointIn", () => {
6 | it("should return true when the codepoint is within the interval", () => {
7 | const codepoint: Codepoint = 65; // 'A'
8 | const interval: CodepointInterval = { start: 60, end: 70 };
9 | expect(codepointIn(codepoint, interval)).toBe(true);
10 | });
11 |
12 | it("should return false when the codepoint is less than the interval start", () => {
13 | const codepoint: Codepoint = 50;
14 | const interval: CodepointInterval = { start: 60, end: 70 };
15 | expect(codepointIn(codepoint, interval)).toBe(false);
16 | });
17 |
18 | it("should return false when the codepoint is greater than the interval end", () => {
19 | const codepoint: Codepoint = 80;
20 | const interval: CodepointInterval = { start: 60, end: 70 };
21 | expect(codepointIn(codepoint, interval)).toBe(false);
22 | });
23 |
24 | it("should return true when the codepoint is equal to the interval start", () => {
25 | const codepoint: Codepoint = 60;
26 | const interval: CodepointInterval = { start: 60, end: 70 };
27 | expect(codepointIn(codepoint, interval)).toBe(true);
28 | });
29 |
30 | it("should return true when the codepoint is equal to the interval end", () => {
31 | const codepoint: Codepoint = 70;
32 | const interval: CodepointInterval = { start: 60, end: 70 };
33 | expect(codepointIn(codepoint, interval)).toBe(true);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/tests/libraries/helpers/hexadecimal.characters.spec.ts:
--------------------------------------------------------------------------------
1 | import {toHexadecimal} from "../../../src/libraries/helpers/toHexadecimal";
2 |
3 | test(
4 | "character `b` is `0062`",
5 | () => {
6 | expect(toHexadecimal({
7 | codepoint: "b",
8 | })).toBe("0062")
9 | }
10 | )
11 |
--------------------------------------------------------------------------------
/tests/libraries/helpers/intervalWithin.spec.ts:
--------------------------------------------------------------------------------
1 | import { intervalWithin } from "../../../src/libraries/helpers/intervalWithin";
2 | import { CodepointInterval } from "../../../src/libraries/types/codepoint/codepointInterval";
3 |
4 | describe("intervalWithin", () => {
5 | it("should return true when the inner interval is completely within the outer interval", () => {
6 | const outer: CodepointInterval = { start: 10, end: 20 };
7 | const inner: CodepointInterval = { start: 12, end: 18 };
8 | expect(intervalWithin(outer, inner)).toBe(true);
9 | });
10 |
11 | it("should return false when the inner interval starts before the outer interval", () => {
12 | const outer: CodepointInterval = { start: 10, end: 20 };
13 | const inner: CodepointInterval = { start: 8, end: 18 };
14 | expect(intervalWithin(outer, inner)).toBe(false);
15 | });
16 |
17 | it("should return false when the inner interval ends after the outer interval", () => {
18 | const outer: CodepointInterval = { start: 10, end: 20 };
19 | const inner: CodepointInterval = { start: 12, end: 22 };
20 | expect(intervalWithin(outer, inner)).toBe(false);
21 | });
22 |
23 | it("should return true when the inner interval is exactly the same as the outer interval", () => {
24 | const outer: CodepointInterval = { start: 10, end: 20 };
25 | const inner: CodepointInterval = { start: 10, end: 20 };
26 | expect(intervalWithin(outer, inner)).toBe(true);
27 | });
28 |
29 | it("should return false when the inner interval is completely outside the outer interval", () => {
30 | const outer: CodepointInterval = { start: 10, end: 20 };
31 | const inner: CodepointInterval = { start: 21, end: 30 };
32 | expect(intervalWithin(outer, inner)).toBe(false);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/tests/libraries/helpers/intervalsEqual.spec.ts:
--------------------------------------------------------------------------------
1 | import {intervalsEqual} from "../../../src/libraries/helpers/intervalsEqual";
2 | import {CodepointInterval} from "../../../src/libraries/types/codepoint/codepointInterval";
3 |
4 | describe("intervalsEqual", () => {
5 | it("should return true for intervals with the same start and end", () => {
6 | const interval1: CodepointInterval = { start: 10, end: 20 };
7 | const interval2: CodepointInterval = { start: 10, end: 20 };
8 |
9 | expect(intervalsEqual(interval1, interval2)).toBe(true);
10 | });
11 |
12 | it("should return false for intervals with different start values", () => {
13 | const interval1: CodepointInterval = { start: 10, end: 20 };
14 | const interval2: CodepointInterval = { start: 15, end: 20 };
15 |
16 | expect(intervalsEqual(interval1, interval2)).toBe(false);
17 | });
18 |
19 | it("should return false for intervals with different end values", () => {
20 | const interval1: CodepointInterval = { start: 10, end: 20 };
21 | const interval2: CodepointInterval = { start: 10, end: 25 };
22 |
23 | expect(intervalsEqual(interval1, interval2)).toBe(false);
24 | });
25 |
26 | it("should return false for completely different intervals", () => {
27 | const interval1: CodepointInterval = { start: 5, end: 15 };
28 | const interval2: CodepointInterval = { start: 10, end: 20 };
29 |
30 | expect(intervalsEqual(interval1, interval2)).toBe(false);
31 | });
32 |
33 | it("should return true for intervals with identical start and end values even if they are zero", () => {
34 | const interval1: CodepointInterval = { start: 0, end: 0 };
35 | const interval2: CodepointInterval = { start: 0, end: 0 };
36 |
37 | expect(intervalsEqual(interval1, interval2)).toBe(true);
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/tests/libraries/helpers/mergeIntervals.spec.ts:
--------------------------------------------------------------------------------
1 | import {CodepointInterval} from "../../../src/libraries/types/codepoint/codepointInterval";
2 | import {mergeIntervals} from "../../../src/libraries/helpers/mergeIntervals";
3 |
4 | describe("mergeIntervals", () => {
5 | it("should merge overlapping intervals", () => {
6 | const input: CodepointInterval[] = [
7 | { start: 1, end: 3 },
8 | { start: 2, end: 6 },
9 | { start: 8, end: 10 },
10 | { start: 15, end: 18 },
11 | ];
12 | const expected: CodepointInterval[] = [
13 | { start: 1, end: 6 },
14 | { start: 8, end: 10 },
15 | { start: 15, end: 18 },
16 | ];
17 | expect(mergeIntervals(input)).toEqual(expected);
18 | });
19 |
20 | it("should handle non-overlapping intervals", () => {
21 | const input: CodepointInterval[] = [
22 | { start: 1, end: 2 },
23 | { start: 3, end: 4 },
24 | { start: 5, end: 6 },
25 | ];
26 | const expected: CodepointInterval[] = [
27 | { start: 1, end: 2 },
28 | { start: 3, end: 4 },
29 | { start: 5, end: 6 },
30 | ];
31 | expect(mergeIntervals(input)).toEqual(expected);
32 | });
33 |
34 | it("should handle a single interval", () => {
35 | const input: CodepointInterval[] = [{ start: 1, end: 5 }];
36 | const expected: CodepointInterval[] = [{ start: 1, end: 5 }];
37 | expect(mergeIntervals(input)).toEqual(expected);
38 | });
39 |
40 | it("should handle an empty array", () => {
41 | const input: CodepointInterval[] = [];
42 | const expected: CodepointInterval[] = [];
43 | expect(mergeIntervals(input)).toEqual(expected);
44 | });
45 |
46 | it("should handle intervals that are already merged", () => {
47 | const input: CodepointInterval[] = [
48 | { start: 1, end: 5 },
49 | { start: 6, end: 10 },
50 | ];
51 | const expected: CodepointInterval[] = [
52 | { start: 1, end: 5 },
53 | { start: 6, end: 10 },
54 | ];
55 | expect(mergeIntervals(input)).toEqual(expected);
56 | });
57 |
58 | it("should handle intervals with the same start and end", () => {
59 | const input: CodepointInterval[] = [
60 | { start: 1, end: 3 },
61 | { start: 1, end: 3 },
62 | ];
63 | const expected: CodepointInterval[] = [{ start: 1, end: 3 }];
64 | expect(mergeIntervals(input)).toEqual(expected);
65 | });
66 |
67 | it("should handle unsorted intervals", () => {
68 | const input: CodepointInterval[] = [
69 | { start: 8, end: 10 },
70 | { start: 1, end: 3 },
71 | { start: 2, end: 6 },
72 | { start: 15, end: 18 },
73 | ];
74 | const expected: CodepointInterval[] = [
75 | { start: 1, end: 6 },
76 | { start: 8, end: 10 },
77 | { start: 15, end: 18 },
78 | ];
79 | expect(mergeIntervals(input)).toEqual(expected);
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "inlineSourceMap": true,
5 | "inlineSources": true,
6 | "module": "ESNext",
7 | "target": "ES6",
8 | "allowJs": true,
9 | "moduleResolution": "node",
10 | "importHelpers": true,
11 | "isolatedModules": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "strict": true,
14 | "noImplicitOverride": true,
15 | "noPropertyAccessFromIndexSignature": true,
16 | "noImplicitReturns": true,
17 | "strictNullChecks": true,
18 | "strictBindCallApply": true,
19 | "strictPropertyInitialization": true,
20 | "strictFunctionTypes": true,
21 | "noImplicitAny": true,
22 | "noImplicitThis": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "lib": [
25 | "DOM",
26 | "ES5",
27 | "ES6",
28 | "ES7"
29 | ]
30 | },
31 | "include": [
32 | "**/*.ts"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------