Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98d9fdfc50 | ||
|
|
504b681ab8 | ||
|
|
aca0ed6326 | ||
|
|
ab941ab329 | ||
|
|
c4e408eede | ||
|
|
f8c86203e4 | ||
|
|
5a4ded44b8 | ||
|
|
295d0c0cdc | ||
|
|
37ce793208 | ||
|
|
2386fc7aa0 | ||
|
|
937c5bb283 | ||
|
|
512dd88d7a | ||
|
|
096314933d | ||
|
|
05c890ee37 | ||
|
|
d4725dfebc | ||
|
|
c4d5eee89d | ||
|
|
fa4d87ee77 | ||
|
|
3cb5a41cc8 | ||
|
|
16a325b798 | ||
|
|
e3171940fb | ||
|
|
8db37967f8 | ||
|
|
e43e51b48a | ||
|
|
7b89683bfc | ||
|
|
72b9bd94ee | ||
|
|
8a4a94bc7e | ||
|
|
67b7b334b8 | ||
|
|
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 |
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"test": {
|
||||||
|
"NODE_OPTIONS": "--experimental-vm-modules"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,9 +12,9 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '22'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
- 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
|
||||||
|
|
||||||
@@ -42,6 +41,7 @@ When you type in a text input area, tags that partially match the text are displ
|
|||||||
- Tags are color-coded by category. The color-coding rules are the same as Danbooru.
|
- Tags are color-coded by category. The color-coding rules are the same as Danbooru.
|
||||||
- Tags that have already been entered are displayed grayed out.
|
- Tags that have already been entered are displayed grayed out.
|
||||||
- You can display Danbooru and e621 tags at the same time. You can also change the priority from the settings.
|
- You can display Danbooru and e621 tags at the same time. You can also change the priority from the settings.
|
||||||
|
- Supports autocomplete for Lora and Embedding inputs. You can enable/disable this feature in the settings.
|
||||||
|
|
||||||
## Related Tags
|
## Related Tags
|
||||||
|
|
||||||
@@ -133,6 +133,9 @@ When the browser is reloaded, you can check the list of loaded CSV files in the
|
|||||||
|
|
||||||
- **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.
|
||||||
|
- **Replace '_' with 'Space'**: Replaces underscores with spaces when inserting tags. This setting also affects related tag display.
|
||||||
|
- **Enable Loras and Embeddings**: Display Lora and Embedding in the suggestions.
|
||||||
|
- **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
|
||||||
|
|
||||||
@@ -141,9 +144,9 @@ When the browser is reloaded, you can check the list of loaded CSV files in the
|
|||||||
- **Default Display Position**: Default display position when ComfyUI starts.
|
- **Default Display Position**: Default display position when ComfyUI starts.
|
||||||
- **Related Tags Trigger Mode**: Which action will trigger displaying related tags (click only, Ctrl+click)
|
- **Related Tags Trigger Mode**: Which action will trigger displaying related tags (click only, Ctrl+click)
|
||||||
|
|
||||||
### Miscellaneous
|
### Display
|
||||||
|
|
||||||
- **Check CSV updates**: Click the "Check Now" button to check if new CSV files are available in HuggingFace and download them if necessary.
|
- **Hide Alias**: Hide/show the Alias column in autocomplete and related tags (default is show)
|
||||||
|
|
||||||
## Advanced Settings
|
## Advanced Settings
|
||||||
|
|
||||||
@@ -172,6 +175,31 @@ You can skip the check process during ComfyUI startup by following these steps:
|
|||||||
- 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.
|
- 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.
|
- 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
|
||||||
@@ -186,7 +214,9 @@ You can skip the check process during ComfyUI startup by following these steps:
|
|||||||
|
|
||||||
## 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.
|
||||||
+34
-5
@@ -17,7 +17,6 @@
|
|||||||
- カーソル位置や既存のテキストを考慮した自然なタグ挿入
|
- カーソル位置や既存のテキストを考慮した自然なタグ挿入
|
||||||
- **:art:デザイン**: ComfyUIのライトテーマとダークテーマの両方に対応
|
- **:art:デザイン**: ComfyUIのライトテーマとダークテーマの両方に対応
|
||||||
- **:pencil:ユーザーCSV**: ユーザーが用意した CSV をオートコンプリート候補に追加可能
|
- **:pencil:ユーザーCSV**: ユーザーが用意した CSV をオートコンプリート候補に追加可能
|
||||||
- **依存ライブラリゼロ**: 外部ライブラリを使用せず、すべての入力支援処理がブラウザで動作
|
|
||||||
|
|
||||||
## インストール
|
## インストール
|
||||||
|
|
||||||
@@ -40,6 +39,7 @@
|
|||||||
- タグのカテゴリ毎に色分けされます。色分けのルールは Danbooru と同じです
|
- タグのカテゴリ毎に色分けされます。色分けのルールは Danbooru と同じです
|
||||||
- 入力済みのタグはグレーアウトで表示されます
|
- 入力済みのタグはグレーアウトで表示されます
|
||||||
- Danbooruとe621のタグを同時に表示出来ます。設定から優先順位を変更できます
|
- Danbooruとe621のタグを同時に表示出来ます。設定から優先順位を変更できます
|
||||||
|
- LoraとEmbeddingの入力補完に対応しています。設定から有効・無効を切り替えられます
|
||||||
|
|
||||||
## 関連タグ
|
## 関連タグ
|
||||||
|
|
||||||
@@ -130,7 +130,10 @@ worst_quality,5,9999999,
|
|||||||
### オートコンプリート
|
### オートコンプリート
|
||||||
|
|
||||||
- **Enable Autocomplete**: オートコンプリート機能の有効化/無効化
|
- **Enable Autocomplete**: オートコンプリート機能の有効化/無効化
|
||||||
- **Max suggestions**: オートコンプリート候補の最大表示件数
|
- **Max Suggestions**: オートコンプリート候補の最大表示件数
|
||||||
|
- **Replace '_' with 'Space'**: タグ挿入時にアンダースコアをスペースに置き換えます。この設定は関連タグ表示にも影響します
|
||||||
|
- **Enable Loras and Embeddings**: LoraとEmbeddingを候補に表示する
|
||||||
|
- **Use Fast Search**: オートコンプリート候補の検索を高速な処理に切り替える(詳細は [オートコンプリートの高速検索について](#オートコンプリートの高速検索について) を確認してください)
|
||||||
|
|
||||||
### 関連タグ
|
### 関連タグ
|
||||||
|
|
||||||
@@ -139,9 +142,9 @@ worst_quality,5,9999999,
|
|||||||
- **Default Display Position**: ComfyUI起動時のデフォルト表示位置
|
- **Default Display Position**: ComfyUI起動時のデフォルト表示位置
|
||||||
- **Related Tags Trigger Mode** : 関連タグを表示する際、どの操作をトリガーとするか(クリックのみ、Ctrl+クリック)
|
- **Related Tags Trigger Mode** : 関連タグを表示する際、どの操作をトリガーとするか(クリックのみ、Ctrl+クリック)
|
||||||
|
|
||||||
### その他
|
### 表示
|
||||||
|
|
||||||
- **Check CSV updates**: 「Check Now」ボタンを押すと新しい CSV ファイルがHuggingFaceにあるか確認し、必要に応じてダウンロードを行います
|
- **Hide Alias**: オートコンプリートと関連タグで表示されるエイリアス列の非表示/表示を切り替え(デフォルトは表示です)
|
||||||
|
|
||||||
## 上級者向け設定
|
## 上級者向け設定
|
||||||
|
|
||||||
@@ -170,6 +173,30 @@ worst_quality,5,9999999,
|
|||||||
- `check_updates_on_startup` の値を再び `true` にするか、 `version` が切り替わるまでチェック処理は行われなくなります
|
- `check_updates_on_startup` の値を再び `true` にするか、 `version` が切り替わるまでチェック処理は行われなくなります
|
||||||
- `check_updates_on_startup` が `false` でも、Autocompelte Plusの設定から `Check CSV updates` のボタンを押す事で手動チェックが可能です
|
- `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) を使用しています
|
||||||
|
|
||||||
## 既知の問題
|
## 既知の問題
|
||||||
|
|
||||||
### パフォーマンス
|
### パフォーマンス
|
||||||
@@ -184,7 +211,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)
|
||||||
|
- オートコンプリートの高速なタグ検索処理の実装に利用させてもらいました
|
||||||
|
|||||||
+22
-1
@@ -1,7 +1,10 @@
|
|||||||
import os
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import folder_paths
|
||||||
import server
|
import server
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from . import downloader as dl
|
from . import downloader as dl
|
||||||
|
|
||||||
# Get the absolute path to the 'data' directory
|
# Get the absolute path to the 'data' directory
|
||||||
@@ -227,3 +230,21 @@ async def get_last_check_time(_request):
|
|||||||
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({"last_check_time": None, "error": str(e)}, status=500)
|
return web.json_response({"last_check_time": None, "error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
@server.PromptServer.instance.routes.get("/autocomplete-plus/embeddings")
|
||||||
|
async def get_embeddings(request):
|
||||||
|
"""
|
||||||
|
Returns a list of embedding files.
|
||||||
|
"""
|
||||||
|
embeddings = folder_paths.get_filename_list("embeddings")
|
||||||
|
return web.json_response(list(map(lambda a: os.path.splitext(a)[0], embeddings)))
|
||||||
|
|
||||||
|
|
||||||
|
@server.PromptServer.instance.routes.get("/autocomplete-plus/loras")
|
||||||
|
async def get_loras(request):
|
||||||
|
"""
|
||||||
|
Returns a list of lora files.
|
||||||
|
"""
|
||||||
|
loras = folder_paths.get_filename_list("loras")
|
||||||
|
return web.json_response(list(map(lambda a: os.path.splitext(a)[0], loras)))
|
||||||
|
|||||||
+11
-19
@@ -235,16 +235,7 @@ class Downloader:
|
|||||||
|
|
||||||
def _check_new_csv_from_hf_dataset(self, dataset_meta: dict, now_utc: datetime, force_check: bool = False):
|
def _check_new_csv_from_hf_dataset(self, dataset_meta: dict, now_utc: datetime, force_check: bool = False):
|
||||||
"""Checks HuggingFace for file updates and updates metadata."""
|
"""Checks HuggingFace for file updates and updates metadata."""
|
||||||
perform_hf_check = True
|
perform_hf_check = force_check
|
||||||
if not force_check and dataset_meta.get("last_remote_check_timestamp"):
|
|
||||||
try:
|
|
||||||
last_check_dt = datetime.fromisoformat(dataset_meta["last_remote_check_timestamp"])
|
|
||||||
if now_utc - last_check_dt < timedelta(days=7):
|
|
||||||
perform_hf_check = False
|
|
||||||
except (ValueError, KeyError, TypeError):
|
|
||||||
print(
|
|
||||||
"[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"]
|
||||||
@@ -303,15 +294,16 @@ class Downloader:
|
|||||||
return f"File {file_name} is missing or empty locally."
|
return f"File {file_name} is missing or empty locally."
|
||||||
|
|
||||||
# Check if the last modified date on HuggingFace is newer than the last download date
|
# Check if the last modified date on HuggingFace is newer than the last download date
|
||||||
try:
|
# Temporarily comment out until changes to remote files can be detected
|
||||||
last_download_dt = datetime.fromisoformat(file_meta_entry["last_download"])
|
# try:
|
||||||
hf_modified_dt = datetime.fromisoformat(file_meta_entry["last_modified_on_hf"])
|
# last_download_dt = datetime.fromisoformat(file_meta_entry["last_download"])
|
||||||
if hf_modified_dt > last_download_dt:
|
# hf_modified_dt = datetime.fromisoformat(file_meta_entry["last_modified_on_hf"])
|
||||||
return f"Remote file {file_name} is newer (HF: {last_download_dt}, Local Download: {hf_modified_dt})."
|
# if hf_modified_dt > last_download_dt:
|
||||||
except (ValueError, TypeError):
|
# return f"Remote file {file_name} is newer (HF: {hf_modified_dt}, Local Download: {last_download_dt})."
|
||||||
file_meta_entry["last_download"] = None
|
# except (ValueError, TypeError):
|
||||||
file_meta_entry["last_modified_on_hf"] = None
|
# file_meta_entry["last_download"] = None
|
||||||
return f"Invalid timestamp format for {file_name}. Forcing download to ensure integrity."
|
# file_meta_entry["last_modified_on_hf"] = None
|
||||||
|
# return f"Invalid timestamp format for {file_name}. Forcing download to ensure integrity."
|
||||||
|
|
||||||
# If the file is missing or empty, but the last download timestamp exists, we need to retry.
|
# If the file is missing or empty, but the last download timestamp exists, we need to retry.
|
||||||
if not check_file_valid(local_file_path) and file_meta_entry.get("last_download") is not None:
|
if not check_file_valid(local_file_path) and file_meta_entry.get("last_download") is not None:
|
||||||
|
|||||||
Generated
+1529
-739
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"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": "^30.2.0",
|
||||||
"jest": "^29.7.0",
|
"env-cmd": "^10.1.0",
|
||||||
|
"jest": "^30.2.0",
|
||||||
"stylelint": "^16.19.1",
|
"stylelint": "^16.19.1",
|
||||||
"stylelint-config-idiomatic-order": "^10.0.0",
|
"stylelint-config-idiomatic-order": "^10.0.0",
|
||||||
"stylelint-config-standard": "^38.0.0"
|
"stylelint-config-standard": "^38.0.0"
|
||||||
|
|||||||
+1
-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.2.0"
|
version = "1.6.0"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = ["",]
|
dependencies = ["",]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
import {
|
||||||
|
__test__
|
||||||
|
} from "../../web/js/autocomplete.js";
|
||||||
|
import {
|
||||||
|
TagData,
|
||||||
|
TagSource,
|
||||||
|
ModelTagSource,
|
||||||
|
autoCompleteData
|
||||||
|
} from "../../web/js/data.js";
|
||||||
|
import {
|
||||||
|
createFlexSearchDocument,
|
||||||
|
createFlexSearchDocumentForModel
|
||||||
|
} from "../../web/js/searchengine.js"
|
||||||
|
|
||||||
|
const {
|
||||||
|
searchCompletionCandidates,
|
||||||
|
sequentialSearch,
|
||||||
|
searchWithFlexSearch,
|
||||||
|
matchWord,
|
||||||
|
getCurrentPartialTag,
|
||||||
|
insertTagToTextArea
|
||||||
|
} = __test__;
|
||||||
|
|
||||||
|
|
||||||
|
// Helper function to create mock textarea element
|
||||||
|
function createMockTextarea(value, selectionStart, selectionEnd = -1) {
|
||||||
|
return {
|
||||||
|
value: value,
|
||||||
|
selectionStart: selectionStart,
|
||||||
|
selectionEnd: selectionEnd > selectionStart ? selectionEnd : selectionStart,
|
||||||
|
nodeName: 'TEXTAREA',
|
||||||
|
focus() { document.setFocus(this); },
|
||||||
|
setSelectionRange: () => { },
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 100
|
||||||
|
}),
|
||||||
|
scrollTop: 0,
|
||||||
|
scrollLeft: 0,
|
||||||
|
ownerDocument: {
|
||||||
|
defaultView: typeof window !== 'undefined' ? window : {},
|
||||||
|
documentElement: typeof document !== 'undefined' ? document.documentElement : {}
|
||||||
|
},
|
||||||
|
dispatchEvent: () => { }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test data setup function
|
||||||
|
function setupTestData() {
|
||||||
|
// Clear existing data
|
||||||
|
Object.keys(autoCompleteData).forEach(key => delete autoCompleteData[key]);
|
||||||
|
|
||||||
|
// Create mock AutocompleteData structure for each source
|
||||||
|
const sources = [...Object.values(TagSource), ...Object.values(ModelTagSource)];
|
||||||
|
|
||||||
|
sources.forEach(source => {
|
||||||
|
autoCompleteData[source] = {
|
||||||
|
flexSearchDocument: null,
|
||||||
|
sortedTags: [],
|
||||||
|
tagMap: new Map(),
|
||||||
|
aliasMap: new Map(),
|
||||||
|
cooccurrenceMap: new Map(),
|
||||||
|
isInitializing: false,
|
||||||
|
initialized: true,
|
||||||
|
baseLoadingProgress: {
|
||||||
|
cooccurrence: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add sample tag data for testing
|
||||||
|
const sampleTags = [
|
||||||
|
new TagData('1girl', 0, 1000000, ['one_girl'], TagSource.Danbooru),
|
||||||
|
new TagData('blue_hair', 0, 500000, ['blue hair'], TagSource.Danbooru),
|
||||||
|
new TagData('long_hair', 0, 800000, [], TagSource.Danbooru),
|
||||||
|
new TagData('__wildcard__', 0, 100, [], TagSource.Danbooru),
|
||||||
|
new TagData(':d', 0, 50000, [], TagSource.Danbooru),
|
||||||
|
new TagData('test_tag', 0, 1000, ['test'], TagSource.E621),
|
||||||
|
new TagData('<lora:test_model>', 0, 0, [], ModelTagSource.Lora),
|
||||||
|
new TagData('embedding:test_embedding', 0, 0, [], ModelTagSource.Embeddings)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Distribute tags across sources
|
||||||
|
sampleTags.forEach(tagData => {
|
||||||
|
const source = tagData.source;
|
||||||
|
autoCompleteData[source].sortedTags.push(tagData);
|
||||||
|
autoCompleteData[source].tagMap.set(tagData.tag, tagData);
|
||||||
|
|
||||||
|
// Add aliases to alias map
|
||||||
|
if (tagData.alias && Array.isArray(tagData.alias)) {
|
||||||
|
tagData.alias.forEach(alias => {
|
||||||
|
autoCompleteData[source].aliasMap.set(alias, tagData.tag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort tags and build flexsearch index
|
||||||
|
sources.forEach(source => {
|
||||||
|
autoCompleteData[source].sortedTags.sort((a, b) => b.count - a.count);
|
||||||
|
|
||||||
|
const isModelSrc = Object.values(ModelTagSource).includes(source);
|
||||||
|
const doc = isModelSrc ? createFlexSearchDocumentForModel() : createFlexSearchDocument();
|
||||||
|
autoCompleteData[source].sortedTags.forEach((tagData, i) => {
|
||||||
|
doc.add(i, tagData);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoCompleteData[source].flexSearchDocument = doc;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Autocomplete Functions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setupTestData();
|
||||||
|
|
||||||
|
// Mock global document
|
||||||
|
global.document = {
|
||||||
|
execCommand: (commandId, showUI, value) => {
|
||||||
|
switch (commandId) {
|
||||||
|
case "insertText":
|
||||||
|
if (document.focusedElement) {
|
||||||
|
document.focusedElement.value += value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Not implemented!');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createElement: () => ({
|
||||||
|
id: '',
|
||||||
|
style: {},
|
||||||
|
innerHTML: '',
|
||||||
|
appendChild: () => { },
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 100
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
setFocus: (element) => {
|
||||||
|
document.focusedElement = element;
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
appendChild: () => { },
|
||||||
|
removeChild: () => { }
|
||||||
|
},
|
||||||
|
focusedElement: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock global window
|
||||||
|
global.window = {
|
||||||
|
getComputedStyle: () => ({
|
||||||
|
lineHeight: '20px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'Arial'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('matchWord', () => {
|
||||||
|
|
||||||
|
test('should match exact queries', () => {
|
||||||
|
const queries = new Set(['1girl', '1girls']);
|
||||||
|
const result = matchWord('1girl', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.isExactMatch).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match partial queries', () => {
|
||||||
|
const queries = new Set(['girl']);
|
||||||
|
const result = matchWord('1girl', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.isExactMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle wildcard prefixes', () => {
|
||||||
|
const queries = new Set(['__wild']);
|
||||||
|
const result = matchWord('__wildcard__', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.isExactMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle symbol-only queries', () => {
|
||||||
|
const queries = new Set([':d']);
|
||||||
|
const result = matchWord(':d', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.isExactMatch).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match after removing common symbols', () => {
|
||||||
|
const queries = new Set(['blue hair']);
|
||||||
|
const result = matchWord('blue_hair', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.isExactMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not match unrelated queries', () => {
|
||||||
|
const queries = new Set(['test']);
|
||||||
|
const result = matchWord('1girl', queries);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.isExactMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCurrentPartialTag', () => {
|
||||||
|
|
||||||
|
test('should extract partial tag before cursor', () => {
|
||||||
|
const textarea = createMockTextarea('1girl, blue_hair', 14);
|
||||||
|
const result = getCurrentPartialTag(textarea);
|
||||||
|
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result).toBe('blue_ha');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return full tag when cursor is at the end', () => {
|
||||||
|
const textarea = createMockTextarea('1girl', 5);
|
||||||
|
const result = getCurrentPartialTag(textarea);
|
||||||
|
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result).toBe('1girl');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty textarea', () => {
|
||||||
|
const textarea = createMockTextarea('', 0);
|
||||||
|
const result = getCurrentPartialTag(textarea);
|
||||||
|
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle newline separators', () => {
|
||||||
|
const textarea = createMockTextarea('1girl\nblue_h', 12);
|
||||||
|
const result = getCurrentPartialTag(textarea);
|
||||||
|
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result).toBe('blue_h');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchCompletionCandidates', () => {
|
||||||
|
test('should return an empty array for empty input', () => {
|
||||||
|
const textarea = createMockTextarea('', 0);
|
||||||
|
const results = searchCompletionCandidates(textarea);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return an empty array for null input', () => {
|
||||||
|
const results = searchCompletionCandidates(null);
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return an empty array for undefined input', () => {
|
||||||
|
const results = searchCompletionCandidates(undefined);
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return multiple candidates for valid input', () => {
|
||||||
|
const textarea = createMockTextarea('test', 4);
|
||||||
|
const results = searchCompletionCandidates(textarea);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.map(tagData => tagData.tag))
|
||||||
|
.toEqual(expect.arrayContaining([
|
||||||
|
'test_tag', '<lora:test_model>', 'embedding:test_embedding'
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find wildcard tag', () => {
|
||||||
|
const textarea = createMockTextarea('__wild', 6);
|
||||||
|
const results = searchCompletionCandidates(textarea);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.map(tagData => tagData.tag))
|
||||||
|
.toEqual(expect.arrayContaining(['__wildcard__']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find emoticon tag', () => {
|
||||||
|
const textarea = createMockTextarea(':d', 2);
|
||||||
|
const results = searchCompletionCandidates(textarea);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.map(tagData => tagData.tag))
|
||||||
|
.toEqual(expect.arrayContaining([':d']));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sequentialSearch', () => {
|
||||||
|
|
||||||
|
test('should find and return matching tags', () => {
|
||||||
|
const partialTag = 'test';
|
||||||
|
const queryVariations = new Set([partialTag.toLowerCase()]);
|
||||||
|
|
||||||
|
const results = sequentialSearch(partialTag, queryVariations);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.map(tagData => tagData.tag))
|
||||||
|
.toEqual(expect.arrayContaining([
|
||||||
|
'test_tag', '<lora:test_model>', 'embedding:test_embedding'
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchWithFlexSearch', () => {
|
||||||
|
|
||||||
|
test('should find and return matching tags using FlexSearch', () => {
|
||||||
|
const partialTag = 'test';
|
||||||
|
const queryVariations = new Set([partialTag.toLowerCase()]);
|
||||||
|
|
||||||
|
const results = searchWithFlexSearch(partialTag, queryVariations);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.map(tagData => tagData.tag))
|
||||||
|
.toEqual(expect.arrayContaining([
|
||||||
|
'test_tag', '<lora:test_model>', 'embedding:test_embedding'
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('insertTagToTextArea', () => {
|
||||||
|
|
||||||
|
test('should insert tag and replace underscore with space', () => {
|
||||||
|
const textarea = createMockTextarea('1girl, ', 7);
|
||||||
|
const tagData = { tag: 'blue_hair', source: 'danbooru' };
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
insertTagToTextArea(textarea, tagData);
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
expect(textarea.value).toBe('1girl, blue hair, ');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should complete model tag and add trailing comma', () => {
|
||||||
|
const textarea = createMockTextarea('<lora:', 6);
|
||||||
|
const tagData = { tag: '<lora:my_model>', source: 'lora' };
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
insertTagToTextArea(textarea, tagData);
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
expect(textarea.value).toBe('<lora:my_model>, ');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle wildcard tag insertion', () => {
|
||||||
|
const textarea = createMockTextarea('__wild', 6);
|
||||||
|
const tagData = { tag: '__wildcard__', source: 'danbooru' };
|
||||||
|
|
||||||
|
insertTagToTextArea(textarea, tagData);
|
||||||
|
|
||||||
|
expect(textarea.value).toBe('__wildcard__, ');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle emoticon tag insertion', () => {
|
||||||
|
const textarea = createMockTextarea(':d', 2);
|
||||||
|
const tagData = { tag: ':d', source: 'danbooru' };
|
||||||
|
|
||||||
|
insertTagToTextArea(textarea, tagData);
|
||||||
|
|
||||||
|
expect(textarea.value).toBe(':d, ');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
createFlexSearchDocument,
|
||||||
|
createFlexSearchDocumentForModel,
|
||||||
|
__test__
|
||||||
|
} from "../../web/js/searchengine.js";
|
||||||
|
|
||||||
|
const { createTagEncoder, createCJKEncoder, createModelEncoder } = __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,コピーライト (シリーズ),コピーライト名,コピーライト,著作"
|
||||||
|
__wildcard__,0,0,
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ModelCSV = `
|
||||||
|
<lora:my_lora1>,0,0,
|
||||||
|
<lora:日本語Lora_v1>,0,0,
|
||||||
|
embedding: my_embedding,0,0,
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mockCSV = [
|
||||||
|
commonCSV, cjkAliasCSV, specialCharCSV
|
||||||
|
].map(csv => csv.trim()).join('\n');
|
||||||
|
|
||||||
|
let mockTags, mockModelTags;
|
||||||
|
|
||||||
|
let tagEncoder, cjkEncoder, modelEncoder;
|
||||||
|
let document, modelDocument;
|
||||||
|
|
||||||
|
let performSearch = function (query, limit = 100) {
|
||||||
|
const ids1 = document.search(query, {
|
||||||
|
field: ["tag", "alias"],
|
||||||
|
limit: limit,
|
||||||
|
suggest: false,
|
||||||
|
merge: true,
|
||||||
|
}).map(r => r.id);
|
||||||
|
|
||||||
|
const result1 = mockTags.filter(tag => ids1.includes(tag.id)).map(tag => tag.tag);
|
||||||
|
|
||||||
|
const ids2 = modelDocument.search(query, {
|
||||||
|
field: ["tag", "alias"],
|
||||||
|
limit: limit,
|
||||||
|
suggest: false,
|
||||||
|
merge: true,
|
||||||
|
}).map(r => r.id);
|
||||||
|
|
||||||
|
const result2 = mockModelTags.filter(tag => ids2.includes(tag.id)).map(tag => tag.tag);
|
||||||
|
|
||||||
|
return [...result1, ...result2];
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
mockModelTags = ModelCSV.split('\n').map((line, id) => {
|
||||||
|
const [tag, category, count, alias] = parseCSVLine(line);
|
||||||
|
return { id, tag, category: parseInt(category), count: parseInt(count), alias };
|
||||||
|
});
|
||||||
|
|
||||||
|
modelEncoder = createModelEncoder();
|
||||||
|
modelDocument = createFlexSearchDocumentForModel();
|
||||||
|
mockModelTags.forEach(data => modelDocument.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']);
|
||||||
|
});
|
||||||
|
test('should properly encode embedding notation', () => {
|
||||||
|
let encoded = modelEncoder.encode('embedding:path/to/my_embed1');
|
||||||
|
expect(encoded).toEqual(['embedding:', 'path', 'to', 'my', 'embed1']);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
modelEncoder.encode('embedding:path\\to\\my-embed1')
|
||||||
|
).toEqual(['embedding:', 'path', 'to', 'my', 'embed1']);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
modelEncoder.encode('embedding:path\\to\\this is my embed. my-negative01 (v1)__by me')
|
||||||
|
).toEqual(['embedding:', 'path', 'to', 'this', 'is', 'my', 'embed', 'my', 'negative01', 'v1', 'by', 'me']);
|
||||||
|
});
|
||||||
|
test('should properly encode lora notation', () => {
|
||||||
|
expect(
|
||||||
|
modelEncoder.encode('<lora:path/to/my_lora1>')
|
||||||
|
).toEqual(['lora:', 'path', 'to', 'my', 'lora1']);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
modelEncoder.encode('<lora:path\\to\\my-lora1>')
|
||||||
|
).toEqual(['lora:', 'path', 'to', 'my', 'lora1']);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
modelEncoder.encode('<lora:path\\to\\this is my lora. my-style01 (v1)__by me>')
|
||||||
|
).toEqual(['lora:', 'path', 'to', 'this', 'is', 'my', 'lora', 'my', 'style01', 'v1', 'by', 'me']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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(2);
|
||||||
|
|
||||||
|
expect(results).toContain("<lora:my_lora1>");
|
||||||
|
expect(results).toContain("<lora:日本語Lora_v1>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match to lora tag2', () => {
|
||||||
|
const tag = 'lora:';
|
||||||
|
const results = performSearch(tag);
|
||||||
|
expect(results.length).toEqual(2);
|
||||||
|
|
||||||
|
expect(results).toContain("<lora:my_lora1>");
|
||||||
|
expect(results).toContain("<lora:日本語Lora_v1>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match to lora that contain CJK characters', () => {
|
||||||
|
const word = 'lora: 日本語';
|
||||||
|
const results = performSearch(word);
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(results).toContain("<lora:日本語Lora_v1>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { settingValues } from '../../web/js/settings.js';
|
||||||
import {
|
import {
|
||||||
extractTagsFromTextArea,
|
extractTagsFromTextArea,
|
||||||
normalizeTagToSearch,
|
normalizeTagToSearch,
|
||||||
@@ -135,6 +136,11 @@ describe('normalizeTagToSearch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('normalizeTagToInsert', () => {
|
describe('normalizeTagToInsert', () => {
|
||||||
|
const originalValue = settingValues.replaceUnderscoreWithSpace;
|
||||||
|
afterEach(() => {
|
||||||
|
settingValues.replaceUnderscoreWithSpace = originalValue;
|
||||||
|
});
|
||||||
|
|
||||||
test('should return null or empty string for invalid inputs', () => {
|
test('should return null or empty string for invalid inputs', () => {
|
||||||
expect(normalizeTagToInsert(null)).toBeNull();
|
expect(normalizeTagToInsert(null)).toBeNull();
|
||||||
expect(normalizeTagToInsert(undefined)).toBeUndefined();
|
expect(normalizeTagToInsert(undefined)).toBeUndefined();
|
||||||
@@ -167,6 +173,58 @@ describe('normalizeTagToInsert', () => {
|
|||||||
expect(normalizeTagToInsert('year:2000')).toBe('year:2000');
|
expect(normalizeTagToInsert('year:2000')).toBe('year:2000');
|
||||||
expect(normalizeTagToInsert('foo:bar')).toBe('foo:bar');
|
expect(normalizeTagToInsert('foo:bar')).toBe('foo:bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should respect replaceUnderscoreWithSpace setting when true', () => {
|
||||||
|
settingValues.replaceUnderscoreWithSpace = true;
|
||||||
|
|
||||||
|
expect(normalizeTagToInsert('blue_hair')).toBe('blue hair');
|
||||||
|
expect(normalizeTagToInsert('red_eyes')).toBe('red eyes');
|
||||||
|
expect(normalizeTagToInsert('long_curly_hair')).toBe('long curly hair');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect replaceUnderscoreWithSpace setting when false', () => {
|
||||||
|
settingValues.replaceUnderscoreWithSpace = false;
|
||||||
|
|
||||||
|
expect(normalizeTagToInsert('blue_hair')).toBe('blue_hair');
|
||||||
|
expect(normalizeTagToInsert('red_eyes')).toBe('red_eyes');
|
||||||
|
expect(normalizeTagToInsert('long_curly_hair')).toBe('long_curly_hair');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect replaceUnderscoreWithSpace setting with parentheses', () => {
|
||||||
|
settingValues.replaceUnderscoreWithSpace = true;
|
||||||
|
expect(normalizeTagToInsert('blue_hair(style)')).toBe('blue hair\\(style\\)');
|
||||||
|
|
||||||
|
settingValues.replaceUnderscoreWithSpace = false;
|
||||||
|
expect(normalizeTagToInsert('blue_hair(style)')).toBe('blue_hair\\(style\\)');
|
||||||
|
|
||||||
|
settingValues.replaceUnderscoreWithSpace = originalValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not replace underscores in wildcard syntax regardless of setting', () => {
|
||||||
|
const originalValue = settingValues.replaceUnderscoreWithSpace;
|
||||||
|
|
||||||
|
// Wildcard syntax should preserve underscores regardless of setting
|
||||||
|
settingValues.replaceUnderscoreWithSpace = true;
|
||||||
|
expect(normalizeTagToInsert('__wildcard__')).toBe('__wildcard__');
|
||||||
|
|
||||||
|
settingValues.replaceUnderscoreWithSpace = false;
|
||||||
|
expect(normalizeTagToInsert('__wildcard__')).toBe('__wildcard__');
|
||||||
|
|
||||||
|
settingValues.replaceUnderscoreWithSpace = originalValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not replace underscores in symbol-only tags regardless of setting', () => {
|
||||||
|
const originalValue = settingValues.replaceUnderscoreWithSpace;
|
||||||
|
|
||||||
|
// Symbol-only tags should not be affected by the setting
|
||||||
|
settingValues.replaceUnderscoreWithSpace = true;
|
||||||
|
expect(normalizeTagToInsert('^_^')).toBe('^_^');
|
||||||
|
|
||||||
|
settingValues.replaceUnderscoreWithSpace = false;
|
||||||
|
expect(normalizeTagToInsert('^_^')).toBe('^_^');
|
||||||
|
|
||||||
|
settingValues.replaceUnderscoreWithSpace = originalValue;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getCurrentTagRange', () => {
|
describe('getCurrentTagRange', () => {
|
||||||
|
|||||||
@@ -97,10 +97,14 @@ body.dark-theme {
|
|||||||
display: grid;
|
display: grid;
|
||||||
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
||||||
grid-auto-rows: auto;
|
grid-auto-rows: auto;
|
||||||
grid-template-columns: max-content 1fr auto auto;
|
grid-template-columns: max-content 1fr auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#autocomplete-plus-list.no-alias {
|
||||||
|
grid-template-columns: max-content auto;
|
||||||
|
}
|
||||||
|
|
||||||
.autocomplete-plus-item {
|
.autocomplete-plus-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -292,10 +296,14 @@ body.dark-theme {
|
|||||||
display: grid;
|
display: grid;
|
||||||
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
||||||
grid-auto-rows: auto;
|
grid-auto-rows: auto;
|
||||||
grid-template-columns: max-content 1fr auto auto;
|
grid-template-columns: max-content 1fr auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#related-tags-list.no-alias {
|
||||||
|
grid-template-columns: max-content auto;
|
||||||
|
}
|
||||||
|
|
||||||
.related-tag-item {
|
.related-tag-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
+186
-48
@@ -1,19 +1,23 @@
|
|||||||
import {
|
import {
|
||||||
|
ModelTagSource,
|
||||||
TagCategory,
|
TagCategory,
|
||||||
TagData,
|
TagData,
|
||||||
autoCompleteData,
|
autoCompleteData,
|
||||||
getEnabledTagSourceInPriorityOrder
|
getEnabledTagSourceInPriorityOrder
|
||||||
} from './data.js';
|
} from './data.js';
|
||||||
import {
|
import {
|
||||||
formatCountHumanReadable,
|
isLongText,
|
||||||
hiraToKata,
|
hiraToKata,
|
||||||
kataToHira,
|
kataToHira,
|
||||||
|
formatCountHumanReadable,
|
||||||
|
escapeHtml,
|
||||||
isContainsLetterOrNumber,
|
isContainsLetterOrNumber,
|
||||||
normalizeTagToInsert,
|
normalizeTagToInsert,
|
||||||
normalizeTagToSearch,
|
normalizeTagToSearch,
|
||||||
extractTagsFromTextArea,
|
extractTagsFromTextArea,
|
||||||
getCurrentTagRange,
|
getCurrentTagRange,
|
||||||
getViewportMargin,
|
getViewportMargin,
|
||||||
|
getScrollbarWidth,
|
||||||
IconSvgHtmlString
|
IconSvgHtmlString
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { settingValues } from './settings.js';
|
import { settingValues } from './settings.js';
|
||||||
@@ -78,20 +82,17 @@ 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.toLowerCase(), normalizeTagToSearch(partialTag).toLowerCase()]);
|
||||||
const kataQuery = hiraToKata(partialTag);
|
const kataQuery = hiraToKata(partialTag);
|
||||||
if (kataQuery !== partialTag) {
|
if (kataQuery !== partialTag) {
|
||||||
queryVariations.add(kataQuery);
|
queryVariations.add(kataQuery);
|
||||||
@@ -101,6 +102,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)
|
||||||
@@ -110,15 +131,14 @@ function searchCompletionCandidates(textareaElement) {
|
|||||||
let matchedAlias = null;
|
let matchedAlias = null;
|
||||||
|
|
||||||
// Check primary tag against all variations for exact/partial match
|
// Check primary tag against all variations for exact/partial match
|
||||||
const tagMatch = matchWord(tagData.tag, queryVariations);
|
const tagMatch = matchWord(tagData.tag.toLowerCase(), queryVariations);
|
||||||
matched = tagMatch.matched;
|
matched = tagMatch.matched;
|
||||||
isExactMatch = tagMatch.isExactMatch;
|
isExactMatch = tagMatch.isExactMatch;
|
||||||
|
|
||||||
// If primary tag didn't match, check aliases against all variations
|
// If primary tag didn't match, check aliases against all variations
|
||||||
if (!matched && tagData.alias && Array.isArray(tagData.alias) && tagData.alias.length > 0) {
|
if (!matched && tagData.alias && Array.isArray(tagData.alias) && tagData.alias.length > 0) {
|
||||||
for (const alias of tagData.alias) {
|
for (const alias of tagData.alias) {
|
||||||
const lowerAlias = alias.toLowerCase();
|
const aliasMatch = matchWord(alias.toLowerCase(), queryVariations);
|
||||||
const aliasMatch = matchWord(lowerAlias, queryVariations);
|
|
||||||
if (aliasMatch.matched) {
|
if (aliasMatch.matched) {
|
||||||
matched = true;
|
matched = true;
|
||||||
isExactMatch = aliasMatch.isExactMatch;
|
isExactMatch = aliasMatch.isExactMatch;
|
||||||
@@ -170,12 +190,79 @@ 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
|
||||||
* @returns {string} The current partial tag.
|
* @returns {string} The current partial tag.
|
||||||
*/
|
*/
|
||||||
function getCurrentPartialTag(inputElement) {
|
function getCurrentPartialTag(inputElement) {
|
||||||
|
if (!inputElement) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
const text = inputElement.value;
|
const text = inputElement.value;
|
||||||
const cursorPos = inputElement.selectionStart;
|
const cursorPos = inputElement.selectionStart;
|
||||||
|
|
||||||
@@ -218,17 +305,38 @@ function getCurrentPartialTag(inputElement) {
|
|||||||
* Inserts the selected tag into the textarea, replacing the partial tag,
|
* Inserts the selected tag into the textarea, replacing the partial tag,
|
||||||
* making the change undoable.
|
* making the change undoable.
|
||||||
* @param {HTMLTextAreaElement} inputElement
|
* @param {HTMLTextAreaElement} inputElement
|
||||||
* @param {string} tagToInsert The raw tag string to insert.
|
* @param {TagData} tagDataToInsert The raw tag string to insert.
|
||||||
*/
|
*/
|
||||||
function insertTagToTextArea(inputElement, tagToInsert) {
|
function insertTagToTextArea(inputElement, tagDataToInsert) {
|
||||||
|
if (!inputElement || !tagDataToInsert) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const text = inputElement.value;
|
const text = inputElement.value;
|
||||||
const cursorPos = inputElement.selectionStart;
|
const cursorPos = inputElement.selectionStart;
|
||||||
|
|
||||||
const { start: tagStart, end: tagEnd, tag: currentTag } = getCurrentTagRange(text, cursorPos);
|
const tagRange = getCurrentTagRange(text, cursorPos);
|
||||||
|
let tagStart, tagEnd, currentTag;
|
||||||
|
|
||||||
|
if (!tagRange) {
|
||||||
|
// Fallback: insert at cursor position
|
||||||
|
tagStart = cursorPos;
|
||||||
|
tagEnd = cursorPos;
|
||||||
|
currentTag = '';
|
||||||
|
} else {
|
||||||
|
({ start: tagStart, end: tagEnd, tag: currentTag } = tagRange);
|
||||||
|
}
|
||||||
|
|
||||||
const replaceStart = Math.min(cursorPos, tagStart);
|
const replaceStart = Math.min(cursorPos, tagStart);
|
||||||
let replaceEnd = cursorPos;
|
let replaceEnd = cursorPos;
|
||||||
|
|
||||||
const normalizedTag = normalizeTagToInsert(tagToInsert);
|
let normalizedTag;
|
||||||
|
if (Object.values(ModelTagSource).includes(tagDataToInsert.source)) {
|
||||||
|
// If the tag is from a model tag source, don't want to normalize it
|
||||||
|
normalizedTag = tagDataToInsert.tag;
|
||||||
|
} else {
|
||||||
|
normalizedTag = normalizeTagToInsert(tagDataToInsert.tag);
|
||||||
|
}
|
||||||
|
|
||||||
const currentTagAfterCursor = text.substring(cursorPos, tagEnd).trimEnd();
|
const currentTagAfterCursor = text.substring(cursorPos, tagEnd).trimEnd();
|
||||||
if (normalizedTag.lastIndexOf(currentTagAfterCursor) !== -1) {
|
if (normalizedTag.lastIndexOf(currentTagAfterCursor) !== -1) {
|
||||||
@@ -297,7 +405,7 @@ class AutocompleteUI {
|
|||||||
this.tagsList.addEventListener('mousedown', (e) => {
|
this.tagsList.addEventListener('mousedown', (e) => {
|
||||||
const row = e.target.closest('.autocomplete-plus-item');
|
const row = e.target.closest('.autocomplete-plus-item');
|
||||||
if (row && row.dataset.tag) {
|
if (row && row.dataset.tag) {
|
||||||
this.#insertTag(row.dataset.tag);
|
this.#insertTag(row.dataset);
|
||||||
e.preventDefault(); // Prevent focus loss from input
|
e.preventDefault(); // Prevent focus loss from input
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
@@ -362,10 +470,12 @@ class AutocompleteUI {
|
|||||||
this.#highlightItem();
|
this.#highlightItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Selects the currently highlighted item */
|
/** Selects the currently highlighted item
|
||||||
|
* @returns {TagData|null} The selected tag data.
|
||||||
|
*/
|
||||||
getSelectedTag() {
|
getSelectedTag() {
|
||||||
if (this.selectedIndex >= 0 && this.selectedIndex < this.candidates.length) {
|
if (this.selectedIndex >= 0 && this.selectedIndex < this.candidates.length) {
|
||||||
return this.candidates[this.selectedIndex].tag;
|
return this.candidates[this.selectedIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
return null; // No valid selection
|
return null; // No valid selection
|
||||||
@@ -381,6 +491,8 @@ class AutocompleteUI {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.tagsList.classList.toggle('no-alias', settingValues.hideAlias);
|
||||||
|
|
||||||
const existingTags = extractTagsFromTextArea(this.target);
|
const existingTags = extractTagsFromTextArea(this.target);
|
||||||
const currentTag = getCurrentPartialTag(this.target);
|
const currentTag = getCurrentPartialTag(this.target);
|
||||||
|
|
||||||
@@ -398,6 +510,7 @@ class AutocompleteUI {
|
|||||||
*/
|
*/
|
||||||
#createTagElement(tagData, isExisting) {
|
#createTagElement(tagData, isExisting) {
|
||||||
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||||
|
const aliasText = tagData.alias.join(', ');
|
||||||
|
|
||||||
const tagRow = document.createElement('div');
|
const tagRow = document.createElement('div');
|
||||||
tagRow.classList.add('autocomplete-plus-item', tagData.source);
|
tagRow.classList.add('autocomplete-plus-item', tagData.source);
|
||||||
@@ -411,9 +524,10 @@ class AutocompleteUI {
|
|||||||
if (settingValues.tagSourceIconPosition == 'hidden') {
|
if (settingValues.tagSourceIconPosition == 'hidden') {
|
||||||
tagName.textContent = tagData.tag;
|
tagName.textContent = tagData.tag;
|
||||||
} else {
|
} else {
|
||||||
|
const escapedTag = escapeHtml(tagData.tag);
|
||||||
tagName.innerHTML = settingValues.tagSourceIconPosition == 'left'
|
tagName.innerHTML = settingValues.tagSourceIconPosition == 'left'
|
||||||
? `${tagSourceIconHtml} ${tagData.tag}`
|
? `${tagSourceIconHtml} ${escapedTag}`
|
||||||
: `${tagData.tag} ${tagSourceIconHtml}`;
|
: `${escapedTag} ${tagSourceIconHtml}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// grayout tag name if it already exists
|
// grayout tag name if it already exists
|
||||||
@@ -426,26 +540,29 @@ class AutocompleteUI {
|
|||||||
alias.className = 'autocomplete-plus-alias';
|
alias.className = 'autocomplete-plus-alias';
|
||||||
|
|
||||||
// Display alias if available
|
// Display alias if available
|
||||||
if (tagData.alias && tagData.alias.length > 0) {
|
if (aliasText.length > 0) {
|
||||||
let aliasText = tagData.alias.join(', ');
|
|
||||||
alias.textContent = `${aliasText}`;
|
alias.textContent = `${aliasText}`;
|
||||||
alias.title = tagData.alias.join(', '); // Full alias on hover
|
alias.title = aliasText; // Full alias on hover
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category
|
|
||||||
const category = document.createElement('span');
|
|
||||||
category.className = `autocomplete-plus-category`;
|
|
||||||
category.textContent = `${categoryText.substring(0, 2)}`;
|
|
||||||
category.title = categoryText; // Full category on hover
|
|
||||||
|
|
||||||
// Count
|
// Count
|
||||||
const tagCount = document.createElement('span');
|
const tagCount = document.createElement('span');
|
||||||
tagCount.className = `autocomplete-plus-tag-count`;
|
tagCount.className = `autocomplete-plus-tag-count`;
|
||||||
tagCount.textContent = formatCountHumanReadable(tagData.count);
|
tagCount.textContent = formatCountHumanReadable(tagData.count);
|
||||||
|
|
||||||
|
// Create tooltip with more info
|
||||||
|
let tooltipText = `Count: ${tagData.count}\nCategory: ${categoryText}`;
|
||||||
|
if (aliasText.length > 0) {
|
||||||
|
tooltipText += `\nAlias: ${aliasText}`;
|
||||||
|
}
|
||||||
|
tagRow.title = tooltipText;
|
||||||
|
|
||||||
tagRow.appendChild(tagName);
|
tagRow.appendChild(tagName);
|
||||||
tagRow.appendChild(alias);
|
|
||||||
tagRow.appendChild(category);
|
if (!settingValues.hideAlias) {
|
||||||
|
tagRow.appendChild(alias);
|
||||||
|
}
|
||||||
|
|
||||||
tagRow.appendChild(tagCount);
|
tagRow.appendChild(tagCount);
|
||||||
this.tagsList.appendChild(tagRow);
|
this.tagsList.appendChild(tagRow);
|
||||||
}
|
}
|
||||||
@@ -483,21 +600,14 @@ class AutocompleteUI {
|
|||||||
let topPosition = targetElmOffset.top + ((caretTop - targetElmOffset.top) + caretLineHeight) * scale;
|
let topPosition = targetElmOffset.top + ((caretTop - targetElmOffset.top) + caretLineHeight) * scale;
|
||||||
let leftPosition = targetElmOffset.left + (caretLeft - targetElmOffset.left) * scale;;
|
let leftPosition = targetElmOffset.left + (caretLeft - targetElmOffset.left) * scale;;
|
||||||
|
|
||||||
const maxWidth = Math.min(rootRect.width, viewportWidth / 2);
|
|
||||||
const naturalHeight = rootRect.height;
|
const naturalHeight = rootRect.height;
|
||||||
|
|
||||||
//Horizontal Collision Detection and Adjustment
|
|
||||||
if (leftPosition + maxWidth > viewportWidth - margin.right) {
|
|
||||||
leftPosition = viewportWidth - maxWidth - margin.right;
|
|
||||||
}
|
|
||||||
if (leftPosition < margin.left) {
|
|
||||||
leftPosition = margin.left;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertical Collision Detection and Adjustment
|
// Vertical Collision Detection and Adjustment
|
||||||
const availableSpaceBelow = viewportHeight - topPosition - margin.bottom;
|
const availableSpaceBelow = viewportHeight - topPosition - margin.bottom;
|
||||||
const availableSpaceAbove = caretTop - margin.top;
|
const availableSpaceAbove = caretTop - margin.top;
|
||||||
|
|
||||||
|
let needsVerticalScroll = false;
|
||||||
|
let maxHeight = rootRect.height;
|
||||||
|
|
||||||
if (naturalHeight <= availableSpaceBelow) {
|
if (naturalHeight <= availableSpaceBelow) {
|
||||||
// Fits perfectly below the caret
|
// Fits perfectly below the caret
|
||||||
@@ -509,14 +619,15 @@ class AutocompleteUI {
|
|||||||
topPosition = caretTop - naturalHeight - margin.top;
|
topPosition = caretTop - naturalHeight - margin.top;
|
||||||
} else {
|
} else {
|
||||||
// Doesn't fit perfectly either below or above, needs scrolling.
|
// Doesn't fit perfectly either below or above, needs scrolling.
|
||||||
|
needsVerticalScroll = true;
|
||||||
// Choose the position (above or below) that offers more space.
|
// Choose the position (above or below) that offers more space.
|
||||||
if (availableSpaceBelow >= availableSpaceAbove) {
|
if (availableSpaceBelow >= availableSpaceAbove) {
|
||||||
// Scroll below: topPosition remains as initially calculated
|
// Scroll below: topPosition remains as initially calculated
|
||||||
this.tagsList.style.maxHeight = `${availableSpaceBelow}px`;
|
maxHeight = availableSpaceBelow;
|
||||||
} else {
|
} else {
|
||||||
// Scroll above: Position near the top edge and set max-height
|
// Scroll above: Position near the top edge and set max-height
|
||||||
topPosition = margin.top;
|
topPosition = margin.top;
|
||||||
this.tagsList.style.maxHeight = `${availableSpaceAbove}px`;
|
maxHeight = availableSpaceAbove;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,16 +636,30 @@ class AutocompleteUI {
|
|||||||
if (topPosition < margin.top) {
|
if (topPosition < margin.top) {
|
||||||
topPosition = margin.top;
|
topPosition = margin.top;
|
||||||
// If pushed down, recalculate max-height if it was set based on top alignment
|
// If pushed down, recalculate max-height if it was set based on top alignment
|
||||||
if (this.tagsList.style.maxHeight && availableSpaceBelow < availableSpaceAbove) {
|
if (availableSpaceBelow < availableSpaceAbove) {
|
||||||
// Recalculate max-height based on space from the top margin
|
// Recalculate max-height based on space from the top margin
|
||||||
this.tagsList.style.maxHeight = `${viewportHeight - margin.top - margin.bottom}px`;
|
maxHeight = viewportHeight - margin.top - margin.bottom;
|
||||||
|
needsVerticalScroll = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate maxWidth considering scrollbar width if vertical scrolling is needed
|
||||||
|
const scrollbarWidth = needsVerticalScroll ? getScrollbarWidth() : 0;
|
||||||
|
const maxWidth = Math.min(rootRect.width + scrollbarWidth, viewportWidth / 2);
|
||||||
|
|
||||||
|
// Horizontal Collision Detection and Adjustment
|
||||||
|
if (leftPosition + maxWidth > viewportWidth - margin.right) {
|
||||||
|
leftPosition = viewportWidth - maxWidth - margin.right;
|
||||||
|
}
|
||||||
|
if (leftPosition < margin.left) {
|
||||||
|
leftPosition = margin.left;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the calculated position and display the element
|
// Apply the calculated position and display the element
|
||||||
this.root.style.left = `${leftPosition}px`;
|
this.root.style.left = `${leftPosition}px`;
|
||||||
this.root.style.top = `${topPosition}px`;
|
this.root.style.top = `${topPosition}px`;
|
||||||
this.root.style.maxWidth = `${maxWidth}px`;
|
this.root.style.maxWidth = `${maxWidth}px`;
|
||||||
|
this.tagsList.style.maxHeight = `${maxHeight}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Highlights the item (row) at the given index */
|
/** Highlights the item (row) at the given index */
|
||||||
@@ -554,10 +679,10 @@ class AutocompleteUI {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the selection of an item
|
* Handles the selection of an item
|
||||||
* @param {string} selectedTag The tag to insert.
|
* @param {TagData} selectedTag The tag to insert.
|
||||||
*/
|
*/
|
||||||
#insertTag(selectedTag) {
|
#insertTag(selectedTag) {
|
||||||
if (!this.target || !selectedTag || selectedTag.length <= 0) {
|
if (!this.target || !selectedTag) {
|
||||||
this.hide();
|
this.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -918,4 +1043,17 @@ export class AutocompleteEventHandler {
|
|||||||
*/
|
*/
|
||||||
handleClick(event) {
|
handleClick(event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export functions for testing
|
||||||
|
const isTestEnvironment = typeof process !== 'undefined' && process.env.NODE_ENV === 'test';
|
||||||
|
export const __test__ = isTestEnvironment
|
||||||
|
? {
|
||||||
|
searchCompletionCandidates,
|
||||||
|
sequentialSearch,
|
||||||
|
searchWithFlexSearch,
|
||||||
|
matchWord,
|
||||||
|
getCurrentPartialTag,
|
||||||
|
insertTagToTextArea
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|||||||
+192
-33
@@ -1,13 +1,20 @@
|
|||||||
import { settingValues } from "./settings.js";
|
import { settingValues, updateMaxTagLength } from "./settings.js";
|
||||||
|
import { createFlexSearchDocument, createFlexSearchDocumentForModel } from "./searchengine.js";
|
||||||
|
|
||||||
// --- Constants ---
|
// --- Constants ---
|
||||||
|
|
||||||
// Tag data sources
|
// Tag sources for booru-like tag data.
|
||||||
export const TagSource = {
|
export const TagSource = {
|
||||||
Danbooru: 'danbooru',
|
Danbooru: 'danbooru',
|
||||||
E621: 'e621',
|
E621: 'e621',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tag sources for model based tag data.
|
||||||
|
export const ModelTagSource = {
|
||||||
|
Embeddings: 'embeddings',
|
||||||
|
Lora: 'lora'
|
||||||
|
}
|
||||||
|
|
||||||
export const TagCategory = {
|
export const TagCategory = {
|
||||||
'danbooru': [
|
'danbooru': [
|
||||||
'general',
|
'general',
|
||||||
@@ -27,6 +34,12 @@ export const TagCategory = {
|
|||||||
'invalid',
|
'invalid',
|
||||||
'meta',
|
'meta',
|
||||||
'lore',
|
'lore',
|
||||||
|
],
|
||||||
|
'embeddings': [
|
||||||
|
'embeddings'
|
||||||
|
],
|
||||||
|
'lora': [
|
||||||
|
'lora'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,19 +52,19 @@ export class TagData {
|
|||||||
/**
|
/**
|
||||||
* Create a tag data object
|
* Create a tag data object
|
||||||
* @param {string} tag - The tag name
|
* @param {string} tag - The tag name
|
||||||
* @param {string[]} [alias=[]] - Array of aliases for the tag
|
* @param {number} [category] - Category index of the tag
|
||||||
* @param {string} [category='general'] - Category of the tag
|
|
||||||
* @param {number} [count=0] - Frequency count/popularity of the tag
|
* @param {number} [count=0] - Frequency count/popularity of the tag
|
||||||
* @param {string} [source=TagSources.Danbooru] - The source of the tag data
|
* @param {string[]} [alias=[]] - Array of aliases for the tag
|
||||||
|
* @param {string} [source=TagSource.Danbooru] - The source of the tag data
|
||||||
*/
|
*/
|
||||||
constructor(tag, alias = [], category = 'general', count = 0, source = TagSource.Danbooru) {
|
constructor(tag, category, count = 0, alias = [], source = TagSource.Danbooru) {
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
this.tag = tag;
|
this.tag = tag;
|
||||||
|
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
this.alias = alias;
|
this.alias = alias;
|
||||||
|
|
||||||
/** @type {string} */
|
/** @type {number} */
|
||||||
this.category = category;
|
this.category = category;
|
||||||
|
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
@@ -63,6 +76,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 +96,6 @@ class AutocompleteData {
|
|||||||
|
|
||||||
// Progress of "base" csv loading
|
// Progress of "base" csv loading
|
||||||
this.baseLoadingProgress = {
|
this.baseLoadingProgress = {
|
||||||
// tags: 0,
|
|
||||||
cooccurrence: 0
|
cooccurrence: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -95,24 +110,32 @@ export const autoCompleteData = {};
|
|||||||
const TAGS_CSV_HEADER = 'tag,category,count,alias';
|
const TAGS_CSV_HEADER = 'tag,category,count,alias';
|
||||||
const TAGS_CSV_HEADER_COLUMNS = TAGS_CSV_HEADER.split(',');
|
const TAGS_CSV_HEADER_COLUMNS = TAGS_CSV_HEADER.split(',');
|
||||||
const TAG_INDEX = TAGS_CSV_HEADER_COLUMNS.indexOf('tag');
|
const TAG_INDEX = TAGS_CSV_HEADER_COLUMNS.indexOf('tag');
|
||||||
const ALIAS_INDEX = TAGS_CSV_HEADER_COLUMNS.indexOf('alias');
|
|
||||||
const CATEGORY_INDEX = TAGS_CSV_HEADER_COLUMNS.indexOf('category');
|
const CATEGORY_INDEX = TAGS_CSV_HEADER_COLUMNS.indexOf('category');
|
||||||
const COUNT_INDEX = TAGS_CSV_HEADER_COLUMNS.indexOf('count');
|
const COUNT_INDEX = TAGS_CSV_HEADER_COLUMNS.indexOf('count');
|
||||||
|
const ALIAS_INDEX = TAGS_CSV_HEADER_COLUMNS.indexOf('alias');
|
||||||
|
|
||||||
// --- Helder Functions ---
|
// --- Helder Functions ---
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the available tag sources in priority order based on the current settings.
|
* Get the available tag sources in priority order based on the current settings.
|
||||||
* @returns {string[]} Array of available tag sources in priority order
|
* @returns {string[]} Array of available tag sources in priority order
|
||||||
*/
|
*/
|
||||||
export function getEnabledTagSourceInPriorityOrder() {
|
export function getEnabledTagSourceInPriorityOrder() {
|
||||||
return Object.values(TagSource)
|
let enabledTagSources = Object.values(TagSource)
|
||||||
.filter((s) => {
|
.filter((s) => {
|
||||||
return settingValues.tagSource === s || settingValues.tagSource === 'all';
|
return settingValues.tagSource === s || settingValues.tagSource === 'all';
|
||||||
})
|
})
|
||||||
.toSorted((a, b) => {
|
.toSorted((a, b) => {
|
||||||
return a === settingValues.primaryTagSource ? -1 : 1;
|
return a === settingValues.primaryTagSource ? -1 : 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Append Loras and Embeddings if enabled
|
||||||
|
if (settingValues.enableModels) {
|
||||||
|
enabledTagSources = [...enabledTagSources, ...Object.values(ModelTagSource)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabledTagSources;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Data Loading Functions ---
|
// --- Data Loading Functions ---
|
||||||
@@ -156,21 +179,13 @@ async function loadTags(csvUrl, siteName) {
|
|||||||
const aliases = aliasStr ? aliasStr.split(',').map(a => a.trim()).filter(a => a.length > 0) : [];
|
const aliases = aliasStr ? aliasStr.split(',').map(a => a.trim()).filter(a => a.length > 0) : [];
|
||||||
|
|
||||||
// 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, category, count, aliases, siteName);
|
||||||
|
|
||||||
|
updateMaxTagLength(tag.length);
|
||||||
|
|
||||||
autoCompleteData[siteName].sortedTags.push(tagData);
|
autoCompleteData[siteName].sortedTags.push(tagData);
|
||||||
} 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}.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by count in descending order
|
// Set the tag and its alias in the maps
|
||||||
autoCompleteData[siteName].sortedTags.sort((a, b) => b.count - a.count);
|
|
||||||
|
|
||||||
// Build maps as before, but ensure not to overwrite if already processed from extra files
|
|
||||||
autoCompleteData[siteName].sortedTags.forEach(tagData => {
|
|
||||||
if (!autoCompleteData[siteName].tagMap.has(tagData.tag)) {
|
|
||||||
autoCompleteData[siteName].tagMap.set(tagData.tag, tagData);
|
autoCompleteData[siteName].tagMap.set(tagData.tag, tagData);
|
||||||
if (tagData.alias && Array.isArray(tagData.alias)) {
|
if (tagData.alias && Array.isArray(tagData.alias)) {
|
||||||
tagData.alias.forEach(alias => {
|
tagData.alias.forEach(alias => {
|
||||||
@@ -179,14 +194,63 @@ async function loadTags(csvUrl, siteName) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} 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}.`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Autocomplete-Plus] Failed to fetch or process tags from ${csvUrl}:`, error);
|
console.error(`[Autocomplete-Plus] Failed to fetch or process tags from ${csvUrl}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build FlexSearch index for the given site name.
|
||||||
|
* @param {string} siteName
|
||||||
|
*/
|
||||||
|
async function buildFlexSearchIndex(siteName) {
|
||||||
|
try {
|
||||||
|
if (autoCompleteData[siteName].sortedTags.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let document = null;
|
||||||
|
if (Object.values(TagSource).includes(siteName)) {
|
||||||
|
document = createFlexSearchDocument();
|
||||||
|
} else if (Object.values(ModelTagSource).includes(siteName)) {
|
||||||
|
document = createFlexSearchDocumentForModel();
|
||||||
|
} else {
|
||||||
|
throw new Error(`[Autocomplete-Plus] Invalid site name: ${siteName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -297,7 +361,11 @@ function parseCSVLine(line) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCsvList() {
|
/**
|
||||||
|
* Fetch the list of CSV files from the API endpoint
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function fetchCsvList() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/autocomplete-plus/csv');
|
const response = await fetch('/autocomplete-plus/csv');
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -313,9 +381,8 @@ export async function fetchCsvList() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the autocomplete data by fetching the list of CSV files and loading them.
|
* Initializes the autocomplete data by fetching the list of CSV files and loading them.
|
||||||
* This function is called when the extension is initialized.
|
|
||||||
*/
|
*/
|
||||||
export async function initializeData(csvListData, source) {
|
async function initializeDataFromCSV(csvListData, source) {
|
||||||
if (autoCompleteData.hasOwnProperty(source) === false) {
|
if (autoCompleteData.hasOwnProperty(source) === false) {
|
||||||
autoCompleteData[source] = new AutocompleteData();
|
autoCompleteData[source] = new AutocompleteData();
|
||||||
}
|
}
|
||||||
@@ -326,7 +393,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)
|
||||||
@@ -375,12 +441,20 @@ 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(() => {
|
||||||
if (csvListData[source].base_tags) {
|
// Sort by count in descending order
|
||||||
console.log(`[Autocomplete-Plus] "${source}" Tags loading complete in ${(endTime - startTime).toFixed(2)}ms`);
|
autoCompleteData[source].sortedTags.sort((a, b) => b.count - a.count);
|
||||||
}
|
|
||||||
}),
|
// 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();
|
||||||
if (csvListData[source].base_cooccurrence) {
|
if (csvListData[source].base_cooccurrence) {
|
||||||
@@ -396,3 +470,88 @@ export async function initializeData(csvListData, source) {
|
|||||||
autoCompleteData[source].isInitializing = false;
|
autoCompleteData[source].isInitializing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Embeddings data from the API endpoint
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function loadEmbeddings() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/autocomplete-plus/embeddings', { cache: "no-store" });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const embeddings = await response.json();
|
||||||
|
const source = ModelTagSource.Embeddings;
|
||||||
|
|
||||||
|
if (autoCompleteData.hasOwnProperty(source) === false) {
|
||||||
|
autoCompleteData[source] = new AutocompleteData();
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddings.forEach(embedding => {
|
||||||
|
if (!autoCompleteData[source].tagMap.has(embedding)) {
|
||||||
|
const tagData = new TagData(`embedding:${embedding}`, 0, 0, [], source);
|
||||||
|
autoCompleteData[source].sortedTags.push(tagData);
|
||||||
|
autoCompleteData[source].tagMap.set(embedding, tagData);
|
||||||
|
|
||||||
|
updateMaxTagLength(embedding.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await buildFlexSearchIndex(ModelTagSource.Embeddings);
|
||||||
|
|
||||||
|
console.log(`[Autocomplete-Plus] Loaded ${embeddings.length} Embeddings`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Autocomplete-Plus] Failed to fetch Embeddings data:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load LoRA data from the API endpoint
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function loadLoras() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/autocomplete-plus/loras', { cache: "no-store" });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const loraNames = await response.json();
|
||||||
|
const source = ModelTagSource.Lora;
|
||||||
|
|
||||||
|
if (autoCompleteData.hasOwnProperty(source) === false) {
|
||||||
|
autoCompleteData[source] = new AutocompleteData();
|
||||||
|
}
|
||||||
|
|
||||||
|
loraNames.forEach(loraName => {
|
||||||
|
if (!autoCompleteData[source].tagMap.has(loraName)) {
|
||||||
|
const tagData = new TagData(`<lora:${loraName}>`, 0, 0, [], source);
|
||||||
|
autoCompleteData[source].sortedTags.push(tagData);
|
||||||
|
autoCompleteData[source].tagMap.set(loraName, tagData);
|
||||||
|
|
||||||
|
updateMaxTagLength(loraName.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await buildFlexSearchIndex(ModelTagSource.Lora);
|
||||||
|
|
||||||
|
console.log(`[Autocomplete-Plus] Loaded ${loraNames.length} LoRA models`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Autocomplete-Plus] Failed to fetch LoRA data:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all data sources asynchronously.
|
||||||
|
*/
|
||||||
|
export async function loadDataAsync() {
|
||||||
|
return Promise.all([
|
||||||
|
fetchCsvList().then((csvList) => {
|
||||||
|
Object.values(TagSource).forEach((source) => {
|
||||||
|
initializeDataFromCSV(csvList, source);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
loadEmbeddings(),
|
||||||
|
loadLoras(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
+82
-38
@@ -3,7 +3,7 @@ import { $el } from "/scripts/ui.js";
|
|||||||
import { ComfyWidgets } from "/scripts/widgets.js";
|
import { ComfyWidgets } from "/scripts/widgets.js";
|
||||||
import { settingValues } from "./settings.js";
|
import { settingValues } from "./settings.js";
|
||||||
import { loadCSS } from "./utils.js";
|
import { loadCSS } from "./utils.js";
|
||||||
import { TagSource, fetchCsvList, initializeData } from "./data.js";
|
import { TagSource, loadDataAsync } from "./data.js";
|
||||||
import { AutocompleteEventHandler } from "./autocomplete.js";
|
import { AutocompleteEventHandler } from "./autocomplete.js";
|
||||||
import { RelatedTagsEventHandler } from "./related-tags.js";
|
import { RelatedTagsEventHandler } from "./related-tags.js";
|
||||||
|
|
||||||
@@ -190,40 +190,41 @@ async function addExtraSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add extra setting for checking new CSV updates
|
// Add extra setting for checking new CSV updates
|
||||||
app.ui.settings.addSetting({
|
// Note: Temporarily comment out until changes to remote files can be detected
|
||||||
id: id + ".check_new_csv",
|
// app.ui.settings.addSetting({
|
||||||
defaultValue: null,
|
// id: id + ".check_new_csv",
|
||||||
name: "Check CSV updates",
|
// defaultValue: null,
|
||||||
category: [name, "Misc", "Check new CSV"],
|
// name: "Check CSV updates",
|
||||||
type: () => {
|
// category: [name, "Misc", "Check new CSV"],
|
||||||
const lastCheckSpan = $el("span", {
|
// type: () => {
|
||||||
textContent: lastCheckTimeText,
|
// const lastCheckSpan = $el("span", {
|
||||||
className: "text-sm text-gray-500",
|
// textContent: lastCheckTimeText,
|
||||||
style: {
|
// className: "text-sm text-gray-500",
|
||||||
marginRight: "16px"
|
// style: {
|
||||||
}
|
// marginRight: "16px"
|
||||||
});
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
const checkButton = $el("button", {
|
// const checkButton = $el("button", {
|
||||||
textContent: "Check now",
|
// textContent: "Check now",
|
||||||
className: "p-button p-component p-button-primary",
|
// className: "p-button p-component p-button-primary",
|
||||||
onclick: async () => {
|
// onclick: async () => {
|
||||||
await performUpdateCheck(checkButton, lastCheckSpan);
|
// await performUpdateCheck(checkButton, lastCheckSpan);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
return $el("div", {
|
// return $el("div", {
|
||||||
className: "flex-row items-center gap-2",
|
// className: "flex-row items-center gap-2",
|
||||||
}, [
|
// }, [
|
||||||
$el("div", {
|
// $el("div", {
|
||||||
className: "p-component",
|
// className: "p-component",
|
||||||
}, [
|
// }, [
|
||||||
lastCheckSpan,
|
// lastCheckSpan,
|
||||||
checkButton,
|
// checkButton,
|
||||||
]),
|
// ]),
|
||||||
]);
|
// ]);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -240,11 +241,7 @@ app.registerExtension({
|
|||||||
let rootPath = import.meta.url.replace("js/main.js", "");
|
let rootPath = import.meta.url.replace("js/main.js", "");
|
||||||
loadCSS(rootPath + "css/autocomplete-plus.css"); // Load CSS for autocomplete
|
loadCSS(rootPath + "css/autocomplete-plus.css"); // Load CSS for autocomplete
|
||||||
|
|
||||||
fetchCsvList().then((csvList) => {
|
await loadDataAsync();
|
||||||
Object.values(TagSource).forEach((source) => {
|
|
||||||
initializeData(csvList, source);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// One the Settings Screen, displays reverse order in same category
|
// One the Settings Screen, displays reverse order in same category
|
||||||
@@ -285,7 +282,41 @@ app.registerExtension({
|
|||||||
settingValues.tagSource = newVal;
|
settingValues.tagSource = newVal;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- 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 + ".enable_models",
|
||||||
|
name: "Enable Loras and Embeddings",
|
||||||
|
tooltip: "Enable Lora and Embedding suggestions",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: true,
|
||||||
|
category: [name, "Autocompletion", "Enable Loras and Embeddings"],
|
||||||
|
onChange: (newVal, oldVal) => {
|
||||||
|
settingValues.enableModels = newVal;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: id + ".replace_underscore_with_space",
|
||||||
|
name: "Replace '_' with 'Space'",
|
||||||
|
tooltip: "This setting also affects related tags display.",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: true,
|
||||||
|
category: [name, "Autocompletion", "Replace Underscore with Space"],
|
||||||
|
onChange: (newVal, oldVal) => {
|
||||||
|
settingValues.replaceUnderscoreWithSpace = newVal;
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: id + ".max_suggestions",
|
id: id + ".max_suggestions",
|
||||||
name: "Max suggestions",
|
name: "Max suggestions",
|
||||||
@@ -364,6 +395,19 @@ app.registerExtension({
|
|||||||
onChange: (newVal, oldVal) => {
|
onChange: (newVal, oldVal) => {
|
||||||
settingValues.enableRelatedTags = newVal;
|
settingValues.enableRelatedTags = newVal;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Display settings ---
|
||||||
|
{
|
||||||
|
id: id + ".display_hide_alias",
|
||||||
|
name: "Hide Alias",
|
||||||
|
tooltip: "Hide alias in the autocomplete and related tags display",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: false,
|
||||||
|
category: [name, "Display", "Hide Alias"],
|
||||||
|
onChange: (newVal, oldVal) => {
|
||||||
|
settingValues.hideAlias = newVal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
+39
-33
@@ -1,13 +1,15 @@
|
|||||||
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,
|
||||||
|
getScrollbarWidth,
|
||||||
getViewportMargin,
|
getViewportMargin,
|
||||||
|
isLongText,
|
||||||
isValidTag,
|
isValidTag,
|
||||||
normalizeTagToInsert,
|
normalizeTagToInsert,
|
||||||
normalizeTagToSearch,
|
normalizeTagToSearch,
|
||||||
getCurrentTagRange,
|
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|
||||||
// --- RelatedTags Logic ---
|
// --- RelatedTags Logic ---
|
||||||
@@ -258,7 +260,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
|
||||||
@@ -309,7 +311,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();
|
||||||
@@ -409,17 +411,12 @@ class RelatedTagsUI {
|
|||||||
.find((tagData) => tagData !== undefined);
|
.find((tagData) => tagData !== undefined);
|
||||||
|
|
||||||
if (!tagData) {
|
if (!tagData) {
|
||||||
tagData = new TagData({
|
// Create a dummy TagData if not found
|
||||||
tag: this.currentTag,
|
tagData = new TagData(this.currentTag, null, 0, [], TagSource.Danbooru);
|
||||||
source: TagSource.Danbooru,
|
|
||||||
category: 'unknown',
|
|
||||||
count: 0,
|
|
||||||
alias: [],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagText = this.currentTag;
|
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||||
const categoryText = TagCategory[tagData.source][tagData.category];
|
const aliasText = tagData.alias.join(', ');
|
||||||
|
|
||||||
// Update header text with current tag
|
// Update header text with current tag
|
||||||
this.headerText.innerHTML = ''; // Clear previous content
|
this.headerText.innerHTML = ''; // Clear previous content
|
||||||
@@ -427,15 +424,15 @@ class RelatedTagsUI {
|
|||||||
|
|
||||||
const tagName = document.createElement('span');
|
const tagName = document.createElement('span');
|
||||||
tagName.classList.add('related-tags-header-tag-name', tagData.source);
|
tagName.classList.add('related-tags-header-tag-name', tagData.source);
|
||||||
tagName.title = `Count: ${tagData.count}\nCategory: ${categoryText}`;
|
tagName.title = `Count: ${tagData.count}\nCategory: ${categoryText}\nAlias: ${aliasText}`;
|
||||||
tagName.dataset.tagCategory = categoryText;
|
tagName.dataset.tagCategory = categoryText;
|
||||||
if (tagData.source && ['left', 'right'].includes(settingValues.tagSourceIconPosition)) {
|
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} ${tagText}`
|
? `${tagSourceIconHtml} ${tagData.tag}`
|
||||||
: `${tagText} ${tagSourceIconHtml}`;
|
: `${tagData.tag} ${tagSourceIconHtml}`;
|
||||||
} else {
|
} else {
|
||||||
tagName.textContent += tagText;
|
tagName.textContent += tagData.tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -446,8 +443,8 @@ class RelatedTagsUI {
|
|||||||
this.headerAlias.innerHTML = '';
|
this.headerAlias.innerHTML = '';
|
||||||
|
|
||||||
// Add alias if available
|
// Add alias if available
|
||||||
if (tagData.alias?.length > 0) {
|
if (aliasText.length > 0 && !settingValues.hideAlias) {
|
||||||
this.headerAlias.textContent = tagData.alias.join(', ');
|
this.headerAlias.textContent = aliasText;
|
||||||
this.headerAlias.style.display = 'block';
|
this.headerAlias.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,6 +481,9 @@ class RelatedTagsUI {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle column class based on settings
|
||||||
|
this.tagsContainer.classList.toggle('no-alias', settingValues.hideAlias);
|
||||||
|
|
||||||
const existingTags = extractTagsFromTextArea(this.target);
|
const existingTags = extractTagsFromTextArea(this.target);
|
||||||
|
|
||||||
// Create tag rows
|
// Create tag rows
|
||||||
@@ -502,6 +502,7 @@ class RelatedTagsUI {
|
|||||||
*/
|
*/
|
||||||
#createTagElement(tagData, isExisting) {
|
#createTagElement(tagData, isExisting) {
|
||||||
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
const categoryText = TagCategory[tagData.source][tagData.category] || "unknown";
|
||||||
|
const aliasText = tagData.alias.join(', ');
|
||||||
|
|
||||||
const tagRow = document.createElement('div');
|
const tagRow = document.createElement('div');
|
||||||
tagRow.classList.add('related-tag-item', tagData.source);
|
tagRow.classList.add('related-tag-item', tagData.source);
|
||||||
@@ -523,33 +524,30 @@ class RelatedTagsUI {
|
|||||||
alias.className = 'related-tag-alias';
|
alias.className = 'related-tag-alias';
|
||||||
|
|
||||||
// Display alias if available
|
// Display alias if available
|
||||||
if (tagData.alias && tagData.alias.length > 0) {
|
if (aliasText.length > 0) {
|
||||||
let aliasText = tagData.alias.join(', ');
|
|
||||||
alias.textContent = `${aliasText}`;
|
alias.textContent = `${aliasText}`;
|
||||||
alias.title = tagData.alias.join(', '); // Full alias on hover
|
alias.title = aliasText; // Full alias on hover
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category
|
|
||||||
const category = document.createElement('span');
|
|
||||||
category.className = `related-tag-category`;
|
|
||||||
category.textContent = `${categoryText.substring(0, 2)}`;
|
|
||||||
|
|
||||||
// Similarity
|
// Similarity
|
||||||
const similarity = document.createElement('span');
|
const similarity = document.createElement('span');
|
||||||
similarity.className = 'related-tag-similarity';
|
similarity.className = 'related-tag-similarity';
|
||||||
similarity.textContent = `${(tagData.similarity * 100).toFixed(2)}%`;
|
similarity.textContent = `${(tagData.similarity * 100).toFixed(2)}%`;
|
||||||
|
|
||||||
// Create tooltip with more info
|
// Create tooltip with more info
|
||||||
let tooltipText = `Tag: ${tagData.tag}\nSimilarity: ${(tagData.similarity * 100).toFixed(2)}%\nCount: ${tagData.count}`;
|
let tooltipText = `Similarity: ${(tagData.similarity * 100).toFixed(2)}%\nCount: ${tagData.count}\nCategory: ${categoryText}`;
|
||||||
if (tagData.alias && tagData.alias.length > 0) {
|
if (aliasText.length > 0) {
|
||||||
tooltipText += `\nAlias: ${tagData.alias.join(', ')}`;
|
tooltipText += `\nAlias: ${aliasText}`;
|
||||||
}
|
}
|
||||||
tagRow.title = tooltipText;
|
tagRow.title = tooltipText;
|
||||||
|
|
||||||
// Add cells to row
|
// Add cells to row
|
||||||
tagRow.appendChild(tagName);
|
tagRow.appendChild(tagName);
|
||||||
tagRow.appendChild(alias);
|
|
||||||
tagRow.appendChild(category);
|
if (!settingValues.hideAlias) {
|
||||||
|
tagRow.appendChild(alias);
|
||||||
|
}
|
||||||
|
|
||||||
tagRow.appendChild(similarity);
|
tagRow.appendChild(similarity);
|
||||||
|
|
||||||
return tagRow;
|
return tagRow;
|
||||||
@@ -579,7 +577,7 @@ class RelatedTagsUI {
|
|||||||
|
|
||||||
const newHeaderRect = this.header.getBoundingClientRect();
|
const newHeaderRect = this.header.getBoundingClientRect();
|
||||||
|
|
||||||
if(this.relatedTags.length > 0){
|
if (this.relatedTags.length > 0) {
|
||||||
this.tagsContainer.style.maxHeight = `${placementArea.height - newHeaderRect.height}px`;
|
this.tagsContainer.style.maxHeight = `${placementArea.height - newHeaderRect.height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,6 +666,10 @@ class RelatedTagsUI {
|
|||||||
area.y = targetRect.bottom;
|
area.y = targetRect.bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate width considering scrollbar width if vertical scrolling is needed
|
||||||
|
const scrollbarWidth = area.height < elemHeight ? getScrollbarWidth() : 0;
|
||||||
|
area.width = Math.min(elemWidth + scrollbarWidth, maxWidth);
|
||||||
|
|
||||||
// Adjust x position to avoid overflow
|
// Adjust x position to avoid overflow
|
||||||
area.x = Math.min(area.x, viewportWidth - area.width - margin.right);
|
area.x = Math.min(area.x, viewportWidth - area.width - margin.right);
|
||||||
} else {
|
} else {
|
||||||
@@ -684,6 +686,10 @@ class RelatedTagsUI {
|
|||||||
area.x = targetRect.right;
|
area.x = targetRect.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate width considering scrollbar width if vertical scrolling is needed
|
||||||
|
const scrollbarWidth = area.height < elemHeight ? getScrollbarWidth() : 0;
|
||||||
|
area.width = Math.min(area.width + scrollbarWidth, viewportWidth - margin.left - margin.right);
|
||||||
|
|
||||||
// Adjust y position to avoid overflow
|
// Adjust y position to avoid overflow
|
||||||
area.y = Math.min(area.y, viewportHeight - area.height - margin.bottom);
|
area.y = Math.min(area.y, viewportHeight - area.height - margin.bottom);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
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, hyphens, 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 an encoder optimized for processing Embedding or Lora notation.
|
||||||
|
* @returns {Encoder} FlexSearch encoder
|
||||||
|
*/
|
||||||
|
function createModelEncoder() {
|
||||||
|
return new Encoder({
|
||||||
|
normalize: true,
|
||||||
|
dedupe: false,
|
||||||
|
numeric: true,
|
||||||
|
cache: true,
|
||||||
|
prepare: function (str) {
|
||||||
|
return str.replace(/^<|>$/g, '').split(/(lora:|embedding:|[^\u0000-\u007f]+)/g).filter(Boolean).join(" ").trim();
|
||||||
|
},
|
||||||
|
split: /(?<=lora:.*|embedding:.*)[_./\(\)\-\s\\]+/
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (word) {
|
||||||
|
return word.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a FlexSearch Document instance optimized for lora or embedding searching.
|
||||||
|
* @returns {Document} Configured FlexSearch document
|
||||||
|
*/
|
||||||
|
export function createFlexSearchDocumentForModel() {
|
||||||
|
const modelEncoder = createModelEncoder();
|
||||||
|
|
||||||
|
|
||||||
|
// Configure the FlexSearch document with optimized indexing settings
|
||||||
|
// Note: alias field is not indexed for lora or embedding search
|
||||||
|
const document = new Document({
|
||||||
|
tokenize: "full", // Allow partial matching from both ends
|
||||||
|
encoder: modelEncoder,
|
||||||
|
document: {
|
||||||
|
id: "id",
|
||||||
|
index: [
|
||||||
|
{
|
||||||
|
field: "tag",
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "alias",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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, createModelEncoder } : undefined;
|
||||||
+18
-1
@@ -7,6 +7,9 @@ export const settingValues = {
|
|||||||
// Autocomplete feature settings
|
// Autocomplete feature settings
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxSuggestions: 10,
|
maxSuggestions: 10,
|
||||||
|
enableModels: true, // Enable Lora and Embedding suggestions
|
||||||
|
useFastSearch: false,
|
||||||
|
replaceUnderscoreWithSpace: true, // Replace underscores with spaces in tag insertion
|
||||||
|
|
||||||
// Related tags feature settings
|
// Related tags feature settings
|
||||||
enableRelatedTags: true,
|
enableRelatedTags: true,
|
||||||
@@ -14,11 +17,25 @@ export const settingValues = {
|
|||||||
relatedTagsDisplayPosition: 'horizontal', // 'horizontal' or 'vertical'
|
relatedTagsDisplayPosition: 'horizontal', // 'horizontal' or 'vertical'
|
||||||
relatedTagsTriggerMode: 'click', // Options: 'click', 'ctrl+Click'
|
relatedTagsTriggerMode: 'click', // Options: 'click', 'ctrl+Click'
|
||||||
|
|
||||||
|
// Display settings
|
||||||
|
hideAlias: false, // Hide alias in the autocomplete and related tags display
|
||||||
|
|
||||||
|
|
||||||
// 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={};
|
||||||
+72
-2
@@ -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.
|
||||||
@@ -98,6 +109,30 @@ export function formatCountHumanReadable(num) {
|
|||||||
return (num / si[i].value).toFixed(1).replace(rx, "$1") + si[i].symbol;
|
return (num / si[i].value).toFixed(1).replace(rx, "$1") + si[i].symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes HTML special characters in a string.
|
||||||
|
* @param {string} str The input string.
|
||||||
|
* @returns {string} The escaped string.
|
||||||
|
*/
|
||||||
|
export function escapeHtml(str) {
|
||||||
|
if (typeof str !== 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapeMap = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
'`': '`',
|
||||||
|
'/': '/'
|
||||||
|
};
|
||||||
|
|
||||||
|
return str.replace(/[&<>"'`/]/g, match =>
|
||||||
|
escapeMap[match]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escapes parentheses in a string for use in prompts.
|
* Escapes parentheses in a string for use in prompts.
|
||||||
* Replaces '(' with '\(' and ')' with '\)'.
|
* Replaces '(' with '\(' and ')' with '\)'.
|
||||||
@@ -197,8 +232,8 @@ export function normalizeTagToInsert(str) {
|
|||||||
if (isContainsLetterOrNumber(str)) {
|
if (isContainsLetterOrNumber(str)) {
|
||||||
const isWildcardCall = str.startsWith('__') && str.endsWith('__') && str.length > 4;
|
const isWildcardCall = str.startsWith('__') && str.endsWith('__') && str.length > 4;
|
||||||
|
|
||||||
if (!isWildcardCall) {
|
if (!isWildcardCall && settingValues.replaceUnderscoreWithSpace) {
|
||||||
// If doesn't wildcard call, replace underscores with spaces
|
// If doesn't wildcard call and setting is enabled, replace underscores with spaces
|
||||||
return escapeParentheses(str.replace(/_/g, " "));
|
return escapeParentheses(str.replace(/_/g, " "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -549,3 +584,38 @@ export function getViewportMargin() {
|
|||||||
right: rightBarRect.width,
|
right: rightBarRect.width,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache for scrollbar width measurement
|
||||||
|
let _cachedScrollbarWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures and returns the scrollbar width for the current browser.
|
||||||
|
* Creates a temporary element to measure the scrollbar width accurately.
|
||||||
|
* @returns {number} The scrollbar width in pixels
|
||||||
|
*/
|
||||||
|
export function getScrollbarWidth() {
|
||||||
|
// Return cached value if available
|
||||||
|
if (_cachedScrollbarWidth !== undefined) {
|
||||||
|
return _cachedScrollbarWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary div with scrollbar
|
||||||
|
const outer = document.createElement('div');
|
||||||
|
outer.style.visibility = 'hidden';
|
||||||
|
outer.style.position = 'absolute';
|
||||||
|
outer.style.overflow = 'scroll';
|
||||||
|
outer.style.top = '-9999px';
|
||||||
|
outer.style.width = '100px';
|
||||||
|
document.body.appendChild(outer);
|
||||||
|
|
||||||
|
const inner = document.createElement('div');
|
||||||
|
inner.style.width = '100%';
|
||||||
|
outer.appendChild(inner);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
_cachedScrollbarWidth = outer.offsetWidth - inner.offsetWidth;
|
||||||
|
|
||||||
|
document.body.removeChild(outer);
|
||||||
|
|
||||||
|
return _cachedScrollbarWidth;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user