Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cdc18e566 | ||
|
|
3bbdb0312d | ||
|
|
5b6fb8f490 | ||
|
|
83dc2585ed | ||
|
|
7e10c106d4 | ||
|
|
bf7823e985 | ||
|
|
6c29336435 | ||
|
|
083c1f5acc | ||
|
|
bdd86ab04d | ||
|
|
bafce3ca47 | ||
|
|
1729d5c4f7 | ||
|
|
d0dfcc031a | ||
|
|
ab2a4bedb3 | ||
|
|
6e7ebdae0a | ||
|
|
bd12dd933f | ||
|
|
784f7019ce | ||
|
|
6b34f3876f | ||
|
|
59938c08d0 | ||
|
|
9c6215139b | ||
|
|
cfda834141 | ||
|
|
b4175e700b | ||
|
|
b6c03233ba | ||
|
|
385f9d1c4f | ||
|
|
f019960d34 | ||
|
|
1d85f3e284 | ||
|
|
e072832216 | ||
|
|
a374ca6c11 | ||
|
|
8750b5dcac | ||
|
|
456bc59240 | ||
|
|
c3e661439f | ||
|
|
ea2ff76fab | ||
|
|
8b38ade1b9 | ||
|
|
944ecff9ce | ||
|
|
a9273fb74d | ||
|
|
cf30c94faa | ||
|
|
828c08df0c | ||
|
|
3dbfd29274 | ||
|
|
86527e35b4 | ||
|
|
1bc609893b | ||
|
|
a536d58cf2 | ||
|
|
4c921da6cb | ||
|
|
98bf626ec1 | ||
|
|
57f80f0991 | ||
|
|
31f8fe4517 | ||
|
|
292c99ae17 | ||
|
|
86b63047c2 | ||
|
|
5e4b4fae04 | ||
|
|
7a73a5b1b3 | ||
|
|
47635d5aea | ||
|
|
4415e22744 | ||
|
|
939569d076 | ||
|
|
c8820c89d1 | ||
|
|
d97900de4e | ||
|
|
8c54defc6c | ||
|
|
74565b442f |
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"test": {
|
||||||
|
"NODE_OPTIONS": "--experimental-vm-modules"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,6 @@
|
|||||||
|
|
||||||
**ComfyUI-Autocomplete-Plus** is a custom node that provides multiple input assistance features for any text area in [ComfyUI](https://github.com/comfyanonymous/ComfyUI). Currently, it supports Danbooru and e621 tags (e621 does not support some functions).
|
**ComfyUI-Autocomplete-Plus** is a custom node that provides multiple input assistance features for any text area in [ComfyUI](https://github.com/comfyanonymous/ComfyUI). Currently, it supports Danbooru and e621 tags (e621 does not support some functions).
|
||||||
|
|
||||||
## Recent Updates :fire:
|
|
||||||
- Added automatic and manual update check functionality for CSV files
|
|
||||||
- Fixed bug where autocomplete suggestions wouldn't display at certain timings with Microsoft IME
|
|
||||||
- Support for loading and displaying e621 tag CSV
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **:zap:No setup required**: Automatically downloads CSV data optimized for Danbooru tags.
|
- **:zap:No setup required**: Automatically downloads CSV data optimized for Danbooru tags.
|
||||||
@@ -24,15 +19,19 @@
|
|||||||
- Natural tag insertion that considers cursor position and existing text.
|
- Natural tag insertion that considers cursor position and existing text.
|
||||||
- **:art:Design**: Supports both light and dark themes of ComfyUI.
|
- **:art:Design**: Supports both light and dark themes of ComfyUI.
|
||||||
- **:pencil:User CSV**: Allows users to add their own CSV files for autocomplete suggestions.
|
- **:pencil:User CSV**: Allows users to add their own CSV files for autocomplete suggestions.
|
||||||
- **Zero Dependencies**: All input assistance processing works in the browser without external libraries.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
### ComfyUI-Manager
|
||||||
|
|
||||||
|
1. Search for `Autocomplete-Plus` in [ComfyUI-Manager](https://github.com/Comfy-Org/ComfyUI-Manager), install the custom node that appears, and restart.
|
||||||
|
2. The necessary CSV data will be automatically downloaded from HuggingFace upon startup.
|
||||||
|
|
||||||
### Manual
|
### Manual
|
||||||
|
|
||||||
1. Clone or copy this repository into the `custom_nodes` folder of ComfyUI.
|
1. Clone or copy this repository into the `custom_nodes` folder of ComfyUI.
|
||||||
`git clone https://github.com/newtextdoc1111/ComfyUI-Autocomplete-Plus.git`
|
`git clone https://github.com/newtextdoc1111/ComfyUI-Autocomplete-Plus.git`
|
||||||
2. Launch ComfyUI. The necessary CSV data will be automatically downloaded from HuggingFace on the first launch only.
|
2. Launch ComfyUI. The necessary CSV data will be automatically downloaded from HuggingFace upon startup.
|
||||||
|
|
||||||
## Autocomplete
|
## Autocomplete
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ When you type in a text input area, tags that partially match the text are displ
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
When you select any tag in a text input area, highly related tags list are displayed. The UI's position and size are automatically adjusted based on the text area being edited.
|
When you select any tag in a text input area, a list of highly related tags is displayed. You can insert a tag by clicking it or by selecting it with the up/down arrow keys and then pressing Enter or Tab. The UI's position and size are automatically adjusted based on the text area being edited.
|
||||||
|
|
||||||
- The display position is primarily at the bottom of the text area and automatically adjusts vertically based on available space.
|
- The display position is primarily at the bottom of the text area and automatically adjusts vertically based on available space.
|
||||||
- You can switch between vertical and horizontal display positions using the "↕️|↔️" button in the header.
|
- You can switch between vertical and horizontal display positions using the "↕️|↔️" button in the header.
|
||||||
@@ -87,7 +86,12 @@ Also, displaying related tags is not supported.
|
|||||||
|
|
||||||
### User CSV
|
### User CSV
|
||||||
|
|
||||||
Users can also use their own CSV files. For example, frequently used meta tags can be added to the autocomplete suggestions by placing a file named `danbooru_tags_meta.csv` in the `data` folder.
|
Users can also use their own CSV files. CSV files should be placed in the `data` folder according to the following naming convention:
|
||||||
|
|
||||||
|
- **CSV for Autocomplete**: `<danbooru | e621>_tags*.csv`
|
||||||
|
- **CSV for Related Tags**: `<danbooru | e621>_tags_cooccurrence*.csv`
|
||||||
|
|
||||||
|
For example, you can add frequently used meta tags to the autocomplete suggestions by placing a file named `danbooru_tags_meta.csv` in the `data` folder.
|
||||||
A header row is not required. A browser reload is necessary to apply the changes.
|
A header row is not required. A browser reload is necessary to apply the changes.
|
||||||
|
|
||||||
**Example of meta tags:**
|
**Example of meta tags:**
|
||||||
@@ -101,6 +105,15 @@ low_quality,5,9999999,
|
|||||||
worst_quality,5,9999999,
|
worst_quality,5,9999999,
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When the browser is reloaded, you can check the list of loaded CSV files in the ComfyUI console log. If a file is not included in the log output, please verify that the file name follows the naming convention.
|
||||||
|
|
||||||
|
**Example of ComfyUI console log output:**
|
||||||
|
```
|
||||||
|
[Autocomplete-Plus] CSV file status:
|
||||||
|
* Danbooru -> base: True, extra: danbooru_tags_meta.csv // If displayed here, meta tags can be autocompleted
|
||||||
|
* E621 -> base: False, extra:
|
||||||
|
```
|
||||||
|
|
||||||
>[!NOTE]
|
>[!NOTE]
|
||||||
> If there are multiple user CSV files, they are loaded in alphabetical order. If the same tag exists in multiple files, the one loaded first is retained. The basic CSV is loaded last.
|
> If there are multiple user CSV files, they are loaded in alphabetical order. If the same tag exists in multiple files, the one loaded first is retained. The basic CSV is loaded last.
|
||||||
|
|
||||||
@@ -119,6 +132,7 @@ worst_quality,5,9999999,
|
|||||||
|
|
||||||
- **Enable Autocomplete**: Enable/disable the autocomplete feature.
|
- **Enable Autocomplete**: Enable/disable the autocomplete feature.
|
||||||
- **Max suggestions**: Maximum number of autocomplete suggestions to display.
|
- **Max suggestions**: Maximum number of autocomplete suggestions to display.
|
||||||
|
- **Use Fast Search**: Switch autocomplete suggestions search to fast processing (see [About Fast Search for Autocomplete](#about-fast-search-for-autocomplete) for details).
|
||||||
|
|
||||||
### Related Tags
|
### Related Tags
|
||||||
|
|
||||||
@@ -131,6 +145,58 @@ worst_quality,5,9999999,
|
|||||||
|
|
||||||
- **Check CSV updates**: Click the "Check Now" button to check if new CSV files are available in HuggingFace and download them if necessary.
|
- **Check CSV updates**: Click the "Check Now" button to check if new CSV files are available in HuggingFace and download them if necessary.
|
||||||
|
|
||||||
|
## Advanced Settings
|
||||||
|
|
||||||
|
### Disabling CSV Update Check on Startup
|
||||||
|
|
||||||
|
By default, ComfyUI performs CSV file update checks and downloads at regular intervals during startup.
|
||||||
|
When starting in an environment without internet access, startup may be delayed until a timeout occurs.
|
||||||
|
|
||||||
|
You can skip the check process during ComfyUI startup by following these steps:
|
||||||
|
|
||||||
|
1. Start ComfyUI once with this custom node installed to generate the `csv_meta.json` file.
|
||||||
|
The `csv_meta.json` file is created directly under this custom node's folder.
|
||||||
|
2. Open `csv_meta.json` in a text editor and change the value of `check_updates_on_startup` from `true` to `false` and save.
|
||||||
|
If `check_updates_on_startup` does not exist, add it under `version`.
|
||||||
|
|
||||||
|
**`csv_meta.json` after modification:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"check_updates_on_startup": false,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Additional notes:**
|
||||||
|
- The check process will not be performed until the value of `check_updates_on_startup` is changed back to `true` or the `version` is switched.
|
||||||
|
- Even when `check_updates_on_startup` is `false`, manual checking is still possible by pressing the `Check CSV updates` button in the Autocomplete Plus settings.
|
||||||
|
|
||||||
|
## More details on how it works
|
||||||
|
|
||||||
|
### About Fast Search for Autocomplete
|
||||||
|
|
||||||
|
In `v1.3.0`, Added a fast search feature functionality to autocomplete. By enabling this function in the settings screen, tag search processing during text input operates faster, improving responsiveness.
|
||||||
|
While fast operation can be expected in any scenario, I particularly recommend **enabling** it in the following use cases:
|
||||||
|
|
||||||
|
- When the loaded CSV files contain a large number of tags or aliases. This is especially useful when the total number of tags exceeds **100,000**.
|
||||||
|
- When using natural language instead of comma-separated tags in prompt input.
|
||||||
|
|
||||||
|
**Browser startup behavior**
|
||||||
|
|
||||||
|
Fast search requires tag index building and is not available immediately after browser startup. The conventional search process is used until building is complete.
|
||||||
|
As of `v1.3.0`, The notification when building is completed is displayed only in the browser's developer tools. This is planned to be improved in future versions.
|
||||||
|
|
||||||
|
For example, when index building for approximately 220,000 tags is completed, the following log is recorded:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Autocomplete-Plus] Building 221787 index for danbooru took 9398.70ms.
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> - Index building occurs in the background even when fast search is disabled in settings
|
||||||
|
> - Fast search uses the full-text search library [nextapps-de/flexsearch](https://github.com/nextapps-de/flexsearch)
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
@@ -145,7 +211,9 @@ worst_quality,5,9999999,
|
|||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- [ComfyUI-Custom-Node](https://github.com/pythongosssss/ComfyUI-Custom-Scripts)
|
- [pythongosssss/ComfyUI-Custom-Node](https://github.com/pythongosssss/ComfyUI-Custom-Scripts)
|
||||||
- Referenced for implementing the autocomplete function.
|
- Referenced for implementing the autocomplete function.
|
||||||
- [DominikDoom/a1111-sd-webui-tagcomplete](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete)
|
- [DominikDoom/a1111-sd-webui-tagcomplete](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete)
|
||||||
- Referenced for autocomplete function and CSV specifications.
|
- Referenced for autocomplete function and CSV specifications.
|
||||||
|
- [nextapps-de/flexsearch](https://github.com/nextapps-de/flexsearch)
|
||||||
|
- Used to implement fast tag search processing for autocomplete.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
from .modules.api import *
|
from .modules.api import *
|
||||||
from .modules import downloader
|
from .modules import downloader
|
||||||
|
|
||||||
# check and download necessary csv files
|
# check and download necessary csv files
|
||||||
|
|||||||
+78
-11
@@ -6,11 +6,6 @@
|
|||||||
|
|
||||||
**ComfyUI-Autocomplete-Plus** は、[ComfyUI](https://github.com/comfyanonymous/ComfyUI) の任意のテキストエリアに複数の入力支援機能を提供するカスタムノードです。現在はDanbooruとe621のタグに対応しています(e621は一部の機能が未対応です)。
|
**ComfyUI-Autocomplete-Plus** は、[ComfyUI](https://github.com/comfyanonymous/ComfyUI) の任意のテキストエリアに複数の入力支援機能を提供するカスタムノードです。現在はDanbooruとe621のタグに対応しています(e621は一部の機能が未対応です)。
|
||||||
|
|
||||||
## 最近の更新 :fire:
|
|
||||||
- CSV ファイルの自動・手動更新チェック機能の追加
|
|
||||||
- Microsoft IMEでオートコンプリート候補が表示されないタイミングがある不具合の修正
|
|
||||||
- e621タグ CSV の読み込みと表示のサポート
|
|
||||||
|
|
||||||
## 特徴
|
## 特徴
|
||||||
|
|
||||||
- **:zap:セットアップ不要**: Danbooruタグに最適化された CSV データを自動でダウンロード
|
- **:zap:セットアップ不要**: Danbooruタグに最適化された CSV データを自動でダウンロード
|
||||||
@@ -22,15 +17,19 @@
|
|||||||
- カーソル位置や既存のテキストを考慮した自然なタグ挿入
|
- カーソル位置や既存のテキストを考慮した自然なタグ挿入
|
||||||
- **:art:デザイン**: ComfyUIのライトテーマとダークテーマの両方に対応
|
- **:art:デザイン**: ComfyUIのライトテーマとダークテーマの両方に対応
|
||||||
- **:pencil:ユーザーCSV**: ユーザーが用意した CSV をオートコンプリート候補に追加可能
|
- **:pencil:ユーザーCSV**: ユーザーが用意した CSV をオートコンプリート候補に追加可能
|
||||||
- **依存ライブラリゼロ**: 外部ライブラリを使用せず、すべての入力支援処理がブラウザで動作
|
|
||||||
|
|
||||||
## インストール
|
## インストール
|
||||||
|
|
||||||
|
### ComfyUI-Manager
|
||||||
|
|
||||||
|
1. [ComfyUI-Manager](https://github.com/Comfy-Org/ComfyUI-Manager) で `Autocomplete-Plus` と検索して表示されたカスタムノードをインストールし、再起動します
|
||||||
|
2. 起動時に必要な CSV データが HuggingFace から自動的にダウンロードされます
|
||||||
|
|
||||||
### マニュアル
|
### マニュアル
|
||||||
|
|
||||||
1. このリポジトリを ComfyUI の `custom_nodes` フォルダにクローンまたはコピーします
|
1. このリポジトリを ComfyUI の `custom_nodes` フォルダにクローンまたはコピーします
|
||||||
`git clone https://github.com/newtextdoc1111/ComfyUI-Autocomplete-Plus.git`
|
`git clone https://github.com/newtextdoc1111/ComfyUI-Autocomplete-Plus.git`
|
||||||
3. ComfyUI を起動します。初回起動時のみ、必要な CSV データが HuggingFace から自動的にダウンロードされます
|
2. ComfyUI を起動します。起動時に必要な CSV データが HuggingFace から自動的にダウンロードされます
|
||||||
|
|
||||||
## オートコンプリート
|
## オートコンプリート
|
||||||
|
|
||||||
@@ -45,7 +44,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
テキスト入力エリアの任意のタグを選択すると、関連性の高いタグを一覧表示します。UIは編集中のテキストエリアを基準に位置とサイズが自動で調整されます。
|
テキスト入力エリアの任意のタグを選択すると、関連性の高いタグを一覧表示します。タグをクリックするか、キーボードの上下キーで選択後にEnterまたはTabキーでタグを挿入出来ます。UIは編集中のテキストエリアを基準に位置とサイズが自動で調整されます。
|
||||||
|
|
||||||
- 表示位置は、テキストエリアの下部を基本とし、空きスペースに応じて上下に自動調整されます
|
- 表示位置は、テキストエリアの下部を基本とし、空きスペースに応じて上下に自動調整されます
|
||||||
- ヘッダーの「↕️|↔️」ボタンで上下と左右の表示位置に切り替えられます
|
- ヘッダーの「↕️|↔️」ボタンで上下と左右の表示位置に切り替えられます
|
||||||
@@ -85,7 +84,12 @@
|
|||||||
|
|
||||||
### ユーザーCSV
|
### ユーザーCSV
|
||||||
|
|
||||||
ユーザーが自身で用意した CSV を使用することも可能です。例として、よく使うメタタグを `danbooru_tags_meta.csv` の名前で `data` フォルダーに配置することでオートコンプリート候補に追加できます。
|
ユーザーが自身で用意した CSV を使用することも可能です。 CSV ファイルは以下の命名規則に従って `data` フォルダーに配置してください。
|
||||||
|
|
||||||
|
- **オートコンプリート用 CSV**: <danbooru | e621>_tags*.csv
|
||||||
|
- **関連タグ用 CSV**: <danbooru | e621>_tags_cooccurrence*.csv
|
||||||
|
|
||||||
|
例として、よく使うメタタグを `danbooru_tags_meta.csv` の名前で `data` フォルダーに配置することでオートコンプリート候補に追加できます。
|
||||||
ヘッダー行はなくても構いません。反映にはブラウザのリロードが必要です。
|
ヘッダー行はなくても構いません。反映にはブラウザのリロードが必要です。
|
||||||
|
|
||||||
**メタタグの例**
|
**メタタグの例**
|
||||||
@@ -99,6 +103,15 @@ low_quality,5,9999999,
|
|||||||
worst_quality,5,9999999,
|
worst_quality,5,9999999,
|
||||||
```
|
```
|
||||||
|
|
||||||
|
ブラウザリロード時、ロードされる CSV ファイル一覧をComfyUIのコンソールのログで確認出来ます。ログ出力に含まれていない場合はファイル名が命名規則に沿っているか確認してください。
|
||||||
|
|
||||||
|
**ComfyUIコンソールログ出力の例:**
|
||||||
|
```
|
||||||
|
[Autocomplete-Plus] CSV file status:
|
||||||
|
* Danbooru -> base: True, extra: danbooru_tags_meta.csv // ここに表示されていればメタタグを入力補完できます
|
||||||
|
* E621 -> base: False, extra:
|
||||||
|
```
|
||||||
|
|
||||||
>[!NOTE]
|
>[!NOTE]
|
||||||
> ユーザー CSV が複数ある場合アルファベット順に読み込まれます。同じタグが複数のファイルに存在する場合は先に読み込まれた方が保持されます。基本 CSV は最後にロードされます。
|
> ユーザー CSV が複数ある場合アルファベット順に読み込まれます。同じタグが複数のファイルに存在する場合は先に読み込まれた方が保持されます。基本 CSV は最後にロードされます。
|
||||||
|
|
||||||
@@ -116,7 +129,8 @@ worst_quality,5,9999999,
|
|||||||
### オートコンプリート
|
### オートコンプリート
|
||||||
|
|
||||||
- **Enable Autocomplete**: オートコンプリート機能の有効化/無効化
|
- **Enable Autocomplete**: オートコンプリート機能の有効化/無効化
|
||||||
- **Max suggestions**: オートコンプリート候補の最大表示件数
|
- **Max Suggestions**: オートコンプリート候補の最大表示件数
|
||||||
|
- **Use Fast Search**: オートコンプリート候補の検索を高速な処理に切り替える(詳細は [オートコンプリートの高速検索について](#オートコンプリートの高速検索について) を確認してください)
|
||||||
|
|
||||||
### 関連タグ
|
### 関連タグ
|
||||||
|
|
||||||
@@ -129,6 +143,57 @@ worst_quality,5,9999999,
|
|||||||
|
|
||||||
- **Check CSV updates**: 「Check Now」ボタンを押すと新しい CSV ファイルがHuggingFaceにあるか確認し、必要に応じてダウンロードを行います
|
- **Check CSV updates**: 「Check Now」ボタンを押すと新しい CSV ファイルがHuggingFaceにあるか確認し、必要に応じてダウンロードを行います
|
||||||
|
|
||||||
|
## 上級者向け設定
|
||||||
|
|
||||||
|
### 起動時のCSV更新チェックを無効化する
|
||||||
|
|
||||||
|
デフォルトの動作では、ComfyUI起動時に一定の間隔で CSV ファイルの更新チェックとダウンロード行います。
|
||||||
|
インターネットにアクセス出来ない環境で起動した場合、タイムアウトが発生するまで起動が遅延する事があります。
|
||||||
|
|
||||||
|
以下の手順を行う事により、ComfyUI起動時のチェック処理をスキップする事が出来ます。
|
||||||
|
|
||||||
|
1. このカスタムノードをインストールした状態でComfyUIを一度起動し、 `csv_meta.json` ファイルを生成する
|
||||||
|
`csv_meta.json` はこのカスタムノードのフォルダー直下に作成されます
|
||||||
|
2. `csv_meat.json` をテキストエディターで開き、`check_updates_on_startup` の値を `true` -> `false` に変更し保存する
|
||||||
|
`check_updates_on_startup` が存在しない場合、 `version` の下に追記してください
|
||||||
|
|
||||||
|
**変更後の `csv_meta.json`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"check_updates_on_startup": false,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**補足事項:**
|
||||||
|
- `check_updates_on_startup` の値を再び `true` にするか、 `version` が切り替わるまでチェック処理は行われなくなります
|
||||||
|
- `check_updates_on_startup` が `false` でも、Autocompelte Plusの設定から `Check CSV updates` のボタンを押す事で手動チェックが可能です
|
||||||
|
|
||||||
|
## 動作に関する詳細
|
||||||
|
|
||||||
|
## オートコンプリートの高速検索について
|
||||||
|
|
||||||
|
`v1.3.0` にて、オートコンプリートにタグの検索を高速化する機能を追加しました。設定画面から有効にする事でテキスト入力時のタグ検索処理が高速に動作し、応答性が改善されます。
|
||||||
|
どのような場面でも高速な動作が期待できますが、特に以下のユースケースで **有効** にすることをお勧めします。
|
||||||
|
|
||||||
|
- 読み込む CSV ファイルに大量のタグやエイリアスが含まれている場合。タグの合計が **10万件** を超える場合は特に有用です
|
||||||
|
- プロンプト入力でタグのカンマ区切りの代わりに自然言語を使う場合
|
||||||
|
|
||||||
|
**ブラウザ起動時の動作**
|
||||||
|
|
||||||
|
高速検索はタグのインデックス構築が必要なため、ブラウザ起動直後は利用できません。構築が完了するまでは従来の検索処理で動作します。
|
||||||
|
`v1.3.0` 時点では構築が完了したタイミングはブラウザーの開発ツールにのみが表示されます。これは将来のバージョンで改善予定です。
|
||||||
|
|
||||||
|
例として、約22万件のタグのインデックス構築が完了した際は以下のようなログが記録されます。
|
||||||
|
```
|
||||||
|
[Autocomplete-Plus] Building 221787 index for danbooru took 9398.70ms.
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> - インデックス構築は設定から高速検索を無効にしてもバックグラウンドで行われます
|
||||||
|
> - 高速検索の実現に全文検索ライブラリの [nextapps-de/flexsearch](https://github.com/nextapps-de/flexsearch) を使用しています
|
||||||
|
|
||||||
## 既知の問題
|
## 既知の問題
|
||||||
|
|
||||||
### パフォーマンス
|
### パフォーマンス
|
||||||
@@ -143,7 +208,9 @@ worst_quality,5,9999999,
|
|||||||
|
|
||||||
## クレジット
|
## クレジット
|
||||||
|
|
||||||
- [ComfyUI-Custom-Node](https://github.com/pythongosssss/ComfyUI-Custom-Scripts)
|
- [pythongosssss/ComfyUI-Custom-Node](https://github.com/pythongosssss/ComfyUI-Custom-Scripts)
|
||||||
- オートコンプリート機能の実装にあたり参考にしました
|
- オートコンプリート機能の実装にあたり参考にしました
|
||||||
- [DominikDoom/a1111-sd-webui-tagcomplete](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete)
|
- [DominikDoom/a1111-sd-webui-tagcomplete](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete)
|
||||||
- オートコンプリートの主要な機能や CSV の仕様で参考にしました
|
- オートコンプリートの主要な機能や CSV の仕様で参考にしました
|
||||||
|
- [nextapps-de/flexsearch](https://github.com/nextapps-de/flexsearch)
|
||||||
|
- オートコンプリートの高速なタグ検索処理の実装に利用させてもらいました
|
||||||
|
|||||||
+89
-81
@@ -10,11 +10,12 @@ from . import downloader as dl
|
|||||||
# os.path.join(..., '..', 'data') goes up one level and then into 'data'
|
# os.path.join(..., '..', 'data') goes up one level and then into 'data'
|
||||||
DATA_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "data"))
|
DATA_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "data"))
|
||||||
|
|
||||||
DANBOORU_PREFIX = 'danbooru'
|
DANBOORU_PREFIX = "danbooru"
|
||||||
E621_PREFIX = 'e621'
|
E621_PREFIX = "e621"
|
||||||
|
|
||||||
|
TAGS_SUFFIX = "tags"
|
||||||
|
COOCCURRENCE_SUFFIX = "tags_cooccurrence"
|
||||||
|
|
||||||
TAGS_SUFFIX = 'tags'
|
|
||||||
COOCCURRENCE_SUFFIX = 'tags_cooccurrence'
|
|
||||||
|
|
||||||
def get_csv_file_status():
|
def get_csv_file_status():
|
||||||
"""
|
"""
|
||||||
@@ -23,17 +24,17 @@ def get_csv_file_status():
|
|||||||
|
|
||||||
data = {
|
data = {
|
||||||
DANBOORU_PREFIX: {
|
DANBOORU_PREFIX: {
|
||||||
'base_tags': False,
|
"base_tags": False,
|
||||||
'extra_tags': [],
|
"extra_tags": [],
|
||||||
'base_cooccurrence': False,
|
"base_cooccurrence": False,
|
||||||
'extra_cooccurrence': [],
|
"extra_cooccurrence": [],
|
||||||
},
|
},
|
||||||
E621_PREFIX: {
|
E621_PREFIX: {
|
||||||
'base_tags': False,
|
"base_tags": False,
|
||||||
'extra_tags': [],
|
"extra_tags": [],
|
||||||
'base_cooccurrence': False,
|
"base_cooccurrence": False,
|
||||||
'extra_cooccurrence': [],
|
"extra_cooccurrence": [],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for prefix in [DANBOORU_PREFIX, E621_PREFIX]:
|
for prefix in [DANBOORU_PREFIX, E621_PREFIX]:
|
||||||
@@ -46,27 +47,28 @@ def get_csv_file_status():
|
|||||||
tags_extra_files = []
|
tags_extra_files = []
|
||||||
cooccurrence_extra_files = []
|
cooccurrence_extra_files = []
|
||||||
|
|
||||||
all_csv_files = [f for f in os.listdir(DATA_DIR) if f.startswith(prefix) and f.endswith('.csv')]
|
all_csv_files = [f for f in os.listdir(DATA_DIR) if f.startswith(prefix) and f.endswith(".csv")]
|
||||||
|
|
||||||
# Create extra CSV files list
|
# Create extra CSV files list
|
||||||
for filename in all_csv_files:
|
for filename in all_csv_files:
|
||||||
if filename in [base_tags_file, base_cooccurrence_file]:
|
if filename in [base_tags_file, base_cooccurrence_file]:
|
||||||
continue # Skip base files
|
continue # Skip base files
|
||||||
if COOCCURRENCE_SUFFIX in filename.lower():
|
if COOCCURRENCE_SUFFIX in filename.lower():
|
||||||
cooccurrence_extra_files.append(filename)
|
cooccurrence_extra_files.append(filename)
|
||||||
elif TAGS_SUFFIX in filename.lower():
|
elif TAGS_SUFFIX in filename.lower():
|
||||||
tags_extra_files.append(filename)
|
tags_extra_files.append(filename)
|
||||||
|
|
||||||
data[prefix] = {
|
data[prefix] = {
|
||||||
'base_tags': tags_base_exists,
|
"base_tags": tags_base_exists,
|
||||||
'extra_tags': tags_extra_files,
|
"extra_tags": tags_extra_files,
|
||||||
'base_cooccurrence': cooccurrence_base_exists,
|
"base_cooccurrence": cooccurrence_base_exists,
|
||||||
'extra_cooccurrence': cooccurrence_extra_files,
|
"extra_cooccurrence": cooccurrence_extra_files,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Return the lists of extra files
|
# Return the lists of extra files
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def get_last_check_time_from_metadata():
|
def get_last_check_time_from_metadata():
|
||||||
"""
|
"""
|
||||||
Helper function to get the last remote check timestamp from csv_meta.json.
|
Helper function to get the last remote check timestamp from csv_meta.json.
|
||||||
@@ -75,23 +77,25 @@ def get_last_check_time_from_metadata():
|
|||||||
try:
|
try:
|
||||||
if not os.path.exists(dl.CSV_META_FILE):
|
if not os.path.exists(dl.CSV_META_FILE):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with open(dl.CSV_META_FILE, 'r', encoding='utf-8') as f:
|
with open(dl.CSV_META_FILE, "r", encoding="utf-8") as f:
|
||||||
metadata = json.load(f)
|
metadata = json.load(f)
|
||||||
|
|
||||||
datasets = metadata.get("hf_datasets", [])
|
datasets = metadata.get("hf_datasets", [])
|
||||||
if datasets and len(datasets) > 0:
|
if datasets and len(datasets) > 0:
|
||||||
return datasets[0].get("last_remote_check_timestamp")
|
return datasets[0].get("last_remote_check_timestamp")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except (IOError, json.JSONDecodeError) as e:
|
except (IOError, json.JSONDecodeError) as e:
|
||||||
print(f"[Autocomplete-Plus] Error reading csv_meta.json: {e}")
|
print(f"[Autocomplete-Plus] Error reading csv_meta.json: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# --- API Endpoints ---
|
# --- API Endpoints ---
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv')
|
|
||||||
|
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv")
|
||||||
async def get_csv_list(_request):
|
async def get_csv_list(_request):
|
||||||
"""
|
"""
|
||||||
Returns CSV file status.
|
Returns CSV file status.
|
||||||
@@ -102,37 +106,48 @@ async def get_csv_list(_request):
|
|||||||
|
|
||||||
response = {
|
response = {
|
||||||
DANBOORU_PREFIX: {
|
DANBOORU_PREFIX: {
|
||||||
'base_tags': csv_file_status[DANBOORU_PREFIX]['base_tags'],
|
"base_tags": csv_file_status[DANBOORU_PREFIX]["base_tags"],
|
||||||
'extra_tags': len(csv_file_status[DANBOORU_PREFIX]['extra_tags']),
|
"extra_tags": csv_file_status[DANBOORU_PREFIX]["extra_tags"],
|
||||||
'base_cooccurrence': csv_file_status[DANBOORU_PREFIX]['base_cooccurrence'],
|
"base_cooccurrence": csv_file_status[DANBOORU_PREFIX]["base_cooccurrence"],
|
||||||
'extra_cooccurrence': len(csv_file_status[DANBOORU_PREFIX]['extra_cooccurrence']),
|
"extra_cooccurrence": csv_file_status[DANBOORU_PREFIX]["extra_cooccurrence"],
|
||||||
},
|
},
|
||||||
E621_PREFIX: {
|
E621_PREFIX: {
|
||||||
'base_tags': csv_file_status[E621_PREFIX]['base_tags'],
|
"base_tags": csv_file_status[E621_PREFIX]["base_tags"],
|
||||||
'extra_tags': len(csv_file_status[E621_PREFIX]['extra_tags']),
|
"extra_tags": csv_file_status[E621_PREFIX]["extra_tags"],
|
||||||
'base_cooccurrence': csv_file_status[E621_PREFIX]['base_cooccurrence'],
|
"base_cooccurrence": csv_file_status[E621_PREFIX]["base_cooccurrence"],
|
||||||
'extra_cooccurrence': len(csv_file_status[E621_PREFIX]['extra_cooccurrence']),
|
"extra_cooccurrence": csv_file_status[E621_PREFIX]["extra_cooccurrence"],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Print csv file status to the console for debugging
|
||||||
|
print(f"""[Autocomplete-Plus] CSV file status:
|
||||||
|
* Danbooru -> base: {response[DANBOORU_PREFIX]["base_tags"]}, extra: [{", ".join(response[DANBOORU_PREFIX]["extra_tags"])}]
|
||||||
|
* E621 -> base: {response[E621_PREFIX]["base_tags"]}, extra: [{", ".join(response[E621_PREFIX]["extra_tags"])}]""")
|
||||||
|
|
||||||
return web.json_response(response)
|
return web.json_response(response)
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv/{source}/{suffix}/base')
|
|
||||||
|
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv/{source}/{suffix}/base")
|
||||||
async def get_base_tags_file(request):
|
async def get_base_tags_file(request):
|
||||||
"""
|
"""
|
||||||
Returns the base tags CSV file.
|
Returns the base tags CSV file.
|
||||||
"""
|
"""
|
||||||
source = str(request.match_info['source'])
|
source = str(request.match_info["source"])
|
||||||
suffix = str(request.match_info['suffix'])
|
suffix = str(request.match_info["suffix"])
|
||||||
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [TAGS_SUFFIX, COOCCURRENCE_SUFFIX]:
|
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [
|
||||||
|
TAGS_SUFFIX,
|
||||||
|
COOCCURRENCE_SUFFIX,
|
||||||
|
]:
|
||||||
return web.json_response({"error": "Invalid tag source or suffix"}, status=400)
|
return web.json_response({"error": "Invalid tag source or suffix"}, status=400)
|
||||||
|
|
||||||
file_path = os.path.join(DATA_DIR, f"{source}_{suffix}.csv")
|
file_path = os.path.join(DATA_DIR, f"{source}_{suffix}.csv")
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
return web.json_response({"error": "Base tags file not found"}, status=404)
|
return web.json_response({"error": "Base tags file not found"}, status=404)
|
||||||
|
|
||||||
return web.FileResponse(file_path)
|
return web.FileResponse(file_path)
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv/{source}/{suffix}/extra/{index}')
|
|
||||||
|
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv/{source}/{suffix}/extra/{index}")
|
||||||
async def get_extra_tags_file(request):
|
async def get_extra_tags_file(request):
|
||||||
"""
|
"""
|
||||||
Returns the extra tags CSV file at the specified index.
|
Returns the extra tags CSV file at the specified index.
|
||||||
@@ -140,16 +155,19 @@ async def get_extra_tags_file(request):
|
|||||||
try:
|
try:
|
||||||
csv_file_status = get_csv_file_status()
|
csv_file_status = get_csv_file_status()
|
||||||
|
|
||||||
source = str(request.match_info['source'])
|
source = str(request.match_info["source"])
|
||||||
suffix = str(request.match_info['suffix'])
|
suffix = str(request.match_info["suffix"])
|
||||||
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [TAGS_SUFFIX, COOCCURRENCE_SUFFIX]:
|
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [
|
||||||
|
TAGS_SUFFIX,
|
||||||
|
COOCCURRENCE_SUFFIX,
|
||||||
|
]:
|
||||||
return web.json_response({"error": "Invalid tag source or suffix"}, status=400)
|
return web.json_response({"error": "Invalid tag source or suffix"}, status=400)
|
||||||
|
|
||||||
index = int(request.match_info['index'])
|
index = int(request.match_info["index"])
|
||||||
if index < 0 or index >= len(csv_file_status[source][f'extra_{suffix}']):
|
if index < 0 or index >= len(csv_file_status[source][f"extra_{suffix}"]):
|
||||||
return web.json_response({"error": "Invalid index"}, status=404)
|
return web.json_response({"error": "Invalid index"}, status=404)
|
||||||
|
|
||||||
file_path = os.path.join(DATA_DIR, csv_file_status[source][f'extra_{suffix}'][index])
|
file_path = os.path.join(DATA_DIR, csv_file_status[source][f"extra_{suffix}"][index])
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
return web.json_response({"error": "Extra tags file not found"}, status=404)
|
return web.json_response({"error": "Extra tags file not found"}, status=404)
|
||||||
|
|
||||||
@@ -158,7 +176,8 @@ async def get_extra_tags_file(request):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return web.json_response({"error": "Invalid index format"}, status=400)
|
return web.json_response({"error": "Invalid index format"}, status=400)
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.post('/autocomplete-plus/csv/force-check-updates')
|
|
||||||
|
@server.PromptServer.instance.routes.post("/autocomplete-plus/csv/force-check-updates")
|
||||||
async def force_check_csv_updates(request):
|
async def force_check_csv_updates(request):
|
||||||
"""
|
"""
|
||||||
Forces a check for CSV file updates from HuggingFace, ignoring cooldown.
|
Forces a check for CSV file updates from HuggingFace, ignoring cooldown.
|
||||||
@@ -166,29 +185,29 @@ async def force_check_csv_updates(request):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
print("[Autocomplete-Plus] Starting forced check for CSV updates from HuggingFace...")
|
print("[Autocomplete-Plus] Starting forced check for CSV updates from HuggingFace...")
|
||||||
|
|
||||||
downloader = dl.Downloader()
|
downloader = dl.Downloader()
|
||||||
downloader.run_check_and_download(force_check=True)
|
downloader.run_check_and_download(force_check=True)
|
||||||
|
|
||||||
print("[Autocomplete-Plus] Forced check completed successfully.")
|
print("[Autocomplete-Plus] Forced check completed successfully.")
|
||||||
|
|
||||||
# Get the updated last check time
|
# Get the updated last check time
|
||||||
last_check_time = get_last_check_time_from_metadata()
|
last_check_time = get_last_check_time_from_metadata()
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response(
|
||||||
"success": True,
|
{
|
||||||
"message": "Force check completed successfully",
|
"success": True,
|
||||||
"last_check_time": last_check_time
|
"message": "Force check completed successfully",
|
||||||
})
|
"last_check_time": last_check_time,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[Autocomplete-Plus] Error during forced check: {e}")
|
print(f"[Autocomplete-Plus] Error during forced check: {e}")
|
||||||
return web.json_response({
|
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||||
"success": False,
|
|
||||||
"error": str(e)
|
|
||||||
}, status=500)
|
|
||||||
|
|
||||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv/last-check-time')
|
|
||||||
|
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv/last-check-time")
|
||||||
async def get_last_check_time(_request):
|
async def get_last_check_time(_request):
|
||||||
"""
|
"""
|
||||||
Returns the last remote check timestamp from csv_meta.json.
|
Returns the last remote check timestamp from csv_meta.json.
|
||||||
@@ -196,26 +215,15 @@ async def get_last_check_time(_request):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(dl.CSV_META_FILE):
|
if not os.path.exists(dl.CSV_META_FILE):
|
||||||
return web.json_response({
|
return web.json_response({"last_check_time": None, "message": "csv_meta.json file not found"})
|
||||||
"last_check_time": None,
|
|
||||||
"message": "csv_meta.json file not found"
|
|
||||||
})
|
|
||||||
|
|
||||||
last_check_time = get_last_check_time_from_metadata()
|
last_check_time = get_last_check_time_from_metadata()
|
||||||
|
|
||||||
if last_check_time is not None:
|
if last_check_time is not None:
|
||||||
return web.json_response({
|
return web.json_response({"last_check_time": last_check_time})
|
||||||
"last_check_time": last_check_time
|
|
||||||
})
|
|
||||||
else:
|
else:
|
||||||
return web.json_response({
|
return web.json_response({"last_check_time": None, "message": "No datasets found in metadata"})
|
||||||
"last_check_time": None,
|
|
||||||
"message": "No datasets found in metadata"
|
|
||||||
})
|
|
||||||
|
|
||||||
except (IOError, json.JSONDecodeError) as e:
|
except (IOError, json.JSONDecodeError) as e:
|
||||||
print(f"[Autocomplete-Plus] Error reading csv_meta.json: {e}")
|
print(f"[Autocomplete-Plus] Error reading csv_meta.json: {e}")
|
||||||
return web.json_response({
|
return web.json_response({"last_check_time": None, "error": str(e)}, status=500)
|
||||||
"last_check_time": None,
|
|
||||||
"error": str(e)
|
|
||||||
}, status=500)
|
|
||||||
|
|||||||
+54
-26
@@ -20,7 +20,8 @@ CSV_META_FILE = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", C
|
|||||||
|
|
||||||
DEFAULT_CSV_METADATA = {
|
DEFAULT_CSV_METADATA = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"hf_datasets" : [
|
"check_updates_on_startup": True,
|
||||||
|
"hf_datasets": [
|
||||||
{
|
{
|
||||||
"hf_dataset_id": "newtextdoc1111/danbooru-tag-csv",
|
"hf_dataset_id": "newtextdoc1111/danbooru-tag-csv",
|
||||||
"last_remote_check_timestamp": None,
|
"last_remote_check_timestamp": None,
|
||||||
@@ -34,12 +35,15 @@ DEFAULT_CSV_METADATA = {
|
|||||||
"file_name": "danbooru_tags_cooccurrence.csv",
|
"file_name": "danbooru_tags_cooccurrence.csv",
|
||||||
"last_download": None,
|
"last_download": None,
|
||||||
"last_modified_on_hf": None,
|
"last_modified_on_hf": None,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- HuggingFace Constants ---
|
||||||
|
HUGGINGFACE_URL = "https://huggingface.co"
|
||||||
|
|
||||||
|
|
||||||
# --- Helper Functions ---
|
# --- Helper Functions ---
|
||||||
def get_file_path(file_name: str) -> str:
|
def get_file_path(file_name: str) -> str:
|
||||||
@@ -82,12 +86,14 @@ class Downloader:
|
|||||||
return default_metadata
|
return default_metadata
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(CSV_META_FILE, 'r', encoding='utf-8') as f:
|
with open(CSV_META_FILE, "r", encoding="utf-8") as f:
|
||||||
metadata = json.load(f)
|
metadata = json.load(f)
|
||||||
|
|
||||||
if not isinstance(metadata, dict) or metadata.get("version") != DEFAULT_CSV_METADATA["version"]:
|
if not isinstance(metadata, dict) or metadata.get("version") != DEFAULT_CSV_METADATA["version"]:
|
||||||
print(f"[Autocomplete-Plus] Metadata version mismatch. Expected {DEFAULT_CSV_METADATA['version']}, "
|
print(
|
||||||
f"found {metadata.get('version')}. Using default metadata.")
|
f"[Autocomplete-Plus] Metadata version mismatch. Expected {DEFAULT_CSV_METADATA['version']}, "
|
||||||
|
f"found {metadata.get('version')}. Using default metadata."
|
||||||
|
)
|
||||||
return default_metadata
|
return default_metadata
|
||||||
else:
|
else:
|
||||||
self.csv_meta_file_exists_at_start = True
|
self.csv_meta_file_exists_at_start = True
|
||||||
@@ -101,7 +107,7 @@ class Downloader:
|
|||||||
"""Saves metadata to CSV_META_FILE."""
|
"""Saves metadata to CSV_META_FILE."""
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.dirname(CSV_META_FILE), exist_ok=True)
|
os.makedirs(os.path.dirname(CSV_META_FILE), exist_ok=True)
|
||||||
with open(CSV_META_FILE, 'w', encoding='utf-8') as f:
|
with open(CSV_META_FILE, "w", encoding="utf-8") as f:
|
||||||
json.dump(self.metadata, f, indent=2)
|
json.dump(self.metadata, f, indent=2)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
print(f"[Autocomplete-Plus] Error saving metadata to {CSV_META_FILE}: {e}")
|
print(f"[Autocomplete-Plus] Error saving metadata to {CSV_META_FILE}: {e}")
|
||||||
@@ -111,12 +117,15 @@ class Downloader:
|
|||||||
Retrieves the Last-Modified header for a file on HuggingFace and returns it as an ISO 8601 string.
|
Retrieves the Last-Modified header for a file on HuggingFace and returns it as an ISO 8601 string.
|
||||||
hf_filename should be the full filename, e.g., "danbooru_tags.csv".
|
hf_filename should be the full filename, e.g., "danbooru_tags.csv".
|
||||||
"""
|
"""
|
||||||
url = f"https://huggingface.co/datasets/{dataset_repo_id}/resolve/main/{hf_filename}"
|
url = f"{HUGGINGFACE_URL}/datasets/{dataset_repo_id}/resolve/main/{hf_filename}"
|
||||||
try:
|
try:
|
||||||
req = urllib.request.Request(url, method='HEAD',
|
req = urllib.request.Request(
|
||||||
headers={"User-Agent": "ComfyUI-Autocomplete-Plus (Python urllib)"})
|
url,
|
||||||
|
method="HEAD",
|
||||||
|
headers={"User-Agent": "ComfyUI-Autocomplete-Plus (Python urllib)"},
|
||||||
|
)
|
||||||
with urllib.request.urlopen(req, timeout=10) as response:
|
with urllib.request.urlopen(req, timeout=10) as response:
|
||||||
last_modified_http = response.getheader('Last-Modified')
|
last_modified_http = response.getheader("Last-Modified")
|
||||||
if last_modified_http:
|
if last_modified_http:
|
||||||
dt_object = parsedate_to_datetime(last_modified_http)
|
dt_object = parsedate_to_datetime(last_modified_http)
|
||||||
if dt_object.tzinfo is None or dt_object.tzinfo.utcoffset(dt_object) is None:
|
if dt_object.tzinfo is None or dt_object.tzinfo.utcoffset(dt_object) is None:
|
||||||
@@ -141,7 +150,7 @@ class Downloader:
|
|||||||
"""
|
"""
|
||||||
Downloads a file synchronously with progress display to a temporary location.
|
Downloads a file synchronously with progress display to a temporary location.
|
||||||
"""
|
"""
|
||||||
download_url = f"https://huggingface.co/datasets/{hf_dataset_id}/resolve/main/{file_name}"
|
download_url = f"{HUGGINGFACE_URL}/datasets/{hf_dataset_id}/resolve/main/{file_name}"
|
||||||
final_path = get_file_path(file_name)
|
final_path = get_file_path(file_name)
|
||||||
temp_path = get_temp_download_path(file_name)
|
temp_path = get_temp_download_path(file_name)
|
||||||
now_utc = datetime.now(timezone.utc).isoformat()
|
now_utc = datetime.now(timezone.utc).isoformat()
|
||||||
@@ -155,18 +164,28 @@ class Downloader:
|
|||||||
if os.path.exists(temp_path):
|
if os.path.exists(temp_path):
|
||||||
os.remove(temp_path)
|
os.remove(temp_path)
|
||||||
|
|
||||||
req = urllib.request.Request(download_url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 11.0; Win64)"})
|
req = urllib.request.Request(
|
||||||
|
download_url,
|
||||||
|
headers={"User-Agent": "Mozilla/5.0 (Windows NT 11.0; Win64)"},
|
||||||
|
)
|
||||||
|
|
||||||
with urllib.request.urlopen(req) as response:
|
with urllib.request.urlopen(req) as response:
|
||||||
total_size_str = response.getheader("Content-Length")
|
total_size_str = response.getheader("Content-Length")
|
||||||
total_size = int(total_size_str) if total_size_str else None
|
total_size = int(total_size_str) if total_size_str else None
|
||||||
chunk_size = 8192
|
chunk_size = 8192
|
||||||
|
|
||||||
with open(temp_path, "wb") as f_out, \
|
with (
|
||||||
tqdm(total=total_size, unit='B', unit_scale=True, unit_divisor=1024,
|
open(temp_path, "wb") as f_out,
|
||||||
desc=f"[Autocomplete-Plus] Downloading {file_name}", leave=False,
|
tqdm(
|
||||||
bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}{postfix}]'
|
total=total_size,
|
||||||
) as pbar:
|
unit="B",
|
||||||
|
unit_scale=True,
|
||||||
|
unit_divisor=1024,
|
||||||
|
desc=f"[Autocomplete-Plus] Downloading {file_name}",
|
||||||
|
leave=False,
|
||||||
|
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}{postfix}]",
|
||||||
|
) as pbar,
|
||||||
|
):
|
||||||
while True:
|
while True:
|
||||||
chunk = response.read(chunk_size)
|
chunk = response.read(chunk_size)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
@@ -197,12 +216,15 @@ class Downloader:
|
|||||||
if os.path.exists(final_path):
|
if os.path.exists(final_path):
|
||||||
try:
|
try:
|
||||||
file_size_at_final = os.path.getsize(final_path)
|
file_size_at_final = os.path.getsize(final_path)
|
||||||
if file_size_at_final == 0 or \
|
if (
|
||||||
(total_size and total_size > 0 and file_size_at_final < total_size) or \
|
file_size_at_final == 0
|
||||||
(not total_size and downloaded_size > 0 and file_size_at_final < downloaded_size):
|
or (total_size and total_size > 0 and file_size_at_final < total_size)
|
||||||
|
or (not total_size and downloaded_size > 0 and file_size_at_final < downloaded_size)
|
||||||
|
):
|
||||||
os.remove(final_path)
|
os.remove(final_path)
|
||||||
print(
|
print(
|
||||||
f"[Autocomplete-Plus] Removed potentially corrupted file at final destination: {final_path}")
|
f"[Autocomplete-Plus] Removed potentially corrupted file at final destination: {final_path}"
|
||||||
|
)
|
||||||
except OSError as rm_e:
|
except OSError as rm_e:
|
||||||
print(f"[Autocomplete-Plus] Error removing potentially corrupted file {final_path}: {rm_e}")
|
print(f"[Autocomplete-Plus] Error removing potentially corrupted file {final_path}: {rm_e}")
|
||||||
|
|
||||||
@@ -221,7 +243,8 @@ class Downloader:
|
|||||||
perform_hf_check = False
|
perform_hf_check = False
|
||||||
except (ValueError, KeyError, TypeError):
|
except (ValueError, KeyError, TypeError):
|
||||||
print(
|
print(
|
||||||
"[Autocomplete-Plus] Invalid or missing timestamp for last_remote_check_timestamp. Will perform remote check.")
|
"[Autocomplete-Plus] Invalid or missing timestamp for last_remote_check_timestamp. Will perform remote check."
|
||||||
|
)
|
||||||
|
|
||||||
if perform_hf_check:
|
if perform_hf_check:
|
||||||
huggingface_dataset_id = dataset_meta["hf_dataset_id"]
|
huggingface_dataset_id = dataset_meta["hf_dataset_id"]
|
||||||
@@ -246,7 +269,8 @@ class Downloader:
|
|||||||
dataset_meta["last_remote_check_timestamp"] = now_utc.isoformat()
|
dataset_meta["last_remote_check_timestamp"] = now_utc.isoformat()
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
"[Autocomplete-Plus] Could not update all remote timestamps from HuggingFace. Will try again later.")
|
"[Autocomplete-Plus] Could not update all remote timestamps from HuggingFace. Will try again later."
|
||||||
|
)
|
||||||
|
|
||||||
def _download_csv_files_if_needed(self, dataset_meta: dict):
|
def _download_csv_files_if_needed(self, dataset_meta: dict):
|
||||||
"""Downloads CSV files if they are missing, outdated, or previously failed."""
|
"""Downloads CSV files if they are missing, outdated, or previously failed."""
|
||||||
@@ -299,10 +323,14 @@ class Downloader:
|
|||||||
"""
|
"""
|
||||||
Orchestrates the process of checking for updates and downloading CSV files.
|
Orchestrates the process of checking for updates and downloading CSV files.
|
||||||
This is the main entry point for the downloader logic.
|
This is the main entry point for the downloader logic.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
force_check: If True, forces a check of HuggingFace regardless of the last check timestamp.
|
force_check: If True, forces a check of HuggingFace regardless of the last check timestamp.
|
||||||
"""
|
"""
|
||||||
|
# If check_updates_on_startup is False and force_check is not set, skip the check
|
||||||
|
if not (self.metadata.get("check_updates_on_startup", True) or force_check):
|
||||||
|
print('[Autocomplete-Plus] "check_updates_on_startup" is disabled. Skipping CSV update check and download.')
|
||||||
|
return
|
||||||
|
|
||||||
now_utc = datetime.now(timezone.utc)
|
now_utc = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|||||||
Generated
+28
@@ -8,6 +8,7 @@
|
|||||||
"@babel/core": "^7.27.1",
|
"@babel/core": "^7.27.1",
|
||||||
"@babel/preset-env": "^7.27.2",
|
"@babel/preset-env": "^7.27.2",
|
||||||
"babel-jest": "^29.7.0",
|
"babel-jest": "^29.7.0",
|
||||||
|
"env-cmd": "^10.1.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"stylelint": "^16.19.1",
|
"stylelint": "^16.19.1",
|
||||||
"stylelint-config-idiomatic-order": "^10.0.0",
|
"stylelint-config-idiomatic-order": "^10.0.0",
|
||||||
@@ -3018,6 +3019,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -3266,6 +3277,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/env-cmd": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^4.0.0",
|
||||||
|
"cross-spawn": "^7.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"env-cmd": "bin/env-cmd.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/env-paths": {
|
"node_modules/env-paths": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest"
|
"test": "env-cmd -e test -- jest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.27.1",
|
"@babel/core": "^7.27.1",
|
||||||
"@babel/preset-env": "^7.27.2",
|
"@babel/preset-env": "^7.27.2",
|
||||||
"babel-jest": "^29.7.0",
|
"babel-jest": "^29.7.0",
|
||||||
|
"env-cmd": "^10.1.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"stylelint": "^16.19.1",
|
"stylelint": "^16.19.1",
|
||||||
"stylelint-config-idiomatic-order": "^10.0.0",
|
"stylelint-config-idiomatic-order": "^10.0.0",
|
||||||
|
|||||||
+10
-1
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-autocomplete-plus"
|
name = "comfyui-autocomplete-plus"
|
||||||
description = "Autocomplete and Related Tag display for ComfyUI"
|
description = "Autocomplete and Related Tag display for ComfyUI"
|
||||||
version = "1.0.0"
|
version = "1.3.1"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = ["",]
|
dependencies = ["",]
|
||||||
|
|
||||||
@@ -9,7 +9,16 @@ dependencies = ["",]
|
|||||||
Repository = "https://github.com/newtextdoc1111/ComfyUI-Autocomplete-Plus"
|
Repository = "https://github.com/newtextdoc1111/ComfyUI-Autocomplete-Plus"
|
||||||
# Used by Comfy Registry https://comfyregistry.org
|
# Used by Comfy Registry https://comfyregistry.org
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"ruff>=0.12.2",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.comfy]
|
[tool.comfy]
|
||||||
PublisherId = "newtextdoc1111"
|
PublisherId = "newtextdoc1111"
|
||||||
DisplayName = "ComfyUI-Autocomplete-Plus"
|
DisplayName = "ComfyUI-Autocomplete-Plus"
|
||||||
Icon = ""
|
Icon = ""
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
# Allow lines to be as long as 120.
|
||||||
|
line-length = 120
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
createFlexSearchDocument,
|
||||||
|
__test__
|
||||||
|
} from "../../web/js/searchengine.js";
|
||||||
|
|
||||||
|
const { createTagEncoder, createCJKEncoder } = __test__;
|
||||||
|
|
||||||
|
function parseCSVLine(line) {
|
||||||
|
const result = [];
|
||||||
|
let current = '';
|
||||||
|
let inQuotes = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const char = line[i];
|
||||||
|
|
||||||
|
if (char === '"') {
|
||||||
|
if (inQuotes && i + 1 < line.length && line[i + 1] === '"') {
|
||||||
|
current += '"';
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
}
|
||||||
|
} else if (char === ',' && !inQuotes) {
|
||||||
|
result.push(current);
|
||||||
|
current = '';
|
||||||
|
} else {
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(current);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FlexSearch Integration', () => {
|
||||||
|
|
||||||
|
const commonCSV = `
|
||||||
|
1girl,0,6008644,"1girls,sole_female"
|
||||||
|
highres,5,5256195,"high_res,high_resolution,hires"
|
||||||
|
solo,0,5000954,"alone,female_solo,single,solo_female,solo_in_panel"
|
||||||
|
long_hair,0,4350743,"/lh,longhair,very_long_hair"
|
||||||
|
one_two_three,0,29389,
|
||||||
|
`;
|
||||||
|
|
||||||
|
const cjkAliasCSV = `
|
||||||
|
blue_hair,0,676176,"青髪,青い髪,水色髪"
|
||||||
|
red_hair,0,413261,"赤髪,紅髪,红发,빨강머리,빨간머리"
|
||||||
|
smile,0,2294308,"笑い,スマイル,笑顔,笑顏,守りたい、この笑顔,笑,笑容,微笑み,微笑,微笑む,미소,守りたいこの笑顔"
|
||||||
|
gloves,0,1105296,"手袋,裸手袋,手袋コキ,てぶくろ,장갑,手套"
|
||||||
|
dragon_girl,0,37930,"竜娘,ドラゴン娘,龍娘,龙娘,メスドラ,辰娘"
|
||||||
|
double_bun,0,103538,"お団子頭,お団子"
|
||||||
|
sanshoku_dango,0,2061,"三色団子,三色团子,花見団子,花见团子"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const specialCharCSV = `
|
||||||
|
:d,0,436700,
|
||||||
|
>:),0,11041,
|
||||||
|
year:1999,0,1999,
|
||||||
|
d.d.,0,1999,
|
||||||
|
copyright_(series),2,1298,"copyright,コピーライト (シリーズ),コピーライト名,コピーライト,著作"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ControlCSV = `
|
||||||
|
__wildcard__,0,1000,
|
||||||
|
<lora:my_lora1:1.0>,0,1000,
|
||||||
|
Embedding: my_embedding,0,1000,
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mockCSV = [
|
||||||
|
commonCSV, cjkAliasCSV, specialCharCSV, ControlCSV
|
||||||
|
].map(csv => csv.trim()).join('\n');
|
||||||
|
|
||||||
|
let mockTags;
|
||||||
|
|
||||||
|
let tagEncoder, cjkEncoder;
|
||||||
|
let document;
|
||||||
|
|
||||||
|
let performSearch = function (query, limit = 100) {
|
||||||
|
const results = document.search(query, {
|
||||||
|
field: ["tag", "alias"],
|
||||||
|
limit: limit,
|
||||||
|
suggest: false,
|
||||||
|
merge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ids = results.map(r => r.id);
|
||||||
|
|
||||||
|
return mockTags.filter(tag => ids.includes(tag.id)).map(tag => tag.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockTags = mockCSV.split('\n').map((line, id) => {
|
||||||
|
const [tag, category, count, alias] = parseCSVLine(line);
|
||||||
|
return { id, tag, category: parseInt(category), count: parseInt(count), alias };
|
||||||
|
});
|
||||||
|
|
||||||
|
tagEncoder = createTagEncoder();
|
||||||
|
cjkEncoder = createCJKEncoder();
|
||||||
|
|
||||||
|
document = createFlexSearchDocument();
|
||||||
|
|
||||||
|
mockTags.forEach(data => document.add(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Encoder', () => {
|
||||||
|
test('should split underscore-separated tags', () => {
|
||||||
|
const encoded = tagEncoder.encode('sanshoku_dango');
|
||||||
|
expect(encoded).toEqual(['sanshoku', 'dango']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should extract words from parentheses', () => {
|
||||||
|
const encoded = tagEncoder.encode('copyright_(series)');
|
||||||
|
expect(encoded).toEqual(['copyright', 'series']);
|
||||||
|
});
|
||||||
|
test('should preserve colon-separated special tags', () => {
|
||||||
|
const encoded = tagEncoder.encode('year:1234');
|
||||||
|
expect(encoded).toEqual(['year:1234']);
|
||||||
|
});
|
||||||
|
test('should preserve dot-separated tags', () => {
|
||||||
|
const encoded = tagEncoder.encode('d.d.');
|
||||||
|
expect(encoded).toEqual(['d.d.']);
|
||||||
|
});
|
||||||
|
test('should preserve double underscore wildcard tags', () => {
|
||||||
|
const encoded = tagEncoder.encode('__wildcard__');
|
||||||
|
expect(encoded).toEqual(['__wildcard__']);
|
||||||
|
});
|
||||||
|
test('should convert katakana to hiragana', () => {
|
||||||
|
const encoded = cjkEncoder.encode('ガーデン');
|
||||||
|
expect(encoded).toEqual(['がーでん']);
|
||||||
|
});
|
||||||
|
test('should remove trailing underscores when splitting', () => {
|
||||||
|
const encoded = tagEncoder.encode('one_two_');
|
||||||
|
expect(encoded).toEqual(['one', 'two']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Basic Search', () => {
|
||||||
|
test('should find a tag by exact match', () => {
|
||||||
|
const results = performSearch('1girl');
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
expect(results).toContain('1girl');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find a tag by partial match (substring)', () => {
|
||||||
|
const results = performSearch('blue');
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
expect(results).toContain('blue_hair');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be case-insensitive', () => {
|
||||||
|
const results = performSearch('BLUE_HAIR');
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(results).toContain('blue_hair');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find a tag by backward', () => {
|
||||||
|
const results = performSearch('gon');
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain('dragon_girl');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find a tag for terms contain space', () => {
|
||||||
|
const results = performSearch('double ');
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain('double_bun');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find a tag for terms contain underscore', () => {
|
||||||
|
const results = performSearch('double_');
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain('double_bun');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Alias Search', () => {
|
||||||
|
test('should find a tag by its Japanese alias', () => {
|
||||||
|
const results = performSearch('髪');
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(results).toContain('blue_hair');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find a tag by one of its multiple aliases', () => {
|
||||||
|
const results = performSearch('笑顔');
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(results).toContain('smile');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find a tag by its partial Japanese alias', () => {
|
||||||
|
const results = performSearch('青い');
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(results).toContain('blue_hair');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find a tag by katakana', () => {
|
||||||
|
const results = performSearch('テブクロ');
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain('gloves');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find a tag by hiragana', () => {
|
||||||
|
const results = performSearch('すまいる');
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain('smile');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not find a tag by english alias substring', () => {
|
||||||
|
const results = performSearch('meg');
|
||||||
|
expect(results.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find a tag by japanese alias substring', () => {
|
||||||
|
const results = performSearch('団子');
|
||||||
|
expect(results.length).toEqual(2);
|
||||||
|
|
||||||
|
expect(results).toContain('sanshoku_dango');
|
||||||
|
expect(results).toContain('double_bun');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Special Characters and Edge Cases', () => {
|
||||||
|
test('should find a tag with parentheses', () => {
|
||||||
|
const results = performSearch('copyright_(series)');
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(results).toContain('copyright_(series)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find a tag by searching for content inside parentheses', () => {
|
||||||
|
const results = performSearch('series');
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(results).toContain('copyright_(series)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find a tag by partial word', () => {
|
||||||
|
const results = performSearch('right');
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(results).toContain('copyright_(series)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return an empty array for a non-existent tag', () => {
|
||||||
|
const results = performSearch('non_existent_tag_xyz');
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match to special character only tag', () => {
|
||||||
|
const tag = '>:)';
|
||||||
|
const results = performSearch(tag);
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain(tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match to contain special character tag', () => {
|
||||||
|
const tag = ':d';
|
||||||
|
const results = performSearch(tag);
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain(tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match to contain special character tag2', () => {
|
||||||
|
const tag = 'year:1999';
|
||||||
|
const results = performSearch(tag);
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain(tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match to contain special character tag3', () => {
|
||||||
|
const tag = 'd.d.';
|
||||||
|
const results = performSearch(tag);
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain(tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match to wildcard tag', () => {
|
||||||
|
const tag = '__';
|
||||||
|
const results = performSearch(tag);
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain('__wildcard__');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match to lora tag', () => {
|
||||||
|
const tag = '<lora';
|
||||||
|
const results = performSearch(tag);
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain("<lora:my_lora1:1.0>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Search Options', () => {
|
||||||
|
test('should respect the limit option', () => {
|
||||||
|
const results = performSearch('hair', 1);
|
||||||
|
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return all matches when limit is higher than results', () => {
|
||||||
|
const results = performSearch('hair', 5);
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(results).toHaveLength(3);
|
||||||
|
expect(results).toContain('long_hair');
|
||||||
|
expect(results).toContain('blue_hair');
|
||||||
|
expect(results).toContain('red_hair');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+144
-147
@@ -37,6 +37,62 @@ body.dark-theme {
|
|||||||
color: var(--input-text);
|
color: var(--input-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .danbooru[data-tag-category="general"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .danbooru[data-tag-category="artist"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .danbooru[data-tag-category="copyright"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .danbooru[data-tag-category="character"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .danbooru[data-tag-category="meta"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .danbooru[data-tag-category="unknown"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .e621[data-tag-category="general"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-sky);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .e621[data-tag-category="artist"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .e621[data-tag-category="copyright"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .e621[data-tag-category="character"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .e621[data-tag-category="species"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .e621[data-tag-category="meta"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .e621[data-tag-category="lore"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-root .e621[data-tag-category="unknown"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||||
|
}
|
||||||
|
|
||||||
#autocomplete-plus-list {
|
#autocomplete-plus-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
||||||
@@ -52,62 +108,6 @@ body.dark-theme {
|
|||||||
grid-template-columns: subgrid;
|
grid-template-columns: subgrid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete-plus-item.danbooru[data-tag-category="general"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.danbooru[data-tag-category="artist"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.danbooru[data-tag-category="copyright"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.danbooru[data-tag-category="character"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.danbooru[data-tag-category="meta"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-yellow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.danbooru[data-tag-category="unknown"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.e621[data-tag-category="general"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-sky);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.e621[data-tag-category="artist"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-orange);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.e621[data-tag-category="copyright"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.e621[data-tag-category="character"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.e621[data-tag-category="species"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.e621[data-tag-category="meta"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.e621[data-tag-category="lore"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item.e621[data-tag-category="unknown"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-plus-item span {
|
.autocomplete-plus-item span {
|
||||||
align-content: center;
|
align-content: center;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
@@ -124,11 +124,7 @@ body.dark-theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete-plus-item.selected span {
|
.autocomplete-plus-item.selected span {
|
||||||
background-color: var(
|
background-color: var(--border-color);
|
||||||
--comfy-menu-bg-selected,
|
|
||||||
var(--comfy-button-bg)
|
|
||||||
);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete-plus-item:hover span {
|
.autocomplete-plus-item:hover span {
|
||||||
@@ -172,26 +168,91 @@ body.dark-theme {
|
|||||||
color: var(--input-text);
|
color: var(--input-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#related-tags-root .danbooru[data-tag-category="general"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .danbooru[data-tag-category="artist"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .danbooru[data-tag-category="copyright"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .danbooru[data-tag-category="character"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .danbooru[data-tag-category="meta"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .danbooru[data-tag-category="unknown"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .e621[data-tag-category="general"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-sky);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .e621[data-tag-category="artist"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .e621[data-tag-category="copyright"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .e621[data-tag-category="character"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .e621[data-tag-category="species"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .e621[data-tag-category="meta"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .e621[data-tag-category="lore"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
#related-tags-root .e621[data-tag-category="unknown"] {
|
||||||
|
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||||
|
}
|
||||||
|
|
||||||
#related-tags-header {
|
#related-tags-header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: 0;
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
padding: 8px;
|
||||||
padding: 8px 12px;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
background-color: var(--comfy-menu-bg);
|
background-color: var(--comfy-menu-bg);
|
||||||
color: var(--descrip-text);
|
color: var(--descrip-text);
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-tags-header-text {
|
.related-tags-header-text-container {
|
||||||
flex-grow: 1;
|
display:flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-tags-header-tag-text {
|
||||||
|
padding: 2px 0;
|
||||||
|
line-height: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-tags-header-tag-name {
|
.related-tags-header-tag-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-tags-header-tag-name .autocomplete-plus-tag-icon-svg {
|
.related-tags-header-tag-name .autocomplete-plus-tag-icon-svg {
|
||||||
@@ -200,17 +261,22 @@ body.dark-theme {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.related-tags-header-tag-alias {
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 5em;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--descrip-text);
|
||||||
|
line-height: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
.related-tags-header-controls {
|
.related-tags-header-controls {
|
||||||
display: flex;
|
margin-left: auto;
|
||||||
align-items: center;
|
|
||||||
margin-left: 16px;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-tags-layout-toggle {
|
.related-tags-header-controls button {
|
||||||
padding: 2px 6px;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--input-text);
|
color: var(--input-text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -218,19 +284,8 @@ body.dark-theme {
|
|||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-tags-layout-toggle:hover {
|
.related-tags-header-controls button:hover {
|
||||||
background-color: var(--comfy-button-bg);
|
background-color: var(--border-color);
|
||||||
}
|
|
||||||
|
|
||||||
.related-tags-pin-toggle {
|
|
||||||
padding: 2px 6px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: none;
|
|
||||||
color: var(--input-text);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#related-tags-list {
|
#related-tags-list {
|
||||||
@@ -248,62 +303,6 @@ body.dark-theme {
|
|||||||
grid-template-columns: subgrid;
|
grid-template-columns: subgrid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-tag-item.danbooru[data-tag-category="general"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.danbooru[data-tag-category="artist"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.danbooru[data-tag-category="copyright"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.danbooru[data-tag-category="character"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.danbooru[data-tag-category="meta"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-yellow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.danbooru[data-tag-category="unknown"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.e621[data-tag-category="general"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-sky);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.e621[data-tag-category="artist"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-orange);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.e621[data-tag-category="copyright"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.e621[data-tag-category="character"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.e621[data-tag-category="species"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.e621[data-tag-category="meta"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.e621[data-tag-category="lore"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-tag-item.e621[data-tag-category="unknown"] {
|
|
||||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* stylelint-disable-next-line no-descending-specificity */
|
/* stylelint-disable-next-line no-descending-specificity */
|
||||||
.related-tag-item span {
|
.related-tag-item span {
|
||||||
align-content: center;
|
align-content: center;
|
||||||
@@ -324,11 +323,7 @@ body.dark-theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.related-tag-item.selected span {
|
.related-tag-item.selected span {
|
||||||
background-color: var(
|
background-color: var(--border-color);
|
||||||
--comfy-menu-bg-selected,
|
|
||||||
var(--comfy-button-bg)
|
|
||||||
);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-tag-item .related-tag-name {
|
.related-tag-item .related-tag-name {
|
||||||
@@ -352,9 +347,11 @@ body.dark-theme {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-tags-loading-message {
|
.related-tags-message {
|
||||||
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
grid-column: 1 / -1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+101
-19
@@ -5,9 +5,10 @@ import {
|
|||||||
getEnabledTagSourceInPriorityOrder
|
getEnabledTagSourceInPriorityOrder
|
||||||
} from './data.js';
|
} from './data.js';
|
||||||
import {
|
import {
|
||||||
formatCountHumanReadable,
|
isLongText,
|
||||||
hiraToKata,
|
hiraToKata,
|
||||||
kataToHira,
|
kataToHira,
|
||||||
|
formatCountHumanReadable,
|
||||||
isContainsLetterOrNumber,
|
isContainsLetterOrNumber,
|
||||||
normalizeTagToInsert,
|
normalizeTagToInsert,
|
||||||
normalizeTagToSearch,
|
normalizeTagToSearch,
|
||||||
@@ -78,18 +79,15 @@ function matchWord(target, queries) {
|
|||||||
* @returns {Array<TagData>} The list of matching candidates.
|
* @returns {Array<TagData>} The list of matching candidates.
|
||||||
*/
|
*/
|
||||||
function searchCompletionCandidates(textareaElement) {
|
function searchCompletionCandidates(textareaElement) {
|
||||||
const startTime = performance.now(); // Record start time for performance measurement
|
|
||||||
|
|
||||||
const ESCAPE_SEQUENCE = ["#", "/"]; // If the first string is that character, autocomplete will not be displayed.
|
const ESCAPE_SEQUENCE = ["#", "/"]; // If the first string is that character, autocomplete will not be displayed.
|
||||||
const partialTag = getCurrentPartialTag(textareaElement);
|
const partialTag = getCurrentPartialTag(textareaElement);
|
||||||
if (!partialTag || partialTag.length <= 0 || ESCAPE_SEQUENCE.some(seq => partialTag.startsWith(seq))) {
|
if (!partialTag || partialTag.length <= 0 ||
|
||||||
return []; // No valid input for autocomplete
|
ESCAPE_SEQUENCE.some(seq => partialTag.startsWith(seq)) ||
|
||||||
|
isLongText(partialTag)) {
|
||||||
|
return []; // No valid input for autocomplete
|
||||||
}
|
}
|
||||||
|
|
||||||
const exactMatches = [];
|
|
||||||
const partialMatches = [];
|
|
||||||
const addedTags = new Set();
|
|
||||||
|
|
||||||
// Generate Hiragana/Katakana variations if applicable
|
// Generate Hiragana/Katakana variations if applicable
|
||||||
const queryVariations = new Set([partialTag, normalizeTagToSearch(partialTag)]);
|
const queryVariations = new Set([partialTag, normalizeTagToSearch(partialTag)]);
|
||||||
const kataQuery = hiraToKata(partialTag);
|
const kataQuery = hiraToKata(partialTag);
|
||||||
@@ -101,6 +99,26 @@ function searchCompletionCandidates(textareaElement) {
|
|||||||
queryVariations.add(hiraQuery);
|
queryVariations.add(hiraQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settingValues.useFastSearch) {
|
||||||
|
return searchWithFlexSearch(partialTag, queryVariations);
|
||||||
|
} else {
|
||||||
|
return sequentialSearch(partialTag, queryVariations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search completion candidates using sequential search.
|
||||||
|
* @param {string} partialTag
|
||||||
|
* @param {Set<string>} queryVariations
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function sequentialSearch(partialTag, queryVariations) {
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
const exactMatches = [];
|
||||||
|
const partialMatches = [];
|
||||||
|
const addedTags = new Set();
|
||||||
|
|
||||||
const sources = getEnabledTagSourceInPriorityOrder();
|
const sources = getEnabledTagSourceInPriorityOrder();
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
// Search in sortedTags (already sorted by count)
|
// Search in sortedTags (already sorted by count)
|
||||||
@@ -170,6 +188,69 @@ function searchCompletionCandidates(textareaElement) {
|
|||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search completion candidates using FlexSearch for fast matching.
|
||||||
|
* @param {string} partialTag
|
||||||
|
* @param {Set<string>} queryVariations
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function searchWithFlexSearch(partialTag, queryVariations) {
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
let mergedResult = [];
|
||||||
|
let totalSearchCount = 0;
|
||||||
|
|
||||||
|
const sources = getEnabledTagSourceInPriorityOrder();
|
||||||
|
for (const source of sources) {
|
||||||
|
if (!autoCompleteData[source].flexSearchDocument) continue;
|
||||||
|
if (mergedResult.length >= settingValues.maxSuggestions) break;
|
||||||
|
|
||||||
|
// Use the FlexSearch Document to search
|
||||||
|
// NOTE: The limit param is reflected separately for "tag" and "alias".
|
||||||
|
let searchResult = autoCompleteData[source].flexSearchDocument.search(partialTag, {
|
||||||
|
field: ["tag", "alias"],
|
||||||
|
limit: Math.min(settingValues.maxSuggestions * 10, 500),
|
||||||
|
merge: true,
|
||||||
|
suggest: false,
|
||||||
|
cache: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!searchResult || searchResult.length <= 0) continue;
|
||||||
|
|
||||||
|
// Sort results based on exact matches and counts
|
||||||
|
searchResult = searchResult
|
||||||
|
.map(r => autoCompleteData[source].sortedTags[r.id])
|
||||||
|
.sort((aTag, bTag) => {
|
||||||
|
if (matchWord(bTag.tag, queryVariations).isExactMatch) {
|
||||||
|
return 999999999999;
|
||||||
|
}
|
||||||
|
if (matchWord(aTag.tag, queryVariations).isExactMatch) {
|
||||||
|
return -999999999999;
|
||||||
|
}
|
||||||
|
if (bTag.alias && bTag.alias.some(alias => matchWord(alias, queryVariations).isExactMatch)) {
|
||||||
|
return 999999999999;
|
||||||
|
}
|
||||||
|
if (aTag.alias && aTag.alias.some(alias => matchWord(alias, queryVariations).isExactMatch)) {
|
||||||
|
return -999999999999;
|
||||||
|
}
|
||||||
|
return bTag.count - aTag.count;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge results into the final array
|
||||||
|
mergedResult = mergedResult.concat(searchResult.slice(0, settingValues.maxSuggestions - mergedResult.length));
|
||||||
|
|
||||||
|
totalSearchCount += searchResult.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingValues._logprocessingTime) {
|
||||||
|
const endTime = performance.now();
|
||||||
|
const duration = endTime - startTime;
|
||||||
|
console.debug(`[Autocomplete-Plus] Fast Search for "${partialTag}" took ${duration.toFixed(2)}ms.Found ${mergedResult.length} candidates within ${totalSearchCount} searches from flexsearch.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedResult;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the current tag being typed before the cursor.
|
* Extracts the current tag being typed before the cursor.
|
||||||
* @param {HTMLTextAreaElement} inputElement
|
* @param {HTMLTextAreaElement} inputElement
|
||||||
@@ -332,10 +413,11 @@ class AutocompleteUI {
|
|||||||
// Calculate caret position using the helper function (returns viewport-relative coordinates)
|
// Calculate caret position using the helper function (returns viewport-relative coordinates)
|
||||||
this.#updatePosition();
|
this.#updatePosition();
|
||||||
|
|
||||||
// Highlight the first item
|
|
||||||
this.#highlightItem();
|
|
||||||
|
|
||||||
this.root.style.display = 'block'; // Make it visible
|
this.root.style.display = 'block'; // Make it visible
|
||||||
|
|
||||||
|
// Highlight the selected item
|
||||||
|
// This function must be called after the route has been displayed, in order to scroll the highlighted item into view.
|
||||||
|
this.#highlightItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -826,7 +908,7 @@ export class AutocompleteEventHandler {
|
|||||||
this.keyDownWithModifier.set(event.key.toLowerCase(), event.ctrlKey || event.altKey || event.metaKey);
|
this.keyDownWithModifier.set(event.key.toLowerCase(), event.ctrlKey || event.altKey || event.metaKey);
|
||||||
|
|
||||||
// Handle autocomplete navigation
|
// Handle autocomplete navigation
|
||||||
if (this.autocompleteUI && this.autocompleteUI.isVisible()) {
|
if (this.autocompleteUI.isVisible()) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -878,11 +960,11 @@ export class AutocompleteEventHandler {
|
|||||||
|
|
||||||
if (this.autocompleteUI.isVisible()) {
|
if (this.autocompleteUI.isVisible()) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case "Escape":
|
case 'ArrowDown':
|
||||||
|
case 'ArrowUp':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.autocompleteUI.hide();
|
return; // Prevent redundant display updates
|
||||||
return; // Return here to prevent updateDisplay after hiding with Escape
|
|
||||||
// Other keys like Enter, Tab, Arrows are handled in keyDown.
|
|
||||||
// For other character keys, Backspace, Delete, we fall through to updateDisplay.
|
// For other character keys, Backspace, Delete, we fall through to updateDisplay.
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -894,9 +976,9 @@ export class AutocompleteEventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the event was not handled by the above (e.g. Escape, or ignored special keys)
|
// If the event was not handled by the above (e.g. Arrow keys, or ignored special keys)
|
||||||
// and default action is not prevented, update the display.
|
// and default action is not prevented, update the display.
|
||||||
// This will typically be for character inputs, Delete, or Backspace.
|
// This will typically be for character inputs, Delete, Backspace or IME composition.
|
||||||
if (!event.defaultPrevented) {
|
if (!event.defaultPrevented) {
|
||||||
this.autocompleteUI.updateDisplay(event.target);
|
this.autocompleteUI.updateDisplay(event.target);
|
||||||
}
|
}
|
||||||
@@ -917,4 +999,4 @@ export class AutocompleteEventHandler {
|
|||||||
*/
|
*/
|
||||||
handleClick(event) {
|
handleClick(event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-15
@@ -1,4 +1,5 @@
|
|||||||
import { settingValues } from "./settings.js";
|
import { settingValues, updateMaxTagLength } from "./settings.js";
|
||||||
|
import { createFlexSearchDocument } from "./searchengine.js";
|
||||||
|
|
||||||
// --- Constants ---
|
// --- Constants ---
|
||||||
|
|
||||||
@@ -63,6 +64,9 @@ export class TagData {
|
|||||||
|
|
||||||
class AutocompleteData {
|
class AutocompleteData {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
/** @type {Document} */
|
||||||
|
this.flexSearchDocument = null;
|
||||||
|
|
||||||
/** @type {TagData[]} */
|
/** @type {TagData[]} */
|
||||||
this.sortedTags = [];
|
this.sortedTags = [];
|
||||||
|
|
||||||
@@ -80,7 +84,6 @@ class AutocompleteData {
|
|||||||
|
|
||||||
// Progress of "base" csv loading
|
// Progress of "base" csv loading
|
||||||
this.baseLoadingProgress = {
|
this.baseLoadingProgress = {
|
||||||
// tags: 0,
|
|
||||||
cooccurrence: 0
|
cooccurrence: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -158,6 +161,8 @@ async function loadTags(csvUrl, siteName) {
|
|||||||
// Create a TagData instance instead of a plain object
|
// Create a TagData instance instead of a plain object
|
||||||
const tagData = new TagData(tag, aliases, category, count, siteName);
|
const tagData = new TagData(tag, aliases, category, count, siteName);
|
||||||
|
|
||||||
|
updateMaxTagLength(tag.length);
|
||||||
|
|
||||||
autoCompleteData[siteName].sortedTags.push(tagData);
|
autoCompleteData[siteName].sortedTags.push(tagData);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[Autocomplete-Plus] Invalid CSV format in line ${i + 1} of ${csvUrl}: ${line}. Expected ${TAGS_CSV_HEADER_COLUMNS.length} columns, but got ${columns.length}.`);
|
console.warn(`[Autocomplete-Plus] Invalid CSV format in line ${i + 1} of ${csvUrl}: ${line}. Expected ${TAGS_CSV_HEADER_COLUMNS.length} columns, but got ${columns.length}.`);
|
||||||
@@ -187,6 +192,45 @@ async function loadTags(csvUrl, siteName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build FlexSearch index for the given site name.
|
||||||
|
* @param {string} siteName
|
||||||
|
*/
|
||||||
|
async function buildFlexSearchIndex(siteName) {
|
||||||
|
try {
|
||||||
|
if (autoCompleteData[siteName].sortedTags.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = createFlexSearchDocument();
|
||||||
|
|
||||||
|
let startIdx = 0;
|
||||||
|
const startTime = performance.now();
|
||||||
|
function processChunkTasks() {
|
||||||
|
const chunkSize = 1000;
|
||||||
|
const end = Math.min(startIdx + chunkSize, autoCompleteData[siteName].sortedTags.length);
|
||||||
|
for (; startIdx < end; startIdx++) {
|
||||||
|
const tagData = autoCompleteData[siteName].sortedTags[startIdx];
|
||||||
|
document.add(startIdx, tagData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIdx < autoCompleteData[siteName].sortedTags.length) {
|
||||||
|
setTimeout(processChunkTasks, 0);
|
||||||
|
// console.log(`[Autocomplete-Plus] Current porcess: ${startIdx}`);
|
||||||
|
} else {
|
||||||
|
autoCompleteData[siteName].flexSearchDocument = document;
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
const duration = endTime - startTime;
|
||||||
|
console.info(`[Autocomplete-Plus] Building ${autoCompleteData[siteName].sortedTags.length} index for ${siteName} took ${duration.toFixed(2)}ms.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processChunkTasks();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Autocomplete-Plus] Failed to building flexSearch index`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads co-occurrence data from a single CSV file.
|
* Loads co-occurrence data from a single CSV file.
|
||||||
* @param {string} csvUrl - The URL of the CSV file to load.
|
* @param {string} csvUrl - The URL of the CSV file to load.
|
||||||
@@ -326,7 +370,6 @@ export async function initializeData(csvListData, source) {
|
|||||||
|
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
autoCompleteData[source].isInitializing = true;
|
autoCompleteData[source].isInitializing = true;
|
||||||
// console.log("[Autocomplete-Plus] Initializing autocomplete data...");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Store functions that return Promises (Promise Factories)
|
// Store functions that return Promises (Promise Factories)
|
||||||
@@ -340,8 +383,8 @@ export async function initializeData(csvListData, source) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraTagsCount = csvListData[source].extra_tags || 0;
|
const extraTagsFileList = csvListData[source].extra_tags || [];
|
||||||
const extraCooccurrenceCount = csvListData[source].extra_cooccurrence || 0;
|
const extraCooccurrenceFileList = csvListData[source].extra_cooccurrence || [];
|
||||||
|
|
||||||
const tagsUrl = `/autocomplete-plus/csv/${source}/tags`;
|
const tagsUrl = `/autocomplete-plus/csv/${source}/tags`;
|
||||||
const cooccurrenceUrl = `/autocomplete-plus/csv/${source}/tags_cooccurrence`;
|
const cooccurrenceUrl = `/autocomplete-plus/csv/${source}/tags_cooccurrence`;
|
||||||
@@ -349,7 +392,7 @@ export async function initializeData(csvListData, source) {
|
|||||||
// Factory for loading tags for the current sourte
|
// Factory for loading tags for the current sourte
|
||||||
const siteTagsLoaderFactory = async () => {
|
const siteTagsLoaderFactory = async () => {
|
||||||
let promiseChain = Promise.resolve();
|
let promiseChain = Promise.resolve();
|
||||||
for (let i = 0; i < extraTagsCount; i++) {
|
for (let i = 0; i < extraTagsFileList.length; i++) {
|
||||||
promiseChain = promiseChain.then(() => loadTags(`${tagsUrl}/extra/${i}`, source));
|
promiseChain = promiseChain.then(() => loadTags(`${tagsUrl}/extra/${i}`, source));
|
||||||
}
|
}
|
||||||
if (csvListData[source].base_tags) {
|
if (csvListData[source].base_tags) {
|
||||||
@@ -362,7 +405,7 @@ export async function initializeData(csvListData, source) {
|
|||||||
// Factory for loading cooccurrence data for the current sourte
|
// Factory for loading cooccurrence data for the current sourte
|
||||||
const siteCooccurrenceLoaderFactory = async () => {
|
const siteCooccurrenceLoaderFactory = async () => {
|
||||||
let promiseChain = Promise.resolve();
|
let promiseChain = Promise.resolve();
|
||||||
for (let i = 0; i < extraCooccurrenceCount; i++) {
|
for (let i = 0; i < extraCooccurrenceFileList.length; i++) {
|
||||||
promiseChain = promiseChain.then(() => loadCooccurrence(`${cooccurrenceUrl}/extra/${i}`, source));
|
promiseChain = promiseChain.then(() => loadCooccurrence(`${cooccurrenceUrl}/extra/${i}`, source));
|
||||||
}
|
}
|
||||||
if (csvListData[source].base_cooccurrence) {
|
if (csvListData[source].base_cooccurrence) {
|
||||||
@@ -375,16 +418,22 @@ export async function initializeData(csvListData, source) {
|
|||||||
// Now, execute all promise factories and wait for their completion.
|
// Now, execute all promise factories and wait for their completion.
|
||||||
// The actual loading (fetch calls) will start when the factories are invoked here.
|
// The actual loading (fetch calls) will start when the factories are invoked here.
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Promise.all(tagsLoadPromiseFactories.map(factory => factory())).then(() => {
|
Promise.all(tagsLoadPromiseFactories.map(factory => factory()))
|
||||||
const endTime = performance.now();
|
.then(() => {
|
||||||
console.log(`[Autocomplete-Plus] "${source}" Tags loading complete in ${(endTime - startTime).toFixed(2)}ms`);
|
// Build FlexSearch index after tags are loaded
|
||||||
}),
|
return buildFlexSearchIndex(source);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
const endTime = performance.now();
|
||||||
|
if (csvListData[source].base_tags) {
|
||||||
|
console.log(`[Autocomplete-Plus] "${source}" Tags loading complete in ${(endTime - startTime).toFixed(2)}ms`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
Promise.all(cooccurrenceLoadPromiseFactories.map(factory => factory())).then(() => {
|
Promise.all(cooccurrenceLoadPromiseFactories.map(factory => factory())).then(() => {
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
// The original log for extraCooccurrenceCount was potentially misleading as it showed
|
if (csvListData[source].base_cooccurrence) {
|
||||||
// the count for the last processed sourte. Removed for clarity.
|
console.log(`[Autocomplete-Plus] "${source}" Co-occurrence loading complete in ${(endTime - startTime).toFixed(2)}ms.`);
|
||||||
// If a total count is needed, it should be calculated across all sites.
|
}
|
||||||
console.log(`[Autocomplete-Plus] "${source}" Co-occurrence loading complete in ${(endTime - startTime).toFixed(2)}ms.`);
|
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -286,6 +286,17 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// --- Autocomplete Settings ---
|
// --- Autocomplete Settings ---
|
||||||
|
{
|
||||||
|
id: id + ".use_fast_search",
|
||||||
|
name: "Use Fast Search",
|
||||||
|
tooltip: "Tag search processing during text input operates faster, improving responsiveness",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: false,
|
||||||
|
category: [name, "Autocompletion", "Use Fast Search"],
|
||||||
|
onChange: (newVal, oldVal) => {
|
||||||
|
settingValues.useFastSearch = newVal;
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: id + ".max_suggestions",
|
id: id + ".max_suggestions",
|
||||||
name: "Max suggestions",
|
name: "Max suggestions",
|
||||||
|
|||||||
+66
-21
@@ -1,13 +1,14 @@
|
|||||||
import { TagSource, TagCategory, TagData, autoCompleteData } from './data.js';
|
import { TagCategory, TagData, TagSource, autoCompleteData } from './data.js';
|
||||||
import { settingValues } from './settings.js';
|
import { settingValues } from './settings.js';
|
||||||
import {
|
import {
|
||||||
extractTagsFromTextArea,
|
extractTagsFromTextArea,
|
||||||
findAllTagPositions,
|
findAllTagPositions,
|
||||||
|
getCurrentTagRange,
|
||||||
getViewportMargin,
|
getViewportMargin,
|
||||||
|
isLongText,
|
||||||
isValidTag,
|
isValidTag,
|
||||||
normalizeTagToInsert,
|
normalizeTagToInsert,
|
||||||
normalizeTagToSearch,
|
normalizeTagToSearch,
|
||||||
getCurrentTagRange,
|
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|
||||||
// --- RelatedTags Logic ---
|
// --- RelatedTags Logic ---
|
||||||
@@ -67,7 +68,7 @@ function searchRelatedTags(tag) {
|
|||||||
|
|
||||||
const tagSource = TagSource.Danbooru; // TODO: Leave the tag source as Danbooru until e621_tags_cooccurrence.csv is ready
|
const tagSource = TagSource.Danbooru; // TODO: Leave the tag source as Danbooru until e621_tags_cooccurrence.csv is ready
|
||||||
|
|
||||||
if (!tag || !autoCompleteData[tagSource].cooccurrenceMap.has(tag)) {
|
if (!tag || !autoCompleteData[tagSource].initialized || !autoCompleteData[tagSource].cooccurrenceMap.has(tag)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,11 +200,20 @@ class RelatedTagsUI {
|
|||||||
this.header = document.createElement('div');
|
this.header = document.createElement('div');
|
||||||
this.header.id = 'related-tags-header';
|
this.header.id = 'related-tags-header';
|
||||||
|
|
||||||
|
this.headerTextContainer = document.createElement('div');
|
||||||
|
this.headerTextContainer.className = 'related-tags-header-text-container';
|
||||||
|
this.header.appendChild(this.headerTextContainer);
|
||||||
|
|
||||||
// Create header text div for the left side
|
// Create header text div for the left side
|
||||||
this.headerText = document.createElement('div');
|
this.headerText = document.createElement('div');
|
||||||
this.headerText.className = 'related-tags-header-text';
|
this.headerText.className = 'related-tags-header-tag-text';
|
||||||
this.headerText.textContent = 'Related Tags';
|
this.headerText.textContent = 'Related Tags';
|
||||||
this.header.appendChild(this.headerText);
|
this.headerTextContainer.appendChild(this.headerText);
|
||||||
|
|
||||||
|
// Create header alias div for the 2nd line
|
||||||
|
this.headerAlias = document.createElement('div');
|
||||||
|
this.headerAlias.className = 'related-tags-header-tag-alias';
|
||||||
|
this.headerTextContainer.appendChild(this.headerAlias);
|
||||||
|
|
||||||
// Create header controls for the right side
|
// Create header controls for the right side
|
||||||
this.headerControls = document.createElement('div');
|
this.headerControls = document.createElement('div');
|
||||||
@@ -249,7 +259,7 @@ class RelatedTagsUI {
|
|||||||
this.headerControls.appendChild(this.pinBtn);
|
this.headerControls.appendChild(this.pinBtn);
|
||||||
|
|
||||||
this.header.appendChild(this.headerControls);
|
this.header.appendChild(this.headerControls);
|
||||||
|
|
||||||
this.root.appendChild(this.header);
|
this.root.appendChild(this.header);
|
||||||
|
|
||||||
// Create a tbody for the tags
|
// Create a tbody for the tags
|
||||||
@@ -300,7 +310,7 @@ class RelatedTagsUI {
|
|||||||
const currentTag = getTagFromCursorPosition(textareaElement);
|
const currentTag = getTagFromCursorPosition(textareaElement);
|
||||||
|
|
||||||
if (!this.isPinned) {
|
if (!this.isPinned) {
|
||||||
if (isValidTag(currentTag)) {
|
if (!isLongText(currentTag) && isValidTag(currentTag)) {
|
||||||
this.currentTag = currentTag
|
this.currentTag = currentTag
|
||||||
} else {
|
} else {
|
||||||
this.hide();
|
this.hide();
|
||||||
@@ -318,11 +328,13 @@ class RelatedTagsUI {
|
|||||||
this.#updateHeader();
|
this.#updateHeader();
|
||||||
this.#updateContent();
|
this.#updateContent();
|
||||||
this.#updatePosition();
|
this.#updatePosition();
|
||||||
this.#highlightItem();
|
|
||||||
|
|
||||||
// Make visible
|
// Make visible
|
||||||
this.root.style.display = 'block';
|
this.root.style.display = 'block';
|
||||||
|
|
||||||
|
// This function must be called after the content is updated and the root is displayed.
|
||||||
|
this.#highlightItem();
|
||||||
|
|
||||||
// Update initialization status if not already done
|
// Update initialization status if not already done
|
||||||
if (!autoCompleteData[TagSource.Danbooru].initialized) {
|
if (!autoCompleteData[TagSource.Danbooru].initialized) {
|
||||||
if (this.autoRefreshTimerId) {
|
if (this.autoRefreshTimerId) {
|
||||||
@@ -389,7 +401,7 @@ class RelatedTagsUI {
|
|||||||
*/
|
*/
|
||||||
#updateHeader() {
|
#updateHeader() {
|
||||||
// Find the tag data for the current tag
|
// Find the tag data for the current tag
|
||||||
const tagData = Object.values(TagSource)
|
let tagData = Object.values(TagSource)
|
||||||
.map((source) => {
|
.map((source) => {
|
||||||
if (source in autoCompleteData && autoCompleteData[source].tagMap.has(this.currentTag)) {
|
if (source in autoCompleteData && autoCompleteData[source].tagMap.has(this.currentTag)) {
|
||||||
return autoCompleteData[source].tagMap.get(this.currentTag);
|
return autoCompleteData[source].tagMap.get(this.currentTag);
|
||||||
@@ -397,21 +409,49 @@ class RelatedTagsUI {
|
|||||||
})
|
})
|
||||||
.find((tagData) => tagData !== undefined);
|
.find((tagData) => tagData !== undefined);
|
||||||
|
|
||||||
|
if (!tagData) {
|
||||||
|
tagData = new TagData({
|
||||||
|
tag: this.currentTag,
|
||||||
|
source: TagSource.Danbooru,
|
||||||
|
category: 'unknown',
|
||||||
|
count: 0,
|
||||||
|
alias: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagText = this.currentTag;
|
||||||
|
const categoryText = TagCategory[tagData.source][tagData.category];
|
||||||
|
|
||||||
// Update header text with current tag
|
// Update header text with current tag
|
||||||
this.headerText.innerHTML = ''; // Clear previous content
|
this.headerText.innerHTML = ''; // Clear previous content
|
||||||
this.headerText.textContent = 'Tags related to: ';
|
this.headerText.textContent = 'Tags related to: ';
|
||||||
|
|
||||||
const tagName = document.createElement('span');
|
const tagName = document.createElement('span');
|
||||||
tagName.className = 'related-tags-header-tag-name';
|
tagName.classList.add('related-tags-header-tag-name', tagData.source);
|
||||||
tagName.textContent = this.currentTag;
|
tagName.title = `Count: ${tagData.count}\nCategory: ${categoryText}`;
|
||||||
if (tagData && ['left', 'right'].includes(settingValues.tagSourceIconPosition)) {
|
tagName.dataset.tagCategory = categoryText;
|
||||||
|
if (tagData.source && ['left', 'right'].includes(settingValues.tagSourceIconPosition)) {
|
||||||
const tagSourceIconHtml = `<svg class="autocomplete-plus-tag-icon-svg"><use xlink:href="#autocomplete-plus-icon-${tagData.source}"></use></svg>`;
|
const tagSourceIconHtml = `<svg class="autocomplete-plus-tag-icon-svg"><use xlink:href="#autocomplete-plus-icon-${tagData.source}"></use></svg>`;
|
||||||
tagName.innerHTML = settingValues.tagSourceIconPosition == 'left'
|
tagName.innerHTML = settingValues.tagSourceIconPosition == 'left'
|
||||||
? `${tagSourceIconHtml} ${tagData.tag}`
|
? `${tagSourceIconHtml} ${tagText}`
|
||||||
: `${tagData.tag} ${tagSourceIconHtml}`;
|
: `${tagText} ${tagSourceIconHtml}`;
|
||||||
|
} else {
|
||||||
|
tagName.textContent += tagText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.headerText.appendChild(tagName);
|
this.headerText.appendChild(tagName);
|
||||||
|
|
||||||
|
// Clear previous alias
|
||||||
|
this.headerAlias.style.display = 'none';
|
||||||
|
this.headerAlias.innerHTML = '';
|
||||||
|
|
||||||
|
// Add alias if available
|
||||||
|
if (tagData.alias?.length > 0) {
|
||||||
|
this.headerAlias.textContent = tagData.alias.join(', ');
|
||||||
|
this.headerAlias.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
// Update pin button
|
// Update pin button
|
||||||
this.pinBtn.textContent = this.isPinned ? '🎯' : '📌';
|
this.pinBtn.textContent = this.isPinned ? '🎯' : '📌';
|
||||||
|
|
||||||
@@ -430,7 +470,7 @@ class RelatedTagsUI {
|
|||||||
if (!autoCompleteData[TagSource.Danbooru].initialized) {
|
if (!autoCompleteData[TagSource.Danbooru].initialized) {
|
||||||
// Show loading message
|
// Show loading message
|
||||||
const messageDiv = document.createElement('div');
|
const messageDiv = document.createElement('div');
|
||||||
messageDiv.className = 'related-tags-loading-message';
|
messageDiv.className = 'related-tags-message';
|
||||||
messageDiv.textContent = `Initializing cooccurrence data... [${autoCompleteData[TagSource.Danbooru].baseLoadingProgress.cooccurrence}%]`;
|
messageDiv.textContent = `Initializing cooccurrence data... [${autoCompleteData[TagSource.Danbooru].baseLoadingProgress.cooccurrence}%]`;
|
||||||
this.tagsContainer.appendChild(messageDiv);
|
this.tagsContainer.appendChild(messageDiv);
|
||||||
return;
|
return;
|
||||||
@@ -439,6 +479,7 @@ class RelatedTagsUI {
|
|||||||
if (!this.relatedTags || this.relatedTags.length === 0) {
|
if (!this.relatedTags || this.relatedTags.length === 0) {
|
||||||
// Show no related tags message
|
// Show no related tags message
|
||||||
const messageDiv = document.createElement('div');
|
const messageDiv = document.createElement('div');
|
||||||
|
messageDiv.className = 'related-tags-message';
|
||||||
messageDiv.textContent = 'No related tags found';
|
messageDiv.textContent = 'No related tags found';
|
||||||
this.tagsContainer.appendChild(messageDiv);
|
this.tagsContainer.appendChild(messageDiv);
|
||||||
return;
|
return;
|
||||||
@@ -528,20 +569,24 @@ class RelatedTagsUI {
|
|||||||
this.root.style.maxWidth = '';
|
this.root.style.maxWidth = '';
|
||||||
this.tagsContainer.style.maxHeight = '';
|
this.tagsContainer.style.maxHeight = '';
|
||||||
const rootRect = this.root.getBoundingClientRect();
|
const rootRect = this.root.getBoundingClientRect();
|
||||||
const headerRect = this.header.getBoundingClientRect();
|
|
||||||
// Hide it again after measurement
|
|
||||||
this.root.style.display = 'none';
|
|
||||||
this.root.style.visibility = 'visible';
|
|
||||||
|
|
||||||
// Get the optimal placement area
|
// Get the optimal placement area
|
||||||
const placementArea = this.#getOptimalPlacementArea(rootRect.width, rootRect.height);
|
const placementArea = this.#getOptimalPlacementArea(rootRect.width, rootRect.height);
|
||||||
|
|
||||||
// Apply Styles
|
// Apply position and size
|
||||||
this.root.style.left = `${placementArea.x}px`;
|
this.root.style.left = `${placementArea.x}px`;
|
||||||
this.root.style.top = `${placementArea.y}px`;
|
this.root.style.top = `${placementArea.y}px`;
|
||||||
this.root.style.maxWidth = `${placementArea.width}px`;
|
this.root.style.maxWidth = `${placementArea.width}px`;
|
||||||
|
|
||||||
this.tagsContainer.style.maxHeight = `${placementArea.height - headerRect.height}px`;
|
const newHeaderRect = this.header.getBoundingClientRect();
|
||||||
|
|
||||||
|
if(this.relatedTags.length > 0){
|
||||||
|
this.tagsContainer.style.maxHeight = `${placementArea.height - newHeaderRect.height}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide it again after measurement
|
||||||
|
this.root.style.display = 'none';
|
||||||
|
this.root.style.visibility = 'visible';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Highlights the item (row) at the given index */
|
/** Highlights the item (row) at the given index */
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Charset, Encoder, Document } from './thirdparty/flexsearch.bundle.module.min.js'
|
||||||
|
import { kataToHira } from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an encoder optimized for processing English tag names.
|
||||||
|
* Handles tag formatting like underscores and parentheses commonly used in Danbooru tags.
|
||||||
|
* @returns {Encoder} FlexSearch encoder for English tags
|
||||||
|
*/
|
||||||
|
function createTagEncoder() {
|
||||||
|
return new Encoder({
|
||||||
|
normalize: true,
|
||||||
|
dedupe: false,
|
||||||
|
numeric: false,
|
||||||
|
cache: true,
|
||||||
|
// filter: new Set(['and', 'to', 'be', 'on']),
|
||||||
|
replacer: [/(?<=[a-zA-Z\)])_$/, ''], // Remove trailing underscores after letters/parentheses
|
||||||
|
split: /(?<=[a-zA-Z\)])_(?=[a-zA-Z\(])|\((?=[a-zA-Z])|(?<=[a-zA-Z\)])\)|[ \n]/ // Split on underscores between words, parentheses, spaces, and newlines
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an encoder optimized for processing CJK (Chinese, Japanese, Korean) characters.
|
||||||
|
* Uses exact character matching and converts katakana to hiragana for better Japanese search.
|
||||||
|
* @returns {Encoder} FlexSearch encoder for CJK text
|
||||||
|
*/
|
||||||
|
function createCJKEncoder() {
|
||||||
|
return new Encoder(Charset.Exact, {
|
||||||
|
dedupe: true,
|
||||||
|
numeric: true,
|
||||||
|
cache: true,
|
||||||
|
filter: new Set(['(', ')']), // Filter out parentheses characters
|
||||||
|
finalize: (term) => { // Convert katakana to hiragana for better Japanese matching
|
||||||
|
return term.map(str => kataToHira(str));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a FlexSearch Document instance optimized for tag searching.
|
||||||
|
* Configures separate encoders for English tags and CJK aliases with appropriate tokenization.
|
||||||
|
* @returns {Document} Configured FlexSearch document for tag indexing
|
||||||
|
*/
|
||||||
|
export function createFlexSearchDocument() {
|
||||||
|
const tagEncoder = createTagEncoder();
|
||||||
|
const cjkEncoder = createCJKEncoder();
|
||||||
|
|
||||||
|
// Custom encoding function for alias field that handles mixed language content
|
||||||
|
const encodeAlias = function (term) {
|
||||||
|
return term.split(",")
|
||||||
|
.flatMap(str => {
|
||||||
|
if (/[^\u0000-\u007f]/.test(str)) {
|
||||||
|
// Contains non-ASCII characters (CJK text)
|
||||||
|
return cjkEncoder.encode(str);
|
||||||
|
} else {
|
||||||
|
// ASCII characters only (English text)
|
||||||
|
return tagEncoder.encode(str);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the FlexSearch document with optimized indexing settings
|
||||||
|
const document = new Document({
|
||||||
|
document: {
|
||||||
|
id: "id",
|
||||||
|
index: [
|
||||||
|
{
|
||||||
|
field: "tag",
|
||||||
|
tokenize: "bidirectional", // Allow partial matching from both ends
|
||||||
|
encoder: tagEncoder, // Use tag-optimized encoder
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "alias", // Index the alias field for multi-language support
|
||||||
|
tokenize: "full", // Full tokenization for complete alias matching
|
||||||
|
encode: encodeAlias, // Use custom multi-language encoding function
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export functions for testing when in test environment
|
||||||
|
const isTestEnvironment = typeof process !== 'undefined' && process.env.NODE_ENV === 'test';
|
||||||
|
export const __test__ = isTestEnvironment ? { createTagEncoder, createCJKEncoder } : undefined;
|
||||||
+13
-1
@@ -7,6 +7,7 @@ export const settingValues = {
|
|||||||
// Autocomplete feature settings
|
// Autocomplete feature settings
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxSuggestions: 10,
|
maxSuggestions: 10,
|
||||||
|
useFastSearch: false,
|
||||||
|
|
||||||
// Related tags feature settings
|
// Related tags feature settings
|
||||||
enableRelatedTags: true,
|
enableRelatedTags: true,
|
||||||
@@ -17,8 +18,19 @@ export const settingValues = {
|
|||||||
|
|
||||||
// Internal logic settings
|
// Internal logic settings
|
||||||
_useFallbackAttachmentForEventListener: false, // Fallback to attach event listener when somthing goes wrong
|
_useFallbackAttachmentForEventListener: false, // Fallback to attach event listener when somthing goes wrong
|
||||||
|
_maxTagLength: 100, // Maximum tag length to prevent performance issues with long text input
|
||||||
|
|
||||||
// Debugging settings (use internally)
|
// Debugging settings (use internally)
|
||||||
_hideWhenOutofFocus: true, // Hide UI when the input is out of focus
|
_hideWhenOutofFocus: true, // Hide UI when the input is out of focus
|
||||||
_logprocessingTime: false, // Log processing time for debugging
|
_logprocessingTime: false, // Log processing time for debugging
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the maximum tag length setting value.
|
||||||
|
* This function ensures that the maximum tag length is always at least as long as the new length provided.
|
||||||
|
* @param {number} newLength
|
||||||
|
*/
|
||||||
|
export function updateMaxTagLength(newLength) {
|
||||||
|
if(isNaN(newLength)) return;
|
||||||
|
settingValues._maxTagLength = Math.max(settingValues._maxTagLength, newLength);
|
||||||
|
}
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
/**!
|
||||||
|
* FlexSearch.js v0.8.200 (Bundle/Module)
|
||||||
|
* Author and Copyright: Thomas Wilkerling
|
||||||
|
* Licence: Apache-2.0
|
||||||
|
* Hosted by Nextapps GmbH
|
||||||
|
* https://github.com/nextapps-de/flexsearch
|
||||||
|
*/
|
||||||
|
var w;function H(a,c,b){const e=typeof b,d=typeof a;if("undefined"!==e){if("undefined"!==d){if(b){if("function"===d&&e===d)return function(h){return a(b(h))};c=a.constructor;if(c===b.constructor){if(c===Array)return b.concat(a);if(c===Map){var f=new Map(b);for(var g of a)f.set(g[0],g[1]);return f}if(c===Set){g=new Set(b);for(f of a.values())g.add(f);return g}}}return a}return b}return"undefined"===d?c:a}function aa(a,c){return"undefined"===typeof a?c:a}function I(){return Object.create(null)}
|
||||||
|
function M(a){return"string"===typeof a}function ba(a){return"object"===typeof a}function ca(a){const c=[];for(const b of a.keys())c.push(b);return c}function ea(a,c){if(M(c))a=a[c];else for(let b=0;a&&b<c.length;b++)a=a[c[b]];return a};const fa=/[^\p{L}\p{N}]+/u,ha=/(\d{3})/g,ia=/(\D)(\d{3})/g,ja=/(\d{3})(\D)/g,ka=/[\u0300-\u036f]/g;function la(a={}){if(!this||this.constructor!==la)return new la(...arguments);if(arguments.length)for(a=0;a<arguments.length;a++)this.assign(arguments[a]);else this.assign(a)}w=la.prototype;
|
||||||
|
w.assign=function(a){this.normalize=H(a.normalize,!0,this.normalize);let c=a.include,b=c||a.exclude||a.split,e;if(b||""===b){if("object"===typeof b&&b.constructor!==RegExp){let d="";e=!c;c||(d+="\\p{Z}");b.letter&&(d+="\\p{L}");b.number&&(d+="\\p{N}",e=!!c);b.symbol&&(d+="\\p{S}");b.punctuation&&(d+="\\p{P}");b.control&&(d+="\\p{C}");if(b=b.char)d+="object"===typeof b?b.join(""):b;try{this.split=new RegExp("["+(c?"^":"")+d+"]+","u")}catch(f){this.split=/\s+/}}else this.split=b,e=!1===b||2>"a1a".split(b).length;
|
||||||
|
this.numeric=H(a.numeric,e)}else{try{this.split=H(this.split,fa)}catch(d){this.split=/\s+/}this.numeric=H(a.numeric,H(this.numeric,!0))}this.prepare=H(a.prepare,null,this.prepare);this.finalize=H(a.finalize,null,this.finalize);b=a.filter;this.filter="function"===typeof b?b:H(b&&new Set(b),null,this.filter);this.dedupe=H(a.dedupe,!0,this.dedupe);this.matcher=H((b=a.matcher)&&new Map(b),null,this.matcher);this.mapper=H((b=a.mapper)&&new Map(b),null,this.mapper);this.stemmer=H((b=a.stemmer)&&new Map(b),
|
||||||
|
null,this.stemmer);this.replacer=H(a.replacer,null,this.replacer);this.minlength=H(a.minlength,1,this.minlength);this.maxlength=H(a.maxlength,1024,this.maxlength);this.rtl=H(a.rtl,!1,this.rtl);if(this.cache=b=H(a.cache,!0,this.cache))this.I=null,this.R="number"===typeof b?b:2E5,this.B=new Map,this.H=new Map,this.M=this.L=128;this.h="";this.N=null;this.A="";this.O=null;if(this.matcher)for(const d of this.matcher.keys())this.h+=(this.h?"|":"")+d;if(this.stemmer)for(const d of this.stemmer.keys())this.A+=
|
||||||
|
(this.A?"|":"")+d;return this};w.addStemmer=function(a,c){this.stemmer||(this.stemmer=new Map);this.stemmer.set(a,c);this.A+=(this.A?"|":"")+a;this.O=null;this.cache&&Q(this);return this};w.addFilter=function(a){"function"===typeof a?this.filter=a:(this.filter||(this.filter=new Set),this.filter.add(a));this.cache&&Q(this);return this};
|
||||||
|
w.addMapper=function(a,c){if("object"===typeof a)return this.addReplacer(a,c);if(1<a.length)return this.addMatcher(a,c);this.mapper||(this.mapper=new Map);this.mapper.set(a,c);this.cache&&Q(this);return this};w.addMatcher=function(a,c){if("object"===typeof a)return this.addReplacer(a,c);if(2>a.length&&(this.dedupe||this.mapper))return this.addMapper(a,c);this.matcher||(this.matcher=new Map);this.matcher.set(a,c);this.h+=(this.h?"|":"")+a;this.N=null;this.cache&&Q(this);return this};
|
||||||
|
w.addReplacer=function(a,c){if("string"===typeof a)return this.addMatcher(a,c);this.replacer||(this.replacer=[]);this.replacer.push(a,c);this.cache&&Q(this);return this};
|
||||||
|
w.encode=function(a,c){if(this.cache&&a.length<=this.L)if(this.I){if(this.B.has(a))return this.B.get(a)}else this.I=setTimeout(Q,50,this);this.normalize&&("function"===typeof this.normalize?a=this.normalize(a):a=ka?a.normalize("NFKD").replace(ka,"").toLowerCase():a.toLowerCase());this.prepare&&(a=this.prepare(a));this.numeric&&3<a.length&&(a=a.replace(ia,"$1 $2").replace(ja,"$1 $2").replace(ha,"$1 "));const b=!(this.dedupe||this.mapper||this.filter||this.matcher||this.stemmer||this.replacer);let e=
|
||||||
|
[],d=I(),f,g,h=this.split||""===this.split?a.split(this.split):[a];for(let m=0,l,n;m<h.length;m++)if((l=n=h[m])&&!(l.length<this.minlength||l.length>this.maxlength)){if(c){if(d[l])continue;d[l]=1}else{if(f===l)continue;f=l}if(b)e.push(l);else if(!this.filter||("function"===typeof this.filter?this.filter(l):!this.filter.has(l))){if(this.cache&&l.length<=this.M)if(this.I){var k=this.H.get(l);if(k||""===k){k&&e.push(k);continue}}else this.I=setTimeout(Q,50,this);if(this.stemmer){this.O||(this.O=new RegExp("(?!^)("+
|
||||||
|
this.A+")$"));let u;for(;u!==l&&2<l.length;)u=l,l=l.replace(this.O,p=>this.stemmer.get(p))}if(l&&(this.mapper||this.dedupe&&1<l.length)){k="";for(let u=0,p="",t,r;u<l.length;u++)t=l.charAt(u),t===p&&this.dedupe||((r=this.mapper&&this.mapper.get(t))||""===r?r===p&&this.dedupe||!(p=r)||(k+=r):k+=p=t);l=k}this.matcher&&1<l.length&&(this.N||(this.N=new RegExp("("+this.h+")","g")),l=l.replace(this.N,u=>this.matcher.get(u)));if(l&&this.replacer)for(k=0;l&&k<this.replacer.length;k+=2)l=l.replace(this.replacer[k],
|
||||||
|
this.replacer[k+1]);this.cache&&n.length<=this.M&&(this.H.set(n,l),this.H.size>this.R&&(this.H.clear(),this.M=this.M/1.1|0));if(l){if(l!==n)if(c){if(d[l])continue;d[l]=1}else{if(g===l)continue;g=l}e.push(l)}}}this.finalize&&(e=this.finalize(e)||e);this.cache&&a.length<=this.L&&(this.B.set(a,e),this.B.size>this.R&&(this.B.clear(),this.L=this.L/1.1|0));return e};function Q(a){a.I=null;a.B.clear();a.H.clear()};function ma(a,c,b){b||(c||"object"!==typeof a?"object"===typeof c&&(b=c,c=0):b=a);b&&(a=b.query||a,c=b.limit||c);let e=""+(c||0);b&&(e+=(b.offset||0)+!!b.context+!!b.suggest+(!1!==b.resolve)+(b.resolution||this.resolution)+(b.boost||0));a=(""+a).toLowerCase();this.cache||(this.cache=new R);let d=this.cache.get(a+e);if(!d){const f=b&&b.cache;f&&(b.cache=!1);d=this.search(a,c,b);f&&(b.cache=f);this.cache.set(a+e,d)}return d}function R(a){this.limit=a&&!0!==a?a:1E3;this.cache=new Map;this.h=""}
|
||||||
|
R.prototype.set=function(a,c){this.cache.set(this.h=a,c);this.cache.size>this.limit&&this.cache.delete(this.cache.keys().next().value)};R.prototype.get=function(a){const c=this.cache.get(a);c&&this.h!==a&&(this.cache.delete(a),this.cache.set(this.h=a,c));return c};R.prototype.remove=function(a){for(const c of this.cache){const b=c[0];c[1].includes(a)&&this.cache.delete(b)}};R.prototype.clear=function(){this.cache.clear();this.h=""};const na={normalize:!1,numeric:!1,dedupe:!1};const qa={};const ra=new Map([["b","p"],["v","f"],["w","f"],["z","s"],["x","s"],["d","t"],["n","m"],["c","k"],["g","k"],["j","k"],["q","k"],["i","e"],["y","e"],["u","o"]]);const sa=new Map([["ae","a"],["oe","o"],["sh","s"],["kh","k"],["th","t"],["ph","f"],["pf","f"]]),ta=[/([^aeo])h(.)/g,"$1$2",/([aeo])h([^aeo]|$)/g,"$1$2",/(.)\1+/g,"$1"];const ua={a:"",e:"",i:"",o:"",u:"",y:"",b:1,f:1,p:1,v:1,c:2,g:2,j:2,k:2,q:2,s:2,x:2,z:2,"\u00df":2,d:3,t:3,l:4,m:5,n:5,r:6};var va={Exact:na,Default:qa,Normalize:qa,LatinBalance:{mapper:ra},LatinAdvanced:{mapper:ra,matcher:sa,replacer:ta},LatinExtra:{mapper:ra,replacer:ta.concat([/(?!^)[aeo]/g,""]),matcher:sa},LatinSoundex:{dedupe:!1,include:{letter:!0},finalize:function(a){for(let b=0;b<a.length;b++){var c=a[b];let e=c.charAt(0),d=ua[e];for(let f=1,g;f<c.length&&(g=c.charAt(f),"h"===g||"w"===g||!(g=ua[g])||g===d||(e+=g,d=g,4!==e.length));f++);a[b]=e}}},CJK:{split:""},LatinExact:na,LatinDefault:qa,LatinSimple:qa};function wa(a,c,b,e){let d=[];for(let f=0,g;f<a.index.length;f++)if(g=a.index[f],c>=g.length)c-=g.length;else{c=g[e?"splice":"slice"](c,b);const h=c.length;if(h&&(d=d.length?d.concat(c):c,b-=h,e&&(a.length-=h),!b))break;c=0}return d}
|
||||||
|
function za(a){if(!this||this.constructor!==za)return new za(a);this.index=a?[a]:[];this.length=a?a.length:0;const c=this;return new Proxy([],{get(b,e){if("length"===e)return c.length;if("push"===e)return function(d){c.index[c.index.length-1].push(d);c.length++};if("pop"===e)return function(){if(c.length)return c.length--,c.index[c.index.length-1].pop()};if("indexOf"===e)return function(d){let f=0;for(let g=0,h,k;g<c.index.length;g++){h=c.index[g];k=h.indexOf(d);if(0<=k)return f+k;f+=h.length}return-1};
|
||||||
|
if("includes"===e)return function(d){for(let f=0;f<c.index.length;f++)if(c.index[f].includes(d))return!0;return!1};if("slice"===e)return function(d,f){return wa(c,d||0,f||c.length,!1)};if("splice"===e)return function(d,f){return wa(c,d||0,f||c.length,!0)};if("constructor"===e)return Array;if("symbol"!==typeof e)return(b=c.index[e/2**31|0])&&b[e]},set(b,e,d){b=e/2**31|0;(c.index[b]||(c.index[b]=[]))[e]=d;c.length++;return!0}})}za.prototype.clear=function(){this.index.length=0};
|
||||||
|
za.prototype.destroy=function(){this.proxy=this.index=null};za.prototype.push=function(){};function S(a=8){if(!this||this.constructor!==S)return new S(a);this.index=I();this.h=[];this.size=0;32<a?(this.B=Aa,this.A=BigInt(a)):(this.B=Ba,this.A=a)}S.prototype.get=function(a){const c=this.index[this.B(a)];return c&&c.get(a)};S.prototype.set=function(a,c){var b=this.B(a);let e=this.index[b];e?(b=e.size,e.set(a,c),(b-=e.size)&&this.size++):(this.index[b]=e=new Map([[a,c]]),this.h.push(e),this.size++)};
|
||||||
|
function T(a=8){if(!this||this.constructor!==T)return new T(a);this.index=I();this.h=[];this.size=0;32<a?(this.B=Aa,this.A=BigInt(a)):(this.B=Ba,this.A=a)}T.prototype.add=function(a){var c=this.B(a);let b=this.index[c];b?(c=b.size,b.add(a),(c-=b.size)&&this.size++):(this.index[c]=b=new Set([a]),this.h.push(b),this.size++)};w=S.prototype;w.has=T.prototype.has=function(a){const c=this.index[this.B(a)];return c&&c.has(a)};
|
||||||
|
w.delete=T.prototype.delete=function(a){const c=this.index[this.B(a)];c&&c.delete(a)&&this.size--};w.clear=T.prototype.clear=function(){this.index=I();this.h=[];this.size=0};w.values=T.prototype.values=function*(){for(let a=0;a<this.h.length;a++)for(let c of this.h[a].values())yield c};w.keys=T.prototype.keys=function*(){for(let a=0;a<this.h.length;a++)for(let c of this.h[a].keys())yield c};w.entries=T.prototype.entries=function*(){for(let a=0;a<this.h.length;a++)for(let c of this.h[a].entries())yield c};
|
||||||
|
function Ba(a){let c=2**this.A-1;if("number"==typeof a)return a&c;let b=0,e=this.A+1;for(let d=0;d<a.length;d++)b=(b*e^a.charCodeAt(d))&c;return 32===this.A?b+2**31:b}function Aa(a){let c=BigInt(2)**this.A-BigInt(1);var b=typeof a;if("bigint"===b)return a&c;if("number"===b)return BigInt(a)&c;b=BigInt(0);let e=this.A+BigInt(1);for(let d=0;d<a.length;d++)b=(b*e^BigInt(a.charCodeAt(d)))&c;return b};let Ca,Da;
|
||||||
|
async function Ea(a){a=a.data;var c=a.task;const b=a.id;let e=a.args;switch(c){case "init":Da=a.options||{};(c=a.factory)?(Function("return "+c)()(self),Ca=new self.FlexSearch.Index(Da),delete self.FlexSearch):Ca=new V(Da);postMessage({id:b});break;default:let d;"export"===c&&(e[1]?(e[0]=Da.export,e[2]=0,e[3]=1):e=null);"import"===c?e[0]&&(a=await Da.import.call(Ca,e[0]),Ca.import(e[0],a)):((d=e&&Ca[c].apply(Ca,e))&&d.then&&(d=await d),d&&d.await&&(d=await d.await),"search"===c&&d.result&&(d=d.result));
|
||||||
|
postMessage("search"===c?{id:b,msg:d}:{id:b})}};function Fa(a){Ga.call(a,"add");Ga.call(a,"append");Ga.call(a,"search");Ga.call(a,"update");Ga.call(a,"remove");Ga.call(a,"searchCache")}let Ha,Ia,Ja;function Ka(){Ha=Ja=0}
|
||||||
|
function Ga(a){this[a+"Async"]=function(){const c=arguments;var b=c[c.length-1];let e;"function"===typeof b&&(e=b,delete c[c.length-1]);Ha?Ja||(Ja=Date.now()-Ia>=this.priority*this.priority*3):(Ha=setTimeout(Ka,0),Ia=Date.now());if(Ja){const f=this;return new Promise(g=>{setTimeout(function(){g(f[a+"Async"].apply(f,c))},0)})}const d=this[a].apply(this,c);b=d.then?d:new Promise(f=>f(d));e&&b.then(e);return b}};let W=0;
|
||||||
|
function La(a={},c){function b(h){function k(m){m=m.data||m;const l=m.id,n=l&&f.h[l];n&&(n(m.msg),delete f.h[l])}this.worker=h;this.h=I();if(this.worker){d?this.worker.on("message",k):this.worker.onmessage=k;if(a.config)return new Promise(function(m){1E9<W&&(W=0);f.h[++W]=function(){m(f)};f.worker.postMessage({id:W,task:"init",factory:e,options:a})});this.priority=a.priority||4;this.encoder=c||null;this.worker.postMessage({task:"init",factory:e,options:a});return this}}if(!this||this.constructor!==La)return new La(a);
|
||||||
|
let e="undefined"!==typeof self?self._factory:"undefined"!==typeof window?window._factory:null;e&&(e=e.toString());const d="undefined"===typeof window,f=this,g=Ma(e,d,a.worker);return g.then?g.then(function(h){return b.call(f,h)}):b.call(this,g)}X("add");X("append");X("search");X("update");X("remove");X("clear");X("export");X("import");La.prototype.searchCache=ma;Fa(La.prototype);
|
||||||
|
function X(a){La.prototype[a]=function(){const c=this,b=[].slice.call(arguments);var e=b[b.length-1];let d;"function"===typeof e&&(d=e,b.pop());e=new Promise(function(f){"export"===a&&"function"===typeof b[0]&&(b[0]=null);1E9<W&&(W=0);c.h[++W]=f;c.worker.postMessage({task:a,id:W,args:b})});return d?(e.then(d),this):e}}
|
||||||
|
function Ma(a,c,b){return c?"undefined"!==typeof module?new(require("worker_threads")["Worker"])(__dirname+"/worker/node.js"):import("worker_threads").then(function(worker){return new worker["Worker"](import.meta.dirname+"/node/node.mjs")}):a?new window.Worker(URL.createObjectURL(new Blob(["onmessage="+Ea.toString()],{type:"text/javascript"}))):new window.Worker("string"===typeof b?b:import.meta.url.replace("/worker.js","/worker/worker.js").replace("flexsearch.bundle.module.min.js",
|
||||||
|
"module/worker/worker.js"),{type:"module"})};Na.prototype.add=function(a,c,b){ba(a)&&(c=a,a=ea(c,this.key));if(c&&(a||0===a)){if(!b&&this.reg.has(a))return this.update(a,c);for(let h=0,k;h<this.field.length;h++){k=this.G[h];var e=this.index.get(this.field[h]);if("function"===typeof k){var d=k(c);d&&e.add(a,d,!1,!0)}else if(d=k.J,!d||d(c))k.constructor===String?k=[""+k]:M(k)&&(k=[k]),Qa(c,k,this.K,0,e,a,k[0],b)}if(this.tag)for(e=0;e<this.F.length;e++){var f=this.F[e];d=this.tag.get(this.P[e]);let h=I();if("function"===typeof f){if(f=f(c),!f)continue}else{var g=
|
||||||
|
f.J;if(g&&!g(c))continue;f.constructor===String&&(f=""+f);f=ea(c,f)}if(d&&f){M(f)&&(f=[f]);for(let k=0,m,l;k<f.length;k++)if(m=f[k],!h[m]&&(h[m]=1,(g=d.get(m))?l=g:d.set(m,l=[]),!b||!l.includes(a))){if(l.length===2**31-1){g=new za(l);if(this.fastupdate)for(let n of this.reg.values())n.includes(l)&&(n[n.indexOf(l)]=g);d.set(m,l=g)}l.push(a);this.fastupdate&&((g=this.reg.get(a))?g.push(l):this.reg.set(a,[l]))}}}if(this.store&&(!b||!this.store.has(a))){let h;if(this.D){h=I();for(let k=0,m;k<this.D.length;k++){m=
|
||||||
|
this.D[k];if((b=m.J)&&!b(c))continue;let l;if("function"===typeof m){l=m(c);if(!l)continue;m=[m.U]}else if(M(m)||m.constructor===String){h[m]=c[m];continue}Ra(c,h,m,0,m[0],l)}}this.store.set(a,h||c)}this.worker&&(this.fastupdate||this.reg.add(a))}return this};function Ra(a,c,b,e,d,f){a=a[d];if(e===b.length-1)c[d]=f||a;else if(a)if(a.constructor===Array)for(c=c[d]=Array(a.length),d=0;d<a.length;d++)Ra(a,c,b,e,d);else c=c[d]||(c[d]=I()),d=b[++e],Ra(a,c,b,e,d)}
|
||||||
|
function Qa(a,c,b,e,d,f,g,h){if(a=a[g])if(e===c.length-1){if(a.constructor===Array){if(b[e]){for(c=0;c<a.length;c++)d.add(f,a[c],!0,!0);return}a=a.join(" ")}d.add(f,a,h,!0)}else if(a.constructor===Array)for(g=0;g<a.length;g++)Qa(a,c,b,e,d,f,g,h);else g=c[++e],Qa(a,c,b,e,d,f,g,h);else d.db&&d.remove(f)};function Sa(a,c,b,e){if(!a.length)return a;if(1===a.length)return a=a[0],a=b||a.length>c?a.slice(b,b+c):a,e?Ta.call(this,a):a;let d=[];for(let f=0,g,h;f<a.length;f++)if((g=a[f])&&(h=g.length)){if(b){if(b>=h){b-=h;continue}g=g.slice(b,b+c);h=g.length;b=0}h>c&&(g=g.slice(0,c),h=c);if(!d.length&&h>=c)return e?Ta.call(this,g):g;d.push(g);c-=h;if(!c)break}d=1<d.length?[].concat.apply([],d):d[0];return e?Ta.call(this,d):d};function Ua(a,c,b,e){var d=e[0];if(d[0]&&d[0].query)return a[c].apply(a,d);if(!("and"!==c&&"not"!==c||a.result.length||a.await||d.suggest))return 1<e.length&&(d=e[e.length-1]),(e=d.resolve)?a.await||a.result:a;let f=[],g=0,h=0,k,m,l;var n;let u;for(c=0;c<e.length;c++)if(d=e[c]){var p=void 0;if(d.constructor===Y)p=d.await||d.result;else if(d.then||d.constructor===Array)p=d;else{g=d.limit||0;h=d.offset||0;l=d.suggest;m=d.resolve;k=(n=d.highlight&&m)||d.enrich&&m;p=d.queue;let t=d.async||p;(n=d.index)?
|
||||||
|
a.index||(a.index=n):n=a.index;if(d.query||d.tag){const r=d.field||d.pluck;r&&(n=a.index.index.get(r));if(p&&(u||a.await)){u=1;let q;const y=a.C.length,v=new Promise(function(A){q=A});(function(A,B){v.h=function(){B.index=null;B.resolve=!1;let E=t?A.searchAsync(B):A.search(B);if(E.then)return E.then(function(C){a.C[y]=C=C.result||C;q(C);return C});E=E.result||E;q(E);return E}})(n,Object.assign({},d));a.C.push(v);f[c]=v;continue}else d.resolve=!1,d.index=null,p=t?n.searchAsync(d):n.search(d),d.resolve=
|
||||||
|
m,d.index=n}else if(d.and)p=Va(d,"and",n);else if(d.or)p=Va(d,"or",n);else if(d.not)p=Va(d,"not",n);else if(d.xor)p=Va(d,"xor",n);else continue}p.await?(u=1,p=p.await):p.then?(u=1,p=p.then(function(t){return t.result||t})):p=p.result||p;f[c]=p}u&&!a.await&&(a.await=new Promise(function(t){a.return=t}));if(u){const t=Promise.all(f).then(function(r){for(let q=0;q<a.C.length;q++)if(a.C[q]===t){a.C[q]=function(){return b.call(a,r,g,h,k,m,l)};break}Wa(a)});a.C.push(t)}else if(a.await)a.C.push(function(){return b.call(a,
|
||||||
|
f,g,h,k,m,l)});else return b.call(a,f,g,h,k,m,l);return m?a.await||a.result:a}function Va(a,c,b){a=a[c];const e=a[0]||a;e.index||(e.index=b);b=new Y(e);1<a.length&&(b=b[c].apply(b,a.slice(1)));return b};Y.prototype.or=function(){return Ua(this,"or",Xa,arguments)};function Xa(a,c,b,e,d){a.length&&(this.result.length&&a.push(this.result),2>a.length?this.result=a[0]:(this.result=Ya(a,c,b,!1,this.h),b=0));d&&(this.await=null);return d?this.resolve(c,b,e):this};Y.prototype.and=function(){return Ua(this,"and",Za,arguments)};function Za(a,c,b,e,d,f){if(!f&&!this.result.length)return d?this.result:this;let g;if(a.length)if(this.result.length&&a.unshift(this.result),2>a.length)this.result=a[0];else{let h=0;for(let k=0,m,l;k<a.length;k++)if((m=a[k])&&(l=m.length))h<l&&(h=l);else if(!f){h=0;break}h?(this.result=$a(a,h,c,b,f,this.h,d),g=!0):this.result=[]}else f||(this.result=a);d&&(this.await=null);return d?this.resolve(c,b,e,g):this};Y.prototype.xor=function(){return Ua(this,"xor",ab,arguments)};
|
||||||
|
function ab(a,c,b,e,d,f){if(a.length)if(this.result.length&&a.unshift(this.result),2>a.length)this.result=a[0];else{a:{f=b;var g=this.h;const h=[],k=I();let m=0;for(let l=0,n;l<a.length;l++)if(n=a[l]){m<n.length&&(m=n.length);for(let u=0,p;u<n.length;u++)if(p=n[u])for(let t=0,r;t<p.length;t++)r=p[t],k[r]=k[r]?2:1}for(let l=0,n,u=0;l<m;l++)for(let p=0,t;p<a.length;p++)if(t=a[p])if(n=t[l])for(let r=0,q;r<n.length;r++)if(q=n[r],1===k[q])if(f)f--;else if(d){if(h.push(q),h.length===c){a=h;break a}}else{const y=
|
||||||
|
l+(p?g:0);h[y]||(h[y]=[]);h[y].push(q);if(++u===c){a=h;break a}}a=h}this.result=a;g=!0}else f||(this.result=a);d&&(this.await=null);return d?this.resolve(c,b,e,g):this};Y.prototype.not=function(){return Ua(this,"not",bb,arguments)};
|
||||||
|
function bb(a,c,b,e,d,f){if(!f&&!this.result.length)return d?this.result:this;if(a.length&&this.result.length){a:{f=b;var g=[];a=new Set(a.flat().flat());for(let h=0,k,m=0;h<this.result.length;h++)if(k=this.result[h])for(let l=0,n;l<k.length;l++)if(n=k[l],!a.has(n))if(f)f--;else if(d){if(g.push(n),g.length===c){a=g;break a}}else if(g[h]||(g[h]=[]),g[h].push(n),++m===c){a=g;break a}a=g}this.result=a;g=!0}d&&(this.await=null);return d?this.resolve(c,b,e,g):this};function Y(a,c){if(!this||this.constructor!==Y)return new Y(a,c);let b=0,e,d,f;if(a&&a.index){const g=a;c=g.index;b=g.boost||0;if(g.query){const h=g.resolve;a=g.async||g.queue;g.resolve=!1;g.index=null;a=a?c.searchAsync(g):c.search(g);g.resolve=h;g.index=c;a=a.result||a}else a=[]}if(a&&a.then){const g=this;a=a.then(function(h){g.C[0]=g.result=h.result||h;Wa(g)});e=[a];a=[];d=new Promise(function(h){f=h})}this.index=c||null;this.result=a||[];this.h=b;this.C=e||[];this.await=d||null;this.return=f||
|
||||||
|
null}Y.prototype.limit=function(a){if(this.result.length){const c=[];for(let b=0,e;b<this.result.length;b++)if(e=this.result[b])if(e.length<=a){if(c[b]=e,a-=e.length,!a)break}else{c[b]=e.slice(0,a);break}this.result=c}return this};Y.prototype.offset=function(a){if(this.result.length){const c=[];for(let b=0,e;b<this.result.length;b++)if(e=this.result[b])e.length<=a?a-=e.length:(c[b]=e.slice(a),a=0);this.result=c}return this};Y.prototype.boost=function(a){this.h+=a;return this};
|
||||||
|
function Wa(a,c){let b=a.result;for(let d=0,f;d<a.C.length;d++)if(f=a.C[d])if("function"===typeof f)b=f(),a.C[d]=b=b.result||b,d--;else if(f.h)b=f.h(),a.C[d]=b=b.result||b,d--;else if(f.then)return a.await;const e=a.return;a.result=b;a.C=[];a.await=null;a.return=null;c||e(b);return b}
|
||||||
|
Y.prototype.resolve=function(a,c,b,e){let d=this.await?Wa(this,!0):this.result;if(d.then){const f=this;return d.then(function(){return f.resolve(a,c,b)})}d.length&&("object"===typeof a&&(b=a.enrich,c=a.offset,a=a.limit),d=e?b?Ta.call(this.index,d):d:Sa.call(this.index,d,a||100,c,b));e=this.return;this.return=this.await=this.C=this.result=this.index=null;e&&e(d);return d};function $a(a,c,b,e,d,f,g){const h=a.length;let k=[],m,l;m=I();for(let n=0,u,p,t,r;n<c;n++)for(let q=0;q<h;q++)if(t=a[q],n<t.length&&(u=t[n]))for(let y=0;y<u.length;y++){p=u[y];(l=m[p])?m[p]++:(l=0,m[p]=1);r=k[l]||(k[l]=[]);if(!g){let v=n+(q||!d?0:f||0);r=r[v]||(r[v]=[])}r.push(p);if(g&&b&&l===h-1&&r.length-e===b)return e?r.slice(e):r}if(a=k.length)if(d)k=1<k.length?Ya(k,b,e,g,f):(k=k[0])&&b&&k.length>b||e?k.slice(e,b+e):k;else{if(a<h)return[];k=k[a-1];if(b||e)if(g){if(k.length>b||e)k=k.slice(e,b+
|
||||||
|
e)}else{d=[];for(let n=0,u;n<k.length;n++)if(u=k[n])if(e&&u.length>e)e-=u.length;else{if(b&&u.length>b||e)u=u.slice(e,b+e),b-=u.length,e&&(e-=u.length);d.push(u);if(!b)break}k=d}}return k}
|
||||||
|
function Ya(a,c,b,e,d){const f=[],g=I();let h;var k=a.length;let m;if(e)for(d=k-1;0<=d;d--){if(m=(e=a[d])&&e.length)for(k=0;k<m;k++)if(h=e[k],!g[h])if(g[h]=1,b)b--;else if(f.push(h),f.length===c)return f}else for(let l=k-1,n,u=0;0<=l;l--){n=a[l];for(let p=0;p<n.length;p++)if(m=(e=n[p])&&e.length)for(let t=0;t<m;t++)if(h=e[t],!g[h])if(g[h]=1,b)b--;else{let r=(p+(l<k-1?d||0:0))/(l+1)|0;(f[r]||(f[r]=[])).push(h);if(++u===c)return f}}return f}
|
||||||
|
function cb(a,c,b){const e=I(),d=[];for(let f=0,g;f<c.length;f++){g=c[f];for(let h=0;h<g.length;h++)e[g[h]]=1}if(b)for(let f=0,g;f<a.length;f++)g=a[f],e[g]&&(d.push(g),e[g]=0);else for(let f=0,g,h;f<a.result.length;f++)for(g=a.result[f],c=0;c<g.length;c++)h=g[c],e[h]&&((d[f]||(d[f]=[])).push(h),e[h]=0);return d};I();function eb(a,c,b,e,d){let f,g,h;"string"===typeof d?(f=d,d=""):f=d.template;g=f.indexOf("$1");h=f.substring(g+2);g=f.substring(0,g);let k=d&&d.boundary,m=!d||!1!==d.clip,l=d&&d.merge&&h&&g&&new RegExp(h+" "+g,"g");d=d&&d.ellipsis;var n=0;if("object"===typeof d){var u=d.template;n=u.length-2;d=d.pattern}"string"!==typeof d&&(d=!1===d?"":"...");n&&(d=u.replace("$1",d));u=d.length-n;let p,t;"object"===typeof k&&(p=k.before,0===p&&(p=-1),t=k.after,0===t&&(t=-1),k=k.total||9E5);n=new Map;for(let Oa=0,
|
||||||
|
da,db,oa;Oa<c.length;Oa++){let pa;if(e)pa=c,oa=e;else{var r=c[Oa];oa=r.field;if(!oa)continue;pa=r.result}db=b.get(oa);da=db.encoder;r=n.get(da);"string"!==typeof r&&(r=da.encode(a),n.set(da,r));for(let xa=0;xa<pa.length;xa++){var q=pa[xa].doc;if(!q)continue;q=ea(q,oa);if(!q)continue;var y=q.trim().split(/\s+/);if(!y.length)continue;q="";var v=[];let ya=[];var A=-1,B=-1,E=0;for(var C=0;C<y.length;C++){var F=y[C],z=da.encode(F);z=1<z.length?z.join(" "):z[0];let x;if(z&&F){var D=F.length,K=(da.split?
|
||||||
|
F.replace(da.split,""):F).length-z.length,G="",N=0;for(var O=0;O<r.length;O++){var P=r[O];if(P){var L=P.length;L+=K;N&&L<=N||(P=z.indexOf(P),-1<P&&(G=(P?F.substring(0,P):"")+g+F.substring(P,P+L)+h+(P+L<D?F.substring(P+L):""),N=L,x=!0))}}G&&(k&&(0>A&&(A=q.length+(q?1:0)),B=q.length+(q?1:0)+G.length,E+=D,ya.push(v.length),v.push({match:G})),q+=(q?" ":"")+G)}if(!x)F=y[C],q+=(q?" ":"")+F,k&&v.push({text:F});else if(k&&E>=k)break}E=ya.length*(f.length-2);if(p||t||k&&q.length-E>k)if(E=k+E-2*u,C=B-A,0<p&&
|
||||||
|
(C+=p),0<t&&(C+=t),C<=E)y=p?A-(0<p?p:0):A-((E-C)/2|0),v=t?B+(0<t?t:0):y+E,m||(0<y&&" "!==q.charAt(y)&&" "!==q.charAt(y-1)&&(y=q.indexOf(" ",y),0>y&&(y=0)),v<q.length&&" "!==q.charAt(v-1)&&" "!==q.charAt(v)&&(v=q.lastIndexOf(" ",v),v<B?v=B:++v)),q=(y?d:"")+q.substring(y,v)+(v<q.length?d:"");else{B=[];A={};E={};C={};F={};z={};G=K=D=0;for(O=N=1;;){var U=void 0;for(let x=0,J;x<ya.length;x++){J=ya[x];if(G)if(K!==G){if(C[x+1])continue;J+=G;if(A[J]){D-=u;E[x+1]=1;C[x+1]=1;continue}if(J>=v.length-1){if(J>=
|
||||||
|
v.length){C[x+1]=1;J>=y.length&&(E[x+1]=1);continue}D-=u}q=v[J].text;if(L=t&&z[x])if(0<L){if(q.length>L)if(C[x+1]=1,m)q=q.substring(0,L);else continue;(L-=q.length)||(L=-1);z[x]=L}else{C[x+1]=1;continue}if(D+q.length+1<=k)q=" "+q,B[x]+=q;else if(m)U=k-D-1,0<U&&(q=" "+q.substring(0,U),B[x]+=q),C[x+1]=1;else{C[x+1]=1;continue}}else{if(C[x])continue;J-=K;if(A[J]){D-=u;C[x]=1;E[x]=1;continue}if(0>=J){if(0>J){C[x]=1;E[x]=1;continue}D-=u}q=v[J].text;if(L=p&&F[x])if(0<L){if(q.length>L)if(C[x]=1,m)q=q.substring(q.length-
|
||||||
|
L);else continue;(L-=q.length)||(L=-1);F[x]=L}else{C[x]=1;continue}if(D+q.length+1<=k)q+=" ",B[x]=q+B[x];else if(m)U=q.length+1-(k-D),0<=U&&U<q.length&&(q=q.substring(U)+" ",B[x]=q+B[x]),C[x]=1;else{C[x]=1;continue}}else{q=v[J].match;p&&(F[x]=p);t&&(z[x]=t);x&&D++;let Pa;J?!x&&u&&(D+=u):(E[x]=1,C[x]=1);J>=y.length-1?Pa=1:J<v.length-1&&v[J+1].match?Pa=1:u&&(D+=u);D-=f.length-2;if(!x||D+q.length<=k)B[x]=q;else{U=N=O=E[x]=0;break}Pa&&(E[x+1]=1,C[x+1]=1)}D+=q.length;U=A[J]=1}if(U)K===G?G++:K++;else{K===
|
||||||
|
G?N=0:O=0;if(!N&&!O)break;N?(K++,G=K):G++}}q="";for(let x=0,J;x<B.length;x++)J=(x&&E[x]?" ":(x&&!d?" ":"")+d)+B[x],q+=J;d&&!E[B.length]&&(q+=d)}l&&(q=q.replace(l," "));pa[xa].highlight=q}if(e)break}return c};Na.prototype.search=function(a,c,b,e){b||(!c&&ba(a)?(b=a,a=""):ba(c)&&(b=c,c=0));let d=[];var f=[];let g;let h,k,m,l,n;let u=0,p=!0,t;if(b){b.constructor===Array&&(b={index:b});a=b.query||a;g=b.pluck;h=b.merge;m=b.boost;n=g||b.field||(n=b.index)&&(n.index?null:n);var r=this.tag&&b.tag;k=b.suggest;p=!1!==b.resolve;l=b.cache;t=p&&this.store&&b.highlight;var q=!!t||p&&this.store&&b.enrich;c=b.limit||c;var y=b.offset||0;c||(c=p?100:0);if(r&&(!this.db||!e)){r.constructor!==Array&&(r=[r]);var v=[];for(let F=
|
||||||
|
0,z;F<r.length;F++)if(z=r[F],z.field&&z.tag){var A=z.tag;if(A.constructor===Array)for(var B=0;B<A.length;B++)v.push(z.field,A[B]);else v.push(z.field,A)}else{A=Object.keys(z);for(let D=0,K,G;D<A.length;D++)if(K=A[D],G=z[K],G.constructor===Array)for(B=0;B<G.length;B++)v.push(K,G[B]);else v.push(K,G)}r=v;if(!a){f=[];if(v.length)for(r=0;r<v.length;r+=2){if(this.db){e=this.index.get(v[r]);if(!e)continue;f.push(e=e.db.tag(v[r+1],c,y,q))}else e=fb.call(this,v[r],v[r+1],c,y,q);d.push(p?{field:v[r],tag:v[r+
|
||||||
|
1],result:e}:[e])}if(f.length){const F=this;return Promise.all(f).then(function(z){for(let D=0;D<z.length;D++)p?d[D].result=z[D]:d[D]=z[D];return p?d:new Y(1<d.length?$a(d,1,0,0,k,m):d[0],F)})}return p?d:new Y(1<d.length?$a(d,1,0,0,k,m):d[0],this)}}p||g||!(n=n||this.field)||(M(n)?g=n:(n.constructor===Array&&1===n.length&&(n=n[0]),g=n.field||n.index));n&&n.constructor!==Array&&(n=[n])}n||(n=this.field);let E;v=(this.worker||this.db)&&!e&&[];for(let F=0,z,D,K;F<n.length;F++){D=n[F];if(this.db&&this.tag&&
|
||||||
|
!this.G[F])continue;let G;M(D)||(G=D,D=G.field,a=G.query||a,c=aa(G.limit,c),y=aa(G.offset,y),k=aa(G.suggest,k),t=p&&this.store&&aa(G.highlight,t),q=!!t||p&&this.store&&aa(G.enrich,q),l=aa(G.cache,l));if(e)z=e[F];else{A=G||b||{};B=A.enrich;var C=this.index.get(D);r&&(this.db&&(A.tag=r,E=C.db.support_tag_search,A.field=n),!E&&B&&(A.enrich=!1));z=l?C.searchCache(a,c,A):C.search(a,c,A);B&&(A.enrich=B);if(v){v[F]=z;continue}}K=(z=z.result||z)&&z.length;if(r&&K){A=[];B=0;if(this.db&&e){if(!E)for(C=n.length;C<
|
||||||
|
e.length;C++){let N=e[C];if(N&&N.length)B++,A.push(N);else if(!k)return p?d:new Y(d,this)}}else for(let N=0,O,P;N<r.length;N+=2){O=this.tag.get(r[N]);if(!O)if(k)continue;else return p?d:new Y(d,this);if(P=(O=O&&O.get(r[N+1]))&&O.length)B++,A.push(O);else if(!k)return p?d:new Y(d,this)}if(B){z=cb(z,A,p);K=z.length;if(!K&&!k)return p?z:new Y(z,this);B--}}if(K)f[u]=D,d.push(z),u++;else if(1===n.length)return p?d:new Y(d,this)}if(v){if(this.db&&r&&r.length&&!E)for(q=0;q<r.length;q+=2){f=this.index.get(r[q]);
|
||||||
|
if(!f)if(k)continue;else return p?d:new Y(d,this);v.push(f.db.tag(r[q+1],c,y,!1))}const F=this;return Promise.all(v).then(function(z){b&&(b.resolve=p);z.length&&(z=F.search(a,c,b,z));return z})}if(!u)return p?d:new Y(d,this);if(g&&(!q||!this.store))return d=d[0],p?d:new Y(d,this);v=[];for(y=0;y<f.length;y++){r=d[y];q&&r.length&&"undefined"===typeof r[0].doc&&(this.db?v.push(r=this.index.get(this.field[0]).db.enrich(r)):r=Ta.call(this,r));if(g)return p?t?eb(a,r,this.index,g,t):r:new Y(r,this);d[y]=
|
||||||
|
{field:f[y],result:r}}if(q&&this.db&&v.length){const F=this;return Promise.all(v).then(function(z){for(let D=0;D<z.length;D++)d[D].result=z[D];t&&(d=eb(a,d,F.index,g,t));return h?gb(d):d})}t&&(d=eb(a,d,this.index,g,t));return h?gb(d):d};
|
||||||
|
function gb(a){const c=[],b=I(),e=I();for(let d=0,f,g,h,k,m,l,n;d<a.length;d++){f=a[d];g=f.field;h=f.result;for(let u=0;u<h.length;u++)if(m=h[u],"object"!==typeof m?m={id:k=m}:k=m.id,(l=b[k])?l.push(g):(m.field=b[k]=[g],c.push(m)),n=m.highlight)l=e[k],l||(e[k]=l={},m.highlight=l),l[g]=n}return c}function fb(a,c,b,e,d){a=this.tag.get(a);if(!a)return[];a=a.get(c);if(!a)return[];c=a.length-e;if(0<c){if(b&&c>b||e)a=a.slice(e,e+b);d&&(a=Ta.call(this,a))}return a}
|
||||||
|
function Ta(a){if(!this||!this.store)return a;if(this.db)return this.index.get(this.field[0]).db.enrich(a);const c=Array(a.length);for(let b=0,e;b<a.length;b++)e=a[b],c[b]={id:e,doc:this.store.get(e)};return c};function Na(a){if(!this||this.constructor!==Na)return new Na(a);const c=a.document||a.doc||a;let b,e;this.G=[];this.field=[];this.K=[];this.key=(b=c.key||c.id)&&hb(b,this.K)||"id";(e=a.keystore||0)&&(this.keystore=e);this.fastupdate=!!a.fastupdate;this.reg=!this.fastupdate||a.worker||a.db?e?new T(e):new Set:e?new S(e):new Map;this.D=(b=c.store||null)&&b&&!0!==b&&[];this.store=b&&(e?new S(e):new Map);this.cache=(b=a.cache||null)&&new R(b);a.cache=!1;this.worker=a.worker||!1;this.priority=a.priority||
|
||||||
|
4;this.index=ib.call(this,a,c);this.tag=null;if(b=c.tag)if("string"===typeof b&&(b=[b]),b.length){this.tag=new Map;this.F=[];this.P=[];for(let d=0,f,g;d<b.length;d++){f=b[d];g=f.field||f;if(!g)throw Error("The tag field from the document descriptor is undefined.");f.custom?this.F[d]=f.custom:(this.F[d]=hb(g,this.K),f.filter&&("string"===typeof this.F[d]&&(this.F[d]=new String(this.F[d])),this.F[d].J=f.filter));this.P[d]=g;this.tag.set(g,new Map)}}if(this.worker){this.fastupdate=!1;a=[];for(const d of this.index.values())d.then&&
|
||||||
|
a.push(d);if(a.length){const d=this;return Promise.all(a).then(function(f){let g=0;for(const h of d.index.entries()){const k=h[0];let m=h[1];m.then&&(m=f[g],d.index.set(k,m),g++)}return d})}}else a.db&&(this.fastupdate=!1,this.mount(a.db))}w=Na.prototype;
|
||||||
|
w.mount=function(a){let c=this.field;if(this.tag)for(let f=0,g;f<this.P.length;f++){g=this.P[f];var b=void 0;this.index.set(g,b=new V({},this.reg));c===this.field&&(c=c.slice(0));c.push(g);b.tag=this.tag.get(g)}b=[];const e={db:a.db,type:a.type,fastupdate:a.fastupdate};for(let f=0,g,h;f<c.length;f++){e.field=h=c[f];g=this.index.get(h);const k=new a.constructor(a.id,e);k.id=a.id;b[f]=k.mount(g);g.document=!0;f?g.bypass=!0:g.store=this.store}const d=this;return this.db=Promise.all(b).then(function(){d.db=
|
||||||
|
!0})};w.commit=async function(a,c){const b=[];for(const e of this.index.values())b.push(e.commit(a,c));await Promise.all(b);this.reg.clear()};w.destroy=function(){const a=[];for(const c of this.index.values())a.push(c.destroy());return Promise.all(a)};
|
||||||
|
function ib(a,c){const b=new Map;let e=c.index||c.field||c;M(e)&&(e=[e]);for(let f=0,g,h;f<e.length;f++){g=e[f];M(g)||(h=g,g=g.field);h=ba(h)?Object.assign({},a,h):a;if(this.worker){var d=void 0;d=(d=h.encoder)&&d.encode?d:new la("string"===typeof d?va[d]:d||{});d=new La(h,d);b.set(g,d)}this.worker||b.set(g,new V(h,this.reg));h.custom?this.G[f]=h.custom:(this.G[f]=hb(g,this.K),h.filter&&("string"===typeof this.G[f]&&(this.G[f]=new String(this.G[f])),this.G[f].J=h.filter));this.field[f]=g}if(this.D){a=
|
||||||
|
c.store;M(a)&&(a=[a]);for(let f=0,g,h;f<a.length;f++)g=a[f],h=g.field||g,g.custom?(this.D[f]=g.custom,g.custom.U=h):(this.D[f]=hb(h,this.K),g.filter&&("string"===typeof this.D[f]&&(this.D[f]=new String(this.D[f])),this.D[f].J=g.filter))}return b}function hb(a,c){const b=a.split(":");let e=0;for(let d=0;d<b.length;d++)a=b[d],"]"===a[a.length-1]&&(a=a.substring(0,a.length-2))&&(c[e]=!0),a&&(b[e++]=a);e<b.length&&(b.length=e);return 1<e?b:b[0]}w.append=function(a,c){return this.add(a,c,!0)};
|
||||||
|
w.update=function(a,c){return this.remove(a).add(a,c)};w.remove=function(a){ba(a)&&(a=ea(a,this.key));for(var c of this.index.values())c.remove(a,!0);if(this.reg.has(a)){if(this.tag&&!this.fastupdate)for(let b of this.tag.values())for(let e of b){c=e[0];const d=e[1],f=d.indexOf(a);-1<f&&(1<d.length?d.splice(f,1):b.delete(c))}this.store&&this.store.delete(a);this.reg.delete(a)}this.cache&&this.cache.remove(a);return this};
|
||||||
|
w.clear=function(){const a=[];for(const c of this.index.values()){const b=c.clear();b.then&&a.push(b)}if(this.tag)for(const c of this.tag.values())c.clear();this.store&&this.store.clear();this.cache&&this.cache.clear();return a.length?Promise.all(a):this};w.contain=function(a){return this.db?this.index.get(this.field[0]).db.has(a):this.reg.has(a)};w.cleanup=function(){for(const a of this.index.values())a.cleanup();return this};
|
||||||
|
w.get=function(a){return this.db?this.index.get(this.field[0]).db.enrich(a).then(function(c){return c[0]&&c[0].doc||null}):this.store.get(a)||null};w.set=function(a,c){"object"===typeof a&&(c=a,a=ea(c,this.key));this.store.set(a,c);return this};w.searchCache=ma;w.export=jb;w.import=kb;Fa(Na.prototype);function lb(a,c=0){let b=[],e=[];c&&(c=25E4/c*5E3|0);for(const d of a.entries())e.push(d),e.length===c&&(b.push(e),e=[]);e.length&&b.push(e);return b}function mb(a,c){c||(c=new Map);for(let b=0,e;b<a.length;b++)e=a[b],c.set(e[0],e[1]);return c}function nb(a,c=0){let b=[],e=[];c&&(c=25E4/c*1E3|0);for(const d of a.entries())e.push([d[0],lb(d[1])[0]]),e.length===c&&(b.push(e),e=[]);e.length&&b.push(e);return b}
|
||||||
|
function ob(a,c){c||(c=new Map);for(let b=0,e,d;b<a.length;b++)e=a[b],d=c.get(e[0]),c.set(e[0],mb(e[1],d));return c}function pb(a){let c=[],b=[];for(const e of a.keys())b.push(e),25E4===b.length&&(c.push(b),b=[]);b.length&&c.push(b);return c}function qb(a,c){c||(c=new Set);for(let b=0;b<a.length;b++)c.add(a[b]);return c}
|
||||||
|
function rb(a,c,b,e,d,f,g=0){const h=e&&e.constructor===Array;var k=h?e.shift():e;if(!k)return this.export(a,c,d,f+1);if((k=a((c?c+".":"")+(g+1)+"."+b,JSON.stringify(k)))&&k.then){const m=this;return k.then(function(){return rb.call(m,a,c,b,h?e:null,d,f,g+1)})}return rb.call(this,a,c,b,h?e:null,d,f,g+1)}
|
||||||
|
function jb(a,c,b=0,e=0){if(b<this.field.length){const g=this.field[b];if((c=this.index.get(g).export(a,g,b,e=1))&&c.then){const h=this;return c.then(function(){return h.export(a,g,b+1)})}return this.export(a,g,b+1)}let d,f;switch(e){case 0:d="reg";f=pb(this.reg);c=null;break;case 1:d="tag";f=this.tag&&nb(this.tag,this.reg.size);c=null;break;case 2:d="doc";f=this.store&&lb(this.store);c=null;break;default:return}return rb.call(this,a,c,d,f,b,e)}
|
||||||
|
function kb(a,c){var b=a.split(".");"json"===b[b.length-1]&&b.pop();const e=2<b.length?b[0]:"";b=2<b.length?b[2]:b[1];if(this.worker&&e)return this.index.get(e).import(a);if(c){"string"===typeof c&&(c=JSON.parse(c));if(e)return this.index.get(e).import(b,c);switch(b){case "reg":this.fastupdate=!1;this.reg=qb(c,this.reg);for(let d=0,f;d<this.field.length;d++)f=this.index.get(this.field[d]),f.fastupdate=!1,f.reg=this.reg;if(this.worker){c=[];for(const d of this.index.values())c.push(d.import(a));return Promise.all(c)}break;
|
||||||
|
case "tag":this.tag=ob(c,this.tag);break;case "doc":this.store=mb(c,this.store)}}}function sb(a,c){let b="";for(const e of a.entries()){a=e[0];const d=e[1];let f="";for(let g=0,h;g<d.length;g++){h=d[g]||[""];let k="";for(let m=0;m<h.length;m++)k+=(k?",":"")+("string"===c?'"'+h[m]+'"':h[m]);k="["+k+"]";f+=(f?",":"")+k}f='["'+a+'",['+f+"]]";b+=(b?",":"")+f}return b};V.prototype.remove=function(a,c){const b=this.reg.size&&(this.fastupdate?this.reg.get(a):this.reg.has(a));if(b){if(this.fastupdate)for(let e=0,d,f;e<b.length;e++){if((d=b[e])&&(f=d.length))if(d[f-1]===a)d.pop();else{const g=d.indexOf(a);0<=g&&d.splice(g,1)}}else tb(this.map,a),this.depth&&tb(this.ctx,a);c||this.reg.delete(a)}this.db&&(this.commit_task.push({del:a}),this.S&&ub(this));this.cache&&this.cache.remove(a);return this};
|
||||||
|
function tb(a,c){let b=0;var e="undefined"===typeof c;if(a.constructor===Array)for(let d=0,f,g,h;d<a.length;d++){if((f=a[d])&&f.length){if(e)return 1;g=f.indexOf(c);if(0<=g){if(1<f.length)return f.splice(g,1),1;delete a[d];if(b)return 1;h=1}else{if(h)return 1;b++}}}else for(let d of a.entries())e=d[0],tb(d[1],c)?b++:a.delete(e);return b};const vb={memory:{resolution:1},performance:{resolution:3,fastupdate:!0,context:{depth:1,resolution:1}},match:{tokenize:"forward"},score:{resolution:9,context:{depth:2,resolution:3}}};V.prototype.add=function(a,c,b,e){if(c&&(a||0===a)){if(!e&&!b&&this.reg.has(a))return this.update(a,c);e=this.depth;c=this.encoder.encode(c,!e);const m=c.length;if(m){const l=I(),n=I(),u=this.resolution;for(let p=0;p<m;p++){let t=c[this.rtl?m-1-p:p];var d=t.length;if(d&&(e||!n[t])){var f=this.score?this.score(c,t,p,null,0):wb(u,m,p),g="";switch(this.tokenize){case "full":if(2<d){for(let r=0,q;r<d;r++)for(f=d;f>r;f--){g=t.substring(r,f);q=this.rtl?d-1-r:r;var h=this.score?this.score(c,t,p,g,q):wb(u,
|
||||||
|
m,p,d,q);xb(this,n,g,h,a,b)}break}case "bidirectional":case "reverse":if(1<d){for(h=d-1;0<h;h--){g=t[this.rtl?d-1-h:h]+g;var k=this.score?this.score(c,t,p,g,h):wb(u,m,p,d,h);xb(this,n,g,k,a,b)}g=""}case "forward":if(1<d){for(h=0;h<d;h++)g+=t[this.rtl?d-1-h:h],xb(this,n,g,f,a,b);break}default:if(xb(this,n,t,f,a,b),e&&1<m&&p<m-1)for(d=I(),g=this.T,f=t,h=Math.min(e+1,this.rtl?p+1:m-p),d[f]=1,k=1;k<h;k++)if((t=c[this.rtl?m-1-p-k:p+k])&&!d[t]){d[t]=1;const r=this.score?this.score(c,f,p,t,k-1):wb(g+(m/
|
||||||
|
2>g?0:1),m,p,h-1,k-1),q=this.bidirectional&&t>f;xb(this,l,q?f:t,r,a,b,q?t:f)}}}}this.fastupdate||this.reg.add(a)}else c=""}this.db&&(c||this.commit_task.push({del:a}),this.S&&ub(this));return this};
|
||||||
|
function xb(a,c,b,e,d,f,g){let h=g?a.ctx:a.map,k;if(!c[b]||g&&!(k=c[b])[g])if(g?(c=k||(c[b]=I()),c[g]=1,(k=h.get(g))?h=k:h.set(g,h=new Map)):c[b]=1,(k=h.get(b))?h=k:h.set(b,h=k=[]),h=h[e]||(h[e]=[]),!f||!h.includes(d)){if(h.length===2**31-1){c=new za(h);if(a.fastupdate)for(let m of a.reg.values())m.includes(h)&&(m[m.indexOf(h)]=c);k[e]=h=c}h.push(d);a.fastupdate&&((e=a.reg.get(d))?e.push(h):a.reg.set(d,[h]))}}
|
||||||
|
function wb(a,c,b,e,d){return b&&1<a?c+(e||0)<=a?b+(d||0):(a-1)/(c+(e||0))*(b+(d||0))+1|0:0};V.prototype.search=function(a,c,b){b||(c||"object"!==typeof a?"object"===typeof c&&(b=c,c=0):(b=a,a=""));if(b&&b.cache)return b.cache=!1,a=this.searchCache(a,c,b),b.cache=!0,a;let e=[],d,f,g,h=0,k,m,l,n,u;b&&(a=b.query||a,c=b.limit||c,h=b.offset||0,f=b.context,g=b.suggest,u=(k=b.resolve)&&b.enrich,l=b.boost,n=b.resolution,m=this.db&&b.tag);"undefined"===typeof k&&(k=this.resolve);f=this.depth&&!1!==f;let p=this.encoder.encode(a,!f);d=p.length;c=c||(k?100:0);if(1===d)return yb.call(this,p[0],"",c,
|
||||||
|
h,k,u,m);if(2===d&&f&&!g)return yb.call(this,p[1],p[0],c,h,k,u,m);let t=I(),r=0,q;f&&(q=p[0],r=1);n||0===n||(n=q?this.T:this.resolution);if(this.db){if(this.db.search&&(b=this.db.search(this,p,c,h,g,k,u,m),!1!==b))return b;const y=this;return async function(){for(let v,A;r<d;r++){if((A=p[r])&&!t[A]){t[A]=1;v=await zb(y,A,q,0,0,!1,!1);if(v=Ab(v,e,g,n)){e=v;break}q&&(g&&v&&e.length||(q=A))}g&&q&&r===d-1&&!e.length&&(n=y.resolution,q="",r=-1,t=I())}return Bb(e,n,c,h,g,l,k)}()}for(let y,v;r<d;r++){if((v=
|
||||||
|
p[r])&&!t[v]){t[v]=1;y=zb(this,v,q,0,0,!1,!1);if(y=Ab(y,e,g,n)){e=y;break}q&&(g&&y&&e.length||(q=v))}g&&q&&r===d-1&&!e.length&&(n=this.resolution,q="",r=-1,t=I())}return Bb(e,n,c,h,g,l,k)};function Bb(a,c,b,e,d,f,g){let h=a.length,k=a;if(1<h)k=$a(a,c,b,e,d,f,g);else if(1===h)return g?Sa.call(null,a[0],b,e):new Y(a[0],this);return g?k:new Y(k,this)}
|
||||||
|
function yb(a,c,b,e,d,f,g){a=zb(this,a,c,b,e,d,f,g);return this.db?a.then(function(h){return d?h||[]:new Y(h,this)}):a&&a.length?d?Sa.call(this,a,b,e):new Y(a,this):d?[]:new Y([],this)}function Ab(a,c,b,e){let d=[];if(a&&a.length){if(a.length<=e){c.push(a);return}for(let f=0,g;f<e;f++)if(g=a[f])d[f]=g;if(d.length){c.push(d);return}}if(!b)return d}
|
||||||
|
function zb(a,c,b,e,d,f,g,h){let k;b&&(k=a.bidirectional&&c>b)&&(k=b,b=c,c=k);if(a.db)return a.db.get(c,b,e,d,f,g,h);a=b?(a=a.ctx.get(b))&&a.get(c):a.map.get(c);return a};function V(a,c){if(!this||this.constructor!==V)return new V(a);if(a){var b=M(a)?a:a.preset;b&&(a=Object.assign({},vb[b],a))}else a={};b=a.context;const e=!0===b?{depth:1}:b||{},d=M(a.encoder)?va[a.encoder]:a.encode||a.encoder||{};this.encoder=d.encode?d:"object"===typeof d?new la(d):{encode:d};this.resolution=a.resolution||9;this.tokenize=b=(b=a.tokenize)&&"default"!==b&&"exact"!==b&&b||"strict";this.depth="strict"===b&&e.depth||0;this.bidirectional=!1!==e.bidirectional;this.fastupdate=!!a.fastupdate;
|
||||||
|
this.score=a.score||null;(b=a.keystore||0)&&(this.keystore=b);this.map=b?new S(b):new Map;this.ctx=b?new S(b):new Map;this.reg=c||(this.fastupdate?b?new S(b):new Map:b?new T(b):new Set);this.T=e.resolution||3;this.rtl=d.rtl||a.rtl||!1;this.cache=(b=a.cache||null)&&new R(b);this.resolve=!1!==a.resolve;if(b=a.db)this.db=this.mount(b);this.S=!1!==a.commit;this.commit_task=[];this.commit_timer=null;this.priority=a.priority||4}w=V.prototype;
|
||||||
|
w.mount=function(a){this.commit_timer&&(clearTimeout(this.commit_timer),this.commit_timer=null);return a.mount(this)};w.commit=function(a,c){this.commit_timer&&(clearTimeout(this.commit_timer),this.commit_timer=null);return this.db.commit(this,a,c)};w.destroy=function(){this.commit_timer&&(clearTimeout(this.commit_timer),this.commit_timer=null);return this.db.destroy()};function ub(a){a.commit_timer||(a.commit_timer=setTimeout(function(){a.commit_timer=null;a.db.commit(a,void 0,void 0)},1))}
|
||||||
|
w.clear=function(){this.map.clear();this.ctx.clear();this.reg.clear();this.cache&&this.cache.clear();this.db&&(this.commit_timer&&clearTimeout(this.commit_timer),this.commit_timer=null,this.commit_task=[{clear:!0}]);return this};w.append=function(a,c){return this.add(a,c,!0)};w.contain=function(a){return this.db?this.db.has(a):this.reg.has(a)};w.update=function(a,c){const b=this,e=this.remove(a);return e&&e.then?e.then(()=>b.add(a,c)):this.add(a,c)};
|
||||||
|
w.cleanup=function(){if(!this.fastupdate)return this;tb(this.map);this.depth&&tb(this.ctx);return this};w.searchCache=ma;w.export=function(a,c,b=0,e=0){let d,f;switch(e){case 0:d="reg";f=pb(this.reg);break;case 1:d="cfg";f=null;break;case 2:d="map";f=lb(this.map,this.reg.size);break;case 3:d="ctx";f=nb(this.ctx,this.reg.size);break;default:return}return rb.call(this,a,c,d,f,b,e)};
|
||||||
|
w.import=function(a,c){if(c)switch("string"===typeof c&&(c=JSON.parse(c)),a=a.split("."),"json"===a[a.length-1]&&a.pop(),3===a.length&&a.shift(),a=1<a.length?a[1]:a[0],a){case "reg":this.fastupdate=!1;this.reg=qb(c,this.reg);break;case "map":this.map=mb(c,this.map);break;case "ctx":this.ctx=ob(c,this.ctx)}};
|
||||||
|
w.serialize=function(a=!0){let c="",b="",e="";if(this.reg.size){let f;for(var d of this.reg.keys())f||(f=typeof d),c+=(c?",":"")+("string"===f?'"'+d+'"':d);c="index.reg=new Set(["+c+"]);";b=sb(this.map,f);b="index.map=new Map(["+b+"]);";for(const g of this.ctx.entries()){d=g[0];let h=sb(g[1],f);h="new Map(["+h+"])";h='["'+d+'",'+h+"]";e+=(e?",":"")+h}e="index.ctx=new Map(["+e+"]);"}return a?"function inject(index){"+c+b+e+"}":c+b+e};Fa(V.prototype);const Cb="undefined"!==typeof window&&(window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB),Db=["map","ctx","tag","reg","cfg"],Eb=I();
|
||||||
|
function Fb(a,c={}){if(!this||this.constructor!==Fb)return new Fb(a,c);"object"===typeof a&&(c=a,a=a.name);a||console.info("Default storage space was used, because a name was not passed.");this.id="flexsearch"+(a?":"+a.toLowerCase().replace(/[^a-z0-9_\-]/g,""):"");this.field=c.field?c.field.toLowerCase().replace(/[^a-z0-9_\-]/g,""):"";this.type=c.type;this.fastupdate=this.support_tag_search=!1;this.db=null;this.h={}}w=Fb.prototype;w.mount=function(a){if(a.index)return a.mount(this);a.db=this;return this.open()};
|
||||||
|
w.open=function(){if(this.db)return this.db;let a=this;navigator.storage&&navigator.storage.persist();Eb[a.id]||(Eb[a.id]=[]);Eb[a.id].push(a.field);const c=Cb.open(a.id,1);c.onupgradeneeded=function(){const b=a.db=this.result;for(let e=0,d;e<Db.length;e++){d=Db[e];for(let f=0,g;f<Eb[a.id].length;f++)g=Eb[a.id][f],b.objectStoreNames.contains(d+("reg"!==d?g?":"+g:"":""))||b.createObjectStore(d+("reg"!==d?g?":"+g:"":""))}};return a.db=Z(c,function(b){a.db=b;a.db.onversionchange=function(){a.close()}})};
|
||||||
|
w.close=function(){this.db&&this.db.close();this.db=null};w.destroy=function(){const a=Cb.deleteDatabase(this.id);return Z(a)};w.clear=function(){const a=[];for(let b=0,e;b<Db.length;b++){e=Db[b];for(let d=0,f;d<Eb[this.id].length;d++)f=Eb[this.id][d],a.push(e+("reg"!==e?f?":"+f:"":""))}const c=this.db.transaction(a,"readwrite");for(let b=0;b<a.length;b++)c.objectStore(a[b]).clear();return Z(c)};
|
||||||
|
w.get=function(a,c,b=0,e=0,d=!0,f=!1){a=this.db.transaction((c?"ctx":"map")+(this.field?":"+this.field:""),"readonly").objectStore((c?"ctx":"map")+(this.field?":"+this.field:"")).get(c?c+":"+a:a);const g=this;return Z(a).then(function(h){let k=[];if(!h||!h.length)return k;if(d){if(!b&&!e&&1===h.length)return h[0];for(let m=0,l;m<h.length;m++)if((l=h[m])&&l.length){if(e>=l.length){e-=l.length;continue}const n=b?e+Math.min(l.length-e,b):l.length;for(let u=e;u<n;u++)k.push(l[u]);e=0;if(k.length===b)break}return f?
|
||||||
|
g.enrich(k):k}return h})};w.tag=function(a,c=0,b=0,e=!1){a=this.db.transaction("tag"+(this.field?":"+this.field:""),"readonly").objectStore("tag"+(this.field?":"+this.field:"")).get(a);const d=this;return Z(a).then(function(f){if(!f||!f.length||b>=f.length)return[];if(!c&&!b)return f;f=f.slice(b,b+c);return e?d.enrich(f):f})};
|
||||||
|
w.enrich=function(a){"object"!==typeof a&&(a=[a]);const c=this.db.transaction("reg","readonly").objectStore("reg"),b=[];for(let e=0;e<a.length;e++)b[e]=Z(c.get(a[e]));return Promise.all(b).then(function(e){for(let d=0;d<e.length;d++)e[d]={id:a[d],doc:e[d]?JSON.parse(e[d]):null};return e})};w.has=function(a){a=this.db.transaction("reg","readonly").objectStore("reg").getKey(a);return Z(a).then(function(c){return!!c})};w.search=null;w.info=function(){};
|
||||||
|
w.transaction=function(a,c,b){a+="reg"!==a?this.field?":"+this.field:"":"";let e=this.h[a+":"+c];if(e)return b.call(this,e);let d=this.db.transaction(a,c);this.h[a+":"+c]=e=d.objectStore(a);const f=b.call(this,e);this.h[a+":"+c]=null;return Z(d).finally(function(){d=e=null;return f})};
|
||||||
|
w.commit=async function(a,c,b){if(c)await this.clear(),a.commit_task=[];else{let e=a.commit_task;a.commit_task=[];for(let d=0,f;d<e.length;d++)if(f=e[d],f.clear){await this.clear();c=!0;break}else e[d]=f.del;c||(b||(e=e.concat(ca(a.reg))),e.length&&await this.remove(e))}a.reg.size&&(await this.transaction("map","readwrite",function(e){for(const d of a.map){const f=d[0],g=d[1];g.length&&(c?e.put(g,f):e.get(f).onsuccess=function(){let h=this.result;var k;if(h&&h.length){const m=Math.max(h.length,g.length);
|
||||||
|
for(let l=0,n,u;l<m;l++)if((u=g[l])&&u.length){if((n=h[l])&&n.length)for(k=0;k<u.length;k++)n.push(u[k]);else h[l]=u;k=1}}else h=g,k=1;k&&e.put(h,f)})}}),await this.transaction("ctx","readwrite",function(e){for(const d of a.ctx){const f=d[0],g=d[1];for(const h of g){const k=h[0],m=h[1];m.length&&(c?e.put(m,f+":"+k):e.get(f+":"+k).onsuccess=function(){let l=this.result;var n;if(l&&l.length){const u=Math.max(l.length,m.length);for(let p=0,t,r;p<u;p++)if((r=m[p])&&r.length){if((t=l[p])&&t.length)for(n=
|
||||||
|
0;n<r.length;n++)t.push(r[n]);else l[p]=r;n=1}}else l=m,n=1;n&&e.put(l,f+":"+k)})}}}),a.store?await this.transaction("reg","readwrite",function(e){for(const d of a.store){const f=d[0],g=d[1];e.put("object"===typeof g?JSON.stringify(g):1,f)}}):a.bypass||await this.transaction("reg","readwrite",function(e){for(const d of a.reg.keys())e.put(1,d)}),a.tag&&await this.transaction("tag","readwrite",function(e){for(const d of a.tag){const f=d[0],g=d[1];g.length&&(e.get(f).onsuccess=function(){let h=this.result;
|
||||||
|
h=h&&h.length?h.concat(g):g;e.put(h,f)})}}),a.map.clear(),a.ctx.clear(),a.tag&&a.tag.clear(),a.store&&a.store.clear(),a.document||a.reg.clear())};function Gb(a,c,b){const e=a.value;let d,f=0;for(let g=0,h;g<e.length;g++){if(h=b?e:e[g]){for(let k=0,m,l;k<c.length;k++)if(l=c[k],m=h.indexOf(l),0<=m)if(d=1,1<h.length)h.splice(m,1);else{e[g]=[];break}f+=h.length}if(b)break}f?d&&a.update(e):a.delete();a.continue()}
|
||||||
|
w.remove=function(a){"object"!==typeof a&&(a=[a]);return Promise.all([this.transaction("map","readwrite",function(c){c.openCursor().onsuccess=function(){const b=this.result;b&&Gb(b,a)}}),this.transaction("ctx","readwrite",function(c){c.openCursor().onsuccess=function(){const b=this.result;b&&Gb(b,a)}}),this.transaction("tag","readwrite",function(c){c.openCursor().onsuccess=function(){const b=this.result;b&&Gb(b,a,!0)}}),this.transaction("reg","readwrite",function(c){for(let b=0;b<a.length;b++)c.delete(a[b])})])};
|
||||||
|
function Z(a,c){return new Promise((b,e)=>{a.onsuccess=a.oncomplete=function(){c&&c(this.result);c=null;b(this.result)};a.onerror=a.onblocked=e;a=null})};export default {Index:V,Charset:va,Encoder:la,Document:Na,Worker:La,Resolver:Y,IndexedDB:Fb,Language:{}};
|
||||||
|
export const Index=V;export const Charset=va;export const Encoder=la;export const Document=Na;export const Worker=La;export const Resolver=Y;export const IndexedDB=Fb;export const Language={};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { settingValues } from './settings.js';
|
||||||
|
|
||||||
// --- Html String constants ---
|
// --- Html String constants ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +46,15 @@ const REG_STRIP_TRAILING_PAREN = /(.*)(?<!\\)\)$/s;
|
|||||||
const REG_WILDCARD_WEIGHTED_TAG = /(\d+)[_\s]*::(.*?)(?=\||$)/g;
|
const REG_WILDCARD_WEIGHTED_TAG = /(\d+)[_\s]*::(.*?)(?=\||$)/g;
|
||||||
const REG_WILDCARD_SIMPLE_WORD = /[^{}_|]+/g;
|
const REG_WILDCARD_SIMPLE_WORD = /[^{}_|]+/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the input text is longer than the maximum allowed tag length.
|
||||||
|
* @param {string} text The text to check.
|
||||||
|
* @return {boolean} True if the text is longer than the maximum tag length, false otherwise.
|
||||||
|
*/
|
||||||
|
export function isLongText(text) {
|
||||||
|
return text?.length > settingValues._maxTagLength;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts Hiragana to Katakana.
|
* Converts Hiragana to Katakana.
|
||||||
* @param {string} str Input string.
|
* @param {string} str Input string.
|
||||||
|
|||||||
Reference in New Issue
Block a user