Compare commits

...
40 Commits
Author SHA1 Message Date
newtextdoc1111 c4d5eee89d feat: Bump version to 1.4.1 2025-10-14 23:54:20 +09:00
newtextdoc1111 fa4d87ee77 Merge pull request #40 from newtextdoc1111/dev
Merge Dev
2025-10-14 23:49:43 +09:00
newtextdoc1111 3cb5a41cc8 fix: Temporarily comment out CSV update check button until remote file detection is properly implemented 2025-10-14 23:46:41 +09:00
newtextdoc1111 16a325b798 fix: Temporarily comment out remote file modification checks in the downloader 2025-08-30 23:40:27 +09:00
newtextdoc1111 e3171940fb fix: Removed the process that periodically checks for CSV file updates. 2025-08-30 23:39:52 +09:00
newtextdoc1111 8db37967f8 fix: Bump version to 1.4.0 2025-07-28 16:01:58 +09:00
newtextdoc1111 e43e51b48a Merge pull request #31 from newtextdoc1111/feature/#26_embedding_and_lora_autocompletion
Feature/#26 embedding and lora autocompletion
2025-07-28 16:00:37 +09:00
newtextdoc1111 7b89683bfc fix: Change to toLowerCase to simplify string normalization 2025-07-28 15:45:23 +09:00
newtextdoc1111 72b9bd94ee fix: Improved the encoder to provide more accurate suggestions 2025-07-28 14:05:49 +09:00
newtextdoc1111 8a4a94bc7e docs: Added Lora and Emb suggestions to README 2025-07-28 05:36:53 +09:00
newtextdoc1111 67b7b334b8 feat: Add embeddings and loras autocompletion 2025-07-28 05:21:57 +09:00
newtextdoc1111 9cdc18e566 Merge pull request #30 from newtextdoc1111/feature/improved_fast_search_behavior
Feature/improved fast search behavior
2025-07-24 03:59:49 +09:00
newtextdoc1111 3bbdb0312d bump: Update version to 1.3.1 2025-07-24 03:56:37 +09:00
newtextdoc1111 5b6fb8f490 refactor: Separates autocomplete logic into two methods: "sequentialSearch" and "searchWithFlexSearch" 2025-07-23 12:39:52 +09:00
newtextdoc1111 83dc2585ed fix: Correct formatting 2025-07-23 11:14:23 +09:00
newtextdoc1111 7e10c106d4 feat: Add env-cmd for managing environment variables in test scripts 2025-07-23 11:13:43 +09:00
newtextdoc1111 bf7823e985 fix: Increase the search result limit for FlexSearch to provide more accurate suggestions. 2025-07-23 11:12:23 +09:00
newtextdoc1111 6c29336435 feat: Optimized FlexSearch document and encoder settings to improve autocompletion 2025-07-23 07:47:52 +09:00
newtextdoc1111 083c1f5acc feat: add FlexSearch integration tests 2025-07-23 07:47:52 +09:00
newtextdoc1111 bdd86ab04d Merge pull request #29 from newtextdoc1111/dev
Merge Dev
2025-07-19 09:30:49 +09:00
newtextdoc1111 bafce3ca47 fix: Remove unused Charset import from FlexSearch module 2025-07-19 09:29:14 +09:00
newtextdoc1111 1729d5c4f7 docs: Fix link format 2025-07-19 09:11:12 +09:00
newtextdoc1111 d0dfcc031a fix: Bump version to 1.3.0 2025-07-19 09:03:43 +09:00
newtextdoc1111 ab2a4bedb3 docs: Add 'Use Fast Search' documentation 2025-07-19 08:59:45 +09:00
newtextdoc1111 6e7ebdae0a feat: Add tooltip for 'Use Fast Search' setting 2025-07-19 08:41:48 +09:00
newtextdoc1111 bd12dd933f docs: Add comment to fast search code 2025-07-19 08:39:07 +09:00
newtextdoc1111 784f7019ce Merge pull request #28 from IceSandwich/fast_search
Improve the performance
2025-07-17 22:30:53 +09:00
IceSandwich 6b34f3876f change flexSearchLimit to flexSearchLimitMultiplier 2025-07-17 20:51:59 +08:00
IceSandwich 59938c08d0 calculate the search limit from CSV files 2025-07-17 11:12:24 +08:00
IceSandwich 9c6215139b display actual candidate count when using fast search 2025-07-17 10:12:15 +08:00
IceSandwich cfda834141 Merge branch 'dev' into fast_search 2025-07-17 09:59:44 +08:00
IceSandwich b4175e700b fix the issue when input in tag and alias at the same time 2025-07-16 15:46:22 +08:00
IceSandwich b6c03233ba fix the behavior of matching words 2025-07-16 14:20:51 +08:00
IceSandwich 385f9d1c4f remove period handling completely 2025-07-16 10:58:03 +08:00
newtextdoc1111 f019960d34 fix: Remove unneccesary period handling in findAllTagPositions 2025-07-16 08:14:33 +09:00
newtextdoc1111 1d85f3e284 feat: Add 'Use Fast Search' setting to autocomplete options 2025-07-16 08:01:45 +09:00
newtextdoc1111 e072832216 fix: Improve sorting logic in searchCompletionCandidates for better tag matching 2025-07-16 08:01:11 +09:00
newtextdoc1111 a374ca6c11 feat: Refactor autocomplete data handling to build FlexSearch index after tags are loaded 2025-07-16 08:00:24 +09:00
IceSandwich 8750b5dcac fix indents 2025-07-14 14:48:20 +08:00
IceSandwich 456bc59240 improve performance by changing the search engine to flexsearch 2025-07-14 14:36:01 +08:00
16 changed files with 1132 additions and 107 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"test": {
"NODE_OPTIONS": "--experimental-vm-modules"
}
}
+32 -3
View File
@@ -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,8 @@ 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.
- **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
@@ -172,6 +174,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 +213,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.
+31 -3
View File
@@ -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,9 @@ worst_quality,5,9999999,
### オートコンプリート ### オートコンプリート
- **Enable Autocomplete**: オートコンプリート機能の有効化/無効化 - **Enable Autocomplete**: オートコンプリート機能の有効化/無効化
- **Max suggestions**: オートコンプリート候補の最大表示件数 - **Max Suggestions**: オートコンプリート候補の最大表示件数
- **Enable Loras and Embeddings**: LoraとEmbeddingを候補に表示する
- **Use Fast Search**: オートコンプリート候補の検索を高速な処理に切り替える(詳細は [オートコンプリートの高速検索について](#オートコンプリートの高速検索について) を確認してください)
### 関連タグ ### 関連タグ
@@ -170,6 +172,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 +210,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
View File
@@ -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
View File
@@ -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:
+28
View File
@@ -8,6 +8,7 @@
"@babel/core": "^7.27.1", "@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.2", "@babel/preset-env": "^7.27.2",
"babel-jest": "^29.7.0", "babel-jest": "^29.7.0",
"env-cmd": "^10.1.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"stylelint": "^16.19.1", "stylelint": "^16.19.1",
"stylelint-config-idiomatic-order": "^10.0.0", "stylelint-config-idiomatic-order": "^10.0.0",
@@ -3018,6 +3019,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3266,6 +3277,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/env-cmd": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz",
"integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^4.0.0",
"cross-spawn": "^7.0.0"
},
"bin": {
"env-cmd": "bin/env-cmd.js"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/env-paths": { "node_modules/env-paths": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+2 -1
View File
@@ -1,12 +1,13 @@
{ {
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "jest" "test": "env-cmd -e test -- jest"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.27.1", "@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.2", "@babel/preset-env": "^7.27.2",
"babel-jest": "^29.7.0", "babel-jest": "^29.7.0",
"env-cmd": "^10.1.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"stylelint": "^16.19.1", "stylelint": "^16.19.1",
"stylelint-config-idiomatic-order": "^10.0.0", "stylelint-config-idiomatic-order": "^10.0.0",
+1 -1
View File
@@ -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.1" version = "1.4.1"
license = {file = "LICENSE"} license = {file = "LICENSE"}
dependencies = ["",] dependencies = ["",]
+387
View File
@@ -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');
});
});
});
+110 -22
View File
@@ -1,4 +1,5 @@
import { import {
ModelTagSource,
TagCategory, TagCategory,
TagData, TagData,
autoCompleteData, autoCompleteData,
@@ -9,6 +10,7 @@ import {
hiraToKata, hiraToKata,
kataToHira, kataToHira,
formatCountHumanReadable, formatCountHumanReadable,
escapeHtml,
isContainsLetterOrNumber, isContainsLetterOrNumber,
normalizeTagToInsert, normalizeTagToInsert,
normalizeTagToSearch, normalizeTagToSearch,
@@ -79,22 +81,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 || if (!partialTag || partialTag.length <= 0 ||
ESCAPE_SEQUENCE.some(seq => partialTag.startsWith(seq)) || ESCAPE_SEQUENCE.some(seq => partialTag.startsWith(seq)) ||
isLongText(partialTag)) { isLongText(partialTag)) {
return []; // No valid input for autocomplete 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);
@@ -104,6 +101,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)
@@ -113,15 +130,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;
@@ -173,6 +189,69 @@ function searchCompletionCandidates(textareaElement) {
return candidates; return candidates;
} }
/**
* Search completion candidates using FlexSearch for fast matching.
* @param {string} partialTag
* @param {Set<string>} queryVariations
* @returns
*/
function searchWithFlexSearch(partialTag, queryVariations) {
const startTime = performance.now();
let mergedResult = [];
let totalSearchCount = 0;
const sources = getEnabledTagSourceInPriorityOrder();
for (const source of sources) {
if (!autoCompleteData[source].flexSearchDocument) continue;
if (mergedResult.length >= settingValues.maxSuggestions) break;
// Use the FlexSearch Document to search
// NOTE: The limit param is reflected separately for "tag" and "alias".
let searchResult = autoCompleteData[source].flexSearchDocument.search(partialTag, {
field: ["tag", "alias"],
limit: Math.min(settingValues.maxSuggestions * 10, 500),
merge: true,
suggest: false,
cache: true,
});
if (!searchResult || searchResult.length <= 0) continue;
// Sort results based on exact matches and counts
searchResult = searchResult
.map(r => autoCompleteData[source].sortedTags[r.id])
.sort((aTag, bTag) => {
if (matchWord(bTag.tag, queryVariations).isExactMatch) {
return 999999999999;
}
if (matchWord(aTag.tag, queryVariations).isExactMatch) {
return -999999999999;
}
if (bTag.alias && bTag.alias.some(alias => matchWord(alias, queryVariations).isExactMatch)) {
return 999999999999;
}
if (aTag.alias && aTag.alias.some(alias => matchWord(alias, queryVariations).isExactMatch)) {
return -999999999999;
}
return bTag.count - aTag.count;
});
// Merge results into the final array
mergedResult = mergedResult.concat(searchResult.slice(0, settingValues.maxSuggestions - mergedResult.length));
totalSearchCount += searchResult.length;
}
if (settingValues._logprocessingTime) {
const endTime = performance.now();
const duration = endTime - startTime;
console.debug(`[Autocomplete-Plus] Fast Search for "${partialTag}" took ${duration.toFixed(2)}ms.Found ${mergedResult.length} candidates within ${totalSearchCount} searches from flexsearch.`);
}
return mergedResult;
}
/** /**
* Extracts the current tag being typed before the cursor. * Extracts the current tag being typed before the cursor.
* @param {HTMLTextAreaElement} inputElement * @param {HTMLTextAreaElement} inputElement
@@ -221,9 +300,9 @@ 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) {
const text = inputElement.value; const text = inputElement.value;
const cursorPos = inputElement.selectionStart; const cursorPos = inputElement.selectionStart;
@@ -231,7 +310,13 @@ function insertTagToTextArea(inputElement, tagToInsert) {
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) {
@@ -300,7 +385,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();
} }
@@ -365,10 +450,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
@@ -414,9 +501,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
@@ -557,10 +645,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;
} }
@@ -921,4 +1009,4 @@ export class AutocompleteEventHandler {
*/ */
handleClick(event) { handleClick(event) {
} }
} }
+181 -19
View File
@@ -1,13 +1,20 @@
import { settingValues, updateMaxTagLength } 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,7 +179,7 @@ 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); updateMaxTagLength(tag.length);
@@ -189,6 +212,52 @@ async function loadTags(csvUrl, siteName) {
} }
} }
/**
* Build FlexSearch index for the given site name.
* @param {string} siteName
*/
async function buildFlexSearchIndex(siteName) {
try {
if (autoCompleteData[siteName].sortedTags.length === 0) {
return;
}
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.
@@ -299,7 +368,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) {
@@ -315,9 +388,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();
} }
@@ -376,12 +448,17 @@ 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) { // Build FlexSearch index after tags are loaded
console.log(`[Autocomplete-Plus] "${source}" Tags loading complete in ${(endTime - startTime).toFixed(2)}ms`); 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) {
@@ -397,3 +474,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(),
]);
}
+57 -38
View File
@@ -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
@@ -286,6 +283,28 @@ app.registerExtension({
} }
}, },
// --- Autocomplete Settings --- // --- Autocomplete Settings ---
{
id: id + ".use_fast_search",
name: "Use Fast Search",
tooltip: "Tag search processing during text input operates faster, improving responsiveness",
type: "boolean",
defaultValue: false,
category: [name, "Autocompletion", "Use Fast Search"],
onChange: (newVal, oldVal) => {
settingValues.useFastSearch = newVal;
}
},
{
id: id + ".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 + ".max_suggestions", id: id + ".max_suggestions",
name: "Max suggestions", name: "Max suggestions",
+133
View File
@@ -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, 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;
+2
View File
@@ -7,6 +7,8 @@ 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,
// Related tags feature settings // Related tags feature settings
enableRelatedTags: true, enableRelatedTags: true,
+106
View File
@@ -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={};
+24
View File
@@ -109,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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;',
'/': '&#x2F;'
};
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 '\)'.