From 631f8ed771713f80ab8aef4af53b54fa9bcb9e08 Mon Sep 17 00:00:00 2001 From: Menci Date: Thu, 1 Jan 2026 03:40:41 +0800 Subject: [PATCH] feat: initial commit --- .editorconfig | 25 + .github/workflows/ci.yaml | 41 + .gitignore | 141 + .vscode/settings.json | 18 + LICENSE | 661 ++ README.md | 20 + apps/demo/index.html | 12 + apps/demo/package.json | 28 + apps/demo/public/assets/.gitignore | 1 + apps/demo/src/App.tsx | 168 + apps/demo/src/data.ts | 26 + apps/demo/src/main.tsx | 12 + apps/demo/tsconfig.json | 24 + apps/demo/uno.config.ts | 30 + apps/demo/vite.config.ts | 13 + apps/playground-bot/package.json | 19 + apps/playground-bot/src/index.ts | 78 + apps/playground-bot/tsconfig.json | 23 + dotnet/.gitignore | 371 + dotnet/Directory.Build.props | 31 + dotnet/Directory.Packages.props | 19 + .../CommonNormalization.cs | 45 + dotnet/MaigoLabs.NeedLe.Common/CommonUtils.cs | 21 + .../Extensions/UnicodeExtensions.cs | 25 + .../MaigoLabs.NeedLe.Common.csproj | 19 + dotnet/MaigoLabs.NeedLe.Common/Trie.cs | 33 + .../Types/CompressedInvertedIndex.cs | 20 + .../Types/OffsetSpan.cs | 9 + .../Types/TokenDefinition.cs | 9 + .../Types/TokenType.cs | 10 + .../Han/HanVariantProvider.cs | 80 + .../Han/PinyinHelper.cs | 19 + .../Han/UnionFindSet.cs | 33 + .../InvertedIndexBuilder.cs | 57 + .../Japanese/JapaneseNormalization.cs | 69 + .../Japanese/JapaneseUtils.cs | 52 + .../Japanese/TranscriptionProvider.cs | 105 + .../MaigoLabs.NeedLe.Indexer.csproj | 29 + dotnet/MaigoLabs.NeedLe.Indexer/Tokenizer.cs | 104 + .../Trie/TrieBuilder.cs | 93 + .../Trie/TrieSerializer.cs | 41 + .../MaigoLabs.NeedLe.Playground.csproj | 18 + dotnet/MaigoLabs.NeedLe.Playground/Program.cs | 162 + .../InvertedIndexLoader.cs | 72 + .../InvertedIndexSearcher.cs | 270 + .../MaigoLabs.NeedLe.Searcher.csproj | 23 + .../SearchResultHighlighter.cs | 37 + .../Trie/TrieDeserializer.cs | 73 + .../Common/CommonNormalizationTests.cs | 126 + .../MaigoLabs.NeedLe.Tests/E2E/SearchTests.cs | 91 + .../E2E/TrieSerializationTests.cs | 143 + .../Indexer/Han/HanVariantProviderTests.cs | 75 + .../Indexer/Han/PinyinHelperTests.cs | 51 + .../Indexer/Han/UnionFindSetTests.cs | 59 + .../Indexer/Japanese/JapaneseUtilsTests.cs | 69 + .../Japanese/TranscriptionProviderTests.cs | 40 + .../Indexer/TokenizerTests.cs | 165 + .../Indexer/TrieTests.cs | 66 + .../MaigoLabs.NeedLe.Tests.csproj | 28 + .../MaigoLabs.NeedLe.Tests/NeedleTestBase.cs | 12 + dotnet/MaigoLabs.NeedLe.slnx | 16 + .../MaigoLabs.NeedLe/MaigoLabs.NeedLe.csproj | 35 + dotnet/README.md | 57 + eslint.config.ts | 157 + example.txt | 1585 ++++ package.json | 36 + packages/needle/LICENSE | 1 + packages/needle/README.md | 72 + packages/needle/jest.config.ts | 18 + packages/needle/package.json | 84 + packages/needle/src/common/index.ts | 4 + packages/needle/src/common/normalize.test.ts | 60 + packages/needle/src/common/normalize.ts | 42 + packages/needle/src/common/trie.ts | 17 + packages/needle/src/common/types.ts | 31 + packages/needle/src/common/utils.ts | 3 + packages/needle/src/e2e/search.test.ts | 73 + packages/needle/src/e2e/trie.test.ts | 111 + packages/needle/src/index.ts | 3 + packages/needle/src/indexer/han.test.ts | 103 + packages/needle/src/indexer/han.ts | 85 + packages/needle/src/indexer/index.ts | 5 + packages/needle/src/indexer/inverted-index.ts | 46 + packages/needle/src/indexer/japanese.test.ts | 66 + packages/needle/src/indexer/japanese.ts | 158 + packages/needle/src/indexer/tokenizer.test.ts | 166 + packages/needle/src/indexer/tokenizer.ts | 93 + packages/needle/src/indexer/trie.test.ts | 51 + packages/needle/src/indexer/trie.ts | 115 + packages/needle/src/searcher/highlight.ts | 26 + packages/needle/src/searcher/index.ts | 4 + .../needle/src/searcher/inverted-index.ts | 59 + packages/needle/src/searcher/search.ts | 258 + packages/needle/src/searcher/trie.ts | 58 + packages/needle/tsconfig.json | 23 + packages/needle/tsdown.config.ts | 15 + pnpm-lock.yaml | 6817 +++++++++++++++++ pnpm-workspace.yaml | 9 + 98 files changed, 14776 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 apps/demo/index.html create mode 100644 apps/demo/package.json create mode 100644 apps/demo/public/assets/.gitignore create mode 100644 apps/demo/src/App.tsx create mode 100644 apps/demo/src/data.ts create mode 100644 apps/demo/src/main.tsx create mode 100644 apps/demo/tsconfig.json create mode 100644 apps/demo/uno.config.ts create mode 100644 apps/demo/vite.config.ts create mode 100644 apps/playground-bot/package.json create mode 100644 apps/playground-bot/src/index.ts create mode 100644 apps/playground-bot/tsconfig.json create mode 100644 dotnet/.gitignore create mode 100644 dotnet/Directory.Build.props create mode 100644 dotnet/Directory.Packages.props create mode 100644 dotnet/MaigoLabs.NeedLe.Common/CommonNormalization.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Common/CommonUtils.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Common/Extensions/UnicodeExtensions.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Common/MaigoLabs.NeedLe.Common.csproj create mode 100644 dotnet/MaigoLabs.NeedLe.Common/Trie.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Common/Types/CompressedInvertedIndex.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Common/Types/OffsetSpan.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Common/Types/TokenDefinition.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Common/Types/TokenType.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Indexer/Han/HanVariantProvider.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Indexer/Han/PinyinHelper.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Indexer/Han/UnionFindSet.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Indexer/InvertedIndexBuilder.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Indexer/Japanese/JapaneseNormalization.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Indexer/Japanese/JapaneseUtils.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Indexer/Japanese/TranscriptionProvider.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Indexer/MaigoLabs.NeedLe.Indexer.csproj create mode 100644 dotnet/MaigoLabs.NeedLe.Indexer/Tokenizer.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Indexer/Trie/TrieBuilder.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Indexer/Trie/TrieSerializer.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Playground/MaigoLabs.NeedLe.Playground.csproj create mode 100644 dotnet/MaigoLabs.NeedLe.Playground/Program.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Searcher/InvertedIndexLoader.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Searcher/InvertedIndexSearcher.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Searcher/MaigoLabs.NeedLe.Searcher.csproj create mode 100644 dotnet/MaigoLabs.NeedLe.Searcher/SearchResultHighlighter.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Searcher/Trie/TrieDeserializer.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/Common/CommonNormalizationTests.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/E2E/SearchTests.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/E2E/TrieSerializationTests.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/HanVariantProviderTests.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/PinyinHelperTests.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/UnionFindSetTests.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/Indexer/Japanese/JapaneseUtilsTests.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/Indexer/Japanese/TranscriptionProviderTests.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/Indexer/TokenizerTests.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/Indexer/TrieTests.cs create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/MaigoLabs.NeedLe.Tests.csproj create mode 100644 dotnet/MaigoLabs.NeedLe.Tests/NeedleTestBase.cs create mode 100644 dotnet/MaigoLabs.NeedLe.slnx create mode 100644 dotnet/MaigoLabs.NeedLe/MaigoLabs.NeedLe.csproj create mode 100644 dotnet/README.md create mode 100644 eslint.config.ts create mode 100644 example.txt create mode 100644 package.json create mode 120000 packages/needle/LICENSE create mode 100644 packages/needle/README.md create mode 100644 packages/needle/jest.config.ts create mode 100644 packages/needle/package.json create mode 100644 packages/needle/src/common/index.ts create mode 100644 packages/needle/src/common/normalize.test.ts create mode 100644 packages/needle/src/common/normalize.ts create mode 100644 packages/needle/src/common/trie.ts create mode 100644 packages/needle/src/common/types.ts create mode 100644 packages/needle/src/common/utils.ts create mode 100644 packages/needle/src/e2e/search.test.ts create mode 100644 packages/needle/src/e2e/trie.test.ts create mode 100644 packages/needle/src/index.ts create mode 100644 packages/needle/src/indexer/han.test.ts create mode 100644 packages/needle/src/indexer/han.ts create mode 100644 packages/needle/src/indexer/index.ts create mode 100644 packages/needle/src/indexer/inverted-index.ts create mode 100644 packages/needle/src/indexer/japanese.test.ts create mode 100644 packages/needle/src/indexer/japanese.ts create mode 100644 packages/needle/src/indexer/tokenizer.test.ts create mode 100644 packages/needle/src/indexer/tokenizer.ts create mode 100644 packages/needle/src/indexer/trie.test.ts create mode 100644 packages/needle/src/indexer/trie.ts create mode 100644 packages/needle/src/searcher/highlight.ts create mode 100644 packages/needle/src/searcher/index.ts create mode 100644 packages/needle/src/searcher/inverted-index.ts create mode 100644 packages/needle/src/searcher/search.ts create mode 100644 packages/needle/src/searcher/trie.ts create mode 100644 packages/needle/tsconfig.json create mode 100644 packages/needle/tsdown.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..922fb7e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +root = true + +[*.cs] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{csproj,props,slnx}] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{ts,tsx,js,json}] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..81c2a27 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + pull_request: + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm typecheck + + - name: Lint + run: pnpm lint + + - name: Test (TypeScript) + run: pnpm test + + - name: Test (.NET) + run: pnpm test:dotnet diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ccb8df --- /dev/null +++ b/.gitignore @@ -0,0 +1,141 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist +.output + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..69f6072 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "editor.tabSize": 4, + "dotnet.defaultSolution": "dotnet/MaigoLabs.NeedLe.slnx", + "files.associations": { + "*.slnx": "xml" + }, + "eslint.useFlatConfig": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.rules.customizations": [ + { + "rule": "*", + "severity": "warn" + } + ], + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue"] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6de83ea --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# needLe + +Fuzzy search engine for small text pieces, with Chinese/Japanese pronunciation support. + +Available in [TypeScript](./packages/needle) and [C#](./dotnet). Click the link for detailed documentation. + +See also [in-browser demo](https://needle.maigo.dev). + +## Packages + +| Platform | Package | Install | +|:--------:|:-------:|:-------:| +| Node.js / Browser | [@maigolabs/needle](https://www.npmjs.com/package/@maigolabs/needle) | `pnpm add @maigolabs/needle` | +| .NET Standard 2.0 | [MaigoLabs.NeedLe](https://www.nuget.org/packages/MaigoLabs.NeedLe) | `dotnet add package MaigoLabs.NeedLe` | + +## The Name + +The word "needle" is from the phrase [Needle in a Haystack](https://en.wikipedia.org/wiki/Needle_in_a_haystack). Normally, searching tasks are finding a small string ("needle") in a large string ("haystack"). However, this project is designed for searching in small strings (specifically, music names) instead of large strings. We are finding needles in needles. + +The capitalized "L" is from the music name [needLe](https://projectsekai.fandom.com/wiki/NeedLe). diff --git a/apps/demo/index.html b/apps/demo/index.html new file mode 100644 index 0000000..09952f3 --- /dev/null +++ b/apps/demo/index.html @@ -0,0 +1,12 @@ + + + + + + MaigoLabs :: needLe + + +
+ + + diff --git a/apps/demo/package.json b/apps/demo/package.json new file mode 100644 index 0000000..e8a74ee --- /dev/null +++ b/apps/demo/package.json @@ -0,0 +1,28 @@ +{ + "name": "@maigolabs/needle-demo", + "version": "1.0.0", + "type": "module", + "scripts": { + "typecheck": "tsc", + "dev": "vite --port 5172", + "build": "tsc -b && vite build" + }, + "license": "AGPL-3.0", + "packageManager": "pnpm@10.20.0", + "private": true, + "dependencies": { + "@maigolabs/needle": "workspace:*", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@iconify-json/svg-spinners": "^1.2.4", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "unocss": "^66.5.12", + "vite": "^7.2.4", + "vite-plugin-top-level-await": "^1.6.0" + } +} diff --git a/apps/demo/public/assets/.gitignore b/apps/demo/public/assets/.gitignore new file mode 100644 index 0000000..7c04f7f --- /dev/null +++ b/apps/demo/public/assets/.gitignore @@ -0,0 +1 @@ +/fonts diff --git a/apps/demo/src/App.tsx b/apps/demo/src/App.tsx new file mode 100644 index 0000000..8725ac9 --- /dev/null +++ b/apps/demo/src/App.tsx @@ -0,0 +1,168 @@ +import { TokenType } from '@maigolabs/needle/common'; +import { + searchInvertedIndex, + highlightSearchResult, + type SearchResult, +} from '@maigolabs/needle/searcher'; +import { useState, type FunctionComponent } from 'react'; + +type Tab = 'search' | 'tokenize'; + +type AppData = typeof import('./data'); +export const Layout: FunctionComponent<{ dataPromise: Promise }> = ({ dataPromise }) => { + const [appData, setAppData] = useState(null); + const [error, setError] = useState(null); + void dataPromise.then(props => setAppData(props)).catch(error => setError((error instanceof Error ? error.stack : undefined) ?? String(error))); + return ( +
+
+
+

MaigoLabs :: needLe

+
+

Fuzzy search engine for small text pieces, with Chinese/Japanese pronunciation support

+

(Available in TypeScript and C#)

+
+ +
+ + { + appData + ? + : error + ?
{error}
+ :
+
Loading...
+
+
Tips:
+
This demo loads Kuromoji/OpenCC/pinyin-pro for tokenization and index building.
+
However, searching on a prebuilt index doesn't require loading any external library/dictionary.
+
+
+ } +
+
+ ); +}; + +interface AppProps { + appData: AppData; +} + +export const App: FunctionComponent = ({ appData: { kuromoji, createTokenizer, invertedIndex } }) => { + const [input, setInput] = useState(''); + const [tab, setTab] = useState('search'); + + const searchResults = tab === 'search' && input.trim() + ? searchInvertedIndex(invertedIndex, input).slice(0, 50) + : []; + + const tokenizeResults = tab === 'tokenize' && input.trim() + ? (() => { + const tokenizer = createTokenizer({ kuromoji }); + const tokens = tokenizer.tokenize(input); + const tokenDefs = tokenizer.tokens; + const codePoints = [...input]; + return tokens.map(t => { + const def = [...tokenDefs.values()].find(d => d.id === t.id)!; + const original = codePoints.slice(t.start, t.end).join(''); + return { ...t, type: def.type, text: def.text, original }; + }); + })() + : []; + + return ( + <> + setInput(e.target.value)} + placeholder={`Type something to ${tab}...`} + className="w-full bg-[#efe5d0] text-[#6b5a48] px-3 py-2 mb-2 outline-none placeholder-[#b8a890] rounded-lg" + /> + +
+ + +
+ +
+ {tab === 'search' && searchResults.map((result, i) => ( + + ))} + + {tab === 'tokenize' && tokenizeResults.length > 0 && ( +
+ {tokenizeResults.map((token, i) => ( +
+ {TokenType[token.type]}: + {JSON.stringify(token.text)} + {' <- '} + {JSON.stringify(token.original)} + {` [${token.start}, ${token.end}]`} +
+ ))} +
+ )} + + {input.trim() && tab === 'search' && searchResults.length === 0 && ( +
No results.
+ )} +
+ + ); +}; + +const SearchResultItem: FunctionComponent<{ result: SearchResult; input: string }> = ({ result, input }) => { + const highlighted = highlightSearchResult(result); + const inputCodePoints = [...input]; + + const stats = [ + `${result.rangeCount} range(s)`, + `${Math.round(result.matchRatio * 100)}%`, + result.prefixMatchCount > 0 ? `${result.prefixMatchCount} prefix` : null, + ].filter(Boolean).join(', '); + + return ( +
+
+
+ {highlighted.map((part, i) => + typeof part === 'string' + ? {part} + : {part.highlight})} +
+
{stats}
+
+ +
+ {result.tokens.map((token, i) => { + const inputText = inputCodePoints.slice(token.inputOffset.start, token.inputOffset.end).join(''); + const docText = result.documentCodePoints.slice(token.documentOffset.start, token.documentOffset.end).join(''); + return ( +
+ {TokenType[token.definition.type]}: + {JSON.stringify(inputText)} + {' -> '} + {JSON.stringify(docText)} + {token.isTokenPrefixMatching && {' (prefix)'}} +
+ ); + })} +
+
+ ); +}; diff --git a/apps/demo/src/data.ts b/apps/demo/src/data.ts new file mode 100644 index 0000000..0251e7c --- /dev/null +++ b/apps/demo/src/data.ts @@ -0,0 +1,26 @@ +import { buildInvertedIndex } from '@maigolabs/needle/indexer'; +import { loadInvertedIndex } from '@maigolabs/needle/searcher'; +import { TokenizerBuilder } from '@patdx/kuromoji'; + +// Indexer loads OpenCC and pinyin-pro which is large, put them in data.ts for dynamic importing. +export { createTokenizer } from '@maigolabs/needle/indexer'; + +const musicNames: string[] = [...new Set( + Object.values( + await (await fetch('https://sekai-world.github.io/sekai-master-db-diff/musics.json')).json(), + ).map(music => (music as { title: string }).title), +)]; + +export const kuromoji = await new TokenizerBuilder({ + loader: { + loadArrayBuffer: async (url: string) => { + url = `https://cdn.jsdelivr.net/npm/@aiktb/kuromoji@1.0.2/dict/${url.replace('.gz', '')}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`Failed to fetch ${url}`); + return await res.arrayBuffer(); + }, + }, +}).build(); + +export const compressed = buildInvertedIndex(musicNames, { kuromoji }); +export const invertedIndex = loadInvertedIndex(compressed); diff --git a/apps/demo/src/main.tsx b/apps/demo/src/main.tsx new file mode 100644 index 0000000..e5bc056 --- /dev/null +++ b/apps/demo/src/main.tsx @@ -0,0 +1,12 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { Layout } from './App'; +import 'virtual:uno.css'; +import '@unocss/reset/tailwind.css'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/apps/demo/tsconfig.json b/apps/demo/tsconfig.json new file mode 100644 index 0000000..e7fe6fa --- /dev/null +++ b/apps/demo/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "jsx": "preserve", + "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], + "types": ["vite/client"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "allowJs": true, + "strict": true, + "strictNullChecks": true, + "noEmit": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "skipLibCheck": true, + "rootDir": ".", + "outDir": "dist" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["dist", "node_modules"] +} diff --git a/apps/demo/uno.config.ts b/apps/demo/uno.config.ts new file mode 100644 index 0000000..23f55ed --- /dev/null +++ b/apps/demo/uno.config.ts @@ -0,0 +1,30 @@ +import { createLocalFontProcessor } from '@unocss/preset-web-fonts/local'; +import { defineConfig, presetWind3, presetTypography, presetWebFonts, transformerVariantGroup, transformerDirectives, presetIcons } from 'unocss'; + +export default defineConfig({ + presets: [ + presetWind3(), + presetTypography(), + presetIcons({ + scale: 1.2, + warn: true, + }), + presetWebFonts({ + fonts: { + mono: { + name: 'Maple Mono', + provider: 'fontsource', + }, + }, + processors: createLocalFontProcessor({ + cacheDir: 'node_modules/.cache/unocss/fonts', + fontAssetsDir: 'public/assets/fonts/cache', + fontServeBaseUrl: '/assets/fonts/cache', + }), + }), + ], + transformers: [ + transformerDirectives(), + transformerVariantGroup(), + ], +}); diff --git a/apps/demo/vite.config.ts b/apps/demo/vite.config.ts new file mode 100644 index 0000000..b1a68b7 --- /dev/null +++ b/apps/demo/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import UnoCSS from 'unocss/vite' +import react from '@vitejs/plugin-react' +import topLevelAwait from 'vite-plugin-top-level-await' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), UnoCSS(), topLevelAwait()], + build: { + assetsInlineLimit: 0, + minify: true + }, +}) diff --git a/apps/playground-bot/package.json b/apps/playground-bot/package.json new file mode 100644 index 0000000..65b9081 --- /dev/null +++ b/apps/playground-bot/package.json @@ -0,0 +1,19 @@ +{ + "name": "@maigolabs/needle-playground-bot", + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "tsx src/index.ts", + "typecheck": "tsc" + }, + "license": "AGPL-3.0", + "packageManager": "pnpm@10.20.0", + "private": true, + "dependencies": { + "@maigolabs/needle": "workspace:*", + "telegraf": "^4.16.3" + }, + "devDependencies": { + "@types/node": "^24.10.4" + } +} diff --git a/apps/playground-bot/src/index.ts b/apps/playground-bot/src/index.ts new file mode 100644 index 0000000..b306136 --- /dev/null +++ b/apps/playground-bot/src/index.ts @@ -0,0 +1,78 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; + +import { TokenType } from '@maigolabs/needle/common'; +import { buildInvertedIndex, createTokenizer } from '@maigolabs/needle/indexer'; +import { loadInvertedIndex, inspectSearchResult, searchInvertedIndex } from '@maigolabs/needle/searcher'; +import { TokenizerBuilder } from '@patdx/kuromoji'; +import NodeDictionaryLoader from '@patdx/kuromoji/node'; +import { Telegraf } from 'telegraf'; + +const botToken = process.env.TELEGRAM_BOT_TOKEN!; +const targetChatId = parseInt(process.env.TARGET_CHAT_ID!); +if (!botToken || isNaN(targetChatId)) throw new Error('Missing environment variables TELEGRAM_BOT_TOKEN or TARGET_CHAT_ID'); + +const bot = new Telegraf(botToken); + +const escapeHtml = (s: string) => s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); + +const commands = await (async () => { + const kuromojiDictPath = path.resolve(url.fileURLToPath(import.meta.resolve('@patdx/kuromoji')), '..', '..', 'dict'); + const kuromoji = await new TokenizerBuilder({ loader: new NodeDictionaryLoader({ dic_path: kuromojiDictPath }) }).build(); + + const documents = (await fs.promises.readFile('../../example.txt', 'utf-8')).split('\n').filter(line => line.length > 0); + const startBuildInvertedIndex = performance.now(); + const compressed = buildInvertedIndex(documents, { kuromoji }); + const endBuildInvertedIndex = performance.now(); + console.log(`Built inverted index in ${endBuildInvertedIndex - startBuildInvertedIndex}ms`); + + const startLoadInvertedIndex = performance.now(); + const invertedIndex = loadInvertedIndex(compressed); + const endLoadInvertedIndex = performance.now(); + console.log(`Loaded inverted index in ${endLoadInvertedIndex - startLoadInvertedIndex}ms`); + + const codify = (text: string) => `${escapeHtml(text)}`; + return { + needle: (text: string) => { + const startSearch = performance.now(); + const results = searchInvertedIndex(invertedIndex, text); + const endSearch = performance.now(); + const searchDuration = (endSearch - startSearch).toFixed(3); + const showingResults = results.slice(0, 5); + return results.length === 0 ? codify(`No results found after ${searchDuration}ms`) : [ + codify(`Search completed in ${searchDuration}ms, showing ${showingResults.length}/${results.length} results:\n`), + ...showingResults.map(result => inspectSearchResult(result, true)), + ].join('\n').trimEnd(); + }, + tokenize: (text: string) => { + const startTokenize = performance.now(); + const tokenizer = createTokenizer({ kuromoji }); + const tokens = tokenizer.tokenize(text); + const tokenDefinitions = [...tokenizer.tokens.values()]; + const endTokenize = performance.now(); + const tokenizeDuration = (endTokenize - startTokenize).toFixed(3); + return codify(tokens.length === 0 ? `No tokens emitted after ${tokenizeDuration}ms` : [ + `Tokenization completed in ${tokenizeDuration}ms, emitted ${tokens.length} tokens:`, + ...tokens + .map(token => [tokenDefinitions[token.id]!, token, [...text].slice(token.start, token.end).join('')] as const) + .map(([token, { start, end }, originalPhrase]) => ` ${TokenType[token.type]}: ${JSON.stringify(token.text)} <- ${JSON.stringify(originalPhrase)} [${start}, ${end}]`), + ].join('\n')); + }, + }; +})(); + +bot.on('message', async ctx => { + const text = 'text' in ctx.message ? ctx.message.text : undefined; + console.log(`${ctx.chat.id ?? 'N/A'}:${ctx.from!.id} ${JSON.stringify(text)}`); + if (ctx.chat.id === targetChatId) { + if (text?.startsWith('/needle ')) { + await ctx.reply(commands.needle(text.slice('/needle '.length)), { parse_mode: 'HTML' }); + } else if (text?.startsWith('/tokenize ')) { + await ctx.reply(commands.tokenize(text.slice('/tokenize '.length)), { parse_mode: 'HTML' }); + } + } +}); + +await bot.launch(); +void bot.telegram.getMe().then(me => console.log(`Bot logged in as ${me.first_name} (@${me.username})`)); diff --git a/apps/playground-bot/tsconfig.json b/apps/playground-bot/tsconfig.json new file mode 100644 index 0000000..90f8798 --- /dev/null +++ b/apps/playground-bot/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ESNext", + "jsx": "preserve", + "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "allowJs": true, + "strict": true, + "strictNullChecks": true, + "noEmit": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "skipLibCheck": true, + "rootDir": ".", + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/dotnet/.gitignore b/dotnet/.gitignore new file mode 100644 index 0000000..22d72f6 --- /dev/null +++ b/dotnet/.gitignore @@ -0,0 +1,371 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/git,visualstudio +# Edit at https://www.toptal.com/developers/gitignore?templates=git,visualstudio + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*[.json, .xml, .info] + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# End of https://www.toptal.com/developers/gitignore/api/git,visualstudio diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props new file mode 100644 index 0000000..55dab47 --- /dev/null +++ b/dotnet/Directory.Build.props @@ -0,0 +1,31 @@ + + + + netstandard2.0 + true + 14 + enable + enable + CA1822 + MaigoLabs.NeedLe + console%3Bverbosity=detailed + + + + false + 1.0.0 + Menci + Fuzzy search engine for small text pieces, with Chinese/Japanese pronunciation support + AGPL-3.0-only + https://github.com/MaigoLabs/needLe + git + https://github.com/MaigoLabs/needLe + search;fuzzy;cjk;chinese;japanese;pinyin;romaji + README.md + + + + + + + diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props new file mode 100644 index 0000000..5f0ebe2 --- /dev/null +++ b/dotnet/Directory.Packages.props @@ -0,0 +1,19 @@ + + + true + + + + + + + + + + + + + + + + diff --git a/dotnet/MaigoLabs.NeedLe.Common/CommonNormalization.cs b/dotnet/MaigoLabs.NeedLe.Common/CommonNormalization.cs new file mode 100644 index 0000000..9cafc1b --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Common/CommonNormalization.cs @@ -0,0 +1,45 @@ +namespace MaigoLabs.NeedLe.Common; + +// This is for global normalization for any input and documents. +public static class CommonNormalization +{ + public static int NormalizeCodePoint(int codePoint) + { + // Fullwidth ASCII -> Halfwidth ASCII + if (codePoint >= 0xFF01 && codePoint <= 0xFF5E) return ToLowerCaseAscii(codePoint - 0xFEE0); + // Fullwidth space -> Halfwidth space + else if (codePoint == /* ' ' */ 0x3000) return ' '; + // Halfwidth kana (U+FF66 - U+FF9D) -> Fullwidth kana + else if (codePoint >= 0xFF66 && codePoint <= 0xFF9D) return HALF_TO_FULL_KANA.TryGetValue(codePoint, out var value) ? value : codePoint; + else if (codePoint == /* '。' */ 0xFF61) return '。'; + else if (codePoint == /* '「' */ 0xFF62) return '「'; + else if (codePoint == /* '」' */ 0xFF63) return '」'; + else if (codePoint == /* '、' */ 0xFF64) return '、'; + else if (codePoint == /* '・' */ 0xFF65) return '・'; + else if (codePoint == /* '゙' */ 0xFF9E || codePoint == /* '゛' */ 0x309B) return 0x3099; // -> COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK + else if (codePoint == /* '゚' */ 0xFF9F || codePoint == /* '゜' */ 0x309C) return 0x309A; // -> COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK + else return ToLowerCaseAscii(codePoint); + } + + private static readonly Dictionary HALF_TO_FULL_KANA = new Dictionary { + ['ヲ'] = 'ヲ', ['ァ'] = 'ァ', ['ィ'] = 'ィ', ['ゥ'] = 'ゥ', ['ェ'] = 'ェ', ['ォ'] = 'ォ', + ['ャ'] = 'ャ', ['ュ'] = 'ュ', ['ョ'] = 'ョ', ['ッ'] = 'ッ', + ['ー'] = 'ー', + ['ア'] = 'ア', ['イ'] = 'イ', ['ウ'] = 'ウ', ['エ'] = 'エ', ['オ'] = 'オ', + ['カ'] = 'カ', ['キ'] = 'キ', ['ク'] = 'ク', ['ケ'] = 'ケ', ['コ'] = 'コ', + ['サ'] = 'サ', ['シ'] = 'シ', ['ス'] = 'ス', ['セ'] = 'セ', ['ソ'] = 'ソ', + ['タ'] = 'タ', ['チ'] = 'チ', ['ツ'] = 'ツ', ['テ'] = 'テ', ['ト'] = 'ト', + ['ナ'] = 'ナ', ['ニ'] = 'ニ', ['ヌ'] = 'ヌ', ['ネ'] = 'ネ', ['ノ'] = 'ノ', + ['ハ'] = 'ハ', ['ヒ'] = 'ヒ', ['フ'] = 'フ', ['ヘ'] = 'ヘ', ['ホ'] = 'ホ', + ['マ'] = 'マ', ['ミ'] = 'ミ', ['ム'] = 'ム', ['メ'] = 'メ', ['モ'] = 'モ', + ['ヤ'] = 'ヤ', ['ユ'] = 'ユ', ['ヨ'] = 'ヨ', + ['ラ'] = 'ラ', ['リ'] = 'リ', ['ル'] = 'ル', ['レ'] = 'レ', ['ロ'] = 'ロ', + ['ワ'] = 'ワ', ['ン'] = 'ン', + }; + + public static int ToLowerCaseAscii(int codePoint) => codePoint >= 0x41 && codePoint <= 0x5A ? codePoint + 0x20 : codePoint; + + public static bool IsHiraganaRange(int codePoint) => (codePoint >= 0x3041 && codePoint <= 0x3096) || (codePoint >= 0x309D && codePoint <= 0x309E); + public static int ToKatakana(int codePoint) => IsHiraganaRange(codePoint) ? codePoint + 0x60 : codePoint; + public static string ToKatakana(string text) => string.Concat(text.Select(c => (char)ToKatakana(c))); +} diff --git a/dotnet/MaigoLabs.NeedLe.Common/CommonUtils.cs b/dotnet/MaigoLabs.NeedLe.Common/CommonUtils.cs new file mode 100644 index 0000000..f8535cb --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Common/CommonUtils.cs @@ -0,0 +1,21 @@ +namespace MaigoLabs.NeedLe.Common; + +public static class CommonUtils +{ + public static bool IsWhitespace(int codePoint) => + codePoint == 0x0009 /* \t */ || + codePoint == 0x000A /* \n */ || + codePoint == 0x000B /* Vertical Tab */ || + codePoint == 0x000C /* \f */ || + codePoint == 0x000D /* \r */ || + codePoint == 0x0020 /* Space */ || + codePoint == 0x0085 /* Next Line (NEL) */ || + codePoint == 0x00A0 /* No-Break Space */ || + codePoint == 0x1680 /* Ogham Space Mark */ || + codePoint >= 0x2000 && codePoint <= 0x200A || + codePoint == 0x2028 /* Line Separator */ || + codePoint == 0x2029 /* Paragraph Separator */ || + codePoint == 0x202F /* Narrow No-Break Space */ || + codePoint == 0x205F /* Medium Mathematical Space */ || + codePoint == 0x3000 /* Ideographic Space */; +} diff --git a/dotnet/MaigoLabs.NeedLe.Common/Extensions/UnicodeExtensions.cs b/dotnet/MaigoLabs.NeedLe.Common/Extensions/UnicodeExtensions.cs new file mode 100644 index 0000000..f038523 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Common/Extensions/UnicodeExtensions.cs @@ -0,0 +1,25 @@ +using System.Text; + +namespace MaigoLabs.NeedLe.Common.Extensions; + +public static class UnicodeExtensions +{ + public static IEnumerable ToCodePoints(this string s) + { + for (int i = 0; i < s.Length; i++) + { + int codePoint = char.ConvertToUtf32(s, i); + if (codePoint > 0xffff) i++; + yield return codePoint; + } + } + + public static StringBuilder ToUtf32StringBuilder(this IEnumerable codePoints) + { + var sb = new StringBuilder(); + foreach (var codePoint in codePoints) sb.Append(char.ConvertFromUtf32(codePoint)); + return sb; + } + + public static string ToUtf32String(this IEnumerable codePoints) => ToUtf32StringBuilder(codePoints).ToString(); +} diff --git a/dotnet/MaigoLabs.NeedLe.Common/MaigoLabs.NeedLe.Common.csproj b/dotnet/MaigoLabs.NeedLe.Common/MaigoLabs.NeedLe.Common.csproj new file mode 100644 index 0000000..a445349 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Common/MaigoLabs.NeedLe.Common.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + Library + $(ProjectName).Common + $(RootNamespace) + + + + true + $(RootNamespace) + + + + + + + diff --git a/dotnet/MaigoLabs.NeedLe.Common/Trie.cs b/dotnet/MaigoLabs.NeedLe.Common/Trie.cs new file mode 100644 index 0000000..0339061 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Common/Trie.cs @@ -0,0 +1,33 @@ +namespace MaigoLabs.NeedLe.Common; + +public class TrieNode +{ + public required TrieNode? Parent { get; set; } + public required Dictionary Children { get; set; } // Unicode code point -> child node + public required List TokenIds { get; set; } + public required List SubTreeTokenIds { get; set; } // Empty on root. +} + +public static class TrieNodeExtensions +{ + public static TrieNode? TraverseStep(this TrieNode? node, int codePoint, bool isIgnorable = false) => + (node?.Children.TryGetValue(codePoint, out var child) ?? false) + ? child + : isIgnorable ? node : null; + + public static TrieNode? Traverse(this TrieNode? node, int[] codePoints, bool isIgnorable = false) + { + if (node == null) return null; + foreach (var codePoint in codePoints) + { + node = node?.TraverseStep(codePoint, isIgnorable); + if (node == null) return null; + } + return node; + } + + public static List GetTokenIds(this TrieNode? node, bool includeSubTree = false) => + (includeSubTree ? node?.SubTreeTokenIds : node?.TokenIds) ?? []; + + public static bool IsTokenExactMatch(this TrieNode? node, int tokenId) => node?.TokenIds.Contains(tokenId) ?? false; +} diff --git a/dotnet/MaigoLabs.NeedLe.Common/Types/CompressedInvertedIndex.cs b/dotnet/MaigoLabs.NeedLe.Common/Types/CompressedInvertedIndex.cs new file mode 100644 index 0000000..43f76ad --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Common/Types/CompressedInvertedIndex.cs @@ -0,0 +1,20 @@ +namespace MaigoLabs.NeedLe.Common.Types; + +#pragma warning disable IDE1006 // Naming rule violation + +// For compatibility with TypeScript, we use camelCase property names here. + +public class CompressedInvertedIndex +{ + public required string[] documents { get; set; } + public required int[] tokenTypes { get; set; } // Use int values here instead of TokenType enum to avoid JSON serialization issues. + public required List[] tokenReferences { get; set; } // tokenId -> [documentId, start1, end1, start2, end2, ...] + public required CompressedInvertedIndexTries tries { get; set; } +} + +public class CompressedInvertedIndexTries +{ + public required int[] romaji { get; set; } + public required int[] kana { get; set; } + public required int[] other { get; set; } +} diff --git a/dotnet/MaigoLabs.NeedLe.Common/Types/OffsetSpan.cs b/dotnet/MaigoLabs.NeedLe.Common/Types/OffsetSpan.cs new file mode 100644 index 0000000..542f0b9 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Common/Types/OffsetSpan.cs @@ -0,0 +1,9 @@ +namespace MaigoLabs.NeedLe.Common.Types; + +public class OffsetSpan +{ + public required int Start { get; init; } + public required int End { get; init; } + + public int Length => End - Start; +} diff --git a/dotnet/MaigoLabs.NeedLe.Common/Types/TokenDefinition.cs b/dotnet/MaigoLabs.NeedLe.Common/Types/TokenDefinition.cs new file mode 100644 index 0000000..d867e26 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Common/Types/TokenDefinition.cs @@ -0,0 +1,9 @@ +namespace MaigoLabs.NeedLe.Common.Types; + +public class TokenDefinition +{ + public required int Id { get; set; } + public required TokenType Type { get; set; } + public required string Text { get; set; } + public required int CodePointLength { get; set; } +} diff --git a/dotnet/MaigoLabs.NeedLe.Common/Types/TokenType.cs b/dotnet/MaigoLabs.NeedLe.Common/Types/TokenType.cs new file mode 100644 index 0000000..406bd93 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Common/Types/TokenType.cs @@ -0,0 +1,10 @@ +namespace MaigoLabs.NeedLe.Common.Types; + +public enum TokenType +{ + Raw, + Kana, + Romaji, + Han, + Pinyin, +} diff --git a/dotnet/MaigoLabs.NeedLe.Indexer/Han/HanVariantProvider.cs b/dotnet/MaigoLabs.NeedLe.Indexer/Han/HanVariantProvider.cs new file mode 100644 index 0000000..d874ea6 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Indexer/Han/HanVariantProvider.cs @@ -0,0 +1,80 @@ +using MaigoLabs.NeedLe.Common.Extensions; +using OpenccNetLib; + +namespace MaigoLabs.NeedLe.Indexer.Han; + +public class HanVariantProvider +{ + private readonly Dictionary EXCHANGE_MAP; + public HanVariantProvider(DictWithMaxLength[]? dicts = null) + { + dicts ??= + [ + DictionaryLib.Provider.hk_variants, + DictionaryLib.Provider.hk_variants_rev, + DictionaryLib.Provider.jp_variants, + DictionaryLib.Provider.jp_variants_rev, + DictionaryLib.Provider.st_characters, + DictionaryLib.Provider.ts_characters, + DictionaryLib.Provider.tw_variants, + DictionaryLib.Provider.tw_variants_rev, + ]; + EXCHANGE_MAP = BuildHanExchangeMap(dicts); + } + + private Dictionary BuildHanExchangeMap(DictWithMaxLength[] dicts) + { + var unionFindSet = new UnionFindSet(); + foreach (var dict in dicts) foreach (var item in dict.Dict) + { + var from = item.Key.ToCodePoints().ToArray(); + var to = item.Value.ToCodePoints().ToArray(); + if (from.Length != 1 || to.Length != 1) continue; + unionFindSet.Union(from[0], to[0]); + } + var variants = new Dictionary>(); + foreach (var x in unionFindSet.Keys) + { + var parent = unionFindSet.Find(x); + if (!variants.TryGetValue(parent, out var list)) variants[parent] = list = []; + if (x != parent) variants[x] = list; + list.Add(x); + } + return variants.ToDictionary(item => item.Key, item => item.Value.OrderBy(x => x).ToArray()); + } + + // https://github.com/google/re2/blob/e7aec5985072c1dbe735add802653ef4b36c231a/re2/unicode_groups.cc#L5590-L5615 + private static readonly (int Min, int Max)[] RE2_SCRIPT_HAN_RENAGES = + [ + // Han_range16 + (11904, 11929), + (11931, 12019), + (12032, 12245), + (12293, 12293), + (12295, 12295), + (12321, 12329), + (12344, 12347), + (13312, 19903), + (19968, 40959), + (63744, 64109), + (64112, 64217), + // Han_range32 + (94178, 94179), + (94192, 94193), + (131072, 173791), + (173824, 177977), + (177984, 178205), + (178208, 183969), + (183984, 191456), + (191472, 192093), + (194560, 195101), + (196608, 201546), + (201552, 205743), + ]; + + public static bool IsHanCharacter(int codePoint) => RE2_SCRIPT_HAN_RENAGES.Any(range => codePoint >= range.Min && codePoint <= range.Max); + + public int[] GetHanVariants(int codePoint) => EXCHANGE_MAP.TryGetValue(codePoint, out var variants) + ? variants + : IsHanCharacter(codePoint) ? [codePoint] : []; +} diff --git a/dotnet/MaigoLabs.NeedLe.Indexer/Han/PinyinHelper.cs b/dotnet/MaigoLabs.NeedLe.Indexer/Han/PinyinHelper.cs new file mode 100644 index 0000000..8a0983c --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Indexer/Han/PinyinHelper.cs @@ -0,0 +1,19 @@ +using hyjiacan.py4n; + +namespace MaigoLabs.NeedLe.Indexer.Han; + +public static class PinyinHelper +{ + private static readonly string[] PINYIN_INITIALS = ["b", "p", "m", "f", "d", "t", "n", "l", "g", "k", "h", "j", "q", "x", "zh", "ch", "sh", "r", "z", "c", "s", "y", "w"]; + private static readonly Dictionary PINYIN_FINALS_FUZZY_MAP = new() { ["ang"] = "an", ["eng"] = "en", ["ing"] = "in" }; + + public static IEnumerable GetPinyinCandidates(int codePoint) => codePoint < char.MinValue || codePoint > char.MaxValue || !PinyinUtil.IsHanzi((char)codePoint) ? [] : + Pinyin4Net.GetPinyin((char)codePoint, PinyinFormat.LOWERCASE | PinyinFormat.WITHOUT_TONE).Where(pinyin => pinyin.Length > 0).SelectMany(pinyin => + { + var initial = PINYIN_INITIALS.FirstOrDefault(initial => pinyin.StartsWith(initial)); + var initialAlphabet = initial != null ? initial[..1] : pinyin[..1]; + var fuzzySuffix = pinyin.Length < 3 ? null : pinyin[^3..]; + var fuzzyPinyin = fuzzySuffix != null && PINYIN_FINALS_FUZZY_MAP.TryGetValue(fuzzySuffix, out var fuzzySuffixTarget) ? pinyin[..^3] + fuzzySuffixTarget : null; + return new string?[] { pinyin, initial, initialAlphabet, fuzzyPinyin }.OfType(); + }).Distinct(); +} diff --git a/dotnet/MaigoLabs.NeedLe.Indexer/Han/UnionFindSet.cs b/dotnet/MaigoLabs.NeedLe.Indexer/Han/UnionFindSet.cs new file mode 100644 index 0000000..b48b092 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Indexer/Han/UnionFindSet.cs @@ -0,0 +1,33 @@ +namespace MaigoLabs.NeedLe.Indexer.Han; + +public class UnionFindSet +{ + private Dictionary Parent { get; set; } = []; + private Dictionary Rank { get; set; } = []; + + public IEnumerable Keys => Parent.Keys; + + public int Find(int x) + { + if (!Parent.TryGetValue(x, out var parent)) return Parent[x] = x; + else if (x == parent) return x; + else return Parent[x] = Find(parent); + } + + public void Union(int x, int y) + { + x = Find(x); + y = Find(y); + if (x == y) return; + int rankX = GetRank(x), rankY = GetRank(y); + if (rankX < rankY) Parent[x] = y; + else if (rankX > rankY) Parent[y] = x; + else + { + Parent[y] = x; + Rank[x] = rankX + 1; + } + } + + private int GetRank(int x) => !Rank.TryGetValue(x, out var rank) ? 0 : rank; +} diff --git a/dotnet/MaigoLabs.NeedLe.Indexer/InvertedIndexBuilder.cs b/dotnet/MaigoLabs.NeedLe.Indexer/InvertedIndexBuilder.cs new file mode 100644 index 0000000..51bdd5a --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Indexer/InvertedIndexBuilder.cs @@ -0,0 +1,57 @@ +using MaigoLabs.NeedLe.Common; +using MaigoLabs.NeedLe.Common.Extensions; +using MaigoLabs.NeedLe.Common.Types; +using MaigoLabs.NeedLe.Indexer.Japanese; +using MaigoLabs.NeedLe.Indexer.Trie; + +namespace MaigoLabs.NeedLe.Indexer; + +public static class InvertedIndexBuilder +{ + private static TrieNode BuildTypedTrie(IEnumerable tokenDefinitions, Func typePredicate) => + TrieBuilder.BuildTrie(tokenDefinitions + .Where(token => typePredicate(token.Type)) + .Select(token => (token.Id, CodePoints: token.Text.ToCodePoints()))); + + public static CompressedInvertedIndex BuildInvertedIndex(string[] documents, TokenizerOptions? tokenizerOptions = null) + { + var tokenizer = new Tokenizer(tokenizerOptions); + var documentTokens = documents.Select(tokenizer.Tokenize).ToArray(); + + var tokenDefinitions = tokenizer.Tokens.Values; + var romajiRoot = BuildTypedTrie(tokenDefinitions, type => type == TokenType.Romaji); + var kanaRoot = BuildTypedTrie(tokenDefinitions, type => type == TokenType.Kana); + var otherRoot = BuildTypedTrie(tokenDefinitions, type => type != TokenType.Romaji && type != TokenType.Kana); + TrieBuilder.GraftTriePaths(romajiRoot, JapaneseNormalization.NORMALIZE_RULES_ROMAJI_CODEPOINTS); + TrieBuilder.GraftTriePaths(kanaRoot, JapaneseNormalization.NORMALIZE_RULES_KANA_DAKUTEN_CODEPOINTS); + + var invertedIndex = new CompressedInvertedIndex + { + documents = documents, + tokenTypes = [.. tokenDefinitions.Select(token => (int)token.Type)], + tokenReferences = [.. tokenDefinitions.Select(_ => new List())], + tries = new CompressedInvertedIndexTries + { + romaji = TrieSerializer.Serialize(romajiRoot), + kana = TrieSerializer.Serialize(kanaRoot), + other = TrieSerializer.Serialize(otherRoot), + }, + }; + for (var documentId = 0; documentId < documents.Length; documentId++) + { + var tokens = documentTokens[documentId]; + var tokenOccurrences = new Dictionary>(); + foreach (var token in tokens) + { + if (!tokenOccurrences.TryGetValue(token.Id, out var occurrences)) tokenOccurrences[token.Id] = occurrences = []; + occurrences.Add(token.Start); + occurrences.Add(token.End); + } + foreach (var (tokenId, occurrences) in tokenOccurrences) + { + invertedIndex.tokenReferences[tokenId].Add([documentId, .. occurrences]); + } + } + return invertedIndex; + } +} diff --git a/dotnet/MaigoLabs.NeedLe.Indexer/Japanese/JapaneseNormalization.cs b/dotnet/MaigoLabs.NeedLe.Indexer/Japanese/JapaneseNormalization.cs new file mode 100644 index 0000000..99b1d92 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Indexer/Japanese/JapaneseNormalization.cs @@ -0,0 +1,69 @@ +using MaigoLabs.NeedLe.Common.Extensions; + +namespace MaigoLabs.NeedLe.Indexer.Japanese; + +public static class JapaneseNormalization +{ + public delegate string Normalizer(string text); + + public static Normalizer CreateNormalizer(Dictionary rules) => text => + { + while (true) + { + var beforeCurrentIteration = text; + foreach (var (from, to) in rules) text = text.Replace(from, to); + if (text == beforeCurrentIteration) break; + } + return text; + }; + + public static IEnumerable<(int[] From, int[] To)> ToCodePointPairs(Dictionary rules) => + rules.Select(rule => (From: rule.Key.ToCodePoints().ToArray(), To: rule.Value.ToCodePoints().ToArray())); + + public static readonly Dictionary NORMALIZE_RULES_ROMAJI = new() + { + // Remove all long vowels (sa-ba- -> saba) + ["-"] = "", + // Collapse consecutive vowels + ["aa"] = "a", + ["ii"] = "i", + ["uu"] = "u", + ["ee"] = "e", + ["oo"] = "o", + ["ou"] = "o", + // mb/mp/mm -> nb/np/nm (shimbun -> shinbun) + ["mb"] = "nb", + ["mp"] = "np", + ["mm"] = "nm", + // Others + ["sha"] = "sya", + ["tsu"] = "tu", + ["chi"] = "ti", + ["shi"] = "si", + ["ji"] = "zi", + }; + public static readonly IEnumerable<(int[] From, int[] To)> NORMALIZE_RULES_ROMAJI_CODEPOINTS = ToCodePointPairs(NORMALIZE_RULES_ROMAJI); + public static readonly Normalizer NormalizeRomaji = CreateNormalizer(NORMALIZE_RULES_ROMAJI); + + public static readonly Dictionary NORMALIZE_RULES_KANA_DAKUTEN = new() + { + ["う\u3099"] = "ゔ", + ["か\u3099"] = "が", ["き\u3099"] = "ぎ", ["く\u3099"] = "ぐ", ["け\u3099"] = "げ", ["こ\u3099"] = "ご", + ["さ\u3099"] = "ざ", ["し\u3099"] = "じ", ["す\u3099"] = "ず", ["せ\u3099"] = "ぜ", ["そ\u3099"] = "ぞ", + ["た\u3099"] = "だ", ["ち\u3099"] = "ぢ", ["つ\u3099"] = "づ", ["て\u3099"] = "で", ["と\u3099"] = "ど", + ["は\u3099"] = "ば", ["ひ\u3099"] = "び", ["ふ\u3099"] = "ぶ", ["へ\u3099"] = "べ", ["ほ\u3099"] = "ぼ", + ["は\u309A"] = "ぱ", ["ひ\u309A"] = "ぴ", ["ふ\u309A"] = "ぷ", ["へ\u309A"] = "ぺ", ["ほ\u309A"] = "ぽ", + ["ゝ\u3099"] = "ゞ", + + ["ウ\u3099"] = "ヴ", + ["カ\u3099"] = "ガ", ["キ\u3099"] = "ギ", ["ク\u3099"] = "グ", ["ケ\u3099"] = "ゲ", ["コ\u3099"] = "ゴ", + ["サ\u3099"] = "ザ", ["シ\u3099"] = "ジ", ["ス\u3099"] = "ズ", ["セ\u3099"] = "ゼ", ["ソ\u3099"] = "ゾ", + ["タ\u3099"] = "ダ", ["チ\u3099"] = "ヂ", ["ツ\u3099"] = "ヅ", ["テ\u3099"] = "デ", ["ト\u3099"] = "ド", + ["ハ\u3099"] = "バ", ["ヒ\u3099"] = "ビ", ["フ\u3099"] = "ブ", ["ヘ\u3099"] = "ベ", ["ホ\u3099"] = "ボ", + ["ハ\u309A"] = "パ", ["ヒ\u309A"] = "ピ", ["フ\u309A"] = "プ", ["ヘ\u309A"] = "ペ", ["ホ\u309A"] = "ポ", + ["ワ\u3099"] = "ヷ", ["ヰ\u3099"] = "ヸ", ["ヱ\u3099"] = "ヹ", ["ヲ\u3099"] = "ヺ", + ["ヽ\u3099"] = "ヾ", + }; + public static readonly IEnumerable<(int[] From, int[] To)> NORMALIZE_RULES_KANA_DAKUTEN_CODEPOINTS = ToCodePointPairs(NORMALIZE_RULES_KANA_DAKUTEN); + public static readonly Normalizer NormalizeKanaDakuten = CreateNormalizer(NORMALIZE_RULES_KANA_DAKUTEN); +} diff --git a/dotnet/MaigoLabs.NeedLe.Indexer/Japanese/JapaneseUtils.cs b/dotnet/MaigoLabs.NeedLe.Indexer/Japanese/JapaneseUtils.cs new file mode 100644 index 0000000..1fae92a --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Indexer/Japanese/JapaneseUtils.cs @@ -0,0 +1,52 @@ +using MaigoLabs.NeedLe.Indexer.Han; +using MyNihongo.KanaConverter; + +namespace MaigoLabs.NeedLe.Indexer.Japanese; + +public static class JapaneseUtils +{ + public static bool IsMaybeJapanese(int codePoint) => + HanVariantProvider.IsHanCharacter(codePoint) || + IsKana(codePoint) || + IsJapaneseSoundMark(codePoint) || + codePoint == 0x3005 || codePoint == 0x3006 || codePoint == 0x30FC; + + // See also Common/Normalization.cs + public static bool IsJapaneseSoundMark(int codePoint) => codePoint == 0x3099 || codePoint == 0x309A; + public static string StripJapaneseSoundMarks(string text) => string.Concat(text.Where(codePoint => !IsJapaneseSoundMark(codePoint))); + + public static bool IsKana(int codePoint) => (codePoint >= 0x3041 && codePoint <= 0x309F) || (codePoint >= 0x30A0 && codePoint <= 0x30FF); + + private static readonly int[] KANAS_CANNOT_BE_FIRST = + [ + 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', + 'ぁ', 'ぃ', 'ぅ', 'ぇ', 'ぉ', + 'ャ', 'ュ', 'ョ', + 'ゃ', 'ゅ', 'ょ', + 'ヮ', 'ゎ', + 'ㇰ', 'ㇱ', 'ㇲ', 'ㇳ', 'ㇴ', 'ㇵ', 'ㇶ', 'ㇷ', 'ㇸ', 'ㇹ', 'ㇺ', 'ㇻ', 'ㇼ', 'ㇽ', 'ㇾ', 'ㇿ', + 'ー', + ]; + + private static readonly int[] KANAS_CANNOT_BE_LAST = + [ + 'ッ', 'っ' + ]; + + public static string ToRomajiStrictly(string kanaText) + { + if (kanaText.Length == 0) return ""; + if (KANAS_CANNOT_BE_FIRST.Contains(kanaText[0])) return ""; + if (KANAS_CANNOT_BE_LAST.Contains(kanaText[^1])) return ""; + string romaji; + try { romaji = kanaText.ToRomaji(); } + catch { return ""; } + if (!romaji.All(c => c is >= 'a' and <= 'z')) return ""; + return romaji; + } + + public static bool IsValidJapanesePhrase(ReadOnlySpan codePoints, int start, int length) => + // Skip splittings that cause sound marks to occur in the first position of a phrase + !IsJapaneseSoundMark(codePoints[start]) && (start + length == codePoints.Length || !IsJapaneseSoundMark(codePoints[start + length])); + public static bool IsValidJapanesePhrase(ReadOnlyMemory codePoints, int start, int length) => IsValidJapanesePhrase(codePoints.Span, start, length); +} diff --git a/dotnet/MaigoLabs.NeedLe.Indexer/Japanese/TranscriptionProvider.cs b/dotnet/MaigoLabs.NeedLe.Indexer/Japanese/TranscriptionProvider.cs new file mode 100644 index 0000000..14e41cf --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Indexer/Japanese/TranscriptionProvider.cs @@ -0,0 +1,105 @@ +using System.Runtime.InteropServices; +using MaigoLabs.NeedLe.Common; +using MaigoLabs.NeedLe.Common.Extensions; +using MeCab; +using MeCab.Core; + +namespace MaigoLabs.NeedLe.Indexer.Japanese; + +public class Transcription +{ + public required int Start { get; set; } + public required int Length { get; set; } + public required string[] Transcriptions { get; set; } +} + +public delegate IEnumerable TranscriptionEnumerator(ReadOnlyMemory codePoints); +public delegate bool IsValidPhraseDelegate(ReadOnlyMemory codePoints, int start, int length); +public delegate HashSet GetAllTranscriptionsDelegate(string phrase); + +public class TranscriptionProvider +{ + public MeCabDictionary[] Dictionaries { get; set; } + + public TranscriptionProvider(MeCabDictionary[]? dictionaries = null) + { + if (dictionaries == null) + { + var param = new MeCabParam(); + param.LoadDicRC(); + var dictionary = new MeCabDictionary(); + dictionary.Open(Path.Combine(param.DicDir, "sys.dic")); + dictionaries = [dictionary]; + } + Dictionaries = dictionaries; + } + + public static TranscriptionEnumerator CreateTranscriptionEnumerator(IsValidPhraseDelegate isValidPhrase, GetAllTranscriptionsDelegate getAllTranscriptions) => codePoints => + { + var resultMap = new Dictionary<(int Start, int Length), Transcription>(); + for (int phraseLength = 1; phraseLength <= codePoints.Length; phraseLength++) for (int start = 0; start + phraseLength <= codePoints.Length; start++) + { + if (!isValidPhrase(codePoints, start, phraseLength)) continue; + var phrase = MemoryMarshal.ToEnumerable(codePoints.Slice(start, phraseLength)).ToUtf32String(); + var atomicTranscriptions = getAllTranscriptions(phrase).Where(transcription => transcription != null).Where(candidateTranscription => + { + if (candidateTranscription.Length == 0) return false; + // Ensure the transcription is atomic (not a combination of multiple shorter transcriptions, separated by any midpoints) + var visitedStates = new HashSet<(int PhrasePosition, int TranscriptionPosition)>(); + var queue = new Queue<(int PhrasePosition, int TranscriptionPosition)>(); + queue.Enqueue((0, 0)); + while (queue.Count > 0) + { + var (phrasePosition, transcriptionPosition) = queue.Dequeue(); + for (int prefixLength = 1; prefixLength <= phraseLength - phrasePosition; prefixLength++) + { + if (!resultMap.TryGetValue((start + phrasePosition, prefixLength), out var prefixResult)) continue; + foreach (var transcription in prefixResult.Transcriptions) if (string.Compare(candidateTranscription, transcriptionPosition, transcription, 0, transcription.Length) == 0) + { + var nextState = (PhrasePosition: phrasePosition + prefixLength, TranscriptionPosition: transcriptionPosition + transcription.Length); + if (nextState.PhrasePosition == phraseLength && nextState.TranscriptionPosition == candidateTranscription.Length) return false; // Found a valid combination + if (visitedStates.Contains(nextState)) continue; + visitedStates.Add(nextState); + queue.Enqueue(nextState); + } + } + } + return true; + }).ToArray(); + if (atomicTranscriptions.Length > 0) resultMap[(start, phraseLength)] = new() { Start = start, Length = phraseLength, Transcriptions = atomicTranscriptions }; + } + return resultMap.Values; + }; + + public HashSet GetAllKanaReadings(string phrase) + { + var result = new HashSet(); + var isKana = phrase.All(ch => JapaneseUtils.IsKana(ch)); + if (isKana) result.Add(CommonNormalization.ToKatakana(phrase)); + if (isKana && phrase.Length == 1) return result; + + foreach (var dictionary in Dictionaries) + { + var searchResult = dictionary.ExactMatchSearch(phrase); + if (searchResult.Value == -1) continue; + var tokens = dictionary.GetToken(searchResult); + foreach (var token in tokens) + { + var feature = dictionary.GetFeature(token.Feature); + var parts = feature.Split(','); + if (parts.Length > 7) result.Add(CommonNormalization.ToKatakana(parts[7])); + } + } + return result; + } + + public HashSet GetAllKanaReadingsWithNormalization(string phrase) => + GetAllKanaReadings(JapaneseUtils.StripJapaneseSoundMarks(JapaneseNormalization.NormalizeKanaDakuten(phrase))); + + public TranscriptionEnumerator EnumerateKanaTranscriptions => CreateTranscriptionEnumerator( + JapaneseUtils.IsValidJapanesePhrase, + GetAllKanaReadingsWithNormalization); + public TranscriptionEnumerator EnumerateRomajiTranscriptions => CreateTranscriptionEnumerator( + JapaneseUtils.IsValidJapanesePhrase, + phrase => [.. GetAllKanaReadingsWithNormalization(phrase).Select(kana => JapaneseNormalization.NormalizeRomaji(JapaneseUtils.ToRomajiStrictly(kana)))]); +} diff --git a/dotnet/MaigoLabs.NeedLe.Indexer/MaigoLabs.NeedLe.Indexer.csproj b/dotnet/MaigoLabs.NeedLe.Indexer/MaigoLabs.NeedLe.Indexer.csproj new file mode 100644 index 0000000..28fc8a0 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Indexer/MaigoLabs.NeedLe.Indexer.csproj @@ -0,0 +1,29 @@ + + + + netstandard2.0 + Library + $(ProjectName).Indexer + $(RootNamespace) + + + + true + $(RootNamespace) + + False + + + + + + + + + + + + + + + diff --git a/dotnet/MaigoLabs.NeedLe.Indexer/Tokenizer.cs b/dotnet/MaigoLabs.NeedLe.Indexer/Tokenizer.cs new file mode 100644 index 0000000..a84426b --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Indexer/Tokenizer.cs @@ -0,0 +1,104 @@ +using MaigoLabs.NeedLe.Common; +using MaigoLabs.NeedLe.Common.Extensions; +using MaigoLabs.NeedLe.Common.Types; +using MaigoLabs.NeedLe.Indexer.Han; +using MaigoLabs.NeedLe.Indexer.Japanese; + +namespace MaigoLabs.NeedLe.Indexer; + +public class TokenizerOptions +{ + public HanVariantProvider? HanVariantProvider { get; set; } + public TranscriptionProvider? TranscriptionProvider { get; set; } +} + +public class Tokenizer(TokenizerOptions? options = null) +{ + public HanVariantProvider HanVariantProvider { get; set; } = options?.HanVariantProvider ?? new HanVariantProvider(); + public TranscriptionProvider TranscriptionProvider { get; set; } = options?.TranscriptionProvider ?? new TranscriptionProvider(); + + public class Token + { + public required int Id { get; set; } + public required int Start { get; set; } + public required int End { get; set; } + } + + public Dictionary<(TokenType Type, string Text), TokenDefinition> Tokens { get; } = []; + private TokenDefinition EnsureToken(TokenType type, string text) + { + var key = (type, text); + if (Tokens.TryGetValue(key, out var tokenDefinition)) return tokenDefinition; + tokenDefinition = new TokenDefinition { Id = Tokens.Count, Type = type, Text = text, CodePointLength = text.ToCodePoints().Count() }; + Tokens.Add(key, tokenDefinition); + return tokenDefinition; + } + + public List Tokenize(string text) + { + var codePoints = text.ToCodePoints().Select(CommonNormalization.NormalizeCodePoint).ToArray(); + var results = new List(); + Action Emitter(int start, int end) => + (tokenType, codePoints) => results.Add(new Token { Id = EnsureToken(tokenType, codePoints).Id, Start = start, End = end }); + + void EmitMaybeJapanese(ReadOnlyMemory codePoints, int offset) + { + foreach (var combination in TranscriptionProvider.EnumerateKanaTranscriptions(codePoints)) + { + var emit = Emitter(offset + combination.Start, offset + combination.Start + combination.Length); + foreach (var transcription in combination.Transcriptions) emit(TokenType.Kana, transcription); + } + foreach (var combination in TranscriptionProvider.EnumerateRomajiTranscriptions(codePoints)) + { + var emit = Emitter(offset + combination.Start, offset + combination.Start + combination.Length); + foreach (var transcription in combination.Transcriptions) emit(TokenType.Romaji, transcription); + } + for (int i = 0; i < codePoints.Length; i++) + { + // Single character may have not only kana readings, but also Chinese pronunciations or Simplified/Traditional/Japanese variants. + var hanAlternates = HanVariantProvider.GetHanVariants(codePoints.Span[i]); // All possible variant characters (Simplified/Traditional/Japanese) + var pinyinAlternates = hanAlternates.SelectMany(PinyinHelper.GetPinyinCandidates).Distinct(); + var emit = Emitter(offset + i, offset + i + 1); + foreach (var han in hanAlternates) emit(TokenType.Han, char.ConvertFromUtf32(han)); + foreach (var pinyin in pinyinAlternates) emit(TokenType.Pinyin, pinyin); + } + } + + var consequentCharsets = new (Func Is, Action, int> Emit)[] + { + (Is: JapaneseUtils.IsMaybeJapanese, Emit: EmitMaybeJapanese), + }; + + void EmitRaw(int codePoint, int offset) => Emitter(offset, offset + 1)(TokenType.Raw, char.ConvertFromUtf32(codePoint)); + + for (int start = 0; start < codePoints.Length; ) + { + var codePoint = codePoints[start]; + var emitted = false; + foreach (var (Is, Emit) in consequentCharsets) + { + var length = 0; + while (start + length < codePoints.Length && Is(codePoints[start + length])) length++; + if (length > 0) + { + Emit(new Memory(codePoints, start, length), start); + start += length; + emitted = true; + break; + } + } + if (emitted) continue; + + // Skip whitespaces + if (CommonUtils.IsWhitespace(codePoint)) + { + start++; + continue; + } + + EmitRaw(codePoint, start); + start++; + } + return results; + } +} diff --git a/dotnet/MaigoLabs.NeedLe.Indexer/Trie/TrieBuilder.cs b/dotnet/MaigoLabs.NeedLe.Indexer/Trie/TrieBuilder.cs new file mode 100644 index 0000000..5b65224 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Indexer/Trie/TrieBuilder.cs @@ -0,0 +1,93 @@ +using MaigoLabs.NeedLe.Common; + +namespace MaigoLabs.NeedLe.Indexer.Trie; + +public static class TrieBuilder +{ + private static TrieNode NewNode(TrieNode? parent) => new() { Parent = parent, Children = [], TokenIds = [], SubTreeTokenIds = [] }; + + public static TrieNode BuildTrie(IEnumerable<(int Id, IEnumerable CodePoints)> tokens) + { + var root = NewNode(null); + foreach (var (id, codePoints) in tokens) + { + var node = root; + foreach (var codePoint in codePoints) + { + node.Children.TryGetValue(codePoint, out var childNode); + if (childNode == null) node.Children[codePoint] = childNode = NewNode(node); + node = childNode; + node.SubTreeTokenIds.Add(id); + } + node.TokenIds.Add(id); + } + return root; + } + + public static void GraftTriePaths(TrieNode root, IEnumerable<(int[] From, int[] To)> rules) + { + foreach (var (inputPhrase, graftTo) in rules) if (graftTo.Length > inputPhrase.Length) throw new ArgumentException($"Graft rule {inputPhrase} -> {graftTo} maps to longer string and may cause infinite loop"); + var visitedNodes = new HashSet(); + void GraftFromNode(TrieNode node, bool recursiveChildren) + { + if (!visitedNodes.Add(node)) return; + if (recursiveChildren) foreach (var child in node.Children.Values) GraftFromNode(child, true); + while (true) + { + var nodesWithNewGraftedChildren = new Dictionary(); + foreach (var (inputPhrase, graftTo) in rules) + { + var targetNode = node.Traverse(graftTo); + if (targetNode == null) continue; + var graftedPath = new TrieNode[inputPhrase.Length - 1]; + var isGrafted = false; + var currentNode = node; + for (var i = 0; i < inputPhrase.Length; i++) + { + var codePoint = inputPhrase[i]; + currentNode.Children.TryGetValue(codePoint, out var childNode); + if (i == inputPhrase.Length - 1) + { + if (childNode != null) + { + if (childNode != targetNode) throw new ArgumentException($"Grafted path {inputPhrase} conflicts with existing path"); + // Already grafted + } + else + { + currentNode.Children[codePoint] = childNode = targetNode; + isGrafted = true; + } + } + else + { + if (childNode == null) + { + childNode = NewNode(currentNode); + childNode.SubTreeTokenIds = targetNode.SubTreeTokenIds; + currentNode.Children[codePoint] = childNode; + } + else + { + // Part of another grafted path? + childNode.SubTreeTokenIds = new HashSet(childNode.SubTreeTokenIds.Concat(targetNode.SubTreeTokenIds)).ToList(); + } + graftedPath[i] = currentNode = childNode; + } + } + if (isGrafted) for (var i = 0; i < graftedPath.Length; i++) nodesWithNewGraftedChildren[graftedPath[i]!] = i + 1; + } + if (nodesWithNewGraftedChildren.Count > 0) + { + // Re-check graft rules on the newly grafted path + // 1. No need to recursive other children (not on this path) since their children are not affected + // 2. No need to consider ancestors of this node since they're handled later (we run in DFS order) + var sortedNodes = nodesWithNewGraftedChildren.OrderByDescending(x => x.Value); + foreach (var (changedNode, _) in sortedNodes) GraftFromNode(changedNode, false); + } + else break; // No new grafts applied + } + } + GraftFromNode(root, true); + } +} diff --git a/dotnet/MaigoLabs.NeedLe.Indexer/Trie/TrieSerializer.cs b/dotnet/MaigoLabs.NeedLe.Indexer/Trie/TrieSerializer.cs new file mode 100644 index 0000000..13e1aa5 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Indexer/Trie/TrieSerializer.cs @@ -0,0 +1,41 @@ +using MaigoLabs.NeedLe.Common; + +namespace MaigoLabs.NeedLe.Indexer.Trie; + +public static class TrieSerializer +{ + private class NodeEntry + { + public int Id { get; set; } + public bool Visited { get; set; } + public int[]? Data { get; set; } + } + + public static int[] Serialize(TrieNode root) + { + var nodeEntries = new Dictionary(); + var currentId = 0; + NodeEntry GetNodeEntry(TrieNode node) => nodeEntries.TryGetValue(node, out var nodeEntry) ? nodeEntry : + nodeEntries[node] = new NodeEntry { Id = ++currentId, Visited = false, Data = null }; + int SerializeNode(TrieNode node) + { + var entry = GetNodeEntry(node); + if (entry.Visited) return entry.Id; + entry.Visited = true; + var children = node.Children.Select(child => (CodePoint: child.Key, ChildId: SerializeNode(child.Value))).ToArray(); + entry.Data = + [ + node.Parent != null ? GetNodeEntry(node.Parent).Id : 0, + .. children.Select(child => child.CodePoint), + .. children.Select(child => child.ChildId), + // End of children list (<= 0 are not valid code points nor node IDs) + .. node.TokenIds.Count > 0 + ? node.TokenIds.Select(tokenId => -(tokenId + 1)) // Use the negative value of (tokenId + 1) + : [0], // End of children list, no token IDs (token IDs are encoded to negative values) + ]; + return entry.Id; + } + SerializeNode(root); + return nodeEntries.Values.OrderBy(entry => entry.Id).SelectMany(entry => entry.Data ?? []).ToArray(); + } +} diff --git a/dotnet/MaigoLabs.NeedLe.Playground/MaigoLabs.NeedLe.Playground.csproj b/dotnet/MaigoLabs.NeedLe.Playground/MaigoLabs.NeedLe.Playground.csproj new file mode 100644 index 0000000..d2cb1a4 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Playground/MaigoLabs.NeedLe.Playground.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + Exe + $(ProjectName).Playground + $(RootNamespace) + + + + + + + + + + + diff --git a/dotnet/MaigoLabs.NeedLe.Playground/Program.cs b/dotnet/MaigoLabs.NeedLe.Playground/Program.cs new file mode 100644 index 0000000..31c0714 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Playground/Program.cs @@ -0,0 +1,162 @@ +using System.Diagnostics; +using System.Text.Encodings.Web; +using System.Text.Json; +using MaigoLabs.NeedLe.Common.Extensions; +using MaigoLabs.NeedLe.Indexer; +using MaigoLabs.NeedLe.Searcher; +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace MaigoLabs.NeedLe.Playground; + +public class Program +{ + private static LoadedInvertedIndex _invertedIndex = null!; + private static long _targetChatId; + + public static async Task Main(string[] args) + { + var botToken = Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN") + ?? throw new InvalidOperationException("Missing environment variable TELEGRAM_BOT_TOKEN"); + var targetChatIdStr = Environment.GetEnvironmentVariable("TARGET_CHAT_ID") + ?? throw new InvalidOperationException("Missing environment variable TARGET_CHAT_ID"); + _targetChatId = long.Parse(targetChatIdStr); + + // Build inverted index + var exampleDocuments = File.ReadAllLines("../../example.txt").Where(line => line.Length > 0).ToArray(); + + var startBuild = Stopwatch.GetTimestamp(); + var compressed = InvertedIndexBuilder.BuildInvertedIndex(exampleDocuments); + var endBuild = Stopwatch.GetTimestamp(); + Console.WriteLine($"Built inverted index in {Stopwatch.GetElapsedTime(startBuild, endBuild).TotalMilliseconds}ms"); + + var startLoad = Stopwatch.GetTimestamp(); + _invertedIndex = InvertedIndexLoader.Load(compressed); + var endLoad = Stopwatch.GetTimestamp(); + Console.WriteLine($"Loaded inverted index in {Stopwatch.GetElapsedTime(startLoad, endLoad).TotalMilliseconds}ms"); + + // Start bot + var bot = new TelegramBotClient(botToken); + var me = await bot.GetMe(); + Console.WriteLine($"Bot logged in as {me.FirstName} (@{me.Username})"); + + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; + + bot.StartReceiving( + updateHandler: HandleUpdateAsync, + errorHandler: HandleErrorAsync, + receiverOptions: new ReceiverOptions { AllowedUpdates = [UpdateType.Message] }, + cancellationToken: cts.Token + ); + await Task.Delay(-1, cts.Token).ContinueWith(_ => { }); + } + + private static async Task HandleUpdateAsync(ITelegramBotClient bot, Update update, CancellationToken ct) + { + if (update.Message is not { Text: { } text, Chat.Id: var chatId, From: { } from }) return; + + Console.WriteLine($"{chatId}:{from.Id} {JsonSerializer.Serialize(text, JsonSerializerOptions)}"); + + if (chatId != _targetChatId) return; + + if (text.StartsWith("/needle ")) + { + var query = text["/needle ".Length..]; + var response = HandleNeedleCommand(query); + await bot.SendMessage(chatId, response, parseMode: ParseMode.Html, cancellationToken: ct); + } + else if (text.StartsWith("/tokenize ")) + { + var query = text["/tokenize ".Length..]; + var response = HandleTokenizeCommand(query); + await bot.SendMessage(chatId, response, parseMode: ParseMode.Html, cancellationToken: ct); + } + } + + private static Task HandleErrorAsync(ITelegramBotClient bot, Exception exception, HandleErrorSource source, CancellationToken ct) + { + Console.WriteLine($"Error: {exception.Message}"); + return Task.CompletedTask; + } + + private static string HandleNeedleCommand(string query) + { + var startSearch = Stopwatch.GetTimestamp(); + var results = InvertedIndexSearcher.Search(_invertedIndex, query); + var endSearch = Stopwatch.GetTimestamp(); + var searchDuration = Stopwatch.GetElapsedTime(startSearch, endSearch).TotalMilliseconds.ToString("F3"); + + if (results.Length == 0) + return Codify($"No results found after {searchDuration}ms"); + + var showingResults = results.Take(5).ToArray(); + return string.Join('\n', + [ + Codify($"Search completed in {searchDuration}ms, showing {showingResults.Length}/{results.Length} results:\n"), + .. showingResults.Select(result => InspectSearchResult(result, true)) + ]).TrimEnd(); + } + + private static string HandleTokenizeCommand(string query) + { + var tokenizer = new Tokenizer(); + var startTokenize = Stopwatch.GetTimestamp(); + var tokens = tokenizer.Tokenize(query); + var tokenDefinitions = tokenizer.Tokens.Values.ToArray(); + var endTokenize = Stopwatch.GetTimestamp(); + var tokenizeDuration = Stopwatch.GetElapsedTime(startTokenize, endTokenize).TotalMilliseconds.ToString("F3"); + if (tokens.Count == 0) return Codify($"No tokens emitted after {tokenizeDuration}ms"); + + var codePoints = query.ToCodePoints().ToArray(); + var lines = new List + { + $"Tokenization completed in {tokenizeDuration}ms, emitted {tokens.Count} tokens:" + }; + foreach (var token in tokens) + { + var tokenDef = tokenDefinitions[token.Id]; + var originalPhrase = codePoints.Skip(token.Start).Take(token.End - token.Start).ToUtf32String(); + lines.Add($" {tokenDef.Type}: {JsonSerializer.Serialize(tokenDef.Text, JsonSerializerOptions)} <- {JsonSerializer.Serialize(originalPhrase, JsonSerializerOptions)} [{token.Start}, {token.End}]"); + } + return Codify(string.Join('\n', lines)); + } + + private static string InspectSearchResult(SearchResult result, bool htmlHighlight) + { + var documentText = result.DocumentText; + var documentCodePoints = result.DocumentCodePoints; + var tokens = result.Tokens; + var rangeCount = result.RangeCount; + var matchRatio = result.MatchRatio; + var matchRatioLevel = result.MatchRatioLevel; + + var resultText = htmlHighlight + ? string.Join("", SearchResultHighlighter.Highlight(result).Select(part => !part.IsHighlighted ? EscapeHtml(part.Text) : $"{EscapeHtml(part.Text)}")) + : documentText; + var description = $" ({rangeCount} ranges, {Math.Round(matchRatio * 10000) / 10000} => L{matchRatioLevel})"; + return string.Join('\n', + [ + resultText + (htmlHighlight ? $"{description}" : description), + .. tokens.Select(token => + { + var escapedTokenText = JsonSerializer.Serialize(token.Definition.Text, JsonSerializerOptions); + var escapedDocumentText = JsonSerializer.Serialize(documentCodePoints.Skip(token.DocumentOffset.Start).Take(token.DocumentOffset.Length).ToUtf32String(), JsonSerializerOptions); + if (htmlHighlight) + { + escapedTokenText = EscapeHtml(escapedTokenText); + escapedDocumentText = EscapeHtml(escapedDocumentText); + } + var line = $" {token.Definition.Type}: {escapedTokenText} -> {escapedDocumentText}" + (token.IsTokenPrefixMatching ? " (prefix match)" : ""); + return htmlHighlight ? $"{line}" : line; + }), + "", + ]); + } + + private static string Codify(string text) => $"{EscapeHtml(text)}"; + private static JsonSerializerOptions JsonSerializerOptions => new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; + private static string EscapeHtml(string text) => text.Replace("&", "&").Replace("<", "<").Replace(">", ">"); +} diff --git a/dotnet/MaigoLabs.NeedLe.Searcher/InvertedIndexLoader.cs b/dotnet/MaigoLabs.NeedLe.Searcher/InvertedIndexLoader.cs new file mode 100644 index 0000000..d08f5e8 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Searcher/InvertedIndexLoader.cs @@ -0,0 +1,72 @@ +using MaigoLabs.NeedLe.Common; +using MaigoLabs.NeedLe.Common.Extensions; +using MaigoLabs.NeedLe.Common.Types; +using MaigoLabs.NeedLe.Searcher.Trie; + +namespace MaigoLabs.NeedLe.Searcher; + +public class LoadedInvertedIndex +{ + public class TokenDocumentReference + { + public required int DocumentId { get; set; } + public required OffsetSpan[] Offsets { get; set; } + } + + public class TokenDefinitionExtended : TokenDefinition + { + public required TokenDocumentReference[] References { get; set; } + } + + public class TypedTries + { + public required TrieNode Romaji { get; set; } + public required TrieNode Kana { get; set; } + public required TrieNode Other { get; set; } + } + + public required string[] Documents { get; set; } + public required int[][] DocumentCodePoints { get; set; } + public required TokenDefinitionExtended[] TokenDefinitions { get; set; } + public required TypedTries Tries { get; set; } +} + +public class InvertedIndexLoader +{ + public static LoadedInvertedIndex Load(CompressedInvertedIndex compressed) + { + var documents = compressed.documents; + var documentCodePoints = documents.Select(document => document.ToCodePoints().ToArray()).ToArray(); + + var romajiTrie = TrieDeserializer.Deserialize(compressed.tries.romaji); + var kanaTrie = TrieDeserializer.Deserialize(compressed.tries.kana); + var otherTrie = TrieDeserializer.Deserialize(compressed.tries.other); + + var tokenCodePoints = romajiTrie.TokenCodePoints.Concat(kanaTrie.TokenCodePoints).Concat(otherTrie.TokenCodePoints) + .ToDictionary(entry => entry.Key, entry => entry.Value); + var tokenDefinitions = compressed.tokenTypes.Select((type, index) => new LoadedInvertedIndex.TokenDefinitionExtended + { + Id = index, Type = (TokenType)type, Text = tokenCodePoints[index].ToUtf32String(), + CodePointLength = tokenCodePoints[index].Length, + References = compressed.tokenReferences[index].Select(data => new LoadedInvertedIndex.TokenDocumentReference + { + DocumentId = data[0], + Offsets = Enumerable.Range(0, data.Length / 2) + .Select(i => new OffsetSpan { Start = data[i * 2 + 1], End = data[i * 2 + 2] }).ToArray(), + }).ToArray(), + }).ToArray(); + + return new LoadedInvertedIndex + { + Documents = documents, + DocumentCodePoints = documentCodePoints, + TokenDefinitions = tokenDefinitions, + Tries = new LoadedInvertedIndex.TypedTries + { + Romaji = romajiTrie.Root, + Kana = kanaTrie.Root, + Other = otherTrie.Root, + }, + }; + } +} diff --git a/dotnet/MaigoLabs.NeedLe.Searcher/InvertedIndexSearcher.cs b/dotnet/MaigoLabs.NeedLe.Searcher/InvertedIndexSearcher.cs new file mode 100644 index 0000000..04e8ff1 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Searcher/InvertedIndexSearcher.cs @@ -0,0 +1,270 @@ +using MaigoLabs.NeedLe.Common; +using MaigoLabs.NeedLe.Common.Extensions; +using MaigoLabs.NeedLe.Common.Types; + +namespace MaigoLabs.NeedLe.Searcher; + +public class SearchResultToken +{ + public required TokenDefinition Definition { get; set; } + public required OffsetSpan DocumentOffset { get; set; } + public required OffsetSpan InputOffset { get; set; } + public required bool IsTokenPrefixMatching { get; set; } +} + +public class SearchResult +{ + public required int DocumentId { get; set; } + public required string DocumentText { get; set; } + public required int[] DocumentCodePoints { get; set; } + public required SearchResultToken[] Tokens { get; set; } + public required int PrefixMatchCount { get; set; } + public required int RangeCount { get; set; } + public required double MatchRatio { get; set; } + public required int MatchRatioLevel { get; set; } +} + +public static class InvertedIndexSearcher +{ + public abstract class ComparableStateBase : IComparable + where T : ComparableStateBase + { + protected abstract int GetRangeCount(); + protected abstract int GetPrefixMatchCount(); + protected abstract OffsetSpan GetFirstTokenDocumentOffset(); + protected abstract OffsetSpan GetLastTokenDocumentOffset(); + protected virtual SearchResultToken? GetLastToken() => null; // Not on intermediate results + protected virtual int? GetMatchRatioLevel() => null; // Not on intermediate/candidate results + protected abstract double GetMatchRatio(); + protected virtual int FallbackCompareTo(T other) => 0; // Called when all other comparisons are equal + + public int CompareTo(T other) + { + // Prefer matches that not relying on end-of-input loose matching (full match over prefix match) + SearchResultToken? aLastToken = GetLastToken(), bLastToken = other.GetLastToken(); + if (aLastToken != null && bLastToken != null) + { + var aDidPrefixMatchByTokenType = aLastToken.IsTokenPrefixMatching && tokenTypePrefixMatchingPolicy[aLastToken.Definition.Type] == TokenTypePrefixMatchingPolicy.AllowOnlyAtInputEnd; + var bDidPrefixMatchByTokenType = bLastToken.IsTokenPrefixMatching && tokenTypePrefixMatchingPolicy[bLastToken.Definition.Type] == TokenTypePrefixMatchingPolicy.AllowOnlyAtInputEnd; + if (aDidPrefixMatchByTokenType != bDidPrefixMatchByTokenType) return aDidPrefixMatchByTokenType ? 1 : -1; + } + + // Prefer results that matched fewer discontinuous ranges over more + int aRangeCount = GetRangeCount(), bRangeCount = other.GetRangeCount(); + if (aRangeCount != bRangeCount) return aRangeCount - bRangeCount; + + // Prefer results that matches first token in document earlier over later + OffsetSpan aFirstTokenDocumentOffset = GetFirstTokenDocumentOffset(), bFirstTokenDocumentOffset = other.GetFirstTokenDocumentOffset(); + if (aFirstTokenDocumentOffset.Start != bFirstTokenDocumentOffset.Start) return aFirstTokenDocumentOffset.Start - bFirstTokenDocumentOffset.Start; + + // Prefer results that has higher match ratio (but don't distinguish similar ratios, so we introduced `matchRatioLevel`) + int? aMatchRatioLevel = GetMatchRatioLevel(), bMatchRatioLevel = other.GetMatchRatioLevel(); + if (aMatchRatioLevel != null && bMatchRatioLevel != null) + { + if (aMatchRatioLevel.Value != bMatchRatioLevel.Value) return bMatchRatioLevel.Value - aMatchRatioLevel.Value; + } + + // Prefer results that last token occurred earlier (if same, ended earlier) in the document over later + OffsetSpan aLastTokenDocumentOffset = GetLastTokenDocumentOffset(), bLastTokenDocumentOffset = other.GetLastTokenDocumentOffset(); + if (aLastTokenDocumentOffset.Start != bLastTokenDocumentOffset.Start) return aLastTokenDocumentOffset.Start - bLastTokenDocumentOffset.Start; + if (aLastTokenDocumentOffset.End != bLastTokenDocumentOffset.End) return aLastTokenDocumentOffset.End - bLastTokenDocumentOffset.End; + + // Prefer results that has higher match ratio (precisely) + double aMatchRatio = GetMatchRatio(), bMatchRatio = other.GetMatchRatio(); + if (aMatchRatio != bMatchRatio) return bMatchRatio < aMatchRatio ? -1 : bMatchRatio > aMatchRatio ? 1 : 0; + + return FallbackCompareTo(other); + } + } + + public class IntermediateResult : ComparableStateBase + { + public required IntermediateResult? PreviousState { get; init; } + public required OffsetSpan FirstTokenDocumentOffset { get; init; } + public required int RangeCount { get; init; } + public required int TokenCount { get; init; } + public required int PrefixMatchCount { get; init; } + public required double MatchedTokenLength { get; init; } + public required int TokenId { get; init; } + public required OffsetSpan DocumentOffset { get; init; } + public required OffsetSpan InputOffset { get; init; } + public required bool IsTokenPrefixMatching { get; init; } + + protected override int GetRangeCount() => RangeCount; + protected override int GetPrefixMatchCount() => PrefixMatchCount; + protected override OffsetSpan GetFirstTokenDocumentOffset() => FirstTokenDocumentOffset; + protected override OffsetSpan GetLastTokenDocumentOffset() => DocumentOffset; + protected override double GetMatchRatio() => MatchedTokenLength; // No need to divide document length since intermediate results are for same document + } + + public class CandidateResult : ComparableStateBase + { + public required SearchResultToken[] Tokens { get; init; } + public required int PrefixMatchCount { get; init; } + public required double MatchedTokenLength { get; init; } + public required int RangeCount { get; init; } + + protected override int GetRangeCount() => RangeCount; + protected override int GetPrefixMatchCount() => PrefixMatchCount; + protected override OffsetSpan GetFirstTokenDocumentOffset() => Tokens[0].DocumentOffset; + protected override OffsetSpan GetLastTokenDocumentOffset() => Tokens[^1].DocumentOffset; + protected override SearchResultToken? GetLastToken() => Tokens[^1]; + protected override double GetMatchRatio() => MatchedTokenLength; // No need to divide document length since intermediate results are for same document + } + + public class FinalResult : ComparableStateBase + { + public required SearchResult Result { get; init; } + + protected override int GetRangeCount() => Result.RangeCount; + protected override int GetPrefixMatchCount() => Result.PrefixMatchCount; + protected override OffsetSpan GetFirstTokenDocumentOffset() => Result.Tokens[0].DocumentOffset; + protected override OffsetSpan GetLastTokenDocumentOffset() => Result.Tokens[^1].DocumentOffset; + protected override SearchResultToken? GetLastToken() => Result.Tokens[^1]; + protected override double GetMatchRatio() => Result.MatchRatio; + protected override int? GetMatchRatioLevel() => Result.MatchRatioLevel; + protected override int FallbackCompareTo(FinalResult other) => string.Compare(Result.DocumentText, other.Result.DocumentText, StringComparison.InvariantCulture); + } + + private static bool IsIgnorableCodePoint(int codePoint) => CommonUtils.IsWhitespace(codePoint) || codePoint == 0x3099 || codePoint == 0x309A; + + public enum TokenTypePrefixMatchingPolicy { + AlwaysAllow, + NeverAllow, + AllowOnlyAtInputEnd, + } + + private static Dictionary tokenTypePrefixMatchingPolicy = new() + { + [TokenType.Romaji] = TokenTypePrefixMatchingPolicy.NeverAllow, + [TokenType.Kana] = TokenTypePrefixMatchingPolicy.AlwaysAllow, + // These token types are in an "other" Trie + [TokenType.Han] = TokenTypePrefixMatchingPolicy.AllowOnlyAtInputEnd, // No effect because always 1 code point + [TokenType.Pinyin] = TokenTypePrefixMatchingPolicy.AllowOnlyAtInputEnd, + [TokenType.Raw] = TokenTypePrefixMatchingPolicy.AllowOnlyAtInputEnd, // No effect because always 1 code point + }; + + private static bool ShouldAllowPrefixMatching(TokenType tokenType, bool isAtInputEnd) => + tokenTypePrefixMatchingPolicy[tokenType] == TokenTypePrefixMatchingPolicy.AlwaysAllow || + (tokenTypePrefixMatchingPolicy[tokenType] != TokenTypePrefixMatchingPolicy.NeverAllow && isAtInputEnd); + + private static bool HasNonEmptyCharacters(int[] documentCodePoints, int start, int end) => + start != end && !documentCodePoints.Skip(start).Take(end - start).All(CommonUtils.IsWhitespace); + + public static SearchResult[] Search(LoadedInvertedIndex invertedIndex, string text) + { + var documents = invertedIndex.Documents; + var documentCodePoints = invertedIndex.DocumentCodePoints; + var tokenDefinitions = invertedIndex.TokenDefinitions; + var tries = invertedIndex.Tries; + + var codePoints = text.ToCodePoints().Select(CommonNormalization.NormalizeCodePoint).Select(CommonNormalization.ToKatakana).ToArray(); + // dp[i] = docId => end => IntermediateResult, starts from dp[-1] (l === 0), ends at dp[N - 1] (r === N - 1) + var dp = Enumerable.Range(0, codePoints.Length).Select(l => new Dictionary>()).ToArray(); + for (var l = 0; l < codePoints.Length; l++) + { + if (l != 0 && dp[l - 1].Count == 0) continue; // No documents match input from beginning to this position + var romajiNode = tries.Romaji; + var kanaNode = tries.Kana; + var otherNode = tries.Other; + for (var r = l; r < codePoints.Length && (romajiNode != null || kanaNode != null || otherNode != null); r++) // [l, r] + { + var codePoint = codePoints[r]; + romajiNode = romajiNode.TraverseStep(codePoint, IsIgnorableCodePoint(codePoint)); + kanaNode = kanaNode.TraverseStep(codePoint, IsIgnorableCodePoint(codePoint)); + otherNode = otherNode.TraverseStep(codePoint, IsIgnorableCodePoint(codePoint)); + var reachingInputEnd = r == codePoints.Length - 1; + HashSet matchingTokenIds = + [ + // Allow suffix matching of romaji/other tokens if we're at the end of the input + .. romajiNode.GetTokenIds(ShouldAllowPrefixMatching(TokenType.Romaji, reachingInputEnd)), + .. kanaNode.GetTokenIds(ShouldAllowPrefixMatching(TokenType.Kana, reachingInputEnd)), + .. otherNode.GetTokenIds(reachingInputEnd), + ]; + foreach (var tokenId in matchingTokenIds) foreach (var reference in tokenDefinitions[tokenId].References) + { + var isTokenPrefixMatching = !romajiNode.IsTokenExactMatch(tokenId) && !kanaNode.IsTokenExactMatch(tokenId) && !otherNode.IsTokenExactMatch(tokenId); + var previousMatchesOfDocument = l != 0 && dp[l - 1].TryGetValue(reference.DocumentId, out var previousMatches) ? previousMatches : null; + if (l != 0 && previousMatchesOfDocument == null) continue; + foreach (var documentOffset in reference.Offsets) + { + int currentStart = documentOffset.Start, currentEnd = documentOffset.End; + if (l == 0) ContributeNextMatchingState(null); + else foreach (var (previousEnd, previousMatch) in previousMatchesOfDocument!) if (currentStart >= previousEnd) ContributeNextMatchingState(previousMatch); + void ContributeNextMatchingState(IntermediateResult? previousState) + { + var nextMatchingMap = dp[r]; + if (!nextMatchingMap.TryGetValue(reference.DocumentId, out var nextMatches)) nextMatches = nextMatchingMap[reference.DocumentId] = []; + var oldResult = nextMatches.TryGetValue(currentEnd, out var result) ? result : null; + var inputOffset = new OffsetSpan { Start = l, End = r + 1 }; + var newResult = new IntermediateResult + { + PreviousState = previousState, + FirstTokenDocumentOffset = previousState?.FirstTokenDocumentOffset ?? documentOffset, + RangeCount = previousState == null ? 1 : + previousState.RangeCount + (HasNonEmptyCharacters(documentCodePoints[reference.DocumentId], previousState.DocumentOffset.End, currentStart) ? 1 : 0), + TokenCount = (previousState?.TokenCount ?? 0) + 1, + PrefixMatchCount = (previousState?.PrefixMatchCount ?? 0) + (isTokenPrefixMatching ? 1 : 0), + MatchedTokenLength = (previousState?.MatchedTokenLength ?? 0) + documentOffset.Length * + Math.Min(isTokenPrefixMatching ? (double)inputOffset.Length / tokenDefinitions[tokenId].CodePointLength : double.PositiveInfinity, 1), + TokenId = tokenId, + DocumentOffset = documentOffset, + InputOffset = inputOffset, + IsTokenPrefixMatching = isTokenPrefixMatching, + }; + nextMatches[currentEnd] = oldResult == null || newResult.CompareTo(oldResult) < 0 ? newResult : oldResult; + } + } + } + } + } + + // Build search results and sort documents + return dp[codePoints.Length - 1].Select(entry => + { + var (documentId, matches) = entry; + var sortedMatches = matches.Values.Select(match => + { + var tokens = new List(); + // Build token list from backtracking + var state = match; + while (state != null) + { + tokens.Add(new SearchResultToken + { + Definition = tokenDefinitions[state.TokenId], + DocumentOffset = state.DocumentOffset, InputOffset = state.InputOffset, + IsTokenPrefixMatching = state.IsTokenPrefixMatching, + }); + state = state.PreviousState; + } + tokens.Reverse(); + return new CandidateResult + { + Tokens = tokens.ToArray(), + PrefixMatchCount = match.PrefixMatchCount, + MatchedTokenLength = match.MatchedTokenLength, + RangeCount = match.RangeCount, + }; + }).OrderBy(match => match); + var bestMatch = sortedMatches.First(); + var documentText = documents[documentId]; + var matchRatio = bestMatch.MatchedTokenLength / documentCodePoints[documentId].Length; + var matchRatioLevel = (int)Math.Round(matchRatio * 5); + return new FinalResult + { + Result = new SearchResult + { + DocumentId = documentId, + DocumentText = documentText, + DocumentCodePoints = documentCodePoints[documentId], + Tokens = bestMatch.Tokens, + PrefixMatchCount = bestMatch.PrefixMatchCount, + RangeCount = bestMatch.RangeCount, + MatchRatio = matchRatio, + MatchRatioLevel = matchRatioLevel, + } + }; + }).OrderBy(result => result).Select(result => result.Result).ToArray(); + } +} diff --git a/dotnet/MaigoLabs.NeedLe.Searcher/MaigoLabs.NeedLe.Searcher.csproj b/dotnet/MaigoLabs.NeedLe.Searcher/MaigoLabs.NeedLe.Searcher.csproj new file mode 100644 index 0000000..a0ddfaf --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Searcher/MaigoLabs.NeedLe.Searcher.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + Library + $(ProjectName).Searcher + $(RootNamespace) + + + + true + $(RootNamespace) + + + + + + + + + + + diff --git a/dotnet/MaigoLabs.NeedLe.Searcher/SearchResultHighlighter.cs b/dotnet/MaigoLabs.NeedLe.Searcher/SearchResultHighlighter.cs new file mode 100644 index 0000000..0cc45c5 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Searcher/SearchResultHighlighter.cs @@ -0,0 +1,37 @@ +using MaigoLabs.NeedLe.Common.Extensions; +using MaigoLabs.NeedLe.Common.Types; + +namespace MaigoLabs.NeedLe.Searcher; + +public class HighlightedTextPart +{ + public required string Text { get; init; } + public required bool IsHighlighted { get; init; } +} + +public static class SearchResultHighlighter +{ + public static List Highlight(SearchResult resultDocument) + { + var result = new List(); + var previousHighlightEnd = 0; + foreach (var token in resultDocument.Tokens) + { + var notHighlightedText = resultDocument.DocumentCodePoints.Skip(previousHighlightEnd).Take(token.DocumentOffset.Start - previousHighlightEnd).ToUtf32String(); + if (notHighlightedText.Length > 0) result.Add(new HighlightedTextPart { Text = notHighlightedText, IsHighlighted = false }); + var highlightEnd = token.IsTokenPrefixMatching && token.Definition.Type == TokenType.Kana + ? token.DocumentOffset.Start + Math.Max( + 1, + (int)Math.Round( + token.DocumentOffset.Length * + Math.Min(1, (double)token.InputOffset.Length / token.Definition.CodePointLength) + ) + ) + : token.DocumentOffset.End; + result.Add(new HighlightedTextPart { Text = resultDocument.DocumentCodePoints.Skip(token.DocumentOffset.Start).Take(highlightEnd - token.DocumentOffset.Start).ToUtf32String(), IsHighlighted = true }); + previousHighlightEnd = highlightEnd; + } + if (previousHighlightEnd < resultDocument.DocumentCodePoints.Length) result.Add(new HighlightedTextPart { Text = resultDocument.DocumentCodePoints.Skip(previousHighlightEnd).ToUtf32String(), IsHighlighted = false }); + return result; + } +} diff --git a/dotnet/MaigoLabs.NeedLe.Searcher/Trie/TrieDeserializer.cs b/dotnet/MaigoLabs.NeedLe.Searcher/Trie/TrieDeserializer.cs new file mode 100644 index 0000000..319c9bd --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Searcher/Trie/TrieDeserializer.cs @@ -0,0 +1,73 @@ +using MaigoLabs.NeedLe.Common; + +namespace MaigoLabs.NeedLe.Searcher.Trie; + +public class DeserializedTrie +{ + public required TrieNode Root { get; set; } + public required Dictionary TokenCodePoints { get; set; } +} + +public static class TrieDeserializer +{ + public static DeserializedTrie Deserialize(int[] data) + { + var nodes = new List(); + TrieNode GetNode(int id) + { + if (id > nodes.Count) nodes.AddRange(Enumerable.Repeat(null, id - nodes.Count)); + return nodes[id - 1] ??= new TrieNode { Parent = null, Children = [], TokenIds = [], SubTreeTokenIds = [] }; + } + var currentId = 0; + for (var i = 0; i < data.Length; ) + { + var node = GetNode(++currentId); + var parentId = data[i++]; + node.Parent = parentId != 0 ? GetNode(parentId) : null; + + var endOfChildren = i; + while (endOfChildren < data.Length && data[endOfChildren] > 0) endOfChildren++; + var numberOfChildren = (endOfChildren - i) / 2; + for (var j = i; j < i + numberOfChildren; j++) + { + var codePoint = data[j]; + var child = GetNode(data[j + numberOfChildren]); + node.Children.Add(codePoint, child); + } + i = endOfChildren; + + if (data[i] == 0) i++; // No token IDs + else while (i < data.Length && data[i] < 0) node.TokenIds.Add(-data[i++] - 1); + } + var root = nodes[0]!; + + // DFS to construct code point paths for each token + var tokenCodePoints = new Dictionary(); + var currentCodePoints = new List(); + void DfsCodePoints(TrieNode node) + { + foreach (var tokenId in node.TokenIds) tokenCodePoints.Add(tokenId, [.. currentCodePoints]); + foreach (var (codePoint, child) in node.Children) + { + if (child.Parent != node) continue; // Skip grafted paths as these are not the canonical representation of the tokens + currentCodePoints.Add(codePoint); + DfsCodePoints(child); + currentCodePoints.RemoveAt(currentCodePoints.Count - 1); + } + } + DfsCodePoints(root); + + // DFS to construct subTreeTokenIds for each node + var visitedNodes = new HashSet(); + List DfsSubTreeTokenIds(TrieNode node) + { + if (visitedNodes.Contains(node)) return node.SubTreeTokenIds; + visitedNodes.Add(node); + node.SubTreeTokenIds = new HashSet(node.TokenIds.Concat(node.Children.Values.SelectMany(DfsSubTreeTokenIds))).ToList(); + return node.SubTreeTokenIds; + }; + DfsSubTreeTokenIds(root); + + return new DeserializedTrie { Root = root, TokenCodePoints = tokenCodePoints }; + } +} diff --git a/dotnet/MaigoLabs.NeedLe.Tests/Common/CommonNormalizationTests.cs b/dotnet/MaigoLabs.NeedLe.Tests/Common/CommonNormalizationTests.cs new file mode 100644 index 0000000..4fd03dc --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/Common/CommonNormalizationTests.cs @@ -0,0 +1,126 @@ +using MaigoLabs.NeedLe.Common; + +namespace MaigoLabs.NeedLe.Tests.Common; + +#region ToKatakana + +public sealed class ToKatakana_ConvertsHiraganaToKatakanaTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal("アイウエオ", CommonNormalization.ToKatakana("あいうえお")); + Assert.Equal("カキクケコ", CommonNormalization.ToKatakana("かきくけこ")); + Assert.Equal("サシスセソ", CommonNormalization.ToKatakana("さしすせそ")); + } +} + +public sealed class ToKatakana_KeepsKatakanaUnchangedTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal("アイウエオ", CommonNormalization.ToKatakana("アイウエオ")); + } +} + +public sealed class ToKatakana_KeepsNonKanaUnchangedTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal("abc123", CommonNormalization.ToKatakana("abc123")); + Assert.Equal("漢字", CommonNormalization.ToKatakana("漢字")); + } +} + +public sealed class ToKatakana_HandlesMixedInputTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal("アアa漢", CommonNormalization.ToKatakana("あアa漢")); + } +} + +#endregion + +#region NormalizeCodePoint + +public sealed class NormalizeCodePoint_ConvertsFullwidthAsciiToHalfwidthLowercaseTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal('a', CommonNormalization.NormalizeCodePoint('A')); + Assert.Equal('b', CommonNormalization.NormalizeCodePoint('B')); + Assert.Equal('c', CommonNormalization.NormalizeCodePoint('C')); + Assert.Equal('1', CommonNormalization.NormalizeCodePoint('1')); + Assert.Equal('2', CommonNormalization.NormalizeCodePoint('2')); + Assert.Equal('3', CommonNormalization.NormalizeCodePoint('3')); + Assert.Equal('!', CommonNormalization.NormalizeCodePoint('!')); + } +} + +public sealed class NormalizeCodePoint_ConvertsFullwidthSpaceToHalfwidthTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal(' ', CommonNormalization.NormalizeCodePoint(' ')); + } +} + +public sealed class NormalizeCodePoint_ConvertsHalfwidthKanaToFullwidthTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal('ア', CommonNormalization.NormalizeCodePoint('ア')); + Assert.Equal('イ', CommonNormalization.NormalizeCodePoint('イ')); + Assert.Equal('ウ', CommonNormalization.NormalizeCodePoint('ウ')); + Assert.Equal('エ', CommonNormalization.NormalizeCodePoint('エ')); + Assert.Equal('オ', CommonNormalization.NormalizeCodePoint('オ')); + Assert.Equal('カ', CommonNormalization.NormalizeCodePoint('カ')); + } +} + +public sealed class NormalizeCodePoint_NormalizesVoicedSoundMarksTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal(0x3099, CommonNormalization.NormalizeCodePoint('゙')); // halfwidth voiced -> combining + Assert.Equal(0x309A, CommonNormalization.NormalizeCodePoint('゚')); // halfwidth semi-voiced -> combining + Assert.Equal(0x3099, CommonNormalization.NormalizeCodePoint('゛')); // fullwidth voiced -> combining + Assert.Equal(0x309A, CommonNormalization.NormalizeCodePoint('゜')); // fullwidth semi-voiced -> combining + } +} + +public sealed class NormalizeCodePoint_ConvertsHalfwidthPunctuationToFullwidthTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal('。', CommonNormalization.NormalizeCodePoint('。')); + Assert.Equal('「', CommonNormalization.NormalizeCodePoint('「')); + Assert.Equal('」', CommonNormalization.NormalizeCodePoint('」')); + Assert.Equal('、', CommonNormalization.NormalizeCodePoint('、')); + Assert.Equal('・', CommonNormalization.NormalizeCodePoint('・')); + } +} + +public sealed class NormalizeCodePoint_LowercasesRegularAsciiTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal('a', CommonNormalization.NormalizeCodePoint('A')); + Assert.Equal('b', CommonNormalization.NormalizeCodePoint('B')); + Assert.Equal('c', CommonNormalization.NormalizeCodePoint('C')); + } +} + +#endregion + + diff --git a/dotnet/MaigoLabs.NeedLe.Tests/E2E/SearchTests.cs b/dotnet/MaigoLabs.NeedLe.Tests/E2E/SearchTests.cs new file mode 100644 index 0000000..cc7dedd --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/E2E/SearchTests.cs @@ -0,0 +1,91 @@ +using MaigoLabs.NeedLe.Indexer; +using MaigoLabs.NeedLe.Searcher; + +namespace MaigoLabs.NeedLe.Tests.E2E; + +public sealed class Search_MatchesWithMixedSearchQueryTest : NeedleTestBase +{ + private static readonly string[] TestDocuments = + [ + "ミーティア", + "エンドマークに希望と涙を添えて", + "宵の鳥", + "僕の和風本当上手", + ]; + + [Fact] + public void Execute() + { + var compressed = InvertedIndexBuilder.BuildInvertedIndex(TestDocuments, TokenizerOptions); + var invertedIndex = InvertedIndexLoader.Load(compressed); + + var results = InvertedIndexSearcher.Search(invertedIndex, "bokunoh风じょう"); + + // Should have at least one result + Assert.NotEmpty(results); + + // The first result should be "僕の和風本当上手" + Assert.Equal("僕の和風本当上手", results[0].DocumentText); + } +} + +public sealed class Search_HighlightsSearchResultCorrectlyTest : NeedleTestBase +{ + private static readonly string[] TestDocuments = + [ + "ミーティア", + "エンドマークに希望と涙を添えて", + "宵の鳥", + "僕の和風本当上手", + ]; + + [Fact] + public void Execute() + { + var compressed = InvertedIndexBuilder.BuildInvertedIndex(TestDocuments, TokenizerOptions); + var invertedIndex = InvertedIndexLoader.Load(compressed); + + var results = InvertedIndexSearcher.Search(invertedIndex, "bokunoh风じょう"); + Assert.NotEmpty(results); + + var highlighted = SearchResultHighlighter.Highlight(results[0]); + + // Should be a list of parts + Assert.NotEmpty(highlighted); + + // Collect highlighted text + var highlightedTexts = highlighted.Where(p => p.IsHighlighted).Select(p => p.Text).ToList(); + var highlightedJoined = string.Join("", highlightedTexts); + + Assert.Contains("僕", highlightedJoined); + Assert.Contains("の", highlightedJoined); + Assert.Contains("和", highlightedJoined); + Assert.Contains("風", highlightedJoined); + Assert.Contains("上", highlightedJoined); + } +} + +public sealed class Search_MatchesRomajiInputToKanaDocumentsTest : NeedleTestBase +{ + private static readonly string[] TestDocuments = + [ + "ミーティア", + "エンドマークに希望と涙を添えて", + "宵の鳥", + "僕の和風本当上手", + ]; + + [Fact] + public void Execute() + { + var compressed = InvertedIndexBuilder.BuildInvertedIndex(TestDocuments, TokenizerOptions); + var invertedIndex = InvertedIndexLoader.Load(compressed); + + // Search for "yoi" should match "宵の鳥" + var results = InvertedIndexSearcher.Search(invertedIndex, "yoi"); + var matchedTexts = results.Select(r => r.DocumentText).ToList(); + + Assert.Contains("宵の鳥", matchedTexts); + } +} + diff --git a/dotnet/MaigoLabs.NeedLe.Tests/E2E/TrieSerializationTests.cs b/dotnet/MaigoLabs.NeedLe.Tests/E2E/TrieSerializationTests.cs new file mode 100644 index 0000000..eec846a --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/E2E/TrieSerializationTests.cs @@ -0,0 +1,143 @@ +using MaigoLabs.NeedLe.Common; +using MaigoLabs.NeedLe.Common.Extensions; +using MaigoLabs.NeedLe.Indexer.Trie; +using MaigoLabs.NeedLe.Searcher.Trie; + +namespace MaigoLabs.NeedLe.Tests.E2E; + +#region Trie Building + +public sealed class TrieBuilding_BuildsTrieWithMultipleDifferentTokensTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var trie = TrieBuilder.BuildTrie([ + (0, "hello".ToCodePoints()), + (1, "help".ToCodePoints()), + (2, "world".ToCodePoints()), + (3, "word".ToCodePoints()), + ]); + + // Traverse to verify structure + var helloNode = trie.Traverse("hello".ToCodePoints().ToArray()); + var helpNode = trie.Traverse("help".ToCodePoints().ToArray()); + var worldNode = trie.Traverse("world".ToCodePoints().ToArray()); + var wordNode = trie.Traverse("word".ToCodePoints().ToArray()); + + Assert.NotNull(helloNode); + Assert.NotNull(helpNode); + Assert.NotNull(worldNode); + Assert.NotNull(wordNode); + + // Check token IDs + Assert.Contains(0, helloNode!.TokenIds); + Assert.Contains(1, helpNode!.TokenIds); + Assert.Contains(2, worldNode!.TokenIds); + Assert.Contains(3, wordNode!.TokenIds); + + // Check that 'hel' prefix node has both tokens in subTree + var helNode = trie.Traverse("hel".ToCodePoints().ToArray()); + Assert.NotNull(helNode); + Assert.Contains(0, helNode!.SubTreeTokenIds); + Assert.Contains(1, helNode.SubTreeTokenIds); + } +} + +public sealed class TrieBuilding_HandlesJapaneseTextTokensTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var trie = TrieBuilder.BuildTrie([ + (0, "さくら".ToCodePoints()), + (1, "サクラ".ToCodePoints()), + (2, "桜".ToCodePoints()), + ]); + + Assert.Contains(0, trie.Traverse("さくら".ToCodePoints().ToArray())?.TokenIds ?? []); + Assert.Contains(1, trie.Traverse("サクラ".ToCodePoints().ToArray())?.TokenIds ?? []); + Assert.Contains(2, trie.Traverse("桜".ToCodePoints().ToArray())?.TokenIds ?? []); + } +} + +#endregion + +#region Trie Serialization + +public sealed class TrieSerialization_SerializesAndDeserializesCorrectlyTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var originalTrie = TrieBuilder.BuildTrie([ + (0, "apple".ToCodePoints()), + (1, "app".ToCodePoints()), + (2, "banana".ToCodePoints()), + ]); + + // Serialize + var serialized = TrieSerializer.Serialize(originalTrie); + Assert.True(serialized.Length > 0); + + // Deserialize + var deserialized = TrieDeserializer.Deserialize(serialized); + var deserializedTrie = deserialized.Root; + var tokenCodePoints = deserialized.TokenCodePoints; + + // Verify structure is preserved + var appleNode = deserializedTrie.Traverse("apple".ToCodePoints().ToArray()); + var appNode = deserializedTrie.Traverse("app".ToCodePoints().ToArray()); + var bananaNode = deserializedTrie.Traverse("banana".ToCodePoints().ToArray()); + + Assert.NotNull(appleNode); + Assert.NotNull(appNode); + Assert.NotNull(bananaNode); + + Assert.Contains(0, appleNode!.TokenIds); + Assert.Contains(1, appNode!.TokenIds); + Assert.Contains(2, bananaNode!.TokenIds); + + // Verify tokenCodePoints map + Assert.Equal("apple", tokenCodePoints[0].ToUtf32String()); + Assert.Equal("app", tokenCodePoints[1].ToUtf32String()); + Assert.Equal("banana", tokenCodePoints[2].ToUtf32String()); + + // Verify subTreeTokenIds are reconstructed + Assert.Contains(0, appNode.SubTreeTokenIds); + Assert.Contains(1, appNode.SubTreeTokenIds); + } +} + +public sealed class TrieSerialization_PreservesParentReferencesAfterDeserializationTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var originalTrie = TrieBuilder.BuildTrie([ + (0, "test".ToCodePoints()), + ]); + + var serialized = TrieSerializer.Serialize(originalTrie); + var deserialized = TrieDeserializer.Deserialize(serialized); + var root = deserialized.Root; + + var testNode = root.Traverse("test".ToCodePoints().ToArray()); + Assert.NotNull(testNode); + + // Walk back to root via parent references + TrieNode? node = testNode; + var depth = 0; + while (node?.Parent != null) + { + node = node.Parent; + depth++; + } + Assert.Equal(4, depth); // 't' -> 'e' -> 's' -> 't' -> root + Assert.Same(root, node); + } +} + +#endregion + + diff --git a/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/HanVariantProviderTests.cs b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/HanVariantProviderTests.cs new file mode 100644 index 0000000..de74884 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/HanVariantProviderTests.cs @@ -0,0 +1,75 @@ +using MaigoLabs.NeedLe.Indexer.Han; + +namespace MaigoLabs.NeedLe.Tests.Indexer.Han; + +#region IsHanCharacter + +public sealed class IsHanCharacter_ReturnsTrueForCjkCharactersTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.True(HanVariantProvider.IsHanCharacter('中')); + Assert.True(HanVariantProvider.IsHanCharacter('国')); + Assert.True(HanVariantProvider.IsHanCharacter('日')); + Assert.True(HanVariantProvider.IsHanCharacter('本')); + } +} + +public sealed class IsHanCharacter_ReturnsFalseForNonCjkCharactersTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.False(HanVariantProvider.IsHanCharacter('a')); + Assert.False(HanVariantProvider.IsHanCharacter('あ')); + Assert.False(HanVariantProvider.IsHanCharacter('ア')); + Assert.False(HanVariantProvider.IsHanCharacter('1')); + } +} + +#endregion + +#region GetHanVariants + +public sealed class GetHanVariants_ReturnsVariantsForSimplifiedTraditionalTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var provider = new HanVariantProvider(); + // 国 (simplified) and 國 (traditional) should be variants of each other + var variants1 = provider.GetHanVariants('国'); + var variants2 = provider.GetHanVariants('國'); + Assert.Contains('国', variants1); + Assert.Contains('國', variants1); + Assert.Contains('国', variants2); + Assert.Contains('國', variants2); + } +} + +public sealed class GetHanVariants_ReturnsCharacterItselfForNoVariantsTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var provider = new HanVariantProvider(); + var variants = provider.GetHanVariants('一'); + Assert.Contains('一', variants); + } +} + +public sealed class GetHanVariants_ReturnsEmptyForNonHanCharactersTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var provider = new HanVariantProvider(); + Assert.Empty(provider.GetHanVariants('a')); + Assert.Empty(provider.GetHanVariants('あ')); + } +} + +#endregion + + diff --git a/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/PinyinHelperTests.cs b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/PinyinHelperTests.cs new file mode 100644 index 0000000..4ae4692 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/PinyinHelperTests.cs @@ -0,0 +1,51 @@ +using MaigoLabs.NeedLe.Indexer.Han; + +namespace MaigoLabs.NeedLe.Tests.Indexer.Han; + +public sealed class GetPinyinCandidates_ReturnsPinyinForHanCharacterTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var candidates = PinyinHelper.GetPinyinCandidates('中').ToList(); + Assert.Contains("zhong", candidates); + Assert.Contains("zh", candidates); // initial + Assert.Contains("z", candidates); // first letter + } +} + +public sealed class GetPinyinCandidates_ReturnsMultiplePinyinForPolyphonicTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + // 行 can be "xing" or "hang" + var candidates = PinyinHelper.GetPinyinCandidates('行').ToList(); + Assert.Contains("xing", candidates); + Assert.Contains("hang", candidates); + } +} + +public sealed class GetPinyinCandidates_IncludesFuzzyPinyinVariantsTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + // 风 is "feng", should also have fuzzy variant "fen" + var candidates = PinyinHelper.GetPinyinCandidates('风').ToList(); + Assert.Contains("feng", candidates); + Assert.Contains("fen", candidates); // fuzzy: eng -> en + } +} + +public sealed class GetPinyinCandidates_ReturnsEmptyForNonHanCharactersTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Empty(PinyinHelper.GetPinyinCandidates('a')); + Assert.Empty(PinyinHelper.GetPinyinCandidates('あ')); + } +} + + diff --git a/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/UnionFindSetTests.cs b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/UnionFindSetTests.cs new file mode 100644 index 0000000..ebec6b4 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Han/UnionFindSetTests.cs @@ -0,0 +1,59 @@ +using MaigoLabs.NeedLe.Indexer.Han; + +namespace MaigoLabs.NeedLe.Tests.Indexer.Han; + +public sealed class UnionFindSet_FindsSelfAsRootInitiallyTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var ufs = new UnionFindSet(); + Assert.Equal(1, ufs.Find(1)); + Assert.Equal(2, ufs.Find(2)); + } +} + +public sealed class UnionFindSet_UnionsTwoElementsTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var ufs = new UnionFindSet(); + ufs.Union(1, 2); + Assert.Equal(ufs.Find(1), ufs.Find(2)); + } +} + +public sealed class UnionFindSet_UnionsMultipleElementsTransitivelyTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var ufs = new UnionFindSet(); + ufs.Union(1, 2); + ufs.Union(2, 3); + ufs.Union(4, 5); + Assert.Equal(ufs.Find(1), ufs.Find(3)); + Assert.NotEqual(ufs.Find(1), ufs.Find(4)); + ufs.Union(3, 4); + Assert.Equal(ufs.Find(1), ufs.Find(5)); + } +} + +public sealed class UnionFindSet_IteratesAllKeysTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var ufs = new UnionFindSet(); + ufs.Union(1, 2); + ufs.Union(3, 4); + var keys = ufs.Keys.ToList(); + Assert.Contains(1, keys); + Assert.Contains(2, keys); + Assert.Contains(3, keys); + Assert.Contains(4, keys); + } +} + + diff --git a/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Japanese/JapaneseUtilsTests.cs b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Japanese/JapaneseUtilsTests.cs new file mode 100644 index 0000000..2bca7f7 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Japanese/JapaneseUtilsTests.cs @@ -0,0 +1,69 @@ +using MaigoLabs.NeedLe.Indexer.Japanese; + +namespace MaigoLabs.NeedLe.Tests.Indexer.Japanese; + +#region ToRomajiStrictly + +public sealed class ToRomajiStrictly_ConvertsBasicKanaToRomajiTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal("a", JapaneseUtils.ToRomajiStrictly("あ")); + Assert.Equal("ka", JapaneseUtils.ToRomajiStrictly("か")); + Assert.Equal("sakura", JapaneseUtils.ToRomajiStrictly("さくら")); + } +} + +public sealed class ToRomajiStrictly_ConvertsKatakanaToRomajiTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal("a", JapaneseUtils.ToRomajiStrictly("ア")); + Assert.Equal("ka", JapaneseUtils.ToRomajiStrictly("カ")); + Assert.Equal("sakura", JapaneseUtils.ToRomajiStrictly("サクラ")); + } +} + +public sealed class ToRomajiStrictly_HandlesLongVowelsTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal("ou", JapaneseUtils.ToRomajiStrictly("おう")); + Assert.Equal("oo", JapaneseUtils.ToRomajiStrictly("おお")); + } +} + +public sealed class ToRomajiStrictly_ReturnsEmptyForInvalidFirstCharacterTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal("", JapaneseUtils.ToRomajiStrictly("ー")); // prolonged sound mark cannot be first + Assert.Equal("", JapaneseUtils.ToRomajiStrictly("ゃ")); // small ya cannot be first + } +} + +public sealed class ToRomajiStrictly_ReturnsEmptyForInvalidLastCharacterTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal("", JapaneseUtils.ToRomajiStrictly("っ")); // small tsu cannot be last + } +} + +public sealed class ToRomajiStrictly_HandlesGeminationTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + Assert.Equal("katta", JapaneseUtils.ToRomajiStrictly("かった")); + } +} + +#endregion + + diff --git a/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Japanese/TranscriptionProviderTests.cs b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Japanese/TranscriptionProviderTests.cs new file mode 100644 index 0000000..995bd1c --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/Japanese/TranscriptionProviderTests.cs @@ -0,0 +1,40 @@ +using MaigoLabs.NeedLe.Indexer.Japanese; + +namespace MaigoLabs.NeedLe.Tests.Indexer.Japanese; + +public sealed class GetAllKanaReadings_ReturnsKatakanaForPureKanaInputTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var provider = new TranscriptionProvider(); + var readings = provider.GetAllKanaReadings("あ"); + Assert.Contains("ア", readings); + } +} + +public sealed class GetAllKanaReadings_ReturnsReadingsForKanjiTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var provider = new TranscriptionProvider(); + var readings = provider.GetAllKanaReadings("僕"); + Assert.NotEmpty(readings); + // 僕 should have reading ボク + Assert.Contains("ボク", readings); + } +} + +public sealed class GetAllKanaReadings_ReturnsReadingsForCompoundWordsTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var provider = new TranscriptionProvider(); + var readings = provider.GetAllKanaReadings("和風"); + Assert.NotEmpty(readings); + } +} + + diff --git a/dotnet/MaigoLabs.NeedLe.Tests/Indexer/TokenizerTests.cs b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/TokenizerTests.cs new file mode 100644 index 0000000..e7a7480 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/TokenizerTests.cs @@ -0,0 +1,165 @@ +using MaigoLabs.NeedLe.Common.Types; +using MaigoLabs.NeedLe.Indexer; + +namespace MaigoLabs.NeedLe.Tests.Indexer; + +public sealed class Tokenizer_TokenizesMixedJapaneseTextTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var tokenizer = new Tokenizer(TokenizerOptions); + var tokens = tokenizer.Tokenize("僕の和風本当上手"); + + var tokenDefs = tokenizer.Tokens.Values.ToList(); + + // Should have tokens of various types + var types = tokenDefs.Select(t => t.Type).ToHashSet(); + Assert.Contains(TokenType.Han, types); + Assert.Contains(TokenType.Pinyin, types); + Assert.Contains(TokenType.Kana, types); + Assert.Contains(TokenType.Romaji, types); + + // Helper to get token texts at a specific position by type + List GetTokenTextsAt(int pos, TokenType type) => tokens + .Where(t => t.Start <= pos && t.End > pos) + .Select(t => tokenDefs.First(d => d.Id == t.Id)) + .Where(d => d.Type == type) + .Select(d => d.Text) + .ToList(); + + // Position 0: 僕 + Assert.Contains("僕", GetTokenTextsAt(0, TokenType.Han)); + Assert.Contains("pu", GetTokenTextsAt(0, TokenType.Pinyin)); + Assert.Contains("ボク", GetTokenTextsAt(0, TokenType.Kana)); + Assert.Contains("boku", GetTokenTextsAt(0, TokenType.Romaji)); + + // Position 1: の (hiragana, no Han/Pinyin) + Assert.Empty(GetTokenTextsAt(1, TokenType.Han)); + Assert.Empty(GetTokenTextsAt(1, TokenType.Pinyin)); + Assert.Contains("ノ", GetTokenTextsAt(1, TokenType.Kana)); + Assert.Contains("no", GetTokenTextsAt(1, TokenType.Romaji)); + + // Position 2: 和 + Assert.Contains("和", GetTokenTextsAt(2, TokenType.Han)); + Assert.Contains("he", GetTokenTextsAt(2, TokenType.Pinyin)); + Assert.Contains("ワ", GetTokenTextsAt(2, TokenType.Kana)); + Assert.Contains("wa", GetTokenTextsAt(2, TokenType.Romaji)); + + // Position 3: 風 + Assert.Contains("風", GetTokenTextsAt(3, TokenType.Han)); + Assert.Contains("风", GetTokenTextsAt(3, TokenType.Han)); // simplified variant + Assert.Contains("feng", GetTokenTextsAt(3, TokenType.Pinyin)); + Assert.Contains("フウ", GetTokenTextsAt(3, TokenType.Kana)); + Assert.Contains("fu", GetTokenTextsAt(3, TokenType.Romaji)); + + // Position 4: 本 + Assert.Contains("本", GetTokenTextsAt(4, TokenType.Han)); + Assert.Contains("ben", GetTokenTextsAt(4, TokenType.Pinyin)); + Assert.Contains("ホン", GetTokenTextsAt(4, TokenType.Kana)); + Assert.Contains("hon", GetTokenTextsAt(4, TokenType.Romaji)); + + // Position 5: 当 + Assert.Contains("当", GetTokenTextsAt(5, TokenType.Han)); + Assert.Contains("當", GetTokenTextsAt(5, TokenType.Han)); // traditional variant + Assert.Contains("dang", GetTokenTextsAt(5, TokenType.Pinyin)); + Assert.Contains("トウ", GetTokenTextsAt(5, TokenType.Kana)); + Assert.Contains("to", GetTokenTextsAt(5, TokenType.Romaji)); // normalized: tou -> to + + // Position 6: 上 + Assert.Contains("上", GetTokenTextsAt(6, TokenType.Han)); + Assert.Contains("shang", GetTokenTextsAt(6, TokenType.Pinyin)); + Assert.Contains("ジョウ", GetTokenTextsAt(6, TokenType.Kana)); + Assert.Contains("jo", GetTokenTextsAt(6, TokenType.Romaji)); // normalized: jou -> jo + + // Position 7: 手 + Assert.Contains("手", GetTokenTextsAt(7, TokenType.Han)); + Assert.Contains("shou", GetTokenTextsAt(7, TokenType.Pinyin)); + Assert.Contains("シュ", GetTokenTextsAt(7, TokenType.Kana)); + Assert.Contains("shu", GetTokenTextsAt(7, TokenType.Romaji)); + } +} + +public sealed class Tokenizer_NoDuplicateTokensTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var tokenizer = new Tokenizer(TokenizerOptions); + + // Tokenize multiple music names that share some characters + tokenizer.Tokenize("僕の和風本当上手"); + tokenizer.Tokenize("僕"); + tokenizer.Tokenize("和風"); + + // Check that there are no duplicate tokens + var tokenDefs = tokenizer.Tokens.Values.ToList(); + var tokenKeys = tokenDefs.Select(t => $"{t.Type}:{t.Text}").ToList(); + var uniqueKeys = tokenKeys.ToHashSet(); + + Assert.Equal(uniqueKeys.Count, tokenKeys.Count); + + // Also check that IDs are unique + var ids = tokenDefs.Select(t => t.Id).ToList(); + var uniqueIds = ids.ToHashSet(); + Assert.Equal(uniqueIds.Count, ids.Count); + } +} + +public sealed class Tokenizer_HandlesRawTokensForNonCjkTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var tokenizer = new Tokenizer(TokenizerOptions); + tokenizer.Tokenize("a-b"); + + var tokenDefs = tokenizer.Tokens.Values.ToList(); + var rawTokenTexts = tokenDefs.Where(t => t.Type == TokenType.Raw).Select(t => t.Text).ToList(); + + Assert.Contains("a", rawTokenTexts); + Assert.Contains("-", rawTokenTexts); + Assert.Contains("b", rawTokenTexts); + } +} + +public sealed class Tokenizer_TokenizesCompoundWordKyouTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var tokenizer = new Tokenizer(TokenizerOptions); + var tokens = tokenizer.Tokenize("今日"); + var tokenDefs = tokenizer.Tokens.Values.ToList(); + + // Helper to get tokens with specific type and span + List GetTokensWithSpan(TokenType type, int start, int end) => tokens + .Where(t => t.Start == start && t.End == end) + .Select(t => tokenDefs.First(d => d.Id == t.Id)) + .Where(d => d.Type == type) + .Select(d => d.Text) + .ToList(); + + // Individual character readings at position 0: 今 + Assert.Contains("今", GetTokensWithSpan(TokenType.Han, 0, 1)); + Assert.Contains("jin", GetTokensWithSpan(TokenType.Pinyin, 0, 1)); + Assert.Contains("コン", GetTokensWithSpan(TokenType.Kana, 0, 1)); + Assert.Contains("イマ", GetTokensWithSpan(TokenType.Kana, 0, 1)); + Assert.Contains("kon", GetTokensWithSpan(TokenType.Romaji, 0, 1)); + Assert.Contains("ima", GetTokensWithSpan(TokenType.Romaji, 0, 1)); + + // Individual character readings at position 1: 日 + Assert.Contains("日", GetTokensWithSpan(TokenType.Han, 1, 2)); + Assert.Contains("ri", GetTokensWithSpan(TokenType.Pinyin, 1, 2)); + Assert.Contains("ニチ", GetTokensWithSpan(TokenType.Kana, 1, 2)); + Assert.Contains("ヒ", GetTokensWithSpan(TokenType.Kana, 1, 2)); + Assert.Contains("niti", GetTokensWithSpan(TokenType.Romaji, 1, 2)); + Assert.Contains("hi", GetTokensWithSpan(TokenType.Romaji, 1, 2)); + + // Combined reading for "今日" [0, 2] - this is an indivisible compound word + Assert.Contains("キョウ", GetTokensWithSpan(TokenType.Kana, 0, 2)); + Assert.Contains("kyo", GetTokensWithSpan(TokenType.Romaji, 0, 2)); // normalized: kyou -> kyo + } +} + + diff --git a/dotnet/MaigoLabs.NeedLe.Tests/Indexer/TrieTests.cs b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/TrieTests.cs new file mode 100644 index 0000000..e73982d --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/Indexer/TrieTests.cs @@ -0,0 +1,66 @@ +using MaigoLabs.NeedLe.Common; +using MaigoLabs.NeedLe.Common.Extensions; +using MaigoLabs.NeedLe.Indexer.Trie; + +namespace MaigoLabs.NeedLe.Tests.Indexer; + +#region GraftTriePaths + +public sealed class GraftTriePaths_GraftsPathsAccordingToNormalizationRulesTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + // Build a trie with tokens containing normalized forms + var trie = TrieBuilder.BuildTrie([ + (0, "sya".ToCodePoints()), // normalized form of "sha" + (1, "tu".ToCodePoints()), // normalized form of "tsu" + ]); + + // Graft paths so that "sha" -> "sya" and "tsu" -> "tu" + TrieBuilder.GraftTriePaths(trie, [ + ("sha".ToCodePoints().ToArray(), "sya".ToCodePoints().ToArray()), + ("tsu".ToCodePoints().ToArray(), "tu".ToCodePoints().ToArray()), + ]); + + // Now we should be able to traverse using both the original and grafted paths + var syaNode = trie.Traverse("sya".ToCodePoints().ToArray()); + var shaNode = trie.Traverse("sha".ToCodePoints().ToArray()); + Assert.NotNull(syaNode); + Assert.NotNull(shaNode); + Assert.Same(syaNode, shaNode); // Both paths should lead to the same node + + var tuNode = trie.Traverse("tu".ToCodePoints().ToArray()); + var tsuNode = trie.Traverse("tsu".ToCodePoints().ToArray()); + Assert.NotNull(tuNode); + Assert.NotNull(tsuNode); + Assert.Same(tuNode, tsuNode); + } +} + +public sealed class GraftTriePaths_HandlesChainedGraftRulesTest : NeedleTestBase +{ + [Fact] + public void Execute() + { + var trie = TrieBuilder.BuildTrie([ + (0, "o".ToCodePoints()), // normalized vowel + ]); + + // Chain: "ou" -> "o", "oo" -> "o" + TrieBuilder.GraftTriePaths(trie, [ + ("ou".ToCodePoints().ToArray(), "o".ToCodePoints().ToArray()), + ("oo".ToCodePoints().ToArray(), "o".ToCodePoints().ToArray()), + ]); + + var oNode = trie.Traverse("o".ToCodePoints().ToArray()); + var ouNode = trie.Traverse("ou".ToCodePoints().ToArray()); + var ooNode = trie.Traverse("oo".ToCodePoints().ToArray()); + + Assert.NotNull(oNode); + Assert.Same(oNode, ouNode); + Assert.Same(oNode, ooNode); + } +} + +#endregion diff --git a/dotnet/MaigoLabs.NeedLe.Tests/MaigoLabs.NeedLe.Tests.csproj b/dotnet/MaigoLabs.NeedLe.Tests/MaigoLabs.NeedLe.Tests.csproj new file mode 100644 index 0000000..9fb4791 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/MaigoLabs.NeedLe.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + $(ProjectName).Tests + $(RootNamespace) + false + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/MaigoLabs.NeedLe.Tests/NeedleTestBase.cs b/dotnet/MaigoLabs.NeedLe.Tests/NeedleTestBase.cs new file mode 100644 index 0000000..bf3fb59 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.Tests/NeedleTestBase.cs @@ -0,0 +1,12 @@ +using MaigoLabs.NeedLe.Indexer; +using MaigoLabs.NeedLe.Indexer.Han; +using MaigoLabs.NeedLe.Indexer.Japanese; + +namespace MaigoLabs.NeedLe.Tests; + +public abstract class NeedleTestBase +{ + public static HanVariantProvider HanVariantProvider { get; set; } = new(); + public static TranscriptionProvider TranscriptionProvider { get; set; } = new(); + public static TokenizerOptions TokenizerOptions => new() { HanVariantProvider = HanVariantProvider, TranscriptionProvider = TranscriptionProvider }; +} diff --git a/dotnet/MaigoLabs.NeedLe.slnx b/dotnet/MaigoLabs.NeedLe.slnx new file mode 100644 index 0000000..c8de822 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe.slnx @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/dotnet/MaigoLabs.NeedLe/MaigoLabs.NeedLe.csproj b/dotnet/MaigoLabs.NeedLe/MaigoLabs.NeedLe.csproj new file mode 100644 index 0000000..7330f52 --- /dev/null +++ b/dotnet/MaigoLabs.NeedLe/MaigoLabs.NeedLe.csproj @@ -0,0 +1,35 @@ + + + + Library + $(ProjectName) + $(RootNamespace) + + + + true + $(RootNamespace) + false + false + true + False + + + + + + + + + + + + + <_PackageFiles Include="..\README.md" PackagePath="/" /> + + + README.md + + + + diff --git a/dotnet/README.md b/dotnet/README.md new file mode 100644 index 0000000..74797e9 --- /dev/null +++ b/dotnet/README.md @@ -0,0 +1,57 @@ +# `MaigoLabs.NeedLe` + +Fuzzy search engine for small text pieces, with Chinese/Japanese pronunciation support. + +See also [in-browser demo](https://needle.maigo.dev) (TypeScript version, but the same features as in C#). + +## Install + +```bash +dotnet add package MaigoLabs.NeedLe +``` + +Or install sub-packages separately: + +```bash +dotnet add package MaigoLabs.NeedLe.Indexer # For building indexes +dotnet add package MaigoLabs.NeedLe.Searcher # For searching only +``` + +## Usage + +### Indexing + +Indexing requires dictionaries. These are installed as dependencies of the `MaigoLabs.NeedLe.Indexer` package: + +* MeCab.DotNet +* OpenccNetLib +* hyjiacan.pinyin4net + +```csharp +using MaigoLabs.NeedLe.Indexer; + +var documents = new[] { "你好世界", "こんにちは" }; +var compressedIndex = InvertedIndexBuilder.BuildInvertedIndex(documents); +// To customize dictionary paths, pass the second argument `TokenizerOptions` to `BuildInvertedIndex`. + +// The built index could be stored for later use, or sent to frontend to load with TypeScript package `@maigolabs/needle`. +// For compatibility with .NET Standard, we don't provide JSON related methods. You can use any JSON library to serialize/deserialize the index in the way you prefer. +var json = JsonSerializer.Serialize(compressedIndex); +``` + +### Searching + +Searching requires a prebuilt index but doesn't require dictionaries. Searcher is a lightweight package without dependencies. + +```csharp +using MaigoLabs.NeedLe.Searcher; + +// Index returned by `BuildInvertedIndex`. +var index = InvertedIndexLoader.Load(compressedIndex); + +var results = InvertedIndexSearcher.Search(index, "sekai"); +foreach (var result in results) Console.WriteLine($"{result.DocumentText} ({result.MatchRatio:P0})") +// → 你好世界 (50%) +``` + +To highlight the search result, see also `SearchResultHighlighter`. diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..eb4d274 --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,157 @@ +import tsParser from '@typescript-eslint/parser'; +import tsPlugin from '@typescript-eslint/eslint-plugin'; +import importPlugin from 'eslint-plugin-import'; +import stylisticPlugin from '@stylistic/eslint-plugin'; + +import type { Linter } from 'eslint'; + +const commonConfig: Linter.Config = { + plugins: { + import: importPlugin, + '@typescript-eslint': tsPlugin as any, + stylistic: stylisticPlugin, + }, + rules: { + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', ['internal', 'parent', 'sibling', 'index']], + pathGroups: [ + { + pattern: '@proj-marina/**', + group: 'internal', + position: 'before', + }, + { + pattern: '@/**', + group: 'internal', + position: 'before', + }, + ], + 'newlines-between': 'always', + distinctGroup: false, + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + }, + ], + 'import/no-duplicates': 'error', + + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'prefer-const': 'error', + 'no-var': 'error', + 'no-debugger': 'error', + 'object-shorthand': 'error', + 'prefer-template': 'error', + eqeqeq: ['error', 'always', { null: 'ignore' }], + + '@typescript-eslint/prefer-optional-chain': 'error', + '@typescript-eslint/prefer-nullish-coalescing': 'error', + '@typescript-eslint/return-await': ['error', 'always'], + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-misused-promises': ['error'], + '@typescript-eslint/prefer-as-const': 'error', + '@typescript-eslint/prefer-for-of': 'error', + '@typescript-eslint/prefer-includes': 'error', + '@typescript-eslint/prefer-string-starts-ends-with': 'error', + '@typescript-eslint/consistent-type-imports': ['error', { disallowTypeAnnotations: false }], + + 'stylistic/indent': ['error', 2, { + 'offsetTernaryExpressions': true + }], + 'stylistic/linebreak-style': ['error', 'unix'], + 'stylistic/semi': ['error', 'always'], + 'stylistic/quotes': ['error', 'single', { + 'avoidEscape': true, + 'allowTemplateLiterals': 'avoidEscape', + }], + 'stylistic/comma-dangle': ['error', 'always-multiline'], + 'stylistic/arrow-parens': ['error', 'as-needed'], + 'stylistic/object-curly-spacing': ['error', 'always'], + 'stylistic/array-bracket-spacing': ['error', 'never'], + 'stylistic/space-before-function-paren': ['error', { + 'anonymous': 'always', + 'named': 'never', + 'asyncArrow': 'always', + }], + 'stylistic/space-in-parens': ['error', 'never'], + 'stylistic/comma-spacing': ['error', { 'before': false, 'after': true }], + 'stylistic/key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }], + 'stylistic/keyword-spacing': ['error'], + 'stylistic/space-before-blocks': ['error', 'always'], + 'stylistic/space-infix-ops': ['error'], + 'stylistic/no-trailing-spaces': ['error'], + 'stylistic/eol-last': ['error', 'always'], + 'stylistic/no-multiple-empty-lines': ['error', { 'max': 1, 'maxEOF': 0 }], + 'stylistic/brace-style': ['error', '1tbs', { 'allowSingleLine': true }], + 'stylistic/object-curly-newline': ['error', { + 'ObjectExpression': { 'multiline': true, 'consistent': true }, + 'ObjectPattern': { 'multiline': true, 'consistent': true }, + 'ImportDeclaration': { 'multiline': true, 'consistent': true }, + 'ExportDeclaration': { 'multiline': true, 'consistent': true } + }], + 'stylistic/array-bracket-newline': ['error', 'consistent'], + 'stylistic/function-paren-newline': ['error', 'consistent'], + 'stylistic/member-delimiter-style': ['error', { + 'multiline': { + 'delimiter': 'semi', + 'requireLast': true + }, + 'singleline': { + 'delimiter': 'semi', + 'requireLast': false + } + }], + 'stylistic/type-annotation-spacing': ['error'], + 'stylistic/jsx-quotes': ['error', 'prefer-double'], + + }, + settings: { + 'import/internal-regex': '^@proj-marina/', + 'import/resolver': { + typescript: { + project: ['./apps/*/tsconfig.json', './packages/*/tsconfig.json'], + noWarnOnMultipleProjects: true, + }, + }, + }, +}; + +const parserOptions: Linter.ParserOptions = { + parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./apps/*/tsconfig.json', './packages/*/tsconfig.json'], + noWarnOnMultipleProjects: true, +}; + +const config: Linter.Config[] = [ + { + ...commonConfig, + files: ['**/*.{ts,tsx}'], + languageOptions: { + parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module', + parserOptions, + }, + }, + { + ignores: [ + '**/node_modules/**', + // Build output + '**/dist/**', + '**/build/**', + '**/coverage/**', + 'eslint.config.ts', + '**/uno.config.ts', + '**/vite.config.ts', + '**/jest.config.ts', + '**/tsdown.config.ts', + ], + }, +]; + +export default config; diff --git a/example.txt b/example.txt new file mode 100644 index 0000000..6e74cae --- /dev/null +++ b/example.txt @@ -0,0 +1,1585 @@ +True Love Song +Color My World +Future +Love You +コネクト +In Chaos +Crush On You +Sun Dance +Sweets×Sweets +Love or Lies +ネコ日和。 +虹と太陽 +炭★坑★節 +NIGHT OF FIRE +come again +jelly +ハッピーシンセサイザ +ルカルカ★ナイトフィーバー +源平大戦絵巻テーマソング +美しく燃える森 +ウッーウッーウマウマ(゚∀゚) +Endless World +Beat Of Mind +檄!帝国華撃団(改) +メランコリック +教えて!! 魔法のLyric +ZIGG-ZAGG +ワールズエンド・ダンスホール +パンダヒーロー +オレンジの夏 +BaBan!! -甘い罠- +ジングルベル +マトリョシカ +メグメグ☆ファイアーエンドレスナイト +天国と地獄 +きみのためなら死ねる +怪盗Rのテーマ +マリアをはげませ +SHOW TIME +City Escape: Act1 +Rooftop Run: Act1 +Reach For The Stars +Urban Crusher [Remix] +Catch The Future +JACKY [Remix] +Tell Your World +おちゃめ機能 +ダンシング☆サムライ +BAD∞END∞NIGHT +ロミオとシンデレラ +Sweetiex2 +腐れ外道とチョコレゐト +Quartet Theme [Reborn] +Sky High [Reborn] +Like the Wind [Reborn] +YA・DA・YO [Reborn] +Space Harrier Main Theme [Reborn] +DADDY MULK -Groove remix- +ぴぴぱぷぅ! +炎歌 -ほむらうた- +犬日和。 +ソーラン☆節 +デコボコ体操第二 +スイートマジック +ゴーゴー幽霊船 +千本桜 +御旗のもとに +FEEL ALIVE +Link +Turn around +We Gonna Party +Night Fly +Feel My Fire +Streak +Spin me harder +Lionheart +Acceleration +Sprintrances +Nerverakes +Black Out +Fragrance +air's gravity +Starlight Disco +39 +地上の戦士 +超絶!Superlative +采配の刻 Power of order +DO RORO DERODERO ON DO RORO +泣き虫O'clock +Natural Flow +maimaiちゃんのテーマ +君の知らない物語 +I ♥ +*ハロー、プラネット。 +イアイア★ナイトオブデザイア +ローリンガール +天ノ弱 +弱虫モンブラン +モザイクロール +脳漿炸裂ガール +SPiCa +セツナトリップ +放課後ストライド +カゲロウデイズ +チルノのパーフェクトさんすう教室 +Bad Apple!! feat.nomico +魔理沙は大変なものを盗んでいきました +Grip & Break down !! +Help me, ERINNNNNN!!(Band ver.) +ナイト・オブ・ナイツ +全人類ノ非想天則 +しゅわスパ大作戦☆ +時空を超えて久しぶり! +Her Dream Is To Be A Fantastic Sorceress +ココロスキャンのうた +神室雪月花 +KONNANじゃないっ! +awake +Mysterious Destiny +Riders Of The Light +Terminal Storm +記憶、記録 +セガサターン起動音[H.][Remix] +夜咄ディセイブ +ウサテイ +sweet little sister +Blew Moon +Garakuta Doll Play +Dreampainter +ぐるぐるWASH!コインランドリー・ディスコ +CYCLES +Heartbeats +Life Feels Good +MYTHOS +End of Twilight +火炎地獄 +ユビキリ +Pixel Voyage +Get Happy +System “Z” +Beat of getting entangled +Cosmic Train +ナミダと流星 +おても☆Yan +maiム・maiム feat.週刊少年マガジン +Danza zandA +Backyun! -悪い女- +Monochrome Rainbow +みんなのマイマイマー +円舞曲、君に +Live & Learn +Burning Hearts ~炎のANGEL~ +いーあるふぁんくらぶ +Nyan Cat EX +ぽっぴっぽー +LUCIA +Death Scythe +BREAK YOU!! +JUMPIN' JUMPIN' +L'épilogue +Save This World νMIX +Living Universe +Ignite Infinity +トルコ行進曲 - オワタ\(^o^)/ +裏表ラバーズ +Outlaw's Lullaby +Brand-new Japanesque +レッツゴー!陰陽師 +炉心融解 +ダブルラリアット +からくりピエロ +ネトゲ廃人シュプレヒコール +むかしむかしのきょうのぼく +magician's operation +六兆年と一夜物語 +みくみくにしてあげる♪【してやんよ】 +リリリリ★バーニングナイト +ハロ/ハワユ +Our Fighting +緋色のDance +明星ロケット +The Great Journey +お嫁にしなさいっ! +ロストワンの号哭 +Sweet Devil +患部で止まってすぐ溶ける~狂気の優曇華院 +ケロ⑨destiny +Endless, Sleepless Night +POP STAR +Back 2 Back +神々の祈り +エピクロスの虹はもう見えない +シアワセうさぎ +ってゐ! ~えいえんてゐVer~ +待チ人ハ来ズ。 +Cosmic Magic Shooter +深海少女 +M.S.S.Planet +東方スイーツ!~鬼畜姉妹と受難メイド~ +幻想のサテライト +最速最高シャッターガール +バラライカ +林檎華憐歌 +YATTA! +Jack-the-Ripper◆ +Windy Hill -Zone 1 +コトバ・カラフル +タカハせ!名人マン +言ノ葉カルマ +かせげ!ジャリンコヒーロー +キズナの物語 +Heart Beats +アゲアゲアゲイン +紅蓮の弓矢 +鼓動 +DRAGONLADY +二息歩行 +ファンタジーゾーン OPA-OPA! -GMT remix- +電車で電車でGO!GO!GO!GC! -GMT remix- +RIDGE RACER STEPS -GMT remix- +リッジでリッジでGO!GO!GO! -GMT mashup- +オパ! オパ! RACER -GMT mashup- +電車で電車でOPA!OPA!OPA! -GMT mashup- +Axeria +ピーマンたべたら +MIRROR of MAGIC +究極焼肉レストラン!お燐の地獄亭! +shake it! +1/3の純情な感情 +毒占欲 +YU-MU +物凄い勢いでけーねが物凄いうた +BETTER CHOICE +Oshama Scramble! +D✪N’T ST✪P R✪CKIN’ +ガラテアの螺旋 +oboro +Dragoon +Stand Up!!!! +アンハッピーリフレイン +One Step Ahead +L9 +planet dancer +Caliburne ~Story of the Legendary sword~ +どうしてこうなった +B.B.K.K.B.K.K. +おこちゃま戦争 +VERTeX +Ignis Danse +Scars of FAUNA +FUJIN Rumble +きたさいたま2000 +FLOWER +Got more raves? +不毛! +デッドレッドガールズ +赤心性:カマトト荒療治 +悪戯 +響縁 +LOL -lots of laugh- +クローバー♣クラブ +妄想税 +どういうことなの!? +初音ミクの消失 +イノコリ先生 +キャプテン・ムラサのケツアンカー +若い力 -SEGA HARD GIRLS MIX- +ウミユリ海底譚 +welcome to maimai!! with マイマイマー +ストリーミングハート +Change Our MIRAI! +Aiolos +LANCE +SAVIOR OF SONG +橙の幻想郷音頭 +東方妖々夢 ~the maximum moving about~ +蒼空に舞え、墨染の桜 +少女幻葬戦慄曲 ~ Necro Fantasia +Contrapasso -paradiso- +幸せになれる隠しコマンドがあるらしい +フラグメンツ -T.V. maimai edit- +願いを呼ぶ季節 +おじゃま虫 +Yet Another ”drizzly rain” +四次元跳躍機関 +最終鬼畜妹・一部声 +はじめまして地球人さん +Pursuing My True Self +Signs Of Love (“Never More” ver.) +specialist (“Never More” ver.) +Time To Make History (AKIRA YAMAOKA Remix) +GEMINI -M- ++♂ +ありふれたせかいせいふく +すろぉもぉしょん +絵の上手かった友達 +頓珍漢の宴 +Garden Of The Dragon +After Burner +Counselor +Glorious Crown +言ノ葉遊戯 +りばーぶ +幾四音-Ixion- +閃鋼のブリューナク +無敵We are one!! +7thSense +FEEL the BEATS +Revive The Rave +スリップフリップ +Jimang Shot +儚きもの人間 +ラブリー☆えんじぇる!! +オモイヨシノ +アージェントシンメトリー +かくしん的☆めたまるふぉ~ぜっ! +極上スマイル +厨病激発ボーイ +HIMITSUスパーク +Party 4U ”holy nite mix” +囲い無き世は一期の月影 +Lividi +Infantoon Fantasy +Jumble Rumble +Imitation:Loud Lounge +CITRUS MONSTER +Hyper Active +AMAZING MIGHTYYYY!!!! +fake!fake! +夏祭り +シュガーソングとビターステップ +Daydream café +セハガガガンバッちゃう!! +キミノヨゾラ哨戒班 +恋愛裁判 +Just Be Friends +ヒビカセ +パーフェクト生命 +StargazeR +だんだん早くなる +ヘルシーエンド +クノイチでも恋がしたい +ココロ +東京リアルワールド +やめろ!聴くな! +ゆっくりしていってね!!! +木彫り鯰と右肩ゾンビ +ECHO +Mr. Wonderland +しんでしまうとはなさけない! +Hand in Hand +ブリキノダンス +brilliant better +ハート・ビート +Invitation +connecting with you +高気圧ねこロック +Prophesy One +洗脳 +Barbed Eye +Nitrous Fury +色は匂へど散りぬるを +月に叢雲華に風 +No Routine +ひれ伏せ愚民どもっ! +エテルニタス・ルドロジー +泡沫、哀のまほろば +華鳥風月 +METATRON +終わりなき物語 +フォルテシモBELL +私の中の幻想的世界観及びその顕現を想起させたある現実での出来事に関する一考察 +DETARAME ROCK&ROLL THEORY +ファッとして桃源郷 +ゴーストルール +チュルリラ・チュルリラ・ダッダッダ! +GOODMEN +夜明けまであと3秒 +Last Brave ~ Go to zero +STAIRWAY TO GENERATION +空威張りビヘイビア +回レ!雪月花 +いっしそう電☆舞舞神拳! +ENJOY POLIS +生きてるおばけは生きている +天火明命 +HERA +分からない +Our Wrenally +Scream out! -maimai SONIC WASHER Edit- +四月の雨 +Selector +ドキドキDREAM!!! +ポップミュージックは僕のもの +吉原ラメント +ねぇ、壊れタ人形ハ何処へ棄テらレるノ? +幽闇に目醒めしは +Starlight Vision +Club Ibuki in Break All +Phantasm Brigade +永遠のメロディ +白ゆき +Panopticon +Sakura Fubuki +conflict +すーぱーぬこになりたい +踊れオーケストラ +きらっせ☆ウッド村ファーム +taboo tears you up +その群青が愛しかったようだった +The wheel to the right +前前前世 +SAKURAスキップ +リンカーネイション +宿題が終わらないっ! +GO BACK 2 YOUR RAVE +天国と地獄 -言ノ葉リンネ- +夢花火 +Paradisus-Paradoxum +オトヒメモリー☆ウタゲーション +ラブチーノ +東京レトロ +ARROW +バッド・ダンス・ホール +KISS CANDY FLAVOR +Maxi +相思創愛 +心象蜃気楼 +光線チューニング +Limit Break +曖昧mind +KING is BACK!! +Ultranova +GET!! 夢&DREAM +日本の米は世界一 +ないせんのうた +Starlight Dance Floor +妖精村の月誕祭 ~Lunate Elf +Calamity Fortune +Justified +Excalibur ~Revived resolution~ +ホシトハナ +名探偵連続殺人事件 +エイリアンエイリアン +My Dearest Song +猛進ソリストライフ! +Let's Go Away +Now Loading!!!! +真・ハンサム体操でズンドコホイ +フラジール +オーディエンスを沸かす程度の能力 feat.タイツォン +人里に下ったアタイがいつの間にか社畜になっていた件 +ギリギリ最強あいまいみー! +CALL HEAVEN!! +Sunshine world tour +ちがう!!! +Moon of Noon +チルノのパーフェクトさんすう教室 ⑨周年バージョン +デスパレイト +ようこそジャパリパークへ +あ・え・い・う・え・お・あお!! +ドーナツホール +MilK +咲キ誇レ常世ノ華 +Magical Flavor +白い雪のプリンセスは +好き!雪!本気マジック +スターナイトスノウ +This game +Los! Los! Los! +Candy Tall Woman +Signature +人生リセットボタン +しねばいいのに +もうみんなしねばいいのに +共感覚おばけ +麒麟 +Credits +ダンスロボットダンス +アルカリレットウセイ +ドクハク +キレキャリオン +猫祭り +TRUST +Still +ガヴリールドロップキック +fantastic dreamer +天狗の落とし文 feat. ytr +疾走あんさんぶる +Doll Judgment +WARNING×WARNING×WARNING +SPILL OVER COLORS +Mare Maris +シャルル +フリィダム ロリィタ +バレリーコ +アウターサイエンス +REVIVER オルタンシア・サーガ -蒼の騎士団- オリジナルVer. +拝啓ドッペルゲンガー +ミラクル・ショッピング +Kinda Way +ラブって♡ジュエリー♪えんじぇる☆ブレイク!! +ヤバい○○ +エンドマークに希望と涙を添えて +ナンセンス文学 +Bang Babang Bang!!! +Tic Tac DREAMIN’ +SPICY SWINGY STYLE +砂の惑星 feat. HATSUNE MIKU +アンノウン・マザーグース +POP TEAM EPIC +にめんせい☆ウラオモテライフ! +うまるん体操 +妄想感傷代償連盟 +LOVE EAST +Fist Bump +ENERGY SYNERGY MATRIX +Session High⤴ +World Vanquisher +FestivaLight +Brain Power +進め!イッスン軍団 -Rebellion of the Dwarfs- +ULTRA B+K +インビジブル +彗星ハネムーン +アンチクロックワイズ +全力☆Summer! +keep hopping +larva +えれくとりっく・えんじぇぅ +ミラクルペイント +Ievan Polkka +初音ミクの激唱 +ガチャガチャきゅ~と・ふぃぎゅ@メイト +true my heart -Lovable mix- +Rodeo Machine +Arrival of Tears +CYBER Sparks +Xevel +INFINITE WORLD +サドマミホリック +Ragnarok +このふざけた素晴らしき世界は、僕の為にある +このピアノでお前を8759632145回ぶん殴る +はやくそれになりたい! +フィクサー +みんなの +アマノジャクリバース feat. ytr +進捗どうですか? +Help me, あーりん! +なるとなぎのパーフェクトロックンロール教室 +あねぺったん +We Gonna Journey +FREEDOM DiVE (tpz Overcute Remix) +SILENT BLUE +甲賀忍法帖 +ジンギスカン +にじよめちゃん体操第一億 +花と、雪と、ドラムンベース。 +Conquista Ciela +怒槌 +Seyana. ~何でも言うことを聞いてくれるアカネチャン~ +おねがいダーリン +My First Phone +SHINY DAYS +NOISY LOVE POWER☆ +ネ!コ! +only my railgun +ロールプレイングゲーム +ヒバナ +ロキ +Money Money +極圏 +Scarlet Lance +セイクリッド ルイン +QZKago Requiem +EVERGREEN +金の聖夜霜雪に朽ちて +不思議の国のクリスマス +Schwarzschild +Magical Sound Shower +片想いサンバ +ナイトメア☆パーティーナイト +フキゲンワルツ +結ンデ開イテ羅刹ト骸 +サウンド +右肩の蝶 +Alea jacta est! +L4TS:2018 (feat. あひる & KTA) +クレイジークレイジーダンサーズ +【東方ニコカラ】秘神マターラ feat.魂音泉【IOSYS】 +隠然 +White Traveling Girl +FFT +-OutsideR:RequieM- +イロトリドリのメロディ +奏者はただ背中と提琴で語るのみ +Deep in Abyss +雷切-RAIKIRI- +キミとボクのミライ +Lost Princess +アリサのテーマ +敗北の少年 +立ち入り禁止 +お気に召すまま +命ばっかり +the EmpErroR +PANDORA PARADOXXX +Believe the Rainbow +Good Bye, Mr. Jack +Altale +最終鬼畜妹フランドール・S +ネクロファンタジア~Arr.Demetori +Imperishable Night 2006 (2016 Refine) +終点 +WORLD'S END UMBRELLA +ワンダーラスト +End Time +カラフル×メロディ +魔法少女になるしかねぇ +Kattobi KEIKYU Rider +B.M.S. +TiamaT:F minor +全世界共通リズム感テスト +Technicians High +Destr0yer +サンバランド +Halcyon +渦状銀河のシンフォニエッタ +VIIIbit Explorer +華の集落、秋のお届け +僕は空気が嫁ない +プリズム△▽リズム +AFTER PANDORA +BLACK ROSE +Secret Sleuth +でらっくmaimai♪てんてこまい! +MAXRAGE +バーチャルダム ネーション +P-qoq +超常マイマイン +Crazy Circle +STEREOSCAPE +STARTLINER +♡マイマイマイラブ♡ +一か罰 +キリキリ舞Mine +福宿音屋魂音泉 +Now or Never +Scarlet Wings +魔ジョ狩リ +TwisteD! XD +Blows Up Everything +Agitation! +管弦楽組曲 第3番 ニ長調「第2曲(G線上のアリア)」BWV.1068-2 +TEmPTaTiON +アポカリプスに反逆の焔を焚べろ +玩具狂奏曲 -終焉- +Titania +BOKUTO +立川浄穢捕物帳 +CHOCOLATE BOMB!!!! +METEOR +LOSER +U.S.A. +HOT LIMIT +新宝島 +UNION +乗ってけ!ジャパリビート +フレ!フレ!ベストフレンズ +メルト +メルティランドナイトメア +アウトサイダー +アディショナルメモリー +ジャガーノート +ナイト・オブ・ナイツ (Cranky Remix) +Bad Apple!! feat.nomico (REDALiCE Remix) +Strobe♡Girl +グリーンライツ・セレナーデ +POPPY PAPPY DAY +Clattanoia +The Gong of Knockout +デンパラダイム +骸骨楽団とリリア +星屑ユートピア +アマツキツネ +徒花ネクロマンシー +目覚めRETURNER +Little "Sister" Bitch +Yakumo >>JOINT STRUGGLE (2019 Update) +げきオコスティックファイナリアリティぷんぷんマスタースパーク +CocktaiL +太陽系デスコ +だからパンを焼いたんだ +東奔西走行進曲 +Soul-ride ON! +アカリがやってきたぞっ +プナイプナイせんそう +未完成人 +デビル☆アイドル +quiet room +Jump!! Jump!! Jump!! +サヨナラチェーンソー +イカサマライフゲイム +だれかの心臓になれたなら +幾望の月 +39みゅーじっく! +STEEL TRANSONIC +Flashkick +Stardust Memories +My My My +UniTas +ここからはじまるプロローグ。 +絡めトリック利己ライザー +モ°ルモ°ル +ブレインジャックシンドローム +共鳴 +Ututu +シエルブルーマルシェ +GRÄNDIR +封焔の135秒 +ヤミツキ +ワードワードワード +Valsqotch +最強 the サマータイム!!!!! +UTAKATA +タテマエと本心の大乱闘 +はちみつアドベンチャー +popcorn +ハム太郎とっとこうた +青空のラプソディ +だから僕は音楽を辞めた +ラブ・ドラマティック feat. 伊原六花 +異世界かるてっと +いつかいい感じにアレしよう +I'm with you +オトモダチフィルム +ビターチョコデコレーション +アンドロイドガール +スターリースカイ☆パレード +スロウダウナー +クレイジー・ビート +バイオレンストリガー +グラーヴェ +KILLER B +すーぱーぬこになれんかった +アスヘノBRAVE +深海シティアンダーグラウンド +表裏一体 +Catch the Wave +ソリッド +全力ハッピーライフ +Oath Act +Witches night +Drive Your Fire +wheel +Black Lair +energy trixxx +NULCTRL +アトロポスと最果の探究者 +六厘歌 +Entrance +Lunar Mare +Saika +CHAOS +Maboroshi +ウマイネームイズうまみちゃん +馬と鹿 +ぼくたちいつでも しゅわっしゅわ! +Boys O’Clock +居並ぶ穀物と溜息まじりの運送屋 +ARAIS +マイオドレ!舞舞タイム +Aetheric Energy +Komplexe +Beautiful Future +Mutation +オリフィス +ユメヒバナ +REAL VOICE +パラボラ +Regulus +ワンダーシャッフェンの法則 +BIRTH +シアトリカル・ケース +ステップアンドライム +届かない花束 +YURUSHITE +ポケットからぬりつぶせ! +トリドリ⇒モリモリ!Lovely fruits☆ +Desperado Waltz +宛城、炎上!! +Climax +レーイレーイ +ノーポイッ! +町かどタンジェント +紅蓮華 +MIRACLE RUSH +お願いマッスル +Virtual to LIVE +命に嫌われている +アンクローズ・ヒューマン +乙女解剖 +絶え間なく藍色 +ガランド +とりあえずアナタがいなくなるまえに +明星ギャラクティカ +絶対にチョコミントを食べるアオイチャン +デリヘル呼んだら君が来た +ベノム +マネマネサイコトロピック +劣等上等 +アスノヨゾラ哨戒班 +悪戯センセーション +Paranoia +絡繰りドール +物凄いヴァイブスで魔理沙が物凄いラップ +Sweet Requiem +Dive into The Sky ~initialized~ +BATTLE NO.1 +Ether Strike +Cyaegha +Grievous Lady +c.s.q.n. +カラッポ・ノンフィクション +emomomo +Arty Party +Jörqer +Sqlupp (Camellia's "Sqleipd*Hiytex" Remix) +felys -final remix- +Pretender +自傷無色 +Prismatic +≠彡"/了→ +BREaK! BREaK! BREaK! +Heavenly Blast +Paradisoda +VANTABLACK RAVER +時計の国のジェミニ +Xenovcipher +星めぐり、果ての君へ。 +スローアライズ +チエルカ/エソテリカ +生命不詳 +Never Give Up! +Starry Colors +ほしぞらスペクタクル +Last Samurai +蒼穹舞楽 +AMABIE +オーケー? オーライ! +サヨナラフリーウェイ +単一指向性オーバーブルーム +みなえをチェック! +ハジマリノピアノ +BOUNCE & DANCE +Kiss Me Kiss +MEGATON BLAST +Splash Dance!! +インフェルノ +冬のこもりうた +God knows... +おジャ魔女カーニバル!! +秒針を噛む +タケモトピアノCMソング +乙女のルートはひとつじゃない! +ビックカメラのテーマソング +電話革命ナイセン +Shiny Smily Story +ネガティブ進化論 +雨とペトラ +永遠にゲームで対戦したいキリタン +KING +幽霊東京 +Alice in 冷凍庫 +キラメキ居残り大戦争 +ディカディズム +猫猫的宇宙論 +ラットが死んだnew words +阿吽のビーツ +再会 +約束 +トラフィック・ジャム +アルティメットセンパイ +ロストワードクロニカル +今、誰が為のかがり火へ +Melody! +下克上々 +ハウリング +トランスダンスアナーキー +Re:End of a Dream +もぺもぺ +Sound Chimera +CO5M1C R4ILR0AD +MAKE IT FUNKY NOW +Rush-More +Strange Bar +Big Daddy +ベースラインやってる?笑 +ALiVE +ヒミツCULT +MEGATON KICK +LOSE CONTROL +宿星審判 +脳天直撃 +とびだせ!TO THE COSMIC!! +噛み係 +トリアージ +NAGAREBOSHI☆ROCKET +U&iVERSE -銀河鸞翔- +BLACK SWAN +星詠みとデスペラード +Round Round Spinning Around +Alcyone +Raven Emperor +Yorugao +ヨミビトシラズ +前衛的Landscape +Trick tear +躯樹の墓守 +ぱくぱく☆がーる +No Limit RED Force +LiftOff +Falsum Atlantis. +We Are Us +Limits +スピカの天秤 +ハードコア・シンドローム +夜に駆ける +うっせぇわ +Shooting Stars +MOON NIGHTのせいにして +テレキャスタービーボーイ +デッドマンズバラッド +イヤホンロマンス +竹 +スーパーシンメトリー +Southern Cross +Upshift +MOBILYS +ドラゴンエネルギー +セカイはまだ始まってすらいない +Sweets Time +紅星ミゼラブル~廃憶編 +幻想に咲いた花 +マツヨイナイトバグ +ポッピンキャンディ☆フィーバー! +どぅーまいべすと! +春を告げる +泥の分際で私だけの大切を奪おうだなんて +VOLTAGE +失敗作少女 +悪魔の踊り方 +ノイローゼ +Seize The Day +グッバイ宣言 +春嵐 +ラグトレイン +なだめスかし Negotiation(TVsize) +インドア系ならトラックメイカー +アカツキアライヴァル +リモコン +on the rocks +響け!CHIREI MY WAY! +神々が恋した幻想郷 +Lia=Fail +アンビバレンス +veil +廻廻奇譚 +さんさーら! +アイドル新鋭隊 +needLe +ヒステリックナイトガール +GIGANTØMAKHIA +ミルキースター・シューティングスター +isophote +パラマウント☆ショータイム!! +Strive against fate +sølips +パーフェクション +デーモンベット +HECATONCHEIR +Irresistible +HAGAKIRI +N3V3R G3T OV3R +Swift Swing +星空パーティーチューン +チューリングの跡 +Sage +OTOGEMA +WiPE OUT MEMORIES +Metamorphosism +白花の天使 +World's end loneliness +Jouez Avec Moi? +テリトリーバトル +Good bye, Merry-Go-Round. +ツクヨミステップ +folern +コスモポップファンクラブ +撩乱乙女†無双劇 +See The Light +エータ・ベータ・イータ +愛のシュプリーム! +くらべられっ子 +あの世行きのバスに乗ってさらば。 +うまぴょい伝説 +ヴァンパイア +エンヴィーベイビー +初音天地開闢神話 +Armageddon +Dreadnought +enchanted love +Caterpillar Song +Photon Melodies +吾輩よ猫であれ + +BLUE ZONE +Estahv +First Dance +Mjölnir +Define +BANG! +Love's Theme of BADASS ~バッド・アス 愛のテーマ~ +オントロジー +剣を抜け!GCCX MAX +Princess♂ +ヒトガタ +Spring of Dreams +大輪の魂 (feat. AO, 司芭扶) +紅に染まる恋の花 +三妖精SAY YA!!! +Reach For The Stars (Re-Colors) +ジャンキーナイトタウンオーケストラ +終焉逃避行 +8-EM +若い力 +遺伝子レベル∞スパイラル +otorii INNOVATED -[i]3- +エゴロック +ただ選択があった +パズルリボン +YONA YONA DANCE +Transcend Lights +ホシシズク +Rainbow Rush Story +Tricolor⁂circuS +[X] +分解収束テイル +mystique as iris +Rising on the horizon +You Mean the World to Me +Neon Kingdom +#狂った民族2 PRAVARGYAZOOQA +VSpook! +RIFFRAIN +Falling +ピリオドサイン +群青シグナル +Beat Opera op.1 +星見草 +"411Ψ892" +康莊大道 +蜘蛛の糸 +Don't Fight The Music +Catch Me If You Can +MAGNETAR GIRL +SUPER AMBULANCE +Ghost Dance +電光石火 +Hainuwele +アノーイング!さんさんウィーク! +アニマル +ジレンマ +踊 +残響散歌 +フォニイ +ヴィラン +EYE +スカーレット警察のゲットーパトロール24時 +田中 +Random +Luminaria +群青讃歌 +セカイ +ワーワーワールド +銀のめぐり +Destiny Runner +アマカミサマ +モンダイナイトリッパー! +マーシャル・マキシマイザー +秋の未確認生物 +Dive into the ZONe +エナドリおいしいソング +Baddest +ばかみたい【Taxi Driver Edition】 +れっつ!みらくる☆はーどこあっ! +Blank Paper (Prod. TEMPLIME) +In my world (Prod. KOTONOHOUSE) +アイム・マイヒーロー +NightTheater +キュートなカノジョ +へべれけジャンキー +きゅうくらりん +回る空うさぎ +Lost Desire +Aegleseeker +最強STRONGER +ボッカデラベリタ +『んっあっあっ。』 +独りんぼエンヴィー +ロータスイーター +ViRTUS +Alice's Suitcase +ピュグマリオンの咒文 +トノサマビーム +enchanted wanderer +Comet Panto Men! +ツムギボシ +VeRForTe αRtE:VEiN +ヱデン +にゃーにゃー冒険譚 +The Great Banquet +Redemption +Ether Second +Straight into the lights +アンバークロニクル +リフヴェイン +Kairos +宵の鳥 +ここからはじまるプロローグ。 (Kanon Remix) +モ°ルモ°ル (MZK Skippin' Remix) +VERTeX (rintaro soma deconstructed remix) +隠密あんみつDX +地球 +Churros Parlor +超熊猫的周遊記(ワンダーパンダートラベラー) +Trrricksters!! +FLUFFY FLASH +STARRED HEART +Y.Y.Y.計画!!!! +Last Kingdom +LAMIA +ヒバリ +Hello, Hologram +不機嫌なスリーカード +神っぽいな +魔法少女とチョコレゐト +阿修羅ちゃん +おとせサンダー +ロウワー +キャットラビング +リスペク風神 +Let you DIVE! +Knight Rider +I’m Here (feat. Merry Kirk-Holmes) +INTERNET OVERDOSE +魂のルフラン +Shooting Shower~DANCE TIME(シンディ)~ +Lights of Muse +tape/stop/night +Final Step! +The 90's Decision +僕の和風本当上手 +Cthugha +はんぶんこ +PUPA +解けないように +インターネットサバイバー +コンティニュー! feat. 藍月なくる +Sunday Night feat Kanata.N +PERSONA feat. PANXI +Halfway(>∀<) +Complex Mind +DROPS feat. Such +あつすぎの歌 +ULTRA SYNERGY MATRIX +花となれ +私のドッペルゲンガー +BULK UP (GAME EXCLUSIVE EDIT) +Vallista +ノンブレス・オブリージュ +テオ +まにまに +感情ディシーブ +ド屑 +絶対敵対メチャキライヤー +つるぺったん +『ウソテイ』 ~一回戦せりなvsしろなvsなずな~ +Bad Apple!! feat.nomico ~五十嵐 撫子 Ver.~ +Snow Colored Score +MAGENTA POTION +NOIZY BOUNCE +サンバディ! +Horoscope Express +Party☆People☆Princess +Latent Kingdom +Mystic Parade +Cry Cry Cry +ぽわわん劇場 +my flow +POWER OF UNITY +Energizing Flame +KHYMΞXΛ +フェイクフェイス・フェイルセイフ +ふらふらふら、 +シックスプラン +フタタビ +SQUAD-Phvntom- +GEOMETRIC DANCE +Ring +インパアフェクシオン・ホワイトガアル +WE'RE BACK!! +熱異常 +1000年生きてる +IMAWANOKIWA +Play merrily NEO +OMAKENO Stroke +青春コンプレックス +アンダーキッズ +さくゆいたいそう +バグ +星界ちゃんと可不ちゃんのおつかい合騒曲 +Burn My Soul +with U +Love Kills U +DANGEROOOOUS JUNGLE +天使光輪 +story +トンデモワンダーズ +神威 +ozma +月面基地 +ずんだもんの朝食 〜目覚ましずんラップ〜 +あなたは世界の終わりにずんだを食べるのだ +HUMANBORG +ULTRA POWER +ジェヘナ +ツギハギスタッカート +HANIPAGANDA +Rush-Hour +QUEEN +天使の翼。 +さよならプリンセス +Apollo +raputa +系ぎて +アルカンシエル +RAD DOGS +アイディスマイル +にっこり^^調査隊のテーマ +PinqPiq (xovevox Remix) +エスオーエス +のじゃロリック +Edelweiss +QuiQ +IF:U +Cider P@rty +勦滅 +Lunatic Vibes +Bloody Trail +RE:INCARNATED DRAGNER +Beginning together! +Shining Ray ~僕らの絆~ +DEVOTION +Geranium +The Cursed Doll +RondeauX of RagnaroQ +ℝ∈Χ LUNATiCA +Λzure Vixen +MarbleBlue. +まっすぐ→→→ストリーム! +最っ高のエンタメだ!! +Baqeela +Ai C +INTERNET YAMERO +過去を喰らう +強風オールバック +寝起きヤシの木 +アイドル +ラビットホール +酔いどれ知らず +新人類 +Freak Out Hr. +さよならヒストリー +New York Back Raise +DUAL ROZES +VOLT +tiny tales continue +ouroboros -twin stroke of the end- +カーニバルハッピー +つづみぐさ +サイバーサンダーサイダー +おくすり飲んで寝よう +バラバラ〜仮初レインボーローズ〜 +QUATTUORUX +匿名M +ライアーダンサー +バカ通信 +LOSTPHANTASIA +Bad Apple!! feat.nomico (Tetsuya Komuro Remix) +Re:Unknown X +INFiNiTE ENERZY -Overdoze- +Make Up Your World feat. キョンシーのCiちゃん & らっぷびと +BOOM! BOOM!! BOOM!!! +Everybody Say HARDCORE TANO*C +ミラクルポップ☆アドベンチャー!!!!! +Trust +ラヴィ +スティールユー +オシオキGIMMICK!! +偉大なる悪魔は実は大天使パトラちゃん様なのだ! +シュガーホリック +MUSIC PЯAYER +壱雫空 +ザムザ +パラドクスイヴ +YKWTD +184億回のマルチトニック +果ての空、僕らが見た光。 +Cryptarithm +Ourania +天蓋 +Deicide +氷滅の135小節 +Divide et impera! +地獄 +シスターシスター +有明/Ariake +Flashback +Colorfull:Encounter +雨露霜雪 +宙天 +Elemental Ethnic +美夜月鏡 +Resolution +いちげき!のテーマ +Radiance +R'N'R Monsta +勇者 +病み垢ステロイド +ダーリンダンス +唱 +愛包ダンスホール +マツケンサンバⅡ +デビルじゃないもん +ずんだパーリナイ +Ultimate taste +Chronomia +人マニア +転生林檎 +メイトなやつら(FEAT. 天開司, 佐藤ホームズ, あっくん大魔王 & 歌衣メイカ) +モンスターカミング +ゆけむり魂温泉 II +人間が大好きなこわれた妖怪の唄 +シリウスの輝きのように +プラネタリウム・レヴュー +演劇 +無間嫉妬劇場『666』 +夢現妄想世界 +snooze +イガク +てらてら +Empire of Winter +迷える音色は恋の唄 +Löschen +Abstruse Dilemma +HYP3RTRIBE +On your mark (104期 Ver.) +はげしこの夜 -Psylent Crazy Night- +エイプリルスター +オーバーライド +右に曲ガール +ウルトラトレーラー +バベル +お願い!コンコンお稲荷さま +いっぱい食べる君が好きだよ +ピポピポ -People People- feat. ななひら +ワールドワイドワンダー +MORNINGLOOM +How To Make 音ゲ~曲! +リアライズ +Fraq +ウタヒメナイトストーム +Feel The Luv +Åntinomiε +ATLAS RUSH +概して過誤 +Unfinished Epic +忙シー日 +FLΛME/FRΦST +IMBRUED:FLUX +砂の函 +Amereistr +World's end BLACKBOX +Xaleid◆scopiX +Ref:rain (for 7th Heaven) +Supersonic Generation +Zitronectar +なんかノイズにきこえる +JIGOKU STATION CENTRAL GATE +Lover's Trick +サウンドプレイヤー +crazy (about you) +Drivessover +Tidal Wave +きゅびずむ +きゅびびびびずむ +ゆりかご +月光 +はいよろこんで +ビビデバ +シカ色デイズ +メズマライザー +ももいろの鍵 +のだ +Idoratrize World +Synthesis. +もういいよ +モエチャッカファイア +マヌルネコのうた +スナネコのうた +M@GICAL☆CURE! LOVE ♥ SHOT! +Overjoy ★ OVERDOSE!! +DATAERR0R +あいたい星人 +ハオ +好きな惣菜発表ドラゴン +Help me, ERINNNNNN!! +超最終鬼畜妹フランドール・S +Destined Marionette +MIRA +空回りライブラリ +お呪い +ありきたりな恋の歌 +MYTH Re:LEASE +∀ +㋰責任集合体 +三日月ステップ 2023 +少女レイ +少女A +帝国少女 +うぇいびー +RED +ホラフキパペット +BlackFlagBreaker!! +クーネル・エンゲイザー +ゼロトーキング +抜錨 +NO ONE YES MAN +Leave All Behind +Retribution ~ Cycle of Redemption ~ +Luminescence +Pixel Galaxy +Xaleid◆scopiX (2) +CiRCLE CAMPAiGN +Magical Paradox +殿ッ!?ご乱心!? +廃墟にいますキャンペーン +Customized Justice +False Amber (from the Black Bazaar, Or by A Kervan Trader from the Lands Afar, Or Buried Beneath the Shifting Sands That Lead Everywhere but Nowhere) +真空都市 +Eternal Return +ぱぱぱらビーチ +Get U ♭ack +ミクマリ +雲外蒼電 -Dreaming Voltage- +鬼女紅妖 +華天月兎 +イーヴィルガール +Dazzle hop +ラストピースに祝福と栄光を +Unwelcome School +彩りキャンバス +死ぬな! +どんぐりGAME +君だったから +Overdose +Bring it on +モニタリング +テトリス +有頂天ドリーマーズ +Eureka +ニルヴの心臓 +エンジェル ドリーム +Hurtling Boys +しゅ~しん?変身☆ハカイシンzzZ +命テステス +拝啓、最高の思い出たち +おべんきょうたいむ +るろうらんる +はじまりの未来 +Worlders +ヒアソビ +電光刹歌 +ウィーアーピコピコハンマーズ!!!! +ジェットブラック +トレジャーガーデン +最愛人生ランナー +初音ミクの暴走 +Let's ミクササイズ!! +スノーマン (Rerec) +JINGLE DEATH +ヤミナベ!!!! +サイエンス +[協]Love You +[蔵]In Chaos +[蛸]チルノのパーフェクトさんすう教室 +[星]しゅわスパ大作戦☆ +[蛸]時空を超えて久しぶり! +[宴]セガサターン起動音[H.][Remix] +[は]Garakuta Doll Play +[宴]CYCLES +[協]ぽっぴっぽー +[狂]タカハせ!名人マン +[宴]Oshama Scramble! +[蔵]Glorious Crown +[協]恋愛裁判 +[覚]東京リアルワールド +[協]Hand in Hand +[疑]DETARAME ROCK&ROLL THEORY +[協]疾走あんさんぶる +[幸]犬日和。(はっぴー歌唱Ver) +[宴]Oshama Scramble! (Cranky Remix) +[耐]Space Harrier Main Theme [Reborn] +[甘]スイートマジック +[J]Garakuta Doll Play +[回]ダブルラリアット +[習]ウミユリ海底譚 +[奏]洗脳 +[奏]チュルリラ・チュルリラ・ダッダッダ! +[協]ポップミュージックは僕のもの +[右]The wheel to the right +[逆]KING is BACK!! +[協]エイリアンエイリアン +[一]もうみんなしねばいいのに +[協]Seyana. ~何でも言うことを聞いてくれるアカネチャン~ +[疑]ロキ +[協]太陽系デスコ +[回]ハム太郎とっとこうた +[奏]マイオドレ!舞舞タイム +[藍]絶え間なく藍色 +[光]BREaK! BREaK! BREaK! +[星]ほしぞらスペクタクル +[奏]テレキャスタービーボーイ +[協]ラグトレイン +[奏]アカツキアライヴァル +[片]神々が恋した幻想郷 +[奏]アンビバレンス +[即]チューリングの跡 +[爆]Love's Theme of BADASS ~バッド・アス 愛のテーマ~ +[奏]ヒトガタ +[協]三妖精SAY YA!!! +[覚]Hainuwele +[覚]スカーレット警察のゲットーパトロール24時 +[回]回る空うさぎ +[蔵]Aegleseeker +[餡]隠密あんみつDX +[蛸]不機嫌なスリーカード +[両]はんぶんこ +[息]ノンブレス・オブリージュ +[逆]WE'RE BACK!! +[協]青春コンプレックス +[奏]さくゆいたいそう +[耐]星界ちゃんと可不ちゃんのおつかい合騒曲 +[奏]のじゃロリック +[音]ラビットホール +[匿]匿名M +[嘘]ライアーダンサー +[協]Ultimate taste +[X]人マニア +[音]snooze +[助]メズマライザー +[好]好きな惣菜発表ドラゴン +[騒]ありきたりな恋の歌 +[耐]死ぬな! +[充]エンジェル ドリーム +[r]Garakuta Doll Play +[譜]好きな惣菜発表ドラゴン +[玉]Garakuta Doll Play +[面]好きな惣菜発表ドラゴン +[某]Garakuta Doll Play +[発]好きな惣菜発表ドラゴン +[表]好きな惣菜発表ドラゴン +[龍]好きな惣菜発表ドラゴン diff --git a/package.json b/package.json new file mode 100644 index 0000000..8ec662c --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "@maigolabs/needle-root", + "version": "1.0.0", + "type": "module", + "scripts": { + "build:packages": "pnpm -F=\"./packages/*\" run build", + "build:demo": "pnpm -F=\"./apps/demo\" build", + "dev:demo": "pnpm -F=\"./apps/demo\" dev", + "typecheck": "pnpm -rF=\"./packages/*\" -F=\"./apps/*\" typecheck", + "test": "pnpm -rF=\"./packages/*\" -F=\"./apps/*\" test", + "test:dotnet": "cd dotnet && dotnet test", + "lint": "eslint --cache --ext .", + "lint:fix": "eslint --cache --ext . --fix" + }, + "license": "AGPL-3.0", + "packageManager": "pnpm@10.20.0", + "private": true, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@stylistic/eslint-plugin": "^5.5.0", + "@typescript-eslint/eslint-plugin": "^8.46.3", + "@typescript-eslint/parser": "^8.46.3", + "cross-env": "^10.1.0", + "eslint": "^9.39.1", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "jiti": "^2.6.1", + "tsdown": "^0.18.4", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "unplugin-unused": "^0.5.6" + }, + "dependencies": { + "@types/node": "^24.10.0" + } +} diff --git a/packages/needle/LICENSE b/packages/needle/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/packages/needle/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/packages/needle/README.md b/packages/needle/README.md new file mode 100644 index 0000000..054f0ca --- /dev/null +++ b/packages/needle/README.md @@ -0,0 +1,72 @@ +# `@maigolabs/needle` + +Fuzzy search engine for small text pieces, with Chinese/Japanese pronunciation support. + +See also [in-browser demo](https://needle.maigo.dev). + +## Install + +Dictionaries are installed as dependencies of the package, but if you don't use the indexer, they could be tree-shaken when bundling. + +```bash +pnpm install @maigolabs/needle +``` + +## Usage + +### Indexing + +NeedLe uses Kuromoji for Japanese tokenization, which loads dictionaries dynamically. You need to create a Kuromoji `TokenizerBuilder` first: + +```ts +// In Node.js you can just load the dictionary from the file system. + +import { TokenizerBuilder } from '@patdx/kuromoji'; +import NodeDictionaryLoader from '@patdx/kuromoji/node'; + +const kuromojiDictPath = path.resolve(url.fileURLToPath(import.meta.resolve('@patdx/kuromoji')), '..', '..', 'dict'); +const kuromoji = await new TokenizerBuilder({ loader: new NodeDictionaryLoader({ dic_path: kuromojiDictPath }) }).build(); + +// In browser you need to provide a custom loader to load the dictionary files with fetch(). + +import { TokenizerBuilder } from '@patdx/kuromoji'; + +// You can load dict files from CDN (See also the README of https://github.com/patdx/kuromoji.js) +const kuromoji = await new TokenizerBuilder({ + loader: { + loadArrayBuffer: async (url: string) => { + url = `https://cdn.jsdelivr.net/npm/@aiktb/kuromoji@1.0.2/dict/${url.replace('.gz', '')}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`Failed to fetch ${url}`); + return await res.arrayBuffer(); + }, + }, +}).build(); +``` + +After creating the Kuromoji instance, you can build the inverted index: + +```ts +import { buildInvertedIndex } from '@maigolabs/needle/indexer'; + +const documents = ['你好世界', 'こんにちは']; +const compressedIndex = buildInvertedIndex(documents, { kuromoji }); + +// The built index could be stored for later use. +const json = JSON.stringify(compressedIndex); +``` + +### Searching + +If you only import the searcher in your frontend code, indexer and dictionary-related dependencies will be tree-shaken. + +```ts +import { loadInvertedIndex, searchInvertedIndex } from '@maigolabs/needle/searcher'; + +const loadedIndex = loadInvertedIndex(compressedIndex); +const results = searchInvertedIndex(loadedIndex, 'sekai'); +for (const result of results) console.log(`${result.documentText} (${(result.matchRatio * 100).toFixed(0)}%)`); +// → 你好世界 (50%) +``` + +To highlight the search result, see also `highlightSearchResult`. diff --git a/packages/needle/jest.config.ts b/packages/needle/jest.config.ts new file mode 100644 index 0000000..b2171de --- /dev/null +++ b/packages/needle/jest.config.ts @@ -0,0 +1,18 @@ +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', + extensionsToTreatAsEsm: ['.ts'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + transform: { + '^.+\\.tsx?$': ['ts-jest', { useESM: true }], + }, + testMatch: ['**/*.test.ts'], + testTimeout: 30000, +}; + +export default config; + diff --git a/packages/needle/package.json b/packages/needle/package.json new file mode 100644 index 0000000..9980715 --- /dev/null +++ b/packages/needle/package.json @@ -0,0 +1,84 @@ +{ + "name": "@maigolabs/needle", + "version": "1.0.1", + "description": "Fuzzy search engine for small text pieces, with Chinese/Japanese pronunciation support.", + "type": "module", + "main": "./src/index.ts", + "scripts": { + "build": "tsdown", + "typecheck": "tsc", + "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", + "prepare": "pnpm run build" + }, + "license": "AGPL-3.0", + "homepage": "https://needle.maigo.dev", + "repository": { + "type": "git", + "url": "git+https://github.com/MaigoLabs/needLe.git", + "directory": "packages/needle" + }, + "bugs": "https://github.com/MaigoLabs/needLe/issues", + "keywords": [ + "needle", + "search", + "fuzzy", + "cjk", + "chinese", + "japanese", + "pinyin", + "romaji" + ], + "author": "Menci ", + "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./common": "./src/common/index.ts", + "./indexer": "./src/indexer/index.ts", + "./searcher": "./src/searcher/index.ts", + "./package.json": "./package.json" + }, + "packageManager": "pnpm@10.20.0", + "dependencies": { + "@patdx/kuromoji": "^1.0.4", + "hepburn": "^1.2.2", + "opencc-js": "^1.0.5", + "pinyin-pro": "^3.27.0" + }, + "devDependencies": { + "@types/hepburn": "^1.2.2", + "@types/jest": "^30.0.0", + "@types/opencc-js": "^1.0.3", + "jest": "^30.2.0", + "ts-jest": "^29.4.6" + }, + "files": [ + "README.md", + "dist", + "package.json" + ], + "publishConfig": { + "access": "public", + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "./common": { + "types": "./dist/common/index.d.mts", + "default": "./dist/common/index.mjs" + }, + "./indexer": { + "types": "./dist/indexer/index.d.mts", + "default": "./dist/indexer/index.mjs" + }, + "./searcher": { + "types": "./dist/searcher/index.d.mts", + "default": "./dist/searcher/index.mjs" + }, + "./package.json": "./package.json" + } + } +} diff --git a/packages/needle/src/common/index.ts b/packages/needle/src/common/index.ts new file mode 100644 index 0000000..00c2e82 --- /dev/null +++ b/packages/needle/src/common/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './utils'; +export * from './normalize'; +export * from './trie'; diff --git a/packages/needle/src/common/normalize.test.ts b/packages/needle/src/common/normalize.test.ts new file mode 100644 index 0000000..1310e75 --- /dev/null +++ b/packages/needle/src/common/normalize.test.ts @@ -0,0 +1,60 @@ +import { normalizeByCodePoint, toKatakana } from './normalize'; + +describe('toKatakana', () => { + it('should convert hiragana to katakana', () => { + expect(toKatakana('あいうえお')).toBe('アイウエオ'); + expect(toKatakana('かきくけこ')).toBe('カキクケコ'); + expect(toKatakana('さしすせそ')).toBe('サシスセソ'); + }); + + it('should keep katakana unchanged', () => { + expect(toKatakana('アイウエオ')).toBe('アイウエオ'); + }); + + it('should keep non-kana characters unchanged', () => { + expect(toKatakana('abc123')).toBe('abc123'); + expect(toKatakana('漢字')).toBe('漢字'); + }); + + it('should handle mixed input', () => { + expect(toKatakana('あアa漢')).toBe('アアa漢'); + }); +}); + +describe('normalizeByCodePoint', () => { + it('should convert fullwidth ASCII to halfwidth lowercase', () => { + expect(normalizeByCodePoint('ABC')).toBe('abc'); + expect(normalizeByCodePoint('123')).toBe('123'); + expect(normalizeByCodePoint('!@#')).toBe('!@#'); + }); + + it('should convert fullwidth space to halfwidth space', () => { + expect(normalizeByCodePoint(' ')).toBe(' '); + }); + + it('should convert halfwidth kana to fullwidth kana', () => { + expect(normalizeByCodePoint('アイウエオ')).toBe('アイウエオ'); + expect(normalizeByCodePoint('カキクケコ')).toBe('カキクケコ'); + }); + + it('should normalize voiced/semi-voiced sound marks', () => { + expect(normalizeByCodePoint('゙')).toBe('\u3099'); // halfwidth voiced -> combining + expect(normalizeByCodePoint('゚')).toBe('\u309A'); // halfwidth semi-voiced -> combining + expect(normalizeByCodePoint('゛')).toBe('\u3099'); // fullwidth voiced -> combining + expect(normalizeByCodePoint('゜')).toBe('\u309A'); // fullwidth semi-voiced -> combining + }); + + it('should convert halfwidth punctuation to fullwidth', () => { + expect(normalizeByCodePoint('。')).toBe('。'); + expect(normalizeByCodePoint('「')).toBe('「'); + expect(normalizeByCodePoint('」')).toBe('」'); + expect(normalizeByCodePoint('、')).toBe('、'); + expect(normalizeByCodePoint('・')).toBe('・'); + }); + + it('should lowercase regular ASCII', () => { + expect(normalizeByCodePoint('ABC')).toBe('abc'); + }); + + // Should keep hiragana unchanged +}); diff --git a/packages/needle/src/common/normalize.ts b/packages/needle/src/common/normalize.ts new file mode 100644 index 0000000..4e4674a --- /dev/null +++ b/packages/needle/src/common/normalize.ts @@ -0,0 +1,42 @@ +export const normalizeByCodePoint = (string: string) => [...string].map(normalizeCodePoint).join(''); + +export const normalizeCodePoint = (char: string) => { + const codePoint = char.codePointAt(0)!; + // Fullwidth ASCII -> Halfwidth ASCII + if (codePoint >= 0xFF01 && codePoint <= 0xFF5E) return String.fromCodePoint(codePoint - 0xFEE0).toLowerCase(); + // Fullwidth space -> Halfwidth space + else if (codePoint === /* ' ' */ 0x3000) return ' '; + // Halfwidth kana (U+FF66 - U+FF9D) -> Fullwidth kana + else if (codePoint >= 0xFF66 && codePoint <= 0xFF9D) return HALF_TO_FULL_KANA[char] ?? char; + else if (codePoint === /* '。' */ 0xFF61) return '。'; + else if (codePoint === /* '「' */ 0xFF62) return '「'; + else if (codePoint === /* '」' */ 0xFF63) return '」'; + else if (codePoint === /* '、' */ 0xFF64) return '、'; + else if (codePoint === /* '・' */ 0xFF65) return '・'; + else if (codePoint === /* '゙' */ 0xFF9E || codePoint === /* '゛' */ 0x309B) return '\u3099'; // -> COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK + else if (codePoint === /* '゚' */ 0xFF9F || codePoint === /* '゜' */ 0x309C) return '\u309A'; // -> COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK + else return char.toLowerCase(); +}; + +const HALF_TO_FULL_KANA: Record = { + 'ヲ': 'ヲ', 'ァ': 'ァ', 'ィ': 'ィ', 'ゥ': 'ゥ', 'ェ': 'ェ', 'ォ': 'ォ', + 'ャ': 'ャ', 'ュ': 'ュ', 'ョ': 'ョ', 'ッ': 'ッ', + 'ー': 'ー', + 'ア': 'ア', 'イ': 'イ', 'ウ': 'ウ', 'エ': 'エ', 'オ': 'オ', + 'カ': 'カ', 'キ': 'キ', 'ク': 'ク', 'ケ': 'ケ', 'コ': 'コ', + 'サ': 'サ', 'シ': 'シ', 'ス': 'ス', 'セ': 'セ', 'ソ': 'ソ', + 'タ': 'タ', 'チ': 'チ', 'ツ': 'ツ', 'テ': 'テ', 'ト': 'ト', + 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': 'ノ', + 'ハ': 'ハ', 'ヒ': 'ヒ', 'フ': 'フ', 'ヘ': 'ヘ', 'ホ': 'ホ', + 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', + 'ヤ': 'ヤ', 'ユ': 'ユ', 'ヨ': 'ヨ', + 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', + 'ワ': 'ワ', 'ン': 'ン', +}; + +const isHiraganaRange = (charCode: number) => (charCode >= 0x3041 && charCode <= 0x3096) || (charCode >= 0x309D && charCode <= 0x309E); +export const toKatakanaSingle = (char: string) => { + const code = char.charCodeAt(0); + return isHiraganaRange(code) ? String.fromCharCode(code + 0x60) : char; +}; +export const toKatakana = (string: string) => [...string].map(toKatakanaSingle).join(''); diff --git a/packages/needle/src/common/trie.ts b/packages/needle/src/common/trie.ts new file mode 100644 index 0000000..663485d --- /dev/null +++ b/packages/needle/src/common/trie.ts @@ -0,0 +1,17 @@ +export interface TrieNode { + parent: TrieNode | undefined; + children: Map; // Unicode code point -> child node + tokenIds: number[]; + subTreeTokenIds: number[]; // Empty on root. Will Uint16Array be faster? +} + +export const traverseTrieStep = (node: TrieNode | undefined, codePoint: string, ignorableCodePoints?: RegExp) => + node?.children.get(codePoint.codePointAt(0)!) ?? (ignorableCodePoints?.test(codePoint) ? node : undefined); +export const traverseTrie = (node: TrieNode | undefined, text: string, ignorableCodePoints?: RegExp) => { + if (!node) return; + for (const codePoint of text) { + node = traverseTrieStep(node, codePoint, ignorableCodePoints); + if (!node) return; + } + return node; +}; diff --git a/packages/needle/src/common/types.ts b/packages/needle/src/common/types.ts new file mode 100644 index 0000000..f88482f --- /dev/null +++ b/packages/needle/src/common/types.ts @@ -0,0 +1,31 @@ +export enum TokenType { + Raw, + Kana, + Romaji, + Han, + Pinyin, +} + +export interface TokenDefinition { + id: number; + type: TokenType; + text: string; + codePointLength: number; +} + +// [start, end) +export interface OffsetSpan { + start: number; + end: number; +} + +export type CompressedInvertedIndex = { + documents: string[]; + tokenTypes: TokenType[]; + tokenReferences: number[][][]; // tokenId -> [documentId, start1, end1, start2, end2, ...][] + tries: { + romaji: number[]; + kana: number[]; + other: number[]; + }; +}; diff --git a/packages/needle/src/common/utils.ts b/packages/needle/src/common/utils.ts new file mode 100644 index 0000000..5b7befb --- /dev/null +++ b/packages/needle/src/common/utils.ts @@ -0,0 +1,3 @@ +import type { OffsetSpan } from './types'; + +export const getSpanLength = (offset: OffsetSpan) => offset.end - offset.start; diff --git a/packages/needle/src/e2e/search.test.ts b/packages/needle/src/e2e/search.test.ts new file mode 100644 index 0000000..26787c3 --- /dev/null +++ b/packages/needle/src/e2e/search.test.ts @@ -0,0 +1,73 @@ +import path from 'node:path'; +import url from 'node:url'; + +import { TokenizerBuilder } from '@patdx/kuromoji'; +import NodeDictionaryLoader from '@patdx/kuromoji/node'; + +import { buildInvertedIndex, type KuromojiTokenizer } from '../indexer'; +import { highlightSearchResult, loadInvertedIndex, searchInvertedIndex } from '../searcher'; + +let kuromoji: KuromojiTokenizer; + +beforeAll(async () => { + const kuromojiDictPath = path.resolve(url.fileURLToPath(import.meta.resolve('@patdx/kuromoji')), '..', '..', 'dict'); + kuromoji = await new TokenizerBuilder({ loader: new NodeDictionaryLoader({ dic_path: kuromojiDictPath }) }).build(); +}); + +describe('search', () => { + const testDocuments = [ + 'ミーティア', + 'エンドマークに希望と涙を添えて', + '宵の鳥', + '僕の和風本当上手', + ]; + + it('should match with mixed search query', () => { + const compressed = buildInvertedIndex(testDocuments, { kuromoji }); + const invertedIndex = loadInvertedIndex(compressed); + + const results = searchInvertedIndex(invertedIndex, 'bokunoh风じょう'); + + // Should have at least one result + expect(results.length).toBeGreaterThan(0); + + // The first result should be "僕の和風本当上手" + expect(results[0]!.documentText).toBe('僕の和風本当上手'); + }); + + it('should highlight search result correctly', () => { + const compressed = buildInvertedIndex(testDocuments, { kuromoji }); + const invertedIndex = loadInvertedIndex(compressed); + + const results = searchInvertedIndex(invertedIndex, 'bokunoh风じょう'); + expect(results.length).toBeGreaterThan(0); + + const highlighted = highlightSearchResult(results[0]!); + + // Should be an array of parts + expect(Array.isArray(highlighted)).toBe(true); + expect(highlighted.length).toBeGreaterThan(0); + + // Collect highlighted text + const highlightedTexts = highlighted + .filter((part): part is { highlight: string } => typeof part !== 'string') + .map(part => part.highlight); + + expect(highlightedTexts.some(text => text.includes('僕'))).toBe(true); + expect(highlightedTexts.some(text => text.includes('の'))).toBe(true); + expect(highlightedTexts.some(text => text.includes('和'))).toBe(true); + expect(highlightedTexts.some(text => text.includes('風'))).toBe(true); + expect(highlightedTexts.some(text => text.includes('上'))).toBe(true); + }); + + it('should match romaji input to kana documents', () => { + const compressed = buildInvertedIndex(testDocuments, { kuromoji }); + const invertedIndex = loadInvertedIndex(compressed); + + // Search for "yoi" should match "宵の鳥" + const results = searchInvertedIndex(invertedIndex, 'yoi'); + const matchedTexts = results.map(r => r.documentText); + + expect(matchedTexts).toContain('宵の鳥'); + }); +}); diff --git a/packages/needle/src/e2e/trie.test.ts b/packages/needle/src/e2e/trie.test.ts new file mode 100644 index 0000000..78eb909 --- /dev/null +++ b/packages/needle/src/e2e/trie.test.ts @@ -0,0 +1,111 @@ +import { traverseTrie } from '../common'; +import { buildTrie, serializeTrie } from '../indexer/trie'; +import { deserializeTrie } from '../searcher/trie'; + +describe('Trie building', () => { + it('should build a Trie with multiple different tokens', () => { + const trie = buildTrie([ + [0, 'hello'], + [1, 'help'], + [2, 'world'], + [3, 'word'], + ]); + + // Traverse to verify structure + const helloNode = traverseTrie(trie, 'hello'); + const helpNode = traverseTrie(trie, 'help'); + const worldNode = traverseTrie(trie, 'world'); + const wordNode = traverseTrie(trie, 'word'); + + expect(helloNode).toBeDefined(); + expect(helpNode).toBeDefined(); + expect(worldNode).toBeDefined(); + expect(wordNode).toBeDefined(); + + // Check token IDs + expect(helloNode!.tokenIds).toContain(0); + expect(helpNode!.tokenIds).toContain(1); + expect(worldNode!.tokenIds).toContain(2); + expect(wordNode!.tokenIds).toContain(3); + + // Check that 'hel' prefix node has both tokens in subTree + const helNode = traverseTrie(trie, 'hel'); + expect(helNode).toBeDefined(); + expect(helNode!.subTreeTokenIds).toContain(0); + expect(helNode!.subTreeTokenIds).toContain(1); + }); + + it('should handle Japanese text tokens', () => { + const trie = buildTrie([ + [0, 'さくら'], + [1, 'サクラ'], + [2, '桜'], + ]); + + expect(traverseTrie(trie, 'さくら')?.tokenIds).toContain(0); + expect(traverseTrie(trie, 'サクラ')?.tokenIds).toContain(1); + expect(traverseTrie(trie, '桜')?.tokenIds).toContain(2); + }); +}); + +describe('Trie serialization and deserialization', () => { + it('should serialize and deserialize a Trie correctly', () => { + const originalTrie = buildTrie([ + [0, 'apple'], + [1, 'app'], + [2, 'banana'], + ]); + + // Serialize + const serialized = serializeTrie(originalTrie); + expect(Array.isArray(serialized)).toBe(true); + expect(serialized.length).toBeGreaterThan(0); + + // Deserialize + const { root: deserializedTrie, tokenCodePoints } = deserializeTrie(serialized); + + // Verify structure is preserved + const appleNode = traverseTrie(deserializedTrie, 'apple'); + const appNode = traverseTrie(deserializedTrie, 'app'); + const bananaNode = traverseTrie(deserializedTrie, 'banana'); + + expect(appleNode).toBeDefined(); + expect(appNode).toBeDefined(); + expect(bananaNode).toBeDefined(); + + expect(appleNode!.tokenIds).toContain(0); + expect(appNode!.tokenIds).toContain(1); + expect(bananaNode!.tokenIds).toContain(2); + + // Verify tokenCodePoints map + expect(tokenCodePoints.get(0)?.join('')).toBe('apple'); + expect(tokenCodePoints.get(1)?.join('')).toBe('app'); + expect(tokenCodePoints.get(2)?.join('')).toBe('banana'); + + // Verify subTreeTokenIds are reconstructed + expect(appNode!.subTreeTokenIds).toContain(0); + expect(appNode!.subTreeTokenIds).toContain(1); + }); + + it('should preserve parent references after deserialization', () => { + const originalTrie = buildTrie([ + [0, 'test'], + ]); + + const serialized = serializeTrie(originalTrie); + const { root } = deserializeTrie(serialized); + + const testNode = traverseTrie(root, 'test'); + expect(testNode).toBeDefined(); + + // Walk back to root via parent references + let node = testNode; + let depth = 0; + while (node?.parent) { + node = node.parent; + depth++; + } + expect(depth).toBe(4); // 't' -> 'e' -> 's' -> 't' -> root + expect(node).toBe(root); + }); +}); diff --git a/packages/needle/src/index.ts b/packages/needle/src/index.ts new file mode 100644 index 0000000..2440853 --- /dev/null +++ b/packages/needle/src/index.ts @@ -0,0 +1,3 @@ +export * from './common'; +export * from './indexer'; +export * from './searcher'; diff --git a/packages/needle/src/indexer/han.test.ts b/packages/needle/src/indexer/han.test.ts new file mode 100644 index 0000000..083aea1 --- /dev/null +++ b/packages/needle/src/indexer/han.test.ts @@ -0,0 +1,103 @@ +import { getHanVariants, getPinyinCandidates, isHanCharacter, unionFindSet } from './han'; + +describe('unionFindSet', () => { + it('should find self as root initially', () => { + const ufs = unionFindSet(); + expect(ufs.find(1)).toBe(1); + expect(ufs.find(2)).toBe(2); + }); + + it('should union two elements', () => { + const ufs = unionFindSet(); + ufs.union(1, 2); + expect(ufs.find(1)).toBe(ufs.find(2)); + }); + + it('should union multiple elements transitively', () => { + const ufs = unionFindSet(); + ufs.union(1, 2); + ufs.union(2, 3); + ufs.union(4, 5); + expect(ufs.find(1)).toBe(ufs.find(3)); + expect(ufs.find(1)).not.toBe(ufs.find(4)); + ufs.union(3, 4); + expect(ufs.find(1)).toBe(ufs.find(5)); + }); + + it('should iterate all keys', () => { + const ufs = unionFindSet(); + ufs.union('a', 'b'); + ufs.union('c', 'd'); + const keys = [...ufs.keys()]; + expect(keys).toContain('a'); + expect(keys).toContain('b'); + expect(keys).toContain('c'); + expect(keys).toContain('d'); + }); +}); + +describe('isHanCharacter', () => { + it('should return true for CJK characters', () => { + expect(isHanCharacter('中')).toBe(true); + expect(isHanCharacter('国')).toBe(true); + expect(isHanCharacter('日')).toBe(true); + expect(isHanCharacter('本')).toBe(true); + }); + + it('should return false for non-CJK characters', () => { + expect(isHanCharacter('a')).toBe(false); + expect(isHanCharacter('あ')).toBe(false); + expect(isHanCharacter('ア')).toBe(false); + expect(isHanCharacter('1')).toBe(false); + }); +}); + +describe('getHanVariants', () => { + it('should return variants for simplified/traditional characters', () => { + // 国 (simplified) and 國 (traditional) should be variants of each other + const variants1 = getHanVariants('国'); + const variants2 = getHanVariants('國'); + expect(variants1).toContain('国'); + expect(variants1).toContain('國'); + expect(variants2).toContain('国'); + expect(variants2).toContain('國'); + }); + + it('should return the character itself for characters without variants', () => { + const variants = getHanVariants('一'); + expect(variants).toContain('一'); + }); + + it('should return empty array for non-Han characters', () => { + expect(getHanVariants('a')).toEqual([]); + expect(getHanVariants('あ')).toEqual([]); + }); +}); + +describe('getPinyinCandidates', () => { + it('should return pinyin for a Han character', () => { + const candidates = getPinyinCandidates('中'); + expect(candidates).toContain('zhong'); + expect(candidates).toContain('zh'); // initial + expect(candidates).toContain('z'); // first letter + }); + + it('should return multiple pinyin for polyphonic characters', () => { + // 行 can be "xing" or "hang" + const candidates = getPinyinCandidates('行'); + expect(candidates).toContain('xing'); + expect(candidates).toContain('hang'); + }); + + it('should include fuzzy pinyin variants', () => { + // 风 is "feng", should also have fuzzy variant "fen" + const candidates = getPinyinCandidates('风'); + expect(candidates).toContain('feng'); + expect(candidates).toContain('fen'); // fuzzy: eng -> en + }); + + it('should return empty array for non-Han characters', () => { + expect(getPinyinCandidates('a')).toEqual([]); + expect(getPinyinCandidates('あ')).toEqual([]); + }); +}); diff --git a/packages/needle/src/indexer/han.ts b/packages/needle/src/indexer/han.ts new file mode 100644 index 0000000..5af6154 --- /dev/null +++ b/packages/needle/src/indexer/han.ts @@ -0,0 +1,85 @@ +// @ts-expect-error No declaration file +import hkVariants from 'opencc-js/dict/HKVariants'; +// @ts-expect-error No declaration file +import hkVariantsRev from 'opencc-js/dict/HKVariantsRev'; +// @ts-expect-error No declaration file +import jpVariants from 'opencc-js/dict/JPVariants'; +// @ts-expect-error No declaration file +import jpVariantsRev from 'opencc-js/dict/JPVariantsRev'; +// @ts-expect-error No declaration file +import stCharacters from 'opencc-js/dict/STCharacters'; +// @ts-expect-error No declaration file +import tsCharacters from 'opencc-js/dict/TSCharacters'; +// @ts-expect-error No declaration file +import twVariants from 'opencc-js/dict/TWVariants'; +// @ts-expect-error No declaration file +import twVariantsRev from 'opencc-js/dict/TWVariantsRev'; +import { polyphonic } from 'pinyin-pro'; + +export const unionFindSet = () => { + const parent = new Map(); + const rank = new Map(); + const find = (x: T): T => { + const p = parent.get(x); + if (p == null) { + parent.set(x, x); + return x; + } else if (p === x) return x; + else { + const root = find(p); + parent.set(x, root); + return root; + } + }; + const union = (x: T, y: T) => { + x = find(x); + y = find(y); + if (x === y) return; + const rankX = rank.get(x) ?? 0, rankY = rank.get(y) ?? 0; + if (rankX < rankY) parent.set(x, y); + else if (rankX > rankY) parent.set(y, x); + else { + parent.set(y, x); + rank.set(x, rankX + 1); + } + }; + const keys = () => parent.keys(); + return { find, union, keys }; +}; + +const exchangeMap = (() => { + const ufs = unionFindSet(); + for (const dict of [hkVariants, hkVariantsRev, jpVariants, jpVariantsRev, stCharacters, tsCharacters, twVariants, twVariantsRev] as string[]) { + for (const [from, to] of dict.split('|').map(pair => pair.split(' '))) { + if (!from || !to || [...from].length !== 1 || [...to].length !== 1) continue; + ufs.union(from, to); + } + } + const map = new Map(); + for (const key of ufs.keys()) { + const root = ufs.find(key); + let list = map.get(root); + if (!list) map.set(root, list = []); + if (key !== root) map.set(key, list); + list.push(key); + } + for (const list of map.values()) list.sort(); + return map; +})(); + +export const isHanCharacter = (phrase: string) => /^[\p{Script=Han}]+$/u.test(phrase); + +export const getHanVariants = (character: string) => exchangeMap.get(character) ?? (isHanCharacter(character) ? [character] : []); + +const PINYIN_INITIALS: string[] = ['b', 'p', 'm', 'f', 'd', 't', 'n', 'l', 'g', 'k', 'h', 'j', 'q', 'x', 'zh', 'ch', 'sh', 'r', 'z', 'c', 's', 'y', 'w']; +const PINYIN_FINALS_FUZZY_MAP: Record = { 'ang': 'an', 'eng': 'en', 'ing': 'in' }; +export const getPinyinCandidates = (character: string) => { + const pinyins = polyphonic(character, { type: 'array', toneType: 'none', removeNonZh: true })[0] ?? []; + return Array.from(new Set(pinyins.filter(fullPinyin => fullPinyin).flatMap(fullPinyin => { + const initial = PINYIN_INITIALS.find(initial => fullPinyin.startsWith(initial)); + const initialAlphabet = initial?.[0] ?? fullPinyin[0]!; + const fuzzySuffix = fullPinyin.slice(-3); + const fuzzyPinyin = fuzzySuffix in PINYIN_FINALS_FUZZY_MAP ? fullPinyin.slice(0, -3) + PINYIN_FINALS_FUZZY_MAP[fuzzySuffix] : undefined; + return [fullPinyin, initial, initialAlphabet, fuzzyPinyin].filter((s): s is string => !!s); + }))); +}; diff --git a/packages/needle/src/indexer/index.ts b/packages/needle/src/indexer/index.ts new file mode 100644 index 0000000..b5af190 --- /dev/null +++ b/packages/needle/src/indexer/index.ts @@ -0,0 +1,5 @@ +export * from './han'; +export * from './japanese'; +export * from './tokenizer'; +export * from './trie'; +export * from './inverted-index'; diff --git a/packages/needle/src/indexer/inverted-index.ts b/packages/needle/src/indexer/inverted-index.ts new file mode 100644 index 0000000..83ac851 --- /dev/null +++ b/packages/needle/src/indexer/inverted-index.ts @@ -0,0 +1,46 @@ +import { NORMALIZE_RULES_KANA_DAKUTEN, NORMALIZE_RULES_ROMAJI } from './japanese'; +import { createTokenizer, type TokenizerOptions } from './tokenizer'; +import { buildTrie, graftTriePaths, serializeTrie } from './trie'; +import type { CompressedInvertedIndex, TokenDefinition } from '../common/types'; +import { TokenType } from '../common/types'; + +const buildTypedTrie = (tokens: TokenDefinition[], typePredicate: (tokenType: TokenType) => boolean) => + buildTrie(tokens.filter(token => typePredicate(token.type)).map(token => [token.id, token.text])); + +export const buildInvertedIndex = (documents: string[], tokenizerOptions: TokenizerOptions) => { + const tokenizer = createTokenizer(tokenizerOptions); + const documentTokens = documents.map(document => tokenizer.tokenize(document)); + + const tokenDefinitions = [...tokenizer.tokens.values()]; + const romajiRoot = buildTypedTrie(tokenDefinitions, type => type === TokenType.Romaji); + const kanaRoot = buildTypedTrie(tokenDefinitions, type => type === TokenType.Kana); + const otherRoot = buildTypedTrie(tokenDefinitions, type => type !== TokenType.Romaji && type !== TokenType.Kana); + graftTriePaths(romajiRoot, NORMALIZE_RULES_ROMAJI); + graftTriePaths(kanaRoot, NORMALIZE_RULES_KANA_DAKUTEN); + + const invertedIndex: CompressedInvertedIndex = { + documents, + tokenTypes: tokenDefinitions.map(token => token.type), + tokenReferences: Array.from({ length: tokenDefinitions.length }, () => []), + tries: { + romaji: serializeTrie(romajiRoot), + kana: serializeTrie(kanaRoot), + other: serializeTrie(otherRoot), + }, + }; + for (const [documentId, tokens] of documentTokens.entries()) { + const tokenOccurrences = new Map(); + for (const token of tokens) { + let occurrences = tokenOccurrences.get(token.id); + if (!occurrences) { + occurrences = []; + tokenOccurrences.set(token.id, occurrences); + } + occurrences.push(token.start, token.end); + } + for (const [tokenId, occurrences] of tokenOccurrences) { + invertedIndex.tokenReferences[tokenId]!.push([documentId, ...occurrences]); + } + } + return invertedIndex; +}; diff --git a/packages/needle/src/indexer/japanese.test.ts b/packages/needle/src/indexer/japanese.test.ts new file mode 100644 index 0000000..8f72089 --- /dev/null +++ b/packages/needle/src/indexer/japanese.test.ts @@ -0,0 +1,66 @@ +import path from 'node:path'; +import url from 'node:url'; + +import { TokenizerBuilder } from '@patdx/kuromoji'; +import NodeDictionaryLoader from '@patdx/kuromoji/node'; + +import { getAllKanaReadings, toRomajiStrictly } from './japanese'; +import type { KuromojiTokenizer } from './tokenizer'; + +let kuromoji: KuromojiTokenizer; + +beforeAll(async () => { + const kuromojiDictPath = path.resolve(url.fileURLToPath(import.meta.resolve('@patdx/kuromoji')), '..', '..', 'dict'); + kuromoji = await new TokenizerBuilder({ loader: new NodeDictionaryLoader({ dic_path: kuromojiDictPath }) }).build(); +}); + +describe('toRomajiStrictly', () => { + it('should convert basic kana to romaji', () => { + expect(toRomajiStrictly('あ')).toBe('a'); + expect(toRomajiStrictly('か')).toBe('ka'); + expect(toRomajiStrictly('さくら')).toBe('sakura'); + }); + + it('should convert katakana to romaji', () => { + expect(toRomajiStrictly('ア')).toBe('a'); + expect(toRomajiStrictly('カ')).toBe('ka'); + expect(toRomajiStrictly('サクラ')).toBe('sakura'); + }); + + it('should handle long vowels', () => { + expect(toRomajiStrictly('おう')).toBe('ou'); + expect(toRomajiStrictly('おお')).toBe('oo'); + }); + + it('should return empty string for invalid first character', () => { + expect(toRomajiStrictly('ー')).toBe(''); // prolonged sound mark cannot be first + expect(toRomajiStrictly('ゃ')).toBe(''); // small ya cannot be first + }); + + it('should return empty string for invalid last character', () => { + expect(toRomajiStrictly('っ')).toBe(''); // small tsu cannot be last + }); + + it('should handle gemination (small tsu)', () => { + expect(toRomajiStrictly('かった')).toBe('katta'); + }); +}); + +describe('getAllKanaReadings', () => { + it('should return katakana reading for pure kana input', () => { + const readings = getAllKanaReadings(kuromoji, 'あ'); + expect(readings).toContain('ア'); + }); + + it('should return readings for kanji', () => { + const readings = getAllKanaReadings(kuromoji, '僕'); + expect(readings.length).toBeGreaterThan(0); + // 僕 should have reading ボク + expect(readings).toContain('ボク'); + }); + + it('should return readings for compound words', () => { + const readings = getAllKanaReadings(kuromoji, '和風'); + expect(readings.length).toBeGreaterThan(0); + }); +}); diff --git a/packages/needle/src/indexer/japanese.ts b/packages/needle/src/indexer/japanese.ts new file mode 100644 index 0000000..d908776 --- /dev/null +++ b/packages/needle/src/indexer/japanese.ts @@ -0,0 +1,158 @@ +import { fromKana } from 'hepburn'; + +import type { KuromojiTokenizer } from './tokenizer'; +import { toKatakana } from '../common'; + +// We have normalized all other sound marks to \u3099 and \u309A (combining kata-hiragana voiced/semi-voiced sound marks) +export const isMaybeJapanese = (phrase: string) => /^[\p{Script=Han}\u3041-\u309F\u30A0-\u30FF\u3005\u3006\u30FC\u3099\u309A]+$/u.test(phrase); + +// See also normalize.ts +export const isJapaneseSoundMark = (phrase: string) => /^[\u3099\u309A]+$/.test(phrase); +export const stripJapaneseSoundMarks = (phrase: string) => phrase.replaceAll('\u3099', '').replaceAll('\u309A', ''); + +export const isKanaSingle = (char: string) => { + const code = char.charCodeAt(0); + return (code >= 0x3041 && code <= 0x309F) || (code >= 0x30A0 && code <= 0x30FF); +}; +export const isKana = (phrase: string) => [...phrase].every(isKanaSingle); + +const KANAS_CANNOT_BE_FIRST = [ + 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', + 'ぁ', 'ぃ', 'ぅ', 'ぇ', 'ぉ', + 'ャ', 'ュ', 'ョ', + 'ゃ', 'ゅ', 'ょ', + 'ヮ', 'ゎ', + 'ㇰ', 'ㇱ', 'ㇲ', 'ㇳ', 'ㇴ', 'ㇵ', 'ㇶ', 'ㇷ', 'ㇸ', 'ㇹ', 'ㇺ', 'ㇻ', 'ㇼ', 'ㇽ', 'ㇾ', 'ㇿ', + 'ー', +]; +const KANAS_CANNOT_BE_LAST = [ + 'ッ', 'っ', +]; +export const toRomajiStrictly = (kana: string) => { + if (KANAS_CANNOT_BE_FIRST.includes(kana[0]!)) return ''; + if (KANAS_CANNOT_BE_LAST.includes(kana[kana.length - 1]!)) return ''; + const romaji = fromKana(kana).toLowerCase() + .replaceAll('ā', 'aa') + .replaceAll('ī', 'ii') + .replaceAll('ū', 'uu') + .replaceAll('ē', 'ee') + .replaceAll('ō', 'ou'); + if (!romaji.match(/^[a-z]+$/)) return ''; + return romaji; +}; + +export const createTranscriptionEnumerator = ( + isValidPhrase: (codePoints: string[], start: number, length: number) => boolean, + getAllTranscriptions: (phrase: string) => string[], +) => (codePoints: string[]) => { + const toKey = (start: number, length: number) => `${start}:${length}`; + const resultMap = new Map(); + for (let phraseLength = 1; phraseLength <= codePoints.length; phraseLength++) for (let start = 0; start + phraseLength <= codePoints.length; start++) { + if (!isValidPhrase(codePoints, start, phraseLength)) continue; + const phrase = codePoints.slice(start, start + phraseLength).join(''); + const atomicTranscriptions = [...new Set(getAllTranscriptions(phrase))].filter(candidateTranscription => { + if (!candidateTranscription) return false; + // Ensure the transcription is atomic (not a combination of multiple shorter transcriptions, separated by any midpoints) + type State = { phrasePosition: number; transcriptionPosition: number }; + const toStateKey = (state: State) => `${state.phrasePosition}:${state.transcriptionPosition}`; + const visitedStates = new Set(); + const queue: State[] = [{ phrasePosition: 0, transcriptionPosition: 0 }]; + while (queue.length > 0) { + const { phrasePosition, transcriptionPosition } = queue.shift()!; + for (let prefixLength = 1; prefixLength <= phraseLength - phrasePosition; prefixLength++) { + const prefixResult = resultMap.get(toKey(start + phrasePosition, prefixLength)); + if (!prefixResult) continue; + for (const transcription of prefixResult.transcriptions) { + if (candidateTranscription.slice(transcriptionPosition, transcriptionPosition + transcription.length) === transcription) { + const nextState: State = { phrasePosition: phrasePosition + prefixLength, transcriptionPosition: transcriptionPosition + transcription.length }; + if (nextState.phrasePosition === phraseLength && nextState.transcriptionPosition === candidateTranscription.length) return false; // Found a valid combination + if (visitedStates.has(toStateKey(nextState))) continue; + visitedStates.add(toStateKey(nextState)); + queue.push(nextState); + } + } + } + } + return true; + }); + if (atomicTranscriptions.length > 0) resultMap.set(toKey(start, phraseLength), { start, length: phraseLength, transcriptions: atomicTranscriptions }); + } + return [...resultMap.values()]; +}; + +export const getAllKanaReadings = (kuromoji: KuromojiTokenizer, phrase: string) => Array.from(new Set( + [ + ...isKana(phrase) ? [toKatakana(phrase)] : [], + ...isKana(phrase) && [...phrase].length === 1 ? [] : ((kuromoji.token_info_dictionary.target_map[kuromoji.viterbi_builder.trie.lookup(phrase)] ?? []) + .map(id => kuromoji.formatter.formatEntry( + id, 0, 'KNOWN', + kuromoji.token_info_dictionary.getFeatures(id as unknown as string)?.split(',') ?? [], + ).reading) + .filter((reading): reading is string => !!reading)) + .map(toKatakana), + ], +)); + +const createNormalizer = (rules: Record) => (text: string) => { + while (true) { + const beforeCurrentIteration = text; + for (const [from, to] of Object.entries(rules)) text = text.replaceAll(from, to); + if (text === beforeCurrentIteration) break; + } + return text; +}; + +export const NORMALIZE_RULES_ROMAJI: Record = { + // Remove all long vowels (sa-ba- -> saba) + '-': '', + // Collapse consecutive vowels + 'aa': 'a', + 'ii': 'i', + 'uu': 'u', + 'ee': 'e', + 'oo': 'o', + 'ou': 'o', + // mb/mp/mm -> nb/np/nm (shimbun -> shinbun) + 'mb': 'nb', + 'mp': 'np', + 'mm': 'nm', + // Others + 'sha': 'sya', + 'tsu': 'tu', + 'chi': 'ti', + 'shi': 'si', + 'ji': 'zi', +}; +export const normalizeRomaji = createNormalizer(NORMALIZE_RULES_ROMAJI); + +export const NORMALIZE_RULES_KANA_DAKUTEN: Record = { + 'う\u3099': 'ゔ', + 'か\u3099': 'が', 'き\u3099': 'ぎ', 'く\u3099': 'ぐ', 'け\u3099': 'げ', 'こ\u3099': 'ご', + 'さ\u3099': 'ざ', 'し\u3099': 'じ', 'す\u3099': 'ず', 'せ\u3099': 'ぜ', 'そ\u3099': 'ぞ', + 'た\u3099': 'だ', 'ち\u3099': 'ぢ', 'つ\u3099': 'づ', 'て\u3099': 'で', 'と\u3099': 'ど', + 'は\u3099': 'ば', 'ひ\u3099': 'び', 'ふ\u3099': 'ぶ', 'へ\u3099': 'べ', 'ほ\u3099': 'ぼ', + 'は\u309A': 'ぱ', 'ひ\u309A': 'ぴ', 'ふ\u309A': 'ぷ', 'へ\u309A': 'ぺ', 'ほ\u309A': 'ぽ', + 'ゝ\u3099': 'ゞ', + + 'ウ\u3099': 'ヴ', + 'カ\u3099': 'ガ', 'キ\u3099': 'ギ', 'ク\u3099': 'グ', 'ケ\u3099': 'ゲ', 'コ\u3099': 'ゴ', + 'サ\u3099': 'ザ', 'シ\u3099': 'ジ', 'ス\u3099': 'ズ', 'セ\u3099': 'ゼ', 'ソ\u3099': 'ゾ', + 'タ\u3099': 'ダ', 'チ\u3099': 'ヂ', 'ツ\u3099': 'ヅ', 'テ\u3099': 'デ', 'ト\u3099': 'ド', + 'ハ\u3099': 'バ', 'ヒ\u3099': 'ビ', 'フ\u3099': 'ブ', 'ヘ\u3099': 'ベ', 'ホ\u3099': 'ボ', + 'ハ\u309A': 'パ', 'ヒ\u309A': 'ピ', 'フ\u309A': 'プ', 'ヘ\u309A': 'ペ', 'ホ\u309A': 'ポ', + 'ワ\u3099': 'ヷ', 'ヰ\u3099': 'ヸ', 'ヱ\u3099': 'ヹ', 'ヲ\u3099': 'ヺ', + 'ヽ\u3099': 'ヾ', +}; +export const normalizeKanaDakuten = createNormalizer(NORMALIZE_RULES_KANA_DAKUTEN); + +const isValidJapanesePhrase = (codePoints: string[], start: number, length: number) => + // Skip splittings that cause sound marks to occur in the first position of a phrase + !isJapaneseSoundMark(codePoints[start]!) && (start + length === codePoints.length || !isJapaneseSoundMark(codePoints[start + length]!)); +export const createKanaTranscriptionEnumerator = (kuromoji: KuromojiTokenizer) => createTranscriptionEnumerator( + isValidJapanesePhrase, + phrase => getAllKanaReadings(kuromoji, stripJapaneseSoundMarks(normalizeKanaDakuten(phrase))), +); +export const createRomajiTranscriptionEnumerator = (kuromoji: KuromojiTokenizer) => createTranscriptionEnumerator( + isValidJapanesePhrase, + phrase => getAllKanaReadings(kuromoji, stripJapaneseSoundMarks(normalizeKanaDakuten(phrase))).map(kana => normalizeRomaji(toRomajiStrictly(kana))), +); diff --git a/packages/needle/src/indexer/tokenizer.test.ts b/packages/needle/src/indexer/tokenizer.test.ts new file mode 100644 index 0000000..2ad05b4 --- /dev/null +++ b/packages/needle/src/indexer/tokenizer.test.ts @@ -0,0 +1,166 @@ +import path from 'node:path'; +import url from 'node:url'; + +import { TokenizerBuilder } from '@patdx/kuromoji'; +import NodeDictionaryLoader from '@patdx/kuromoji/node'; + +import { createTokenizer, type KuromojiTokenizer } from './tokenizer'; +import { TokenType } from '../common/types'; + +let kuromoji: KuromojiTokenizer; + +beforeAll(async () => { + const kuromojiDictPath = path.resolve(url.fileURLToPath(import.meta.resolve('@patdx/kuromoji')), '..', '..', 'dict'); + kuromoji = await new TokenizerBuilder({ loader: new NodeDictionaryLoader({ dic_path: kuromojiDictPath }) }).build(); +}); + +describe('tokenizer', () => { + it('should tokenize mixed Japanese text', () => { + const tokenizer = createTokenizer({ kuromoji }); + const tokens = tokenizer.tokenize('僕の和風本当上手'); + + // Get all token definitions + const tokenDefs = [...tokenizer.tokens.values()]; + + // Should have tokens of various types + const types = new Set(tokenDefs.map(t => t.type)); + expect(types.has(TokenType.Han)).toBe(true); + expect(types.has(TokenType.Pinyin)).toBe(true); + expect(types.has(TokenType.Kana)).toBe(true); + expect(types.has(TokenType.Romaji)).toBe(true); + + const getTokenTextsAt = (pos: number, type: TokenType) => tokens + .filter(t => t.start <= pos && t.end > pos && tokenDefs.find(d => d.id === t.id)?.type === type) + .map(t => tokenDefs.find(d => d.id === t.id)!.text); + + // Position 0: 僕 + expect(getTokenTextsAt(0, TokenType.Han)).toContain('僕'); + expect(getTokenTextsAt(0, TokenType.Pinyin)).toContain('pu'); + expect(getTokenTextsAt(0, TokenType.Kana)).toContain('ボク'); + expect(getTokenTextsAt(0, TokenType.Romaji)).toContain('boku'); + + // Position 1: の (hiragana, no Han/Pinyin) + expect(getTokenTextsAt(1, TokenType.Han)).toEqual([]); + expect(getTokenTextsAt(1, TokenType.Pinyin)).toEqual([]); + expect(getTokenTextsAt(1, TokenType.Kana)).toContain('ノ'); + expect(getTokenTextsAt(1, TokenType.Romaji)).toContain('no'); + + // Position 2: 和 + expect(getTokenTextsAt(2, TokenType.Han)).toContain('和'); + expect(getTokenTextsAt(2, TokenType.Pinyin)).toContain('he'); + expect(getTokenTextsAt(2, TokenType.Kana)).toContain('ワ'); + expect(getTokenTextsAt(2, TokenType.Romaji)).toContain('wa'); + + // Position 3: 風 + expect(getTokenTextsAt(3, TokenType.Han)).toContain('風'); + expect(getTokenTextsAt(3, TokenType.Han)).toContain('风'); // simplified variant + expect(getTokenTextsAt(3, TokenType.Pinyin)).toContain('feng'); + expect(getTokenTextsAt(3, TokenType.Kana)).toContain('フウ'); + expect(getTokenTextsAt(3, TokenType.Romaji)).toContain('fu'); + + // Position 4: 本 + expect(getTokenTextsAt(4, TokenType.Han)).toContain('本'); + expect(getTokenTextsAt(4, TokenType.Pinyin)).toContain('ben'); + expect(getTokenTextsAt(4, TokenType.Kana)).toContain('ホン'); + expect(getTokenTextsAt(4, TokenType.Romaji)).toContain('hon'); + + // Position 5: 当 + expect(getTokenTextsAt(5, TokenType.Han)).toContain('当'); + expect(getTokenTextsAt(5, TokenType.Han)).toContain('當'); // traditional variant + expect(getTokenTextsAt(5, TokenType.Pinyin)).toContain('dang'); + expect(getTokenTextsAt(5, TokenType.Kana)).toContain('トウ'); + expect(getTokenTextsAt(5, TokenType.Romaji)).toContain('to'); // normalized: tou -> to + + // Position 6: 上 + expect(getTokenTextsAt(6, TokenType.Han)).toContain('上'); + expect(getTokenTextsAt(6, TokenType.Pinyin)).toContain('shang'); + expect(getTokenTextsAt(6, TokenType.Kana)).toContain('ジョウ'); + expect(getTokenTextsAt(6, TokenType.Romaji)).toContain('jo'); // normalized: jou -> jo + + // Position 7: 手 + expect(getTokenTextsAt(7, TokenType.Han)).toContain('手'); + expect(getTokenTextsAt(7, TokenType.Pinyin)).toContain('shou'); + expect(getTokenTextsAt(7, TokenType.Kana)).toContain('シュ'); + expect(getTokenTextsAt(7, TokenType.Romaji)).toContain('shu'); + + // Check that tokens cover the entire input + expect(tokens.length).toBeGreaterThan(0); + + // Check some specific token definitions exist + const hanTokenTexts = tokenDefs.filter(t => t.type === TokenType.Han).map(t => t.text); + expect(hanTokenTexts).toContain('僕'); + expect(hanTokenTexts).toContain('和'); + expect(hanTokenTexts).toContain('風'); + + // Check kana readings exist for kanji + const kanaTokenTexts = tokenDefs.filter(t => t.type === TokenType.Kana).map(t => t.text); + expect(kanaTokenTexts).toContain('ボク'); // 僕 -> ボク + + // Check romaji readings exist + const romajiTokenTexts = tokenDefs.filter(t => t.type === TokenType.Romaji).map(t => t.text); + expect(romajiTokenTexts).toContain('boku'); // 僕 -> boku + }); + + it('should not create duplicate tokens when tokenizing multiple documents', () => { + const tokenizer = createTokenizer({ kuromoji }); + + // Tokenize multiple music names that share some characters + tokenizer.tokenize('僕の和風本当上手'); + tokenizer.tokenize('僕'); + tokenizer.tokenize('和風'); + + // Check that there are no duplicate tokens + const tokenDefs = [...tokenizer.tokens.values()]; + const tokenKeys = tokenDefs.map(t => `${t.type}:${t.text}`); + const uniqueKeys = new Set(tokenKeys); + + expect(tokenKeys.length).toBe(uniqueKeys.size); + + // Also check that IDs are unique + const ids = tokenDefs.map(t => t.id); + const uniqueIds = new Set(ids); + expect(ids.length).toBe(uniqueIds.size); + }); + + it('should handle Raw tokens for non-CJK characters', () => { + const tokenizer = createTokenizer({ kuromoji }); + tokenizer.tokenize('a-b'); + + const tokenDefs = [...tokenizer.tokens.values()]; + const rawTokenTexts = tokenDefs.filter(t => t.type === TokenType.Raw).map(t => t.text); + + expect(rawTokenTexts).toContain('a'); // normalized to lowercase + expect(rawTokenTexts).toContain('-'); + expect(rawTokenTexts).toContain('b'); + }); + + it('should tokenize compound word "今日" with both individual and combined readings', () => { + const tokenizer = createTokenizer({ kuromoji }); + const tokens = tokenizer.tokenize('今日'); + const tokenDefs = [...tokenizer.tokens.values()]; + + const getTokensWithSpan = (type: TokenType, start: number, end: number) => tokens + .filter(t => t.start === start && t.end === end && tokenDefs.find(d => d.id === t.id)?.type === type) + .map(t => tokenDefs.find(d => d.id === t.id)!.text); + + // Individual character readings at position 0: 今 + expect(getTokensWithSpan(TokenType.Han, 0, 1)).toContain('今'); + expect(getTokensWithSpan(TokenType.Pinyin, 0, 1)).toContain('jin'); + expect(getTokensWithSpan(TokenType.Kana, 0, 1)).toContain('コン'); + expect(getTokensWithSpan(TokenType.Kana, 0, 1)).toContain('イマ'); + expect(getTokensWithSpan(TokenType.Romaji, 0, 1)).toContain('kon'); + expect(getTokensWithSpan(TokenType.Romaji, 0, 1)).toContain('ima'); + + // Individual character readings at position 1: 日 + expect(getTokensWithSpan(TokenType.Han, 1, 2)).toContain('日'); + expect(getTokensWithSpan(TokenType.Pinyin, 1, 2)).toContain('ri'); + expect(getTokensWithSpan(TokenType.Kana, 1, 2)).toContain('ニチ'); + expect(getTokensWithSpan(TokenType.Kana, 1, 2)).toContain('ヒ'); + expect(getTokensWithSpan(TokenType.Romaji, 1, 2)).toContain('niti'); + expect(getTokensWithSpan(TokenType.Romaji, 1, 2)).toContain('hi'); + + // Combined reading for "今日" [0, 2] - this is an indivisible compound word + expect(getTokensWithSpan(TokenType.Kana, 0, 2)).toContain('キョウ'); + expect(getTokensWithSpan(TokenType.Romaji, 0, 2)).toContain('kyo'); // normalized: kyou -> kyo + }); +}); diff --git a/packages/needle/src/indexer/tokenizer.ts b/packages/needle/src/indexer/tokenizer.ts new file mode 100644 index 0000000..4a29f13 --- /dev/null +++ b/packages/needle/src/indexer/tokenizer.ts @@ -0,0 +1,93 @@ +import type { TokenizerBuilder } from '@patdx/kuromoji'; + +import { getHanVariants, getPinyinCandidates } from './han'; +import { createKanaTranscriptionEnumerator, createRomajiTranscriptionEnumerator, isMaybeJapanese } from './japanese'; +import { normalizeByCodePoint } from '../common/normalize'; +import { TokenType, type TokenDefinition } from '../common/types'; + +export interface Token { + id: number; + start: number; + end: number; +} + +export type KuromojiTokenizer = Awaited>; +export interface TokenizerOptions { + kuromoji: KuromojiTokenizer; +} +export const createTokenizer = (options: TokenizerOptions) => { + const tokens = new Map(); + let nextId = 0; + const ensureToken = (type: TokenType, text: string) => { + const key = `${type}:${text}`; + let tokenDefinition = tokens.get(key); + if (tokenDefinition) return tokenDefinition; + tokenDefinition = { id: nextId++, type, text, codePointLength: [...text].length }; + tokens.set(key, tokenDefinition); + return tokenDefinition; + }; + + const enumerateAllKanaCombinations = createKanaTranscriptionEnumerator(options.kuromoji); + const enumerateAllRomajiCombinations = createRomajiTranscriptionEnumerator(options.kuromoji); + const tokenize = (text: string) => { + const results: Token[] = []; + const emitter = (start: number, end: number) => (type: TokenType, text: string) => results.push({ id: ensureToken(type, text).id, start, end }); + + const emitMaybeJapanese = (codePoints: string[], offset: number) => { + for (const { start, length, transcriptions } of enumerateAllKanaCombinations(codePoints)) { + const emit = emitter(offset + start, offset + start + length); + for (const transcription of transcriptions) emit(TokenType.Kana, transcription); + } + for (const { start, length, transcriptions } of enumerateAllRomajiCombinations(codePoints)) { + const emit = emitter(offset + start, offset + start + length); + for (const transcription of transcriptions) emit(TokenType.Romaji, transcription); + } + for (let i = 0; i < codePoints.length; i++) { + // Single character may have not only kana readings, but also Chinese pronunciations or Simplified/Traditional/Japanese variants. + const character = codePoints[i]!; + const hanAlternates = getHanVariants(character); // All possible variant characters (Simplified/Traditional/Japanese) + const pinyinAlternates = Array.from(new Set(hanAlternates.flatMap(han => getPinyinCandidates(han)))); // All possible pinyin candidates + const emit = emitter(offset + i, offset + i + 1); + for (const han of hanAlternates) emit(TokenType.Han, han); + for (const pinyin of pinyinAlternates) emit(TokenType.Pinyin, pinyin); + } + }; + const emitRaw = (codePoint: string, offset: number) => emitter(offset, offset + 1)(TokenType.Raw, codePoint); + + const codePoints = [...normalizeByCodePoint(text)]; + for (let start = 0; start < codePoints.length;) { + const codePoint = codePoints[start]!; + + const consequentCharsets = [ + { is: isMaybeJapanese, emit: emitMaybeJapanese }, + ]; + let emitted = false; + for (const { is, emit } of consequentCharsets) { + let length = 0; + while (start + length < codePoints.length && is(codePoints[start + length]!)) length++; + if (length > 0) { + emit(codePoints.slice(start, start + length), start); + start += length; + emitted = true; + break; + } + } + if (emitted) continue; + + // Skip whitespaces + if (/\s/.test(codePoint)) { + start++; + continue; + } + + emitRaw(codePoint, start); + start++; + } + return results; + }; + + return { + tokens, + tokenize, + }; +}; diff --git a/packages/needle/src/indexer/trie.test.ts b/packages/needle/src/indexer/trie.test.ts new file mode 100644 index 0000000..0294b1f --- /dev/null +++ b/packages/needle/src/indexer/trie.test.ts @@ -0,0 +1,51 @@ +import { traverseTrie } from '../common'; +import { buildTrie, graftTriePaths } from './trie'; + +describe('graftTriePaths', () => { + it('should graft paths according to normalization rules', () => { + // Build a trie with tokens containing normalized forms + const trie = buildTrie([ + [0, 'sya'], // normalized form of "sha" + [1, 'tu'], // normalized form of "tsu" + ]); + + // Graft paths so that "sha" -> "sya" and "tsu" -> "tu" + graftTriePaths(trie, { + sha: 'sya', + tsu: 'tu', + }); + + // Now we should be able to traverse using both the original and grafted paths + const syaNode = traverseTrie(trie, 'sya'); + const shaNode = traverseTrie(trie, 'sha'); + expect(syaNode).toBeDefined(); + expect(shaNode).toBeDefined(); + expect(syaNode).toBe(shaNode); // Both paths should lead to the same node + + const tuNode = traverseTrie(trie, 'tu'); + const tsuNode = traverseTrie(trie, 'tsu'); + expect(tuNode).toBeDefined(); + expect(tsuNode).toBeDefined(); + expect(tuNode).toBe(tsuNode); + }); + + it('should handle chained graft rules', () => { + const trie = buildTrie([ + [0, 'o'], // normalized vowel + ]); + + // Chain: "ou" -> "o", "oo" -> "o" + graftTriePaths(trie, { + ou: 'o', + oo: 'o', + }); + + const oNode = traverseTrie(trie, 'o'); + const ouNode = traverseTrie(trie, 'ou'); + const ooNode = traverseTrie(trie, 'oo'); + + expect(oNode).toBeDefined(); + expect(ouNode).toBe(oNode); + expect(ooNode).toBe(oNode); + }); +}); diff --git a/packages/needle/src/indexer/trie.ts b/packages/needle/src/indexer/trie.ts new file mode 100644 index 0000000..030c868 --- /dev/null +++ b/packages/needle/src/indexer/trie.ts @@ -0,0 +1,115 @@ +import { traverseTrie, type TrieNode } from '../common'; + +const newNode = (parent?: TrieNode): TrieNode => ({ parent, children: new Map(), tokenIds: [], subTreeTokenIds: [] }); + +// Assume tokens are unique. +export const buildTrie = (tokens: [id: number, text: string][]) => { + const root = newNode(undefined); + for (const [id, text] of tokens) { + let node = root; + for (const char of text) { + const codePoint = char.codePointAt(0)!; + let childNode = node.children.get(codePoint); + if (!childNode) { + childNode = newNode(node); + node.children.set(codePoint, childNode); + } + node = childNode; + node.subTreeTokenIds.push(id); + } + node.tokenIds.push(id); + } + return root; +}; + +export const graftTriePaths = (root: TrieNode, rules: Record) => { + for (const [inputPhrase, graftTo] of Object.entries(rules)) if ([...graftTo].length > [...inputPhrase].length) throw new Error(`Graft rule ${inputPhrase} -> ${graftTo} maps to longer string and may cause infinite loop`); + const visitedNodes = new Set(); + const graftFromNode = (node: TrieNode, recursiveChildren: boolean) => { + if (visitedNodes.has(node)) return; + visitedNodes.add(node); + if (recursiveChildren) for (const [, childNode] of node.children) graftFromNode(childNode, true); + while (true) { + const nodesWithNewGraftedChildren = new Map(); + for (const [inputPhrase, graftTo] of Object.entries(rules)) { + const targetNode = traverseTrie(node, graftTo); + if (!targetNode) continue; + const codePoints = [...inputPhrase]; + const graftedPath = Array.from({ length: codePoints.length - 1 }); + let isGrafted = false; + let currentNode = node; + for (let i = 0; i < codePoints.length; i++) { + const codePoint = codePoints[i]!.codePointAt(0)!; + let childNode = currentNode.children.get(codePoint); + if (i === codePoints.length - 1) { + if (childNode) { + if (childNode !== targetNode) throw new Error(`Grafted path ${inputPhrase} conflicts with existing path`); + // Already grafted + } else { + currentNode.children.set(codePoint, childNode = targetNode); + isGrafted = true; + } + } else { + if (!childNode) { + childNode = newNode(currentNode); + childNode.subTreeTokenIds = targetNode.subTreeTokenIds; + currentNode.children.set(codePoint, childNode); + } else { + // Part of another grafted path? + childNode.subTreeTokenIds = Array.from(new Set([...childNode.subTreeTokenIds, ...targetNode.subTreeTokenIds])); + } + graftedPath[i] = currentNode = childNode; + } + } + if (isGrafted) for (const [i, nodeToAdd] of graftedPath.entries()) nodesWithNewGraftedChildren.set(nodeToAdd, i + 1); + } + + if (nodesWithNewGraftedChildren.size > 0) { + // Re-check graft rules on the newly grafted path + // 1. No need to recursive other children (not on this path) since their children are not affected + // 2. No need to consider ancestors of this node since they're handled later (we run in DFS order) + const sortedNodes = [...nodesWithNewGraftedChildren.entries()].sort((a, b) => b[1] - a[1]); + for (const [changedNode] of sortedNodes) graftFromNode(changedNode, false); + } else { + // No new grafts applied + break; + } + } + }; + graftFromNode(root, true); +}; + +export const serializeTrie = (root: TrieNode) => { + const nodeEntries = new Map(); + let currentId = 0; + const getNodeEntry = (node: TrieNode) => { + let entry = nodeEntries.get(node); + if (!entry) { + entry = { id: ++currentId, visited: false }; + nodeEntries.set(node, entry); + } + return entry; + }; + const serializeNode = (node: TrieNode) => { + const entry = getNodeEntry(node); + if (entry.visited) return entry.id; + entry.visited = true; + const children = [...node.children.entries()].map(([codePoint, childNode]) => [codePoint, serializeNode(childNode)] as const); + entry.data = [ + node.parent ? getNodeEntry(node.parent).id : 0, + ...children.map(child => child[0]), // code points + ...children.map(child => child[1]), // child node ids + // End of children list (<= 0 are not valid code points nor node IDs) + ...node.tokenIds.length > 0 + ? node.tokenIds.map(tokenId => -(tokenId + 1)) // Use the negative value of (tokenId + 1) + : [0], // End of children list, no token IDs (token IDs are encoded to negative values) + ]; + return entry.id; + }; + serializeNode(root); + return [...nodeEntries.values()].sort((a, b) => a.id - b.id).flatMap(node => node.data ?? []); +}; diff --git a/packages/needle/src/searcher/highlight.ts b/packages/needle/src/searcher/highlight.ts new file mode 100644 index 0000000..64476eb --- /dev/null +++ b/packages/needle/src/searcher/highlight.ts @@ -0,0 +1,26 @@ +import { getSpanLength, TokenType } from '../common'; +import type { SearchResult } from './search'; + +export type HighlightedTextPart = /* not highlighted */ string | /* highlighted */ { highlight: string }; + +export const highlightSearchResult = (resultDocument: SearchResult): HighlightedTextPart[] => { + const highlightResult: HighlightedTextPart[] = []; + let previousHighlightEnd = 0; + for (const token of resultDocument.tokens) { + const notHighlightedText = resultDocument.documentCodePoints.slice(previousHighlightEnd, token.documentOffset.start).join(''); + if (notHighlightedText.length > 0) highlightResult.push(notHighlightedText); + const highlightEnd = token.isTokenPrefixMatching && (token.definition.type === TokenType.Kana) + ? token.documentOffset.start + Math.max( + 1, + Math.round( + getSpanLength(token.documentOffset) * + Math.min(1, getSpanLength(token.inputOffset) / token.definition.codePointLength), + ), + ) + : token.documentOffset.end; + highlightResult.push({ highlight: resultDocument.documentCodePoints.slice(token.documentOffset.start, highlightEnd).join('') }); + previousHighlightEnd = highlightEnd; + } + if (previousHighlightEnd < resultDocument.documentCodePoints.length) highlightResult.push(resultDocument.documentCodePoints.slice(previousHighlightEnd).join('')); + return highlightResult; +}; diff --git a/packages/needle/src/searcher/index.ts b/packages/needle/src/searcher/index.ts new file mode 100644 index 0000000..530cc8f --- /dev/null +++ b/packages/needle/src/searcher/index.ts @@ -0,0 +1,4 @@ +export * from './trie'; +export * from './inverted-index'; +export * from './search'; +export * from './highlight'; diff --git a/packages/needle/src/searcher/inverted-index.ts b/packages/needle/src/searcher/inverted-index.ts new file mode 100644 index 0000000..11731f0 --- /dev/null +++ b/packages/needle/src/searcher/inverted-index.ts @@ -0,0 +1,59 @@ +import { deserializeTrie } from './trie'; +import type { TrieNode } from '../common'; +import type { CompressedInvertedIndex, OffsetSpan, TokenDefinition } from '../common/types'; + +export interface TokenDocumentReference { + documentId: number; + offsets: OffsetSpan[]; +} + +interface TokenDefinitionExtended extends TokenDefinition { + references: TokenDocumentReference[]; +}; + +const mergeMap = (...maps: Map[]) => { + const result = new Map(); + for (const map of maps) for (const [key, value] of map.entries()) result.set(key, value); + return result; +}; + +export interface LoadedInvertedIndex { + documents: string[]; + documentCodePoints: string[][]; + tokenDefinitions: TokenDefinitionExtended[]; + tries: { + romaji: TrieNode; + kana: TrieNode; + other: TrieNode; + }; +} + +export const loadInvertedIndex = (compressed: CompressedInvertedIndex): LoadedInvertedIndex => { + const documents = compressed.documents; + const documentCodePoints = documents.map(document => [...document]); + + const romajiTrie = deserializeTrie(compressed.tries.romaji); + const kanaTrie = deserializeTrie(compressed.tries.kana); + const otherTrie = deserializeTrie(compressed.tries.other); + + const tokenCodePoints = mergeMap(romajiTrie.tokenCodePoints, kanaTrie.tokenCodePoints, otherTrie.tokenCodePoints); + const tokenDefinitions = compressed.tokenTypes.map((type, index) => ({ + id: index, type, text: tokenCodePoints.get(index)!.join(''), + codePointLength: tokenCodePoints.get(index)!.length, + references: compressed.tokenReferences[index]!.map(([documentId, ...offsets]) => ({ + documentId: documentId!, + offsets: Array.from({ length: offsets.length / 2 }, (_, i) => ({ start: offsets[i * 2]!, end: offsets[i * 2 + 1]! })), + })), + })); + + return { + documents, + documentCodePoints, + tokenDefinitions, + tries: { + romaji: romajiTrie.root, + kana: kanaTrie.root, + other: otherTrie.root, + }, + }; +}; diff --git a/packages/needle/src/searcher/search.ts b/packages/needle/src/searcher/search.ts new file mode 100644 index 0000000..9499916 --- /dev/null +++ b/packages/needle/src/searcher/search.ts @@ -0,0 +1,258 @@ +import { highlightSearchResult } from './highlight'; +import { getTrieNodeTokenIds } from './trie'; +import type { TrieNode } from '../common'; +import { traverseTrieStep } from '../common'; +import type { LoadedInvertedIndex } from './inverted-index'; +import { normalizeByCodePoint, toKatakana } from '../common/normalize'; +import { type OffsetSpan, type TokenDefinition, TokenType } from '../common/types'; +import { getSpanLength } from '../common/utils'; + +const IGNORABLE_CODE_POINTS = /[\s\u3099\u309A]/u; + +enum TokenTypePrefixMatchingPolicy { + AlwaysAllow, + NeverAllow, + AllowOnlyAtInputEnd, +} +const tokenTypePrefixMatchingPolicy: Record = { + [TokenType.Romaji]: TokenTypePrefixMatchingPolicy.NeverAllow, + [TokenType.Kana]: TokenTypePrefixMatchingPolicy.AlwaysAllow, + // These token types are in an "other" Trie + [TokenType.Han]: TokenTypePrefixMatchingPolicy.AllowOnlyAtInputEnd, // No effect because always 1 code point + [TokenType.Pinyin]: TokenTypePrefixMatchingPolicy.AllowOnlyAtInputEnd, + [TokenType.Raw]: TokenTypePrefixMatchingPolicy.AllowOnlyAtInputEnd, // No effect because always 1 code point +}; +const shouldAllowPrefixMatching = (tokenType: TokenType, isAtInputEnd: boolean) => + tokenTypePrefixMatchingPolicy[tokenType] === TokenTypePrefixMatchingPolicy.AlwaysAllow || + (tokenTypePrefixMatchingPolicy[tokenType] !== TokenTypePrefixMatchingPolicy.NeverAllow && isAtInputEnd); + +export interface SearchResultToken { + definition: TokenDefinition; + documentOffset: OffsetSpan; + inputOffset: OffsetSpan; + isTokenPrefixMatching: boolean; +} + +interface ComparableStateTraits { + getRangeCount: (state: T) => number; + getPrefixMatchCount: (state: T) => number; + getFirstTokenDocumentOffset: (state: T) => OffsetSpan; + getLastTokenDocumentOffset: (state: T) => OffsetSpan; + getLastToken?: (state: T) => SearchResultToken; // Not on intermediate results + getMatchRatioLevel?: (state: T) => number; // Not on intermediate/candidate results + getMatchRatio: (state: T) => number; + // Called when all other comparisons are equal + nextComparer?: (a: T, b: T) => number; // Not on intermediate/candidate results +} + +const getComparerForTraits = (traits: ComparableStateTraits) => (a: T, b: T) => { + // Prefer matches that not relying on end-of-input loose matching (full match over prefix match) + if (traits.getLastToken) { + const aLastToken = traits.getLastToken(a), bLastToken = traits.getLastToken(b); + const aDidPrefixMatchByTokenType = aLastToken.isTokenPrefixMatching && tokenTypePrefixMatchingPolicy[aLastToken.definition.type] === TokenTypePrefixMatchingPolicy.AllowOnlyAtInputEnd; + const bDidPrefixMatchByTokenType = bLastToken.isTokenPrefixMatching && tokenTypePrefixMatchingPolicy[bLastToken.definition.type] === TokenTypePrefixMatchingPolicy.AllowOnlyAtInputEnd; + if (aDidPrefixMatchByTokenType !== bDidPrefixMatchByTokenType) return aDidPrefixMatchByTokenType ? 1 : -1; + } + + // Prefer results that matched fewer discontinuous ranges over more + const aRangeCount = traits.getRangeCount(a), bRangeCount = traits.getRangeCount(b); + if (aRangeCount !== bRangeCount) return aRangeCount - bRangeCount; + + // Prefer results that matches first token in document earlier over later + const aFirstTokenDocumentOffset = traits.getFirstTokenDocumentOffset(a), bFirstTokenDocumentOffset = traits.getFirstTokenDocumentOffset(b); + if (aFirstTokenDocumentOffset.start !== bFirstTokenDocumentOffset.start) return aFirstTokenDocumentOffset.start - bFirstTokenDocumentOffset.start; + + // Prefer results that has higher match ratio (but don't distinguish similar ratios, so we introduced `matchRatioLevel`) + if (traits.getMatchRatioLevel) { + const aMatchRatioLevel = traits.getMatchRatioLevel(a), bMatchRatioLevel = traits.getMatchRatioLevel(b); + if (aMatchRatioLevel !== bMatchRatioLevel) return bMatchRatioLevel - aMatchRatioLevel; + } + + // Prefer results that last token occurred earlier (if same, ended earlier) in the document over later + const aLastTokenDocumentOffset = traits.getLastTokenDocumentOffset(a), bLastTokenDocumentOffset = traits.getLastTokenDocumentOffset(b); + if (aLastTokenDocumentOffset.start !== bLastTokenDocumentOffset.start) return aLastTokenDocumentOffset.start - bLastTokenDocumentOffset.start; + if (aLastTokenDocumentOffset.end !== bLastTokenDocumentOffset.end) return aLastTokenDocumentOffset.end - bLastTokenDocumentOffset.end; + + // Prefer results that has higher match ratio (precisely) + const aMatchRatio = traits.getMatchRatio(a), bMatchRatio = traits.getMatchRatio(b); + if (aMatchRatio !== bMatchRatio) return bMatchRatio - aMatchRatio; + + return traits.nextComparer?.(a, b) ?? 0; +}; + +interface IntermediateResult { + previousState?: IntermediateResult; + firstTokenDocumentOffset: OffsetSpan; + rangeCount: number; + tokenCount: number; + prefixMatchCount: number; + matchedTokenLength: number; + tokenId: number; + documentOffset: OffsetSpan; + inputOffset: OffsetSpan; + isTokenPrefixMatching: boolean; +} +const compareIntermediateResult = getComparerForTraits({ + getRangeCount: state => state.rangeCount, + getPrefixMatchCount: state => state.prefixMatchCount, + getFirstTokenDocumentOffset: state => state.firstTokenDocumentOffset, + getLastTokenDocumentOffset: state => state.documentOffset, + getMatchRatio: state => state.matchedTokenLength, // No need to divide document length since intermediate results are for same document +}); + +interface CandidateResult { + tokens: SearchResultToken[]; + prefixMatchCount: number; + matchedTokenLength: number; + rangeCount: number; +} +const compareCandidateResult = getComparerForTraits({ + getRangeCount: state => state.rangeCount, + getPrefixMatchCount: state => state.prefixMatchCount, + getFirstTokenDocumentOffset: state => state.tokens[0]!.documentOffset, + getLastTokenDocumentOffset: state => state.tokens[state.tokens.length - 1]!.documentOffset, + getLastToken: state => state.tokens[state.tokens.length - 1]!, + getMatchRatio: state => state.matchedTokenLength, // No need to divide document length since candidate results are for same document +}); + +export interface SearchResult { + documentId: number; + documentText: string; + documentCodePoints: string[]; + tokens: SearchResultToken[]; + prefixMatchCount: number; + rangeCount: number; + matchRatio: number; + matchRatioLevel: number; +} +const compareFinalResult = getComparerForTraits({ + getRangeCount: state => state.rangeCount, + getPrefixMatchCount: state => state.prefixMatchCount, + getFirstTokenDocumentOffset: state => state.tokens[0]!.documentOffset, + getLastTokenDocumentOffset: state => state.tokens[state.tokens.length - 1]!.documentOffset, + getLastToken: state => state.tokens[state.tokens.length - 1]!, + getMatchRatio: state => state.matchRatio, + getMatchRatioLevel: state => Math.round(state.matchRatio * 5), + nextComparer: (a, b) => a.documentText === b.documentText ? 0 : a.documentText < b.documentText ? -1 : 1, +}); + +const hasNonEmptyCharacters = (documentCodePoints: string[], start: number, end: number) => start !== end && !documentCodePoints.slice(start, end).every(char => /\s/.test(char)); + +export const searchInvertedIndex = (invertedIndex: LoadedInvertedIndex, text: string): SearchResult[] => { + const { documents, documentCodePoints, tokenDefinitions, tries } = invertedIndex; + + const codePoints = [...toKatakana(normalizeByCodePoint(text))]; + // dp[i] = docId => end => IntermediateResult, starts from dp[-1] (l === 0), ends at dp[N - 1] (r === N - 1) + const dp = Array.from({ length: codePoints.length }, () => new Map>()); + for (let l = 0; l < codePoints.length; l++) { + if (l !== 0 && dp[l - 1]!.size === 0) continue; // No documents match input from beginning to this position + let romajiNode: TrieNode | undefined = tries.romaji; + let kanaNode: TrieNode | undefined = tries.kana; + let otherNode: TrieNode | undefined = tries.other; + for (let r = l; r < codePoints.length && (romajiNode || kanaNode || otherNode); r++) { // [l, r] + const codePoint = codePoints[r]!; + romajiNode = traverseTrieStep(romajiNode, codePoint, IGNORABLE_CODE_POINTS); + kanaNode = traverseTrieStep(kanaNode, codePoint, IGNORABLE_CODE_POINTS); + otherNode = traverseTrieStep(otherNode, codePoint, IGNORABLE_CODE_POINTS); + const reachingInputEnd = r === codePoints.length - 1; + const matchingTokenIds = new Set([ + // Allow suffix matching of romaji/other tokens if we're at the end of the input + ...getTrieNodeTokenIds(romajiNode, shouldAllowPrefixMatching(TokenType.Romaji, reachingInputEnd)), + ...getTrieNodeTokenIds(kanaNode, shouldAllowPrefixMatching(TokenType.Kana, reachingInputEnd)), + ...getTrieNodeTokenIds(otherNode, reachingInputEnd), + ]); + for (const tokenId of matchingTokenIds) for (const { documentId, offsets } of tokenDefinitions[tokenId]!.references) { + const isTokenPrefixMatching = !romajiNode?.tokenIds.includes(tokenId) && !kanaNode?.tokenIds.includes(tokenId) && !otherNode?.tokenIds.includes(tokenId); + const previousMatchesOfDocument = dp[l - 1]?.get(documentId); + if (l !== 0 && !previousMatchesOfDocument) continue; + for (const documentOffset of offsets) { + const { start: currentStart, end: currentEnd } = documentOffset; + const contributeNextMatchingState = (previousState: IntermediateResult | undefined) => { + const nextMatchingMap = dp[r]!; + let nextMatchesOfDocument = nextMatchingMap.get(documentId); + if (!nextMatchesOfDocument) { + nextMatchesOfDocument = Object.create(null) as Record; + nextMatchingMap.set(documentId, nextMatchesOfDocument); + } + const oldResult = nextMatchesOfDocument[currentEnd]; + const inputOffset = { start: l, end: r + 1 }; + const newResult: IntermediateResult = { + previousState, + firstTokenDocumentOffset: previousState?.firstTokenDocumentOffset ?? documentOffset, + rangeCount: !previousState ? 1 + : (previousState.rangeCount + (hasNonEmptyCharacters(documentCodePoints[documentId]!, previousState.documentOffset.end, currentStart) ? 1 : 0)), + tokenCount: (previousState?.tokenCount ?? 0) + 1, + prefixMatchCount: (previousState?.prefixMatchCount ?? 0) + (isTokenPrefixMatching ? 1 : 0), + matchedTokenLength: (previousState?.matchedTokenLength ?? 0) + getSpanLength(documentOffset) * + Math.min(isTokenPrefixMatching ? getSpanLength(inputOffset) / tokenDefinitions[tokenId]!.codePointLength : Infinity, 1), + tokenId, + documentOffset, + inputOffset, + isTokenPrefixMatching, + }; + nextMatchesOfDocument[currentEnd] = !oldResult || compareIntermediateResult(newResult, oldResult) < 0 ? newResult : oldResult; + }; + if (l === 0) contributeNextMatchingState(undefined); + else for (const previousEnd in previousMatchesOfDocument) if (currentStart >= Number(previousEnd)) + contributeNextMatchingState(previousMatchesOfDocument[previousEnd as unknown as number]!); + // Don't `break` here because keys of `previousMatchesOfDocument` are not essentially ordered + } + } + } + } + + // Build search results and sort documents + return [...dp[codePoints.length - 1]!.entries()].map(([documentId, matches]) => { + const sortedMatches = Object.values(matches).map(match => { + const tokens: SearchResultToken[] = []; + // Build token list from backtracking + let state: IntermediateResult | undefined = match; + while (state) { + tokens.unshift({ + definition: tokenDefinitions[state.tokenId]!, + documentOffset: state.documentOffset, inputOffset: state.inputOffset, + isTokenPrefixMatching: state.isTokenPrefixMatching, + }); + state = state.previousState; + } + return { tokens, prefixMatchCount: match.prefixMatchCount, matchedTokenLength: match.matchedTokenLength, rangeCount: match.rangeCount }; + }).sort(compareCandidateResult); + const bestMatchOfDocument = sortedMatches[0]!; + const documentText = documents[documentId]!; + const matchRatio = bestMatchOfDocument.matchedTokenLength / documentCodePoints[documentId]!.length; + const matchRatioLevel = Math.round(matchRatio * 5); + return { + documentId, + documentText, + documentCodePoints: documentCodePoints[documentId]!, + tokens: bestMatchOfDocument.tokens, + prefixMatchCount: bestMatchOfDocument.prefixMatchCount, + rangeCount: bestMatchOfDocument.rangeCount, + matchRatio, + matchRatioLevel, + }; + }).sort(compareFinalResult); +}; + +// For debugging +export const inspectSearchResult = (resultDocument: SearchResult, htmlHighlight: boolean) => { + const { documentText, tokens, rangeCount, matchRatio, matchRatioLevel } = resultDocument; + const escapeHtml = (s: string) => s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); + const escapedText = htmlHighlight ? highlightSearchResult(resultDocument).map(part => + typeof part === 'string' ? escapeHtml(part) : `${escapeHtml(part.highlight)}`).join('') : JSON.stringify(documentText); + const description = ` (${rangeCount} ranges, ${Math.round(matchRatio * 10000) / 10000} => L${matchRatioLevel})`; + return [ + escapedText + (htmlHighlight ? `${description}` : description), + ...tokens.map(token => { + let escapedTokenText = JSON.stringify(token.definition.text); + let escapedDocumentText = JSON.stringify([...documentText].slice(token.documentOffset.start, token.documentOffset.end).join('')); + if (htmlHighlight) { + escapedTokenText = escapeHtml(escapedTokenText); + escapedDocumentText = escapeHtml(escapedDocumentText); + } + const line = ` ${TokenType[token.definition.type]}: ${escapedTokenText} -> ${escapedDocumentText}${token.isTokenPrefixMatching ? ' (prefix match)' : ''}`; + return htmlHighlight ? `${line}` : line; + }), + '', + ].join('\n'); +}; diff --git a/packages/needle/src/searcher/trie.ts b/packages/needle/src/searcher/trie.ts new file mode 100644 index 0000000..e608b45 --- /dev/null +++ b/packages/needle/src/searcher/trie.ts @@ -0,0 +1,58 @@ +import type { TrieNode } from '../common'; + +export const deserializeTrie = (data: number[]) => { + const nodes: TrieNode[] = []; + const getNode = (id: number) => nodes[id - 1] ??= { parent: undefined, children: new Map(), tokenIds: [], subTreeTokenIds: [] }; + let currentId = 0; + for (let i = 0; i < data.length;) { + const node = getNode(++currentId); + const parentId = data[i++]!; + node.parent = parentId !== 0 ? getNode(parentId) : undefined; + + let endOfChildren = i; + while (endOfChildren < data.length && data[endOfChildren]! > 0) endOfChildren++; + const numberOfChildren = (endOfChildren - i) / 2; + for (let j = i; j < i + numberOfChildren; j++) { + const codePoint = data[j]!; + const child = getNode(data[j + numberOfChildren]!); + node.children.set(codePoint, child); + } + i = endOfChildren; + + if (data[i] === 0) i++; // No token IDs + else while (i < data.length && data[i]! < 0) node.tokenIds.push(-data[i++]! - 1); + } + const root = nodes[0]!; + + // DFS to construct code point paths for each token + const tokenCodePoints = new Map(); + const currentCodePoints: string[] = []; + const dfsCodePoints = (node: TrieNode) => { + for (const tokenId of node.tokenIds) tokenCodePoints.set(tokenId, [...currentCodePoints]); + for (const [codePoint, child] of node.children.entries()) { + if (child.parent !== node) continue; // Skip grafted paths as these are not the canonical representation of the tokens + currentCodePoints.push(String.fromCodePoint(codePoint)); + dfsCodePoints(child); + currentCodePoints.pop(); + } + }; + dfsCodePoints(root); + + // DFS to construct subTreeTokenIds for each node + const visitedNodes = new Set(); + const dfsSubTreeTokenIds = (node: TrieNode) => { + if (visitedNodes.has(node)) return node.subTreeTokenIds; + visitedNodes.add(node); + node.subTreeTokenIds = [...node.tokenIds, ...new Set([...node.children.values()].flatMap(child => dfsSubTreeTokenIds(child)))]; + return node.subTreeTokenIds; + }; + dfsSubTreeTokenIds(root); + + return { + root, + tokenCodePoints, + }; +}; + +export const getTrieNodeTokenIds = (node: TrieNode | undefined, includeSubTree: boolean) => + (includeSubTree ? node?.subTreeTokenIds : node?.tokenIds) ?? []; diff --git a/packages/needle/tsconfig.json b/packages/needle/tsconfig.json new file mode 100644 index 0000000..90f8798 --- /dev/null +++ b/packages/needle/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ESNext", + "jsx": "preserve", + "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "allowJs": true, + "strict": true, + "strictNullChecks": true, + "noEmit": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "skipLibCheck": true, + "rootDir": ".", + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/needle/tsdown.config.ts b/packages/needle/tsdown.config.ts new file mode 100644 index 0000000..9fb7bc4 --- /dev/null +++ b/packages/needle/tsdown.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: [ + './src/index.ts', + './src/searcher/index.ts', + './src/indexer/index.ts', + './src/common/index.ts', + ], + dts: true, + unused: true, + fixedExtension: true, + unbundle: true, + sourcemap: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..278c911 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,6817 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@types/node': + specifier: ^24.10.0 + version: 24.10.4 + devDependencies: + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.2 + '@stylistic/eslint-plugin': + specifier: ^5.5.0 + version: 5.6.1(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': + specifier: ^8.46.3 + version: 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.46.3 + version: 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + cross-env: + specifier: ^10.1.0 + version: 10.1.0 + eslint: + specifier: ^9.39.1 + version: 9.39.2(jiti@2.6.1) + eslint-import-resolver-typescript: + specifier: ^4.4.4 + version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: + specifier: ^2.32.0 + version: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + jiti: + specifier: ^2.6.1 + version: 2.6.1 + tsdown: + specifier: ^0.18.4 + version: 0.18.4(synckit@0.11.11)(typescript@5.9.3)(unplugin-unused@0.5.6) + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + unplugin-unused: + specifier: ^0.5.6 + version: 0.5.6 + + apps/demo: + dependencies: + '@maigolabs/needle': + specifier: workspace:* + version: link:../../packages/needle + react: + specifier: ^19.2.0 + version: 19.2.3 + react-dom: + specifier: ^19.2.0 + version: 19.2.3(react@19.2.3) + devDependencies: + '@iconify-json/svg-spinners': + specifier: ^1.2.4 + version: 1.2.4 + '@types/node': + specifier: ^24.10.1 + version: 24.10.4 + '@types/react': + specifier: ^19.2.5 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.7) + '@vitejs/plugin-react': + specifier: ^5.1.1 + version: 5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) + unocss: + specifier: ^66.5.12 + version: 66.5.12(postcss@8.5.6)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) + vite: + specifier: ^7.2.4 + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) + vite-plugin-top-level-await: + specifier: ^1.6.0 + version: 1.6.0(rollup@4.54.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) + + apps/playground-bot: + dependencies: + '@maigolabs/needle': + specifier: workspace:* + version: link:../../packages/needle + telegraf: + specifier: ^4.16.3 + version: 4.16.3 + devDependencies: + '@types/node': + specifier: ^24.10.4 + version: 24.10.4 + + packages/needle: + dependencies: + '@patdx/kuromoji': + specifier: ^1.0.4 + version: 1.0.4 + hepburn: + specifier: ^1.2.2 + version: 1.2.2 + opencc-js: + specifier: ^1.0.5 + version: 1.0.5 + pinyin-pro: + specifier: ^3.27.0 + version: 3.27.0 + devDependencies: + '@types/hepburn': + specifier: ^1.2.2 + version: 1.2.2 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/opencc-js': + specifier: ^1.0.3 + version: 1.0.3 + jest: + specifier: ^30.2.0 + version: 30.2.0(@types/node@24.10.4) + ts-jest: + specifier: ^29.4.6 + version: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.4))(typescript@5.9.3) + +packages: + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.7': + resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.7': + resolution: {integrity: sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@emnapi/core@1.7.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@iconify-json/svg-spinners@1.2.4': + resolution: {integrity: sha512-ayn0pogFPwJA1WFZpDnoq9/hjDxN+keeCMyThaX4d3gSJ3y0mdKUxIA/b1YXWGtY9wVtZmxwcvOIeEieG4+JNg==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@30.2.0': + resolution: {integrity: sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/core@30.2.0': + resolution: {integrity: sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/diff-sequences@30.0.1': + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/environment@30.2.0': + resolution: {integrity: sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect-utils@30.2.0': + resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect@30.2.0': + resolution: {integrity: sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/fake-timers@30.2.0': + resolution: {integrity: sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/globals@30.2.0': + resolution: {integrity: sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.0.1': + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/reporters@30.2.0': + resolution: {integrity: sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/snapshot-utils@30.2.0': + resolution: {integrity: sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/source-map@30.0.1': + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-result@30.2.0': + resolution: {integrity: sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-sequencer@30.2.0': + resolution: {integrity: sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/transform@30.2.0': + resolution: {integrity: sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/types@30.2.0': + resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@oxc-project/types@0.103.0': + resolution: {integrity: sha512-bkiYX5kaXWwUessFRSoXFkGIQTmc6dLGdxuRTrC+h8PSnIdZyuXHHlLAeTmOue5Br/a0/a7dHH0Gca6eXn9MKg==} + + '@patdx/kuromoji@1.0.4': + resolution: {integrity: sha512-RyDyFh33XLxbsANNnai2h9LyR8AS1NCkqbO8Ts836g8cCb9XPBHQgzFjkja6PSGH8SKMaPUISIft7l9NUkm2PQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm64@1.0.0-beta.57': + resolution: {integrity: sha512-GoOVDy8bjw9z1K30Oo803nSzXJS/vWhFijFsW3kzvZCO8IZwFnNa6pGctmbbJstKl3Fv6UBwyjJQN6msejW0IQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.57': + resolution: {integrity: sha512-9c4FOhRGpl+PX7zBK5p17c5efpF9aSpTPgyigv57hXf5NjQUaJOOiejPLAtFiKNBIfm5Uu6yFkvLKzOafNvlTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.57': + resolution: {integrity: sha512-6RsB8Qy4LnGqNGJJC/8uWeLWGOvbRL/KG5aJ8XXpSEupg/KQtlBEiFaYU/Ma5Usj1s+bt3ItkqZYAI50kSplBA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.57': + resolution: {integrity: sha512-uA9kG7+MYkHTbqwv67Tx+5GV5YcKd33HCJIi0311iYBd25yuwyIqvJfBdt1VVB8tdOlyTb9cPAgfCki8nhwTQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': + resolution: {integrity: sha512-3KkS0cHsllT2T+Te+VZMKHNw6FPQihYsQh+8J4jkzwgvAQpbsbXmrqhkw3YU/QGRrD8qgcOvBr6z5y6Jid+rmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': + resolution: {integrity: sha512-A3/wu1RgsHhqP3rVH2+sM81bpk+Qd2XaHTl8LtX5/1LNR7QVBFBCpAoiXwjTdGnI5cMdBVi7Z1pi52euW760Fw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': + resolution: {integrity: sha512-d0kIVezTQtazpyWjiJIn5to8JlwfKITDqwsFv0Xc6s31N16CD2PC/Pl2OtKgS7n8WLOJbfqgIp5ixYzTAxCqMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': + resolution: {integrity: sha512-E199LPijo98yrLjPCmETx8EF43sZf9t3guSrLee/ej1rCCc3zDVTR4xFfN9BRAapGVl7/8hYqbbiQPTkv73kUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': + resolution: {integrity: sha512-++EQDpk/UJ33kY/BNsh7A7/P1sr/jbMuQ8cE554ZIy+tCUWCivo9zfyjDUoiMdnxqX6HLJEqqGnbGQOvzm2OMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': + resolution: {integrity: sha512-voDEBcNqxbUv/GeXKFtxXVWA+H45P/8Dec4Ii/SbyJyGvCqV1j+nNHfnFUIiRQ2Q40DwPe/djvgYBs9PpETiMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': + resolution: {integrity: sha512-bRhcF7NLlCnpkzLVlVhrDEd0KH22VbTPkPTbMjlYvqhSmarxNIq5vtlQS8qmV7LkPKHrNLWyJW/V/sOyFba26Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': + resolution: {integrity: sha512-rnDVGRks2FQ2hgJ2g15pHtfxqkGFGjJQUDWzYznEkE8Ra2+Vag9OffxdbJMZqBWXHVM0iS4dv8qSiEn7bO+n1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': + resolution: {integrity: sha512-OqIUyNid1M4xTj6VRXp/Lht/qIP8fo25QyAZlCP+p6D2ATCEhyW4ZIFLnC9zAGN/HMbXoCzvwfa8Jjg/8J4YEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + + '@rolldown/pluginutils@1.0.0-beta.57': + resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==} + + '@rollup/plugin-virtual@3.0.2': + resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.54.0': + resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.54.0': + resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.54.0': + resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.54.0': + resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.54.0': + resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.54.0': + resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.54.0': + resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.54.0': + resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.54.0': + resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.54.0': + resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.54.0': + resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.54.0': + resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sinclair/typebox@0.34.45': + resolution: {integrity: sha512-qJcFVfCa5jxBFSuv7S5WYbA8XdeCPmhnaVVfX/2Y6L8WYg8sk3XY2+6W0zH+3mq1Cz+YC7Ki66HfqX6IHAwnkg==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@13.0.5': + resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + + '@stylistic/eslint-plugin@5.6.1': + resolution: {integrity: sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=9.0.0' + + '@swc/core-darwin-arm64@1.15.8': + resolution: {integrity: sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.8': + resolution: {integrity: sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.8': + resolution: {integrity: sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.8': + resolution: {integrity: sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.8': + resolution: {integrity: sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.8': + resolution: {integrity: sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.8': + resolution: {integrity: sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.8': + resolution: {integrity: sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.8': + resolution: {integrity: sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.8': + resolution: {integrity: sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.8': + resolution: {integrity: sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + + '@swc/wasm@1.15.8': + resolution: {integrity: sha512-RG2BxGbbsjtddFCo1ghKH6A/BMXbY1eMBfpysV0lJMCpI4DZOjW1BNBnxvBt7YsYmlJtmy5UXIg9/4ekBTFFaQ==} + + '@telegraf/types@7.1.0': + resolution: {integrity: sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hepburn@1.2.2': + resolution: {integrity: sha512-DWLYgf44kFJNKYyaZdGlWkAaIaK9mPpxoiZ7BCpr020qiWpgyagWEDvSFPc8OQpN2kPh2JJTGTQE0uMR3bob3Q==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@24.10.4': + resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} + + '@types/opencc-js@1.0.3': + resolution: {integrity: sha512-TENp7YkI2hNlc4dplhivSHj0hU4DORCK56VY7rniaSfA5f87uD3uv+kPIRuH9h64TGv976iVFi4gEHZZtS2y8Q==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@typescript-eslint/eslint-plugin@8.50.1': + resolution: {integrity: sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.50.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.50.1': + resolution: {integrity: sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.50.1': + resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.50.1': + resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.50.1': + resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.50.1': + resolution: {integrity: sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.50.1': + resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.50.1': + resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.50.1': + resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.50.1': + resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unocss/astro@66.5.12': + resolution: {integrity: sha512-ynhlljsTGTHAcQHbpqxe3IXEDXjPm9IdeDWAhPet7UiGXhW230vEZ+1/OoARqLysVSVz4pPb81MDgS167Oo4Nw==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 + peerDependenciesMeta: + vite: + optional: true + + '@unocss/cli@66.5.12': + resolution: {integrity: sha512-aqjhYSiYneGfzXH6iYCY4StVN1QyeRKLhuBPkJO7gzad+1RNeqH2se1l4c5Fnvf+1rU9xRM8cw1CUSIn9UOxYQ==} + engines: {node: '>=14'} + hasBin: true + + '@unocss/config@66.5.12': + resolution: {integrity: sha512-rgV7Jj1nBZsLgk/FIFMDzKVLzIZlbKT5T0SB+odo9xZUsN5xwZZMl7I8TfZj5VxQaYqFEgSpS/Y4QCWlZ+7scQ==} + engines: {node: '>=14'} + + '@unocss/core@66.5.12': + resolution: {integrity: sha512-/m6su0OXcCYRwIMf8sobBjZTC25iBLUnQVcfyvHOJwLJzOMr8dtNmZbqTs7+Kouz40jlPF7pR+ufFrN+s5ZD7g==} + + '@unocss/extractor-arbitrary-variants@66.5.12': + resolution: {integrity: sha512-UGzHhLaaSu/YT0rmXtdoE1ttLvwWsI/RVTwNNy3QnL/y4Hvmo7T1MtG5Ri5btfqfDWPzrQLQiTvI8loGCD8lFQ==} + + '@unocss/inspector@66.5.12': + resolution: {integrity: sha512-X8Ygo842Yy0g46JNlgUGvqDhvr5BuVfFwMJeWSFJBYHzPKsZFxTU29aGxNDNDascTnNdWjZZqerPpG5esa+K2Q==} + + '@unocss/postcss@66.5.12': + resolution: {integrity: sha512-fTGrn19I45jzoP9Jsxty9/PXix3PFftj3tgrIsYjZ0R4tpCffW0s7X/iEl3GwfR45kpe5NlQ5ghskd3CFHUp+Q==} + engines: {node: '>=14'} + peerDependencies: + postcss: ^8.4.21 + + '@unocss/preset-attributify@66.5.12': + resolution: {integrity: sha512-9h/Zgiztzjp1Zf/c/DHAgm1bvzh5oLxAHhPMHmEFjNO335vEjd+PUZBzXXymKM+VoBlMz5DADpAVlTvq1N1aJA==} + + '@unocss/preset-icons@66.5.12': + resolution: {integrity: sha512-3bgkN8tTrcOSGuBcJSDrtDfBt7WU3chFjfw7zo4ign+Z0L6qANB2O62AOdOMJOxKjlppJ6a8AceHthhPZP2PDA==} + + '@unocss/preset-mini@66.5.12': + resolution: {integrity: sha512-JEyhb0vKIguaZnrGw0CXcgU6/9cWubVL8BTiLl26hsC+6vFHVSnaDHIWOJ8sTShzEQjPSxKDlAj/lGQCC2+88Q==} + + '@unocss/preset-tagify@66.5.12': + resolution: {integrity: sha512-gzQ+986lNxpqMeGxeYlDRpfrzcRt2DFjVpfmuNYD6daK4AFRbetQbhynnZyf8zwf++2YUDGf6xI9TfTTSG2QQA==} + + '@unocss/preset-typography@66.5.12': + resolution: {integrity: sha512-ckOD1coTCLXhO3oDCINqm0W292dgtYWtUYeQneNARJz3jjdNqANFPOP/y9Kpfe7WGNegVySRlDizi/L6VSdqJQ==} + + '@unocss/preset-uno@66.5.12': + resolution: {integrity: sha512-jTLhDeRqhTrCSbEgCQIg0K0PLFDtukG4eeOH5ff7Q4CtmkmsCUK0pqeXegi6ZCyatDwm72qc2WABMSqDMBdhtw==} + + '@unocss/preset-web-fonts@66.5.12': + resolution: {integrity: sha512-NSUf+H5X0jZ1PLWW6D5ldBERERpbH8VvkpJJhxNTCS54Lj5vJiZ1S06UYxBB57vuUOaHpQOGTbKUSc204LCqdw==} + + '@unocss/preset-wind3@66.5.12': + resolution: {integrity: sha512-SUzX12aQcM1ikzfv4rqwd/xuXtK5GKvhV0/JjvtG/kDTMGaKv161F2ytduj+2pBHtpJO5fUmreCD5ycTUIkxhQ==} + + '@unocss/preset-wind4@66.5.12': + resolution: {integrity: sha512-JVddnLJ6NOk7hOXA0Y8SYbQEu+JpURbE9o/IHVCkRClVRkE81b9KgJf7WQa/8KIr1O20wRRFdt9QRH4m3pZJ/A==} + + '@unocss/preset-wind@66.5.12': + resolution: {integrity: sha512-wp1/8JqQriv1AqpxskKbZYD9TNqZLQ9VBr7nNN6OkiPXBE1egEwnyb/fY+sS7IpEgwi4N9uehwQgk0/xs84SWg==} + + '@unocss/reset@66.5.12': + resolution: {integrity: sha512-wGTMu1sXVdxnzAonzHk/yUsyDyGrr8OiXCDSC7pVNep6eXhhf0g85v/Gx9FoAjZRyCppm6ePDWXtWYS8zglfCQ==} + + '@unocss/rule-utils@66.5.12': + resolution: {integrity: sha512-2UQvdjS6nD3QHLEwcXlDhXFNiOUQDuOC+itX4tjqvnjP/hj5A99WEUHemb8WEHAlHAt7khe9591+BkHHo3BX/w==} + engines: {node: '>=14'} + + '@unocss/transformer-attributify-jsx@66.5.12': + resolution: {integrity: sha512-h88voRNzSDDBf8In9A/wT0x7IlpRSnOnS62hBIcWk3Ci6w2+I/5eMFP+Rl1kY3zAz4hJ1/Ei6d9Rup3eS5037w==} + + '@unocss/transformer-compile-class@66.5.12': + resolution: {integrity: sha512-EV9LCrIfwUrevHOAhcQD/4HO5NdDzd1ALXNSDbaRxPjDVquWIRs/DujUmihyV2wqu2qEnkOumC+kyDPfZ7/u3w==} + + '@unocss/transformer-directives@66.5.12': + resolution: {integrity: sha512-oRTqR2a5du6b1md549JUX8doXcXY0XNTkiar7R0HZInF4ic0BbjG+nflifd1UtTbI1TUOtcZLQHm+/4tQqM4MA==} + + '@unocss/transformer-variant-group@66.5.12': + resolution: {integrity: sha512-iNHzliFCIVjbbmM9PVexqFhPa1t6C/6Ma3ZtkQRMq9KD2YsLvxdabvESEbjHA3iooR+bjPkiROC9whyRLWnyqQ==} + + '@unocss/vite@66.5.12': + resolution: {integrity: sha512-BSbUmUCLF3303Cu0y+gbhibXkXPpcR6lVFNN2g06EXTDNJEoS/1VKvZEUBU8RP8d1mLkv5mqN4FzdltZ+vA3uw==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + babel-jest@30.2.0: + resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-0 + + babel-plugin-istanbul@7.0.1: + resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} + engines: {node: '>=12'} + + babel-plugin-jest-hoist@30.2.0: + resolution: {integrity: sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@30.2.0: + resolution: {integrity: sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + hasBin: true + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-alloc-unsafe@1.1.0: + resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} + + buffer-alloc@1.2.0: + resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} + + buffer-fill@1.0.0: + resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001762: + resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} + engines: {node: '>=8'} + + cjs-module-lexer@2.1.1: + resolution: {integrity: sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dts-resolver@2.1.3: + resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} + engines: {node: '>=20.19.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@4.4.4: + resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} + engines: {node: ^16.17.0 || >=18.6.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} + + expect@30.2.0: + resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hepburn@1.2.2: + resolution: {integrity: sha512-DeykBc4XmfAWsnN+Y1Svi9uaQnnz21Q/ARuGWvIBxP1iUFeMIWL41DfVkgTh7tU23LFIbmIBO2Bk17BTPu0kVA==} + engines: {node: '>=4'} + + hookable@6.0.1: + resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + import-without-cache@0.2.5: + resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==} + engines: {node: '>=20.19.0'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jest-changed-files@30.2.0: + resolution: {integrity: sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-circus@30.2.0: + resolution: {integrity: sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-cli@30.2.0: + resolution: {integrity: sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@30.2.0: + resolution: {integrity: sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@30.2.0: + resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-docblock@30.2.0: + resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-each@30.2.0: + resolution: {integrity: sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-environment-node@30.2.0: + resolution: {integrity: sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-haste-map@30.2.0: + resolution: {integrity: sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-leak-detector@30.2.0: + resolution: {integrity: sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-matcher-utils@30.2.0: + resolution: {integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-message-util@30.2.0: + resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-mock@30.2.0: + resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@30.0.1: + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve-dependencies@30.2.0: + resolution: {integrity: sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve@30.2.0: + resolution: {integrity: sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runner@30.2.0: + resolution: {integrity: sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runtime@30.2.0: + resolution: {integrity: sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-snapshot@30.2.0: + resolution: {integrity: sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-util@30.2.0: + resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-validate@30.2.0: + resolution: {integrity: sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-watcher@30.2.0: + resolution: {integrity: sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-worker@30.2.0: + resolution: {integrity: sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest@30.2.0: + resolution: {integrity: sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + opencc-js@1.0.5: + resolution: {integrity: sha512-LD+1SoNnZdlRwtYTjnQdFrSVCAaYpuDqL5CkmOaHOkKoKh7mFxUicLTRVNLU5C+Jmi1vXQ3QL4jWdgSaa4sKjg==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-timeout@4.1.0: + resolution: {integrity: sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@2.0.0: + resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pinyin-pro@3.27.0: + resolution: {integrity: sha512-Osdgjwe7Rm17N2paDMM47yW+jUIUH3+0RGo8QP39ZTLpTaJVDK0T58hOLaMQJbcMmAebVuK2ePunTEVEx1clNQ==} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@30.2.0: + resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + rolldown-plugin-dts@0.20.0: + resolution: {integrity: sha512-cLAY1kN2ilTYMfZcFlGWbXnu6Nb+8uwUBsi+Mjbh4uIx7IN8uMOmJ7RxrrRgPsO4H7eSz3E+JwGoL1gyugiyUA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20250601.1' + rolldown: ^1.0.0-beta.57 + typescript: ^5.0.0 + vue-tsc: ~3.2.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0-beta.57: + resolution: {integrity: sha512-lMMxcNN71GMsSko8RyeTaFoATHkCh4IWU7pYF73ziMYjhHZWfVesC6GQ+iaJCvZmVjvgSks9Ks1aaqEkBd8udg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.54.0: + resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-compare@1.1.4: + resolution: {integrity: sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + sandwich-stream@2.0.2: + resolution: {integrity: sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==} + engines: {node: '>= 0.10'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + + telegraf@4.16.3: + resolution: {integrity: sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==} + engines: {node: ^12.20.0 || >=14.13.1} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-jest@29.4.6: + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tsdown@0.18.4: + resolution: {integrity: sha512-J/tRS6hsZTkvqmt4+xdELUCkQYDuUCXgBv0fw3ImV09WPGbEKfsPD65E+WUjSu3E7Z6tji9XZ1iWs8rbGqB/ZA==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@vitejs/devtools': '*' + publint: ^0.3.0 + typescript: ^5.0.0 + unplugin-lightningcss: ^0.4.0 + unplugin-unused: ^0.5.0 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + typescript: + optional: true + unplugin-lightningcss: + optional: true + unplugin-unused: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + unconfig-core@7.4.2: + resolution: {integrity: sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg==} + + unconfig@7.4.2: + resolution: {integrity: sha512-nrMlWRQ1xdTjSnSUqvYqJzbTBFugoqHobQj58B2bc8qxHKBBHMNNsWQFP3Cd3/JZK907voM2geYPWqD4VK3MPQ==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unocss@66.5.12: + resolution: {integrity: sha512-3WdSuM+SOjVpXDtffTuSvYTMuufpFzBehu2b4Tr7DcoIUxGouZn3mdxCLx3PiEuK0ih40Fo7Sjm+J4mccHfwLg==} + engines: {node: '>=14'} + peerDependencies: + '@unocss/webpack': 66.5.12 + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 + peerDependenciesMeta: + '@unocss/webpack': + optional: true + vite: + optional: true + + unplugin-unused@0.5.6: + resolution: {integrity: sha512-nuMhConeGhmYRFVvO3ZEJtAo6GrM09UqTJrOjKnTSkyr9zRjjkqN1M+mPZhYMN19+WHBR+JuNmq/gLo/ZajfdQ==} + engines: {node: '>=20.19.0'} + + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + unrun@0.2.21: + resolution: {integrity: sha512-VuwI4YKtwBpDvM7hCEop2Im/ezS82dliqJpkh9pvS6ve8HcUsBDvESHxMmUfImXR03GkmfdDynyrh/pUJnlguw==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + synckit: ^0.11.11 + peerDependenciesMeta: + synckit: + optional: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + vite-plugin-top-level-await@1.6.0: + resolution: {integrity: sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww==} + peerDependencies: + vite: '>=2.8' + + vite@7.3.0: + resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vue-flow-layout@0.2.0: + resolution: {integrity: sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.27.7': + dependencies: + '@babel/types': 7.28.5 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/traverse@7.27.7': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + + '@emnapi/core@1.7.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@epic-web/invariant@1.0.0': {} + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@2.6.1))': + dependencies: + eslint: 9.39.2(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@iconify-json/svg-spinners@1.2.4': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.0 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@30.2.0': + dependencies: + '@jest/types': 30.2.0 + '@types/node': 24.10.4 + chalk: 4.1.2 + jest-message-util: 30.2.0 + jest-util: 30.2.0 + slash: 3.0.0 + + '@jest/core@30.2.0': + dependencies: + '@jest/console': 30.2.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.4 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.3.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.2.0 + jest-config: 30.2.0(@types/node@24.10.4) + jest-haste-map: 30.2.0 + jest-message-util: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-resolve-dependencies: 30.2.0 + jest-runner: 30.2.0 + jest-runtime: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + jest-watcher: 30.2.0 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + '@jest/diff-sequences@30.0.1': {} + + '@jest/environment@30.2.0': + dependencies: + '@jest/fake-timers': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.4 + jest-mock: 30.2.0 + + '@jest/expect-utils@30.2.0': + dependencies: + '@jest/get-type': 30.1.0 + + '@jest/expect@30.2.0': + dependencies: + expect: 30.2.0 + jest-snapshot: 30.2.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@30.2.0': + dependencies: + '@jest/types': 30.2.0 + '@sinonjs/fake-timers': 13.0.5 + '@types/node': 24.10.4 + jest-message-util: 30.2.0 + jest-mock: 30.2.0 + jest-util: 30.2.0 + + '@jest/get-type@30.1.0': {} + + '@jest/globals@30.2.0': + dependencies: + '@jest/environment': 30.2.0 + '@jest/expect': 30.2.0 + '@jest/types': 30.2.0 + jest-mock: 30.2.0 + transitivePeerDependencies: + - supports-color + + '@jest/pattern@30.0.1': + dependencies: + '@types/node': 24.10.4 + jest-regex-util: 30.0.1 + + '@jest/reporters@30.2.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 24.10.4 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit-x: 0.2.2 + glob: 10.5.0 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + jest-message-util: 30.2.0 + jest-util: 30.2.0 + jest-worker: 30.2.0 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.45 + + '@jest/snapshot-utils@30.2.0': + dependencies: + '@jest/types': 30.2.0 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + '@jest/source-map@30.0.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@30.2.0': + dependencies: + '@jest/console': 30.2.0 + '@jest/types': 30.2.0 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@30.2.0': + dependencies: + '@jest/test-result': 30.2.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.2.0 + slash: 3.0.0 + + '@jest/transform@30.2.0': + dependencies: + '@babel/core': 7.28.5 + '@jest/types': 30.2.0 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 7.0.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.2.0 + jest-regex-util: 30.0.1 + jest-util: 30.2.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + + '@jest/types@30.2.0': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 24.10.4 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.103.0': {} + + '@patdx/kuromoji@1.0.4': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.2.9': {} + + '@polka/url@1.0.0-next.29': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.53': {} + + '@rolldown/pluginutils@1.0.0-beta.57': {} + + '@rollup/plugin-virtual@3.0.2(rollup@4.54.0)': + optionalDependencies: + rollup: 4.54.0 + + '@rollup/rollup-android-arm-eabi@4.54.0': + optional: true + + '@rollup/rollup-android-arm64@4.54.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.54.0': + optional: true + + '@rollup/rollup-darwin-x64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.54.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.54.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.54.0': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@sinclair/typebox@0.34.45': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@13.0.5': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@stylistic/eslint-plugin@5.6.1(eslint@9.39.2(jiti@2.6.1))': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/types': 8.50.1 + eslint: 9.39.2(jiti@2.6.1) + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + estraverse: 5.3.0 + picomatch: 4.0.3 + + '@swc/core-darwin-arm64@1.15.8': + optional: true + + '@swc/core-darwin-x64@1.15.8': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.8': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.8': + optional: true + + '@swc/core-linux-arm64-musl@1.15.8': + optional: true + + '@swc/core-linux-x64-gnu@1.15.8': + optional: true + + '@swc/core-linux-x64-musl@1.15.8': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.8': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.8': + optional: true + + '@swc/core-win32-x64-msvc@1.15.8': + optional: true + + '@swc/core@1.15.8': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.25 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.8 + '@swc/core-darwin-x64': 1.15.8 + '@swc/core-linux-arm-gnueabihf': 1.15.8 + '@swc/core-linux-arm64-gnu': 1.15.8 + '@swc/core-linux-arm64-musl': 1.15.8 + '@swc/core-linux-x64-gnu': 1.15.8 + '@swc/core-linux-x64-musl': 1.15.8 + '@swc/core-win32-arm64-msvc': 1.15.8 + '@swc/core-win32-ia32-msvc': 1.15.8 + '@swc/core-win32-x64-msvc': 1.15.8 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.25': + dependencies: + '@swc/counter': 0.1.3 + + '@swc/wasm@1.15.8': {} + + '@telegraf/types@7.1.0': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/estree@1.0.8': {} + + '@types/hepburn@1.2.2': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@30.0.0': + dependencies: + expect: 30.2.0 + pretty-format: 30.2.0 + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@24.10.4': + dependencies: + undici-types: 7.16.0 + + '@types/opencc-js@1.0.3': {} + + '@types/react-dom@19.2.3(@types/react@19.2.7)': + dependencies: + '@types/react': 19.2.7 + + '@types/react@19.2.7': + dependencies: + csstype: 3.2.3 + + '@types/stack-utils@2.0.3': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/type-utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 + eslint: 9.39.2(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.50.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.50.1': + dependencies: + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 + + '@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.50.1': {} + + '@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.50.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.50.1': + dependencies: + '@typescript-eslint/types': 8.50.1 + eslint-visitor-keys: 4.2.1 + + '@ungap/structured-clone@1.3.0': {} + + '@unocss/astro@66.5.12(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + '@unocss/core': 66.5.12 + '@unocss/reset': 66.5.12 + '@unocss/vite': 66.5.12(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) + optionalDependencies: + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) + + '@unocss/cli@66.5.12': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.5.12 + '@unocss/core': 66.5.12 + '@unocss/preset-uno': 66.5.12 + cac: 6.7.14 + chokidar: 5.0.0 + colorette: 2.0.20 + consola: 3.4.2 + magic-string: 0.30.21 + pathe: 2.0.3 + perfect-debounce: 2.0.0 + tinyglobby: 0.2.15 + unplugin-utils: 0.3.1 + + '@unocss/config@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + unconfig: 7.4.2 + + '@unocss/core@66.5.12': {} + + '@unocss/extractor-arbitrary-variants@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + + '@unocss/inspector@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + '@unocss/rule-utils': 66.5.12 + colorette: 2.0.20 + gzip-size: 6.0.0 + sirv: 3.0.2 + vue-flow-layout: 0.2.0 + + '@unocss/postcss@66.5.12(postcss@8.5.6)': + dependencies: + '@unocss/config': 66.5.12 + '@unocss/core': 66.5.12 + '@unocss/rule-utils': 66.5.12 + css-tree: 3.1.0 + postcss: 8.5.6 + tinyglobby: 0.2.15 + + '@unocss/preset-attributify@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + + '@unocss/preset-icons@66.5.12': + dependencies: + '@iconify/utils': 3.1.0 + '@unocss/core': 66.5.12 + ofetch: 1.5.1 + + '@unocss/preset-mini@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + '@unocss/extractor-arbitrary-variants': 66.5.12 + '@unocss/rule-utils': 66.5.12 + + '@unocss/preset-tagify@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + + '@unocss/preset-typography@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + '@unocss/rule-utils': 66.5.12 + + '@unocss/preset-uno@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + '@unocss/preset-wind3': 66.5.12 + + '@unocss/preset-web-fonts@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + ofetch: 1.5.1 + + '@unocss/preset-wind3@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + '@unocss/preset-mini': 66.5.12 + '@unocss/rule-utils': 66.5.12 + + '@unocss/preset-wind4@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + '@unocss/extractor-arbitrary-variants': 66.5.12 + '@unocss/rule-utils': 66.5.12 + + '@unocss/preset-wind@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + '@unocss/preset-wind3': 66.5.12 + + '@unocss/reset@66.5.12': {} + + '@unocss/rule-utils@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + magic-string: 0.30.21 + + '@unocss/transformer-attributify-jsx@66.5.12': + dependencies: + '@babel/parser': 7.27.7 + '@babel/traverse': 7.27.7 + '@unocss/core': 66.5.12 + transitivePeerDependencies: + - supports-color + + '@unocss/transformer-compile-class@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + + '@unocss/transformer-directives@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + '@unocss/rule-utils': 66.5.12 + css-tree: 3.1.0 + + '@unocss/transformer-variant-group@66.5.12': + dependencies: + '@unocss/core': 66.5.12 + + '@unocss/vite@66.5.12(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.5.12 + '@unocss/core': 66.5.12 + '@unocss/inspector': 66.5.12 + chokidar: 5.0.0 + magic-string: 0.30.21 + pathe: 2.0.3 + tinyglobby: 0.2.15 + unplugin-utils: 0.3.1 + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) + transitivePeerDependencies: + - supports-color + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.28.5 + pathe: 2.0.3 + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + babel-jest@30.2.0(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + '@jest/transform': 30.2.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 7.0.1 + babel-preset-jest: 30.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@7.0.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@30.2.0: + dependencies: + '@types/babel__core': 7.20.5 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.5) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.5) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.5) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.5) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.5) + + babel-preset-jest@30.2.0(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + babel-plugin-jest-hoist: 30.2.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.11: {} + + birpc@4.0.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001762 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-alloc-unsafe@1.1.0: {} + + buffer-alloc@1.2.0: + dependencies: + buffer-alloc-unsafe: 1.1.0 + buffer-fill: 1.0.0 + + buffer-fill@1.0.0: {} + + buffer-from@1.1.2: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001762: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + ci-info@4.3.1: {} + + cjs-module-lexer@2.1.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + co@4.6.0: {} + + collect-v8-coverage@1.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + convert-source-map@2.0.0: {} + + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + csstype@3.2.3: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dedent@1.7.1: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + defu@6.1.4: {} + + destr@2.0.5: {} + + detect-newline@3.1.0: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dts-resolver@2.1.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.267: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + empathic@2.0.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + escalade@3.2.0: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-import-context@0.1.9(unrs-resolver@1.11.1): + dependencies: + get-tsconfig: 4.13.0 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.11.1 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash-x: 0.2.0 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + event-target-shim@5.0.1: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit-x@0.2.2: {} + + expect@30.2.0: + dependencies: + '@jest/expect-utils': 30.2.0 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.2.0 + jest-message-util: 30.2.0 + jest-mock: 30.2.0 + jest-util: 30.2.0 + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@11.12.0: {} + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hepburn@1.2.2: {} + + hookable@6.0.1: {} + + html-escaper@2.0.2: {} + + human-signals@2.1.0: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + import-without-cache@0.2.5: {} + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.3 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-changed-files@30.2.0: + dependencies: + execa: 5.1.1 + jest-util: 30.2.0 + p-limit: 3.1.0 + + jest-circus@30.2.0: + dependencies: + '@jest/environment': 30.2.0 + '@jest/expect': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.4 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.1 + is-generator-fn: 2.1.0 + jest-each: 30.2.0 + jest-matcher-utils: 30.2.0 + jest-message-util: 30.2.0 + jest-runtime: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + p-limit: 3.1.0 + pretty-format: 30.2.0 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@30.2.0(@types/node@24.10.4): + dependencies: + '@jest/core': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.2.0(@types/node@24.10.4) + jest-util: 30.2.0 + jest-validate: 30.2.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-config@30.2.0(@types/node@24.10.4): + dependencies: + '@babel/core': 7.28.5 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 4.3.1 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.2.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-runner: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.2.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.10.4 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@30.2.0: + dependencies: + '@jest/diff-sequences': 30.0.1 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.2.0 + + jest-docblock@30.2.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@30.2.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.2.0 + chalk: 4.1.2 + jest-util: 30.2.0 + pretty-format: 30.2.0 + + jest-environment-node@30.2.0: + dependencies: + '@jest/environment': 30.2.0 + '@jest/fake-timers': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.4 + jest-mock: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + + jest-haste-map@30.2.0: + dependencies: + '@jest/types': 30.2.0 + '@types/node': 24.10.4 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.0.1 + jest-util: 30.2.0 + jest-worker: 30.2.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@30.2.0: + dependencies: + '@jest/get-type': 30.1.0 + pretty-format: 30.2.0 + + jest-matcher-utils@30.2.0: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.2.0 + pretty-format: 30.2.0 + + jest-message-util@30.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 30.2.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@30.2.0: + dependencies: + '@jest/types': 30.2.0 + '@types/node': 24.10.4 + jest-util: 30.2.0 + + jest-pnp-resolver@1.2.3(jest-resolve@30.2.0): + optionalDependencies: + jest-resolve: 30.2.0 + + jest-regex-util@30.0.1: {} + + jest-resolve-dependencies@30.2.0: + dependencies: + jest-regex-util: 30.0.1 + jest-snapshot: 30.2.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@30.2.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.2.0 + jest-pnp-resolver: 1.2.3(jest-resolve@30.2.0) + jest-util: 30.2.0 + jest-validate: 30.2.0 + slash: 3.0.0 + unrs-resolver: 1.11.1 + + jest-runner@30.2.0: + dependencies: + '@jest/console': 30.2.0 + '@jest/environment': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.4 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-haste-map: 30.2.0 + jest-leak-detector: 30.2.0 + jest-message-util: 30.2.0 + jest-resolve: 30.2.0 + jest-runtime: 30.2.0 + jest-util: 30.2.0 + jest-watcher: 30.2.0 + jest-worker: 30.2.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@30.2.0: + dependencies: + '@jest/environment': 30.2.0 + '@jest/fake-timers': 30.2.0 + '@jest/globals': 30.2.0 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.4 + chalk: 4.1.2 + cjs-module-lexer: 2.1.1 + collect-v8-coverage: 1.0.3 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.2.0 + jest-message-util: 30.2.0 + jest-mock: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@30.2.0: + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + '@babel/types': 7.28.5 + '@jest/expect-utils': 30.2.0 + '@jest/get-type': 30.1.0 + '@jest/snapshot-utils': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + expect: 30.2.0 + graceful-fs: 4.2.11 + jest-diff: 30.2.0 + jest-matcher-utils: 30.2.0 + jest-message-util: 30.2.0 + jest-util: 30.2.0 + pretty-format: 30.2.0 + semver: 7.7.3 + synckit: 0.11.11 + transitivePeerDependencies: + - supports-color + + jest-util@30.2.0: + dependencies: + '@jest/types': 30.2.0 + '@types/node': 24.10.4 + chalk: 4.1.2 + ci-info: 4.3.1 + graceful-fs: 4.2.11 + picomatch: 4.0.3 + + jest-validate@30.2.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.2.0 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.2.0 + + jest-watcher@30.2.0: + dependencies: + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.4 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.2.0 + string-length: 4.0.2 + + jest-worker@30.2.0: + dependencies: + '@types/node': 24.10.4 + '@ungap/structured-clone': 1.3.0 + jest-util: 30.2.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@30.2.0(@types/node@24.10.4): + dependencies: + '@jest/core': 30.2.0 + '@jest/types': 30.2.0 + import-local: 3.2.0 + jest-cli: 30.2.0(@types/node@24.10.4) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + math-intrinsics@1.1.0: {} + + mdn-data@2.12.2: {} + + merge-stream@2.0.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@2.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + node-fetch-native@1.6.7: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-int64@0.4.0: {} + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + obug@2.1.1: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + opencc-js@1.0.5: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-timeout@4.1.0: {} + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.6.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + pathe@2.0.3: {} + + perfect-debounce@2.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pinyin-pro@3.27.0: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + pretty-format@30.2.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + punycode@2.3.1: {} + + pure-rand@7.0.1: {} + + quansync@1.0.0: {} + + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + + react-is@18.3.1: {} + + react-refresh@0.18.0: {} + + react@19.2.3: {} + + readdirp@5.0.0: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-directory@2.1.1: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rolldown-plugin-dts@0.20.0(rolldown@1.0.0-beta.57)(typescript@5.9.3): + dependencies: + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + ast-kit: 2.2.0 + birpc: 4.0.0 + dts-resolver: 2.1.3 + get-tsconfig: 4.13.0 + obug: 2.1.1 + rolldown: 1.0.0-beta.57 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.0.0-beta.57: + dependencies: + '@oxc-project/types': 0.103.0 + '@rolldown/pluginutils': 1.0.0-beta.57 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.57 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.57 + '@rolldown/binding-darwin-x64': 1.0.0-beta.57 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.57 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.57 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.57 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.57 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.57 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.57 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.57 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.57 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.57 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.57 + + rollup@4.54.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.54.0 + '@rollup/rollup-android-arm64': 4.54.0 + '@rollup/rollup-darwin-arm64': 4.54.0 + '@rollup/rollup-darwin-x64': 4.54.0 + '@rollup/rollup-freebsd-arm64': 4.54.0 + '@rollup/rollup-freebsd-x64': 4.54.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.54.0 + '@rollup/rollup-linux-arm-musleabihf': 4.54.0 + '@rollup/rollup-linux-arm64-gnu': 4.54.0 + '@rollup/rollup-linux-arm64-musl': 4.54.0 + '@rollup/rollup-linux-loong64-gnu': 4.54.0 + '@rollup/rollup-linux-ppc64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-musl': 4.54.0 + '@rollup/rollup-linux-s390x-gnu': 4.54.0 + '@rollup/rollup-linux-x64-gnu': 4.54.0 + '@rollup/rollup-linux-x64-musl': 4.54.0 + '@rollup/rollup-openharmony-arm64': 4.54.0 + '@rollup/rollup-win32-arm64-msvc': 4.54.0 + '@rollup/rollup-win32-ia32-msvc': 4.54.0 + '@rollup/rollup-win32-x64-gnu': 4.54.0 + '@rollup/rollup-win32-x64-msvc': 4.54.0 + fsevents: 2.3.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-compare@1.1.4: + dependencies: + buffer-alloc: 1.2.0 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + sandwich-stream@2.0.2: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.3: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + sprintf-js@1.0.3: {} + + stable-hash-x@0.2.0: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + + telegraf@4.16.3: + dependencies: + '@telegraf/types': 7.1.0 + abort-controller: 3.0.0 + debug: 4.4.3 + mri: 1.2.0 + node-fetch: 2.7.0 + p-timeout: 4.1.0 + safe-compare: 1.1.4 + sandwich-stream: 2.0.2 + transitivePeerDependencies: + - encoding + - supports-color + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + tr46@0.0.3: {} + + tree-kill@1.2.2: {} + + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.4))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 30.2.0(@types/node@24.10.4) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.5 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + jest-util: 30.2.0 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsdown@0.18.4(synckit@0.11.11)(typescript@5.9.3)(unplugin-unused@0.5.6): + dependencies: + ansis: 4.2.0 + cac: 6.7.14 + defu: 6.1.4 + empathic: 2.0.0 + hookable: 6.0.1 + import-without-cache: 0.2.5 + obug: 2.1.1 + picomatch: 4.0.3 + rolldown: 1.0.0-beta.57 + rolldown-plugin-dts: 0.20.0(rolldown@1.0.0-beta.57)(typescript@5.9.3) + semver: 7.7.3 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + unconfig-core: 7.4.2 + unrun: 0.2.21(synckit@0.11.11) + optionalDependencies: + typescript: 5.9.3 + unplugin-unused: 0.5.6 + transitivePeerDependencies: + - '@ts-macro/tsc' + - '@typescript/native-preview' + - oxc-resolver + - synckit + - vue-tsc + + tslib@2.8.1: + optional: true + + tsx@4.21.0: + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + ufo@1.6.1: {} + + uglify-js@3.19.3: + optional: true + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + unconfig-core@7.4.2: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unconfig@7.4.2: + dependencies: + '@quansync/fs': 1.0.0 + defu: 6.1.4 + jiti: 2.6.1 + quansync: 1.0.0 + unconfig-core: 7.4.2 + + undici-types@7.16.0: {} + + unocss@66.5.12(postcss@8.5.6)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)): + dependencies: + '@unocss/astro': 66.5.12(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) + '@unocss/cli': 66.5.12 + '@unocss/core': 66.5.12 + '@unocss/postcss': 66.5.12(postcss@8.5.6) + '@unocss/preset-attributify': 66.5.12 + '@unocss/preset-icons': 66.5.12 + '@unocss/preset-mini': 66.5.12 + '@unocss/preset-tagify': 66.5.12 + '@unocss/preset-typography': 66.5.12 + '@unocss/preset-uno': 66.5.12 + '@unocss/preset-web-fonts': 66.5.12 + '@unocss/preset-wind': 66.5.12 + '@unocss/preset-wind3': 66.5.12 + '@unocss/preset-wind4': 66.5.12 + '@unocss/transformer-attributify-jsx': 66.5.12 + '@unocss/transformer-compile-class': 66.5.12 + '@unocss/transformer-directives': 66.5.12 + '@unocss/transformer-variant-group': 66.5.12 + '@unocss/vite': 66.5.12(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) + optionalDependencies: + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) + transitivePeerDependencies: + - postcss + - supports-color + + unplugin-unused@0.5.6: + dependencies: + empathic: 2.0.0 + escape-string-regexp: 5.0.0 + js-tokens: 9.0.1 + unplugin: 2.3.11 + + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + unrun@0.2.21(synckit@0.11.11): + dependencies: + rolldown: 1.0.0-beta.57 + optionalDependencies: + synckit: 0.11.11 + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + uuid@10.0.0: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + vite-plugin-top-level-await@1.6.0(rollup@4.54.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)): + dependencies: + '@rollup/plugin-virtual': 3.0.2(rollup@4.54.0) + '@swc/core': 1.15.8 + '@swc/wasm': 1.15.8 + uuid: 10.0.0 + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) + transitivePeerDependencies: + - '@swc/helpers' + - rollup + + vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.54.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.4 + fsevents: 2.3.3 + jiti: 2.6.1 + tsx: 4.21.0 + + vue-flow-layout@0.2.0: {} + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + webidl-conversions@3.0.1: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..0969cee --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,9 @@ +packages: + - packages/* + - apps/* + +nodeLinker: hoisted + +onlyBuiltDependencies: + - '@swc/core' + - unrs-resolver