diff --git a/.onedev-buildspec.yml b/.onedev-buildspec.yml index d1e6dc2..193f40e 100644 --- a/.onedev-buildspec.yml +++ b/.onedev-buildspec.yml @@ -1,17 +1,17 @@ version: 39 jobs: -- name: Github Push - steps: - - !PushRepository - name: gc-alexandria - remoteUrl: https://github.com/ShadowySupercode/gc-alexandria - passwordSecret: github_access_token - force: false - condition: ALL_PREVIOUS_STEPS_WERE_SUCCESSFUL - triggers: - - !BranchUpdateTrigger {} - - !TagCreateTrigger {} - retryCondition: never - maxRetries: 3 - retryDelay: 30 - timeout: 14400 \ No newline at end of file + - name: Github Push + steps: + - !PushRepository + name: gc-alexandria + remoteUrl: https://github.com/ShadowySupercode/gc-alexandria + passwordSecret: github_access_token + force: false + condition: ALL_PREVIOUS_STEPS_WERE_SUCCESSFUL + triggers: + - !BranchUpdateTrigger {} + - !TagCreateTrigger {} + retryCondition: never + maxRetries: 3 + retryDelay: 30 + timeout: 14400 diff --git a/.prettierrc b/.prettierrc index 5cce348..a05eb6c 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,3 @@ { - "plugins":["prettier-plugin-svelte"] + "plugins": ["prettier-plugin-svelte"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index e06c2f4..3ff535b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,4 +11,4 @@ "files.associations": { "*.svelte": "svelte" } -} \ No newline at end of file +} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index f11d306..0000000 --- a/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM node:23-alpine AS build - -WORKDIR /app - -COPY . ./ -COPY package.json ./ -COPY package-lock.json ./ -RUN npm install -RUN npm run build - -EXPOSE 80 -FROM nginx:1.27.4 -COPY --from=build /app/build /usr/share/nginx/html \ No newline at end of file diff --git a/README.md b/README.md index ed56197..5615dac 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Roman scrolls](https://i.nostr.build/M5qXa.jpg) +![Roman scrolls](https://i.nostr.build/M5qXa.jpg) # Alexandria @@ -18,45 +18,53 @@ You can also contact us [on Nostr](https://next-alexandria.gitcitadel.eu/events? Make sure that you have [Node.js](https://nodejs.org/en/download/package-manager) (v22 or above) or [Deno](https://docs.deno.com/runtime/getting_started/installation/) (v2) installed. Once you've cloned this repo, install dependencies with NPM: + ```bash npm install ``` or with Deno: + ```bash deno install ``` then start a development server with Node: + ```bash npm run dev ``` or with Deno: + ```bash deno task dev ``` ## Building -Alexandria is configured to run on a Node server. The [Node adapter](https://svelte.dev/docs/kit/adapter-node) works on Deno as well. +Alexandria is configured to run on a Node server. The [Node adapter](https://svelte.dev/docs/kit/adapter-node) works on Deno as well. To build a production version of your app with Node, use: + ```bash npm run build ``` or with Deno: + ```bash deno task build ``` You can preview the (non-static) production build with: + ```bash npm run preview ``` or with Deno: + ```bash deno task preview ``` @@ -66,11 +74,13 @@ deno task preview This docker container performs the build. To build the container: + ```bash docker build . -t gc-alexandria ``` To run the container, in detached mode (-d): + ```bash docker run -d --rm --name=gc-alexandria -p 4174:80 gc-alexandria ``` @@ -83,7 +93,7 @@ If you want to see the container process (assuming it's the last process to star docker ps -l ``` -which should return something like: +which should return something like: ```bash CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES @@ -92,32 +102,36 @@ CONTAINER ID IMAGE COMMAND CREATED STATUS ## Docker + Deno -This application is configured to use the Deno runtime. A Docker container is provided to handle builds and deployments. +This application is configured to use the Deno runtime. A Docker container is provided to handle builds and deployments. To build the app for local development: + ```bash docker build -t local-alexandria -f Dockerfile.local . ``` To run the local development build: + ```bash docker run -d -p 3000:3000 local-alexandria ``` ## Testing -*These tests are under development, but will run. They will later be added to the container.* +_These tests are under development, but will run. They will later be added to the container._ To run the Vitest suite we've built, install the program locally and run the tests. + ```bash npm run test ``` For the Playwright end-to-end (e2e) tests: + ```bash npx playwright test ``` ## Markup Support -Alexandria supports both Markdown and AsciiDoc markup for different content types. For a detailed list of supported tags and features in the basic and advanced markdown parsers, as well as information about AsciiDoc usage for publications and wikis, see [MarkupInfo.md](./src/lib/utils/markup/MarkupInfo.md). \ No newline at end of file +Alexandria supports both Markdown and AsciiDoc markup for different content types. For a detailed list of supported tags and features in the basic and advanced markdown parsers, as well as information about AsciiDoc usage for publications and wikis, see [MarkupInfo.md](./src/lib/utils/markup/MarkupInfo.md). diff --git a/deno.json b/deno.json index e53d975..8b474cb 100644 --- a/deno.json +++ b/deno.json @@ -4,4 +4,4 @@ "allowJs": true, "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"] } -} \ No newline at end of file +} diff --git a/deno.lock b/deno.lock index 9206145..6e9e011 100644 --- a/deno.lock +++ b/deno.lock @@ -3201,8 +3201,10 @@ "npm:@types/d3@^7.4.3", "npm:@types/he@1.2", "npm:@types/node@22", + "npm:@types/qrcode@^1.5.5", "npm:asciidoctor@3.0", "npm:autoprefixer@10", + "npm:bech32@2", "npm:d3@^7.9.0", "npm:eslint-plugin-svelte@2", "npm:flowbite-svelte-icons@2.1", @@ -3217,6 +3219,7 @@ "npm:postcss@8", "npm:prettier-plugin-svelte@3", "npm:prettier@3", + "npm:qrcode@^1.5.4", "npm:svelte-check@4", "npm:svelte@5", "npm:tailwind-merge@^3.3.0", diff --git a/docker-compose.yaml b/docker-compose.yaml index 4c4dfb5..ad37163 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,4 @@ -version: '3' +version: "3" services: wikinostr: diff --git a/import_map.json b/import_map.json index 4c3af16..811c3ed 100644 --- a/import_map.json +++ b/import_map.json @@ -16,4 +16,4 @@ "flowbite-svelte-icons": "npm:flowbite-svelte-icons@2.1.x", "child_process": "node:child_process" } -} \ No newline at end of file +} diff --git a/maintainers.yaml b/maintainers.yaml index 42e1f9b..7b8d3c9 100644 --- a/maintainers.yaml +++ b/maintainers.yaml @@ -1,8 +1,8 @@ identifier: Alexandria maintainers: -- npub1m3xdppkd0njmrqe2ma8a6ys39zvgp5k8u22mev8xsnqp4nh80srqhqa5sf -- npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z -- npub1wqfzz2p880wq0tumuae9lfwyhs8uz35xd0kr34zrvrwyh3kvrzuskcqsyn + - npub1m3xdppkd0njmrqe2ma8a6ys39zvgp5k8u22mev8xsnqp4nh80srqhqa5sf + - npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z + - npub1wqfzz2p880wq0tumuae9lfwyhs8uz35xd0kr34zrvrwyh3kvrzuskcqsyn relays: -- wss://theforest.nostr1.com -- wss://thecitadel.nostr1.com + - wss://theforest.nostr1.com + - wss://thecitadel.nostr1.com diff --git a/package-lock.json b/package-lock.json index b631ab9..983a123 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "alexandria", "version": "0.0.6", "dependencies": { - "@nostr-dev-kit/ndk": "2.11.x", + "@nostr-dev-kit/ndk": "^2.14.32", "@nostr-dev-kit/ndk-cache-dexie": "2.5.x", "@popperjs/core": "2.11.x", "@tailwindcss/forms": "0.5.x", @@ -20,6 +20,7 @@ "highlight.js": "^11.11.1", "node-emoji": "^2.2.0", "nostr-tools": "2.10.x", + "plantuml-encoder": "^1.4.0", "qrcode": "^1.5.4" }, "devDependencies": { @@ -55,6 +56,8 @@ }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "license": "MIT", "engines": { "node": ">=10" @@ -65,6 +68,8 @@ }, "node_modules/@ampproject/remapping": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -77,6 +82,8 @@ }, "node_modules/@asciidoctor/cli": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@asciidoctor/cli/-/cli-4.0.0.tgz", + "integrity": "sha512-x2T9gW42921Zd90juEagtbViPZHNP2MWf0+6rJEkOzW7E9m3TGJtz+Guye9J0gwrpZsTMGCpfYMQy1We3X7osg==", "license": "MIT", "dependencies": { "yargs": "17.3.1" @@ -95,6 +102,8 @@ }, "node_modules/@asciidoctor/core": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-3.0.4.tgz", + "integrity": "sha512-41SDMi7iRRBViPe0L6VWFTe55bv6HEOJeRqMj5+E5wB1YPdUPuTucL4UAESPZM6OWmn4t/5qM5LusXomFUVwVQ==", "license": "MIT", "dependencies": { "@asciidoctor/opal-runtime": "3.0.1", @@ -107,6 +116,8 @@ }, "node_modules/@asciidoctor/opal-runtime": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@asciidoctor/opal-runtime/-/opal-runtime-3.0.1.tgz", + "integrity": "sha512-iW7ACahOG0zZft4A/4CqDcc7JX+fWRNjV5tFAVkNCzwZD+EnFolPaUOPYt8jzadc0+Bgd80cQTtRMQnaaV1kkg==", "license": "MIT", "dependencies": { "glob": "8.1.0", @@ -118,6 +129,8 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -125,16 +138,20 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.2", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -144,7 +161,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.1", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -154,8 +173,282 @@ "node": ">=6.9.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -169,8 +462,112 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -188,6 +585,8 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -199,6 +598,8 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", "peer": true, @@ -207,7 +608,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -221,7 +624,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -231,6 +636,8 @@ }, "node_modules/@eslint/core": { "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -243,6 +650,8 @@ }, "node_modules/@eslint/eslintrc": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "peer": true, @@ -265,7 +674,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.27.0", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", + "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", "dev": true, "license": "MIT", "peer": true, @@ -278,50 +689,76 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", "dev": true, "license": "Apache-2.0", "peer": true, + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@eslint/core": "^0.14.0", - "levn": "^0.4.1" + "@types/json-schema": "^7.0.15" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@floating-ui/core": { - "version": "1.7.0", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.0", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.0", - "@floating-ui/utils": "^0.2.9" + "@floating-ui/core": "^1.7.2", + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.9", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "dev": true, "license": "MIT" }, "node_modules/@humanfs/core": { "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -331,6 +768,8 @@ }, "node_modules/@humanfs/node": { "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -344,6 +783,8 @@ }, "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -357,6 +798,8 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -370,6 +813,8 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -383,6 +828,8 @@ }, "node_modules/@isaacs/cliui": { "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -398,6 +845,8 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { "node": ">=12" @@ -408,6 +857,8 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -418,10 +869,14 @@ }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -437,6 +892,8 @@ }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -450,6 +907,8 @@ }, "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -464,37 +923,34 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -503,13 +959,17 @@ }, "node_modules/@noble/ciphers": { "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/curves": { - "version": "1.9.1", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", + "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", "license": "MIT", "dependencies": { "@noble/hashes": "1.8.0" @@ -523,6 +983,8 @@ }, "node_modules/@noble/hashes": { "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -532,7 +994,9 @@ } }, "node_modules/@noble/secp256k1": { - "version": "2.2.3", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.3.0.tgz", + "integrity": "sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" @@ -540,6 +1004,8 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -551,6 +1017,8 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "license": "MIT", "engines": { "node": ">= 8" @@ -558,6 +1026,8 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -568,7 +1038,9 @@ } }, "node_modules/@nostr-dev-kit/ndk": { - "version": "2.11.2", + "version": "2.14.32", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.14.32.tgz", + "integrity": "sha512-LUBO35RCB9/emBYsXNDece7m/WO2rGYR8j4SD0Crb3z8GcKTJq6P8OjpZ6+Kr+sLNo8N0uL07XxtAvEBnp2OqQ==", "license": "MIT", "dependencies": { "@noble/curves": "^1.6.0", @@ -577,18 +1049,20 @@ "@scure/base": "^1.1.9", "debug": "^4.3.6", "light-bolt11-decoder": "^3.2.0", - "nostr-tools": "^2.7.1", - "tseep": "^1.2.2", - "typescript-lru-cache": "^2.0.0", - "utf8-buffer": "^1.0.0", - "websocket-polyfill": "^0.0.3" + "tseep": "^1.3.1", + "typescript-lru-cache": "^2" }, "engines": { "node": ">=16" + }, + "peerDependencies": { + "nostr-tools": "^2" } }, "node_modules/@nostr-dev-kit/ndk-cache-dexie": { "version": "2.5.15", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.15.tgz", + "integrity": "sha512-6icRT+tqob0tWqGjQqoaeNDimfO+0gaooG9kch5OQcqlkQh2u1/ySUa47SC/m2E8q3MQVQbU66r8ZjssN2BVmw==", "license": "MIT", "dependencies": { "@nostr-dev-kit/ndk": "2.12.2", @@ -600,6 +1074,8 @@ }, "node_modules/@nostr-dev-kit/ndk-cache-dexie/node_modules/@nostr-dev-kit/ndk": { "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.12.2.tgz", + "integrity": "sha512-uvautgwbpk3AgddoFpew67/FiaV/zpKwwvSnjCvbE/tAdJBpUUS+VjWR5WfUnJvxTy/ZZpPW+X2TkwVFHhUdvA==", "license": "MIT", "dependencies": { "@noble/curves": "^1.6.0", @@ -618,131 +1094,415 @@ "node": ">=16" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.53.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.2.tgz", + "integrity": "sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.53.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.6.tgz", + "integrity": "sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", + "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.2.tgz", + "integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.2.tgz", + "integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.2.tgz", + "integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.2.tgz", + "integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.2.tgz", + "integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.2.tgz", + "integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.2.tgz", + "integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.2.tgz", + "integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.2.tgz", + "integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.2.tgz", + "integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.2.tgz", + "integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.2.tgz", + "integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.2.tgz", + "integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", "optional": true, - "engines": { - "node": ">=14" - } + "os": [ + "linux" + ] }, - "node_modules/@playwright/test": { - "version": "1.52.0", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.2.tgz", + "integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.52.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.2.tgz", + "integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.3", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz", + "integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.2.tgz", + "integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.1", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.2.tgz", + "integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.4", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.2.tgz", + "integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.2.tgz", + "integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==", "cpu": [ "x64" ], @@ -750,11 +1510,13 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, "node_modules/@scure/base": { - "version": "1.2.5", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" @@ -762,6 +1524,8 @@ }, "node_modules/@scure/bip32": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", "license": "MIT", "dependencies": { "@noble/curves": "~1.1.0", @@ -774,6 +1538,8 @@ }, "node_modules/@scure/bip32/node_modules/@noble/curves": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", "license": "MIT", "dependencies": { "@noble/hashes": "1.3.1" @@ -784,6 +1550,8 @@ }, "node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", "license": "MIT", "engines": { "node": ">= 16" @@ -794,6 +1562,8 @@ }, "node_modules/@scure/bip32/node_modules/@noble/hashes": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", "license": "MIT", "engines": { "node": ">= 16" @@ -804,6 +1574,8 @@ }, "node_modules/@scure/bip32/node_modules/@scure/base": { "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" @@ -811,6 +1583,8 @@ }, "node_modules/@scure/bip39": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", "license": "MIT", "dependencies": { "@noble/hashes": "~1.3.0", @@ -822,6 +1596,8 @@ }, "node_modules/@scure/bip39/node_modules/@noble/hashes": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", "license": "MIT", "engines": { "node": ">= 16" @@ -832,6 +1608,8 @@ }, "node_modules/@scure/bip39/node_modules/@scure/base": { "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" @@ -839,6 +1617,8 @@ }, "node_modules/@sindresorhus/is": { "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "license": "MIT", "engines": { "node": ">=10" @@ -849,6 +1629,8 @@ }, "node_modules/@sveltejs/acorn-typescript": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -857,6 +1639,8 @@ }, "node_modules/@sveltejs/adapter-auto": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz", + "integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==", "dev": true, "license": "MIT", "dependencies": { @@ -868,6 +1652,8 @@ }, "node_modules/@sveltejs/adapter-node": { "version": "5.2.12", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz", + "integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -882,6 +1668,8 @@ }, "node_modules/@sveltejs/adapter-static": { "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz", + "integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -889,7 +1677,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.21.0", + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.22.2.tgz", + "integrity": "sha512-2MvEpSYabUrsJAoq5qCOBGAlkICjfjunrnLcx3YAk2XV7TvAIhomlKsAgR4H/4uns5rAfYmj7Wet5KRtc8dPIg==", "dev": true, "license": "MIT", "dependencies": { @@ -904,7 +1694,8 @@ "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", - "sirv": "^3.0.0" + "sirv": "^3.0.0", + "vitefu": "^1.0.6" }, "bin": { "svelte-kit": "svelte-kit.js" @@ -913,13 +1704,15 @@ "node": ">=18.13" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3 || ^6.0.0" + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" } }, "node_modules/@sveltejs/vite-plugin-svelte": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz", + "integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", "dev": true, "license": "MIT", "dependencies": { @@ -940,6 +1733,8 @@ }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz", + "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", "dev": true, "license": "MIT", "dependencies": { @@ -956,6 +1751,8 @@ }, "node_modules/@tailwindcss/forms": { "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", + "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", "license": "MIT", "dependencies": { "mini-svg-data-uri": "^1.2.3" @@ -966,6 +1763,8 @@ }, "node_modules/@tailwindcss/typography": { "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", "license": "MIT", "dependencies": { "lodash.castarray": "^4.4.0", @@ -977,13 +1776,27 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "dev": true, "license": "MIT" }, "node_modules/@types/d3": { "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", "dev": true, "license": "MIT", "dependencies": { @@ -1021,11 +1834,15 @@ }, "node_modules/@types/d3-array": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-axis": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1034,6 +1851,8 @@ }, "node_modules/@types/d3-brush": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", "dev": true, "license": "MIT", "dependencies": { @@ -1042,16 +1861,22 @@ }, "node_modules/@types/d3-chord": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-contour": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", "dev": true, "license": "MIT", "dependencies": { @@ -1061,16 +1886,22 @@ }, "node_modules/@types/d3-delaunay": { "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-dispatch": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-drag": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1079,16 +1910,22 @@ }, "node_modules/@types/d3-dsv": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-fetch": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", "dev": true, "license": "MIT", "dependencies": { @@ -1097,16 +1934,22 @@ }, "node_modules/@types/d3-force": { "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-format": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-geo": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1115,11 +1958,15 @@ }, "node_modules/@types/d3-hierarchy": { "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "dev": true, "license": "MIT", "dependencies": { @@ -1128,26 +1975,36 @@ }, "node_modules/@types/d3-path": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-polygon": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-quadtree": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-random": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "dev": true, "license": "MIT", "dependencies": { @@ -1156,16 +2013,22 @@ }, "node_modules/@types/d3-scale-chromatic": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-selection": { "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-shape": { "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "dev": true, "license": "MIT", "dependencies": { @@ -1174,21 +2037,29 @@ }, "node_modules/@types/d3-time": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-time-format": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-transition": { "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "dev": true, "license": "MIT", "dependencies": { @@ -1197,6 +2068,8 @@ }, "node_modules/@types/d3-zoom": { "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", "dev": true, "license": "MIT", "dependencies": { @@ -1204,29 +2077,46 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.7", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/geojson": { "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "dev": true, "license": "MIT" }, "node_modules/@types/he": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz", + "integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT", "peer": true }, "node_modules/@types/node": { - "version": "22.15.18", + "version": "22.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.1.tgz", + "integrity": "sha512-oaNE4MzsA6uO7HcsjUvqzz19lYIRsV6I1Dc6iOvgwYYDiOeF7/9b2E/PE0UW2ccwpgWPVUedjltYXQXVKFd4EA==", "dev": true, "license": "MIT", "dependencies": { @@ -1245,16 +2135,21 @@ }, "node_modules/@types/resolve": { "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true, "license": "MIT" }, "node_modules/@vitest/expect": { - "version": "3.1.3", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1263,11 +2158,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.3", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", + "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1276,7 +2173,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1289,6 +2186,8 @@ }, "node_modules/@vitest/mocker/node_modules/estree-walker": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -1296,7 +2195,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -1307,23 +2208,28 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -1332,23 +2238,27 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.3", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.1.3", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -1357,15 +2267,21 @@ }, "node_modules/@yr/monotone-cubic-spline": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", "dev": true, "license": "MIT" }, "node_modules/a-sync-waterfall": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", "license": "MIT" }, "node_modules/acorn": { - "version": "8.14.1", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1377,6 +2293,8 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1385,6 +2303,8 @@ }, "node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "peer": true, @@ -1401,6 +2321,8 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -1408,6 +2330,8 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1421,10 +2345,14 @@ }, "node_modules/any-promise": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -1436,6 +2364,8 @@ }, "node_modules/anymatch/node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -1446,6 +2376,8 @@ }, "node_modules/apexcharts": { "version": "3.54.1", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.54.1.tgz", + "integrity": "sha512-E4et0h/J1U3r3EwS/WlqJCQIbepKbp6wGUmaAwJOMjHUP4Ci0gxanLa7FR3okx6p9coi4st6J853/Cb1NP0vpA==", "dev": true, "license": "MIT", "dependencies": { @@ -1460,16 +2392,22 @@ }, "node_modules/arg": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0", "peer": true }, "node_modules/aria-query": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1478,10 +2416,14 @@ }, "node_modules/asap": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, "node_modules/asciidoctor": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/asciidoctor/-/asciidoctor-3.0.4.tgz", + "integrity": "sha512-hIc0Bx73wePxtic+vWBHOIgMfKSNiCmRz7BBfkyykXATrw20YGd5a3CozCHvqEPH+Wxp5qKD4aBsgtokez8nEA==", "license": "MIT", "dependencies": { "@asciidoctor/cli": "4.0.0", @@ -1502,10 +2444,14 @@ }, "node_modules/assert-never": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", "license": "MIT" }, "node_modules/assertion-error": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -1514,10 +2460,14 @@ }, "node_modules/async": { "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, "node_modules/autoprefixer": { "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", "dev": true, "funding": [ { @@ -1554,6 +2504,8 @@ }, "node_modules/axobject-query": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1562,6 +2514,8 @@ }, "node_modules/babel-walk": { "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", "license": "MIT", "dependencies": { "@babel/types": "^7.9.6" @@ -1572,6 +2526,8 @@ }, "node_modules/balanced-match": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/bech32": { @@ -1582,6 +2538,8 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "license": "MIT", "engines": { "node": ">=8" @@ -1591,7 +2549,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1600,6 +2560,8 @@ }, "node_modules/braces": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -1609,7 +2571,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "dev": true, "funding": [ { @@ -1627,8 +2591,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -1641,6 +2605,8 @@ }, "node_modules/bufferutil": { "version": "4.0.9", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1652,6 +2618,8 @@ }, "node_modules/cac": { "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, "license": "MIT", "engines": { @@ -1660,6 +2628,8 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1671,6 +2641,8 @@ }, "node_modules/call-bound": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1685,6 +2657,8 @@ }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "peer": true, @@ -1703,13 +2677,17 @@ }, "node_modules/camelcase-css": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", "dev": true, "funding": [ { @@ -1728,7 +2706,9 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.2.0", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", "dev": true, "license": "MIT", "dependencies": { @@ -1739,11 +2719,13 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -1758,6 +2740,8 @@ }, "node_modules/char-regex": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "license": "MIT", "engines": { "node": ">=10" @@ -1765,6 +2749,8 @@ }, "node_modules/character-parser": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", "license": "MIT", "dependencies": { "is-regex": "^1.0.3" @@ -1772,6 +2758,8 @@ }, "node_modules/check-error": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, "license": "MIT", "engines": { @@ -1816,6 +2804,8 @@ }, "node_modules/cliui": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -1825,6 +2815,8 @@ }, "node_modules/clsx": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "dev": true, "license": "MIT", "engines": { @@ -1833,6 +2825,8 @@ }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1843,10 +2837,14 @@ }, "node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/commander": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "license": "MIT", "engines": { "node": ">= 10" @@ -1854,15 +2852,21 @@ }, "node_modules/commondir": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true, "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, "node_modules/constantinople": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.6.0", @@ -1871,6 +2875,8 @@ }, "node_modules/cookie": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, "license": "MIT", "engines": { @@ -1879,6 +2885,8 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1891,6 +2899,8 @@ }, "node_modules/cssesc": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -1901,6 +2911,8 @@ }, "node_modules/d": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", "license": "ISC", "dependencies": { "es5-ext": "^0.10.64", @@ -1912,6 +2924,8 @@ }, "node_modules/d3": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", "license": "ISC", "dependencies": { "d3-array": "3", @@ -1951,6 +2965,8 @@ }, "node_modules/d3-array": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -1961,6 +2977,8 @@ }, "node_modules/d3-axis": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", "license": "ISC", "engines": { "node": ">=12" @@ -1968,6 +2986,8 @@ }, "node_modules/d3-brush": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -1982,6 +3002,8 @@ }, "node_modules/d3-chord": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", "license": "ISC", "dependencies": { "d3-path": "1 - 3" @@ -1992,6 +3014,8 @@ }, "node_modules/d3-color": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "engines": { "node": ">=12" @@ -1999,6 +3023,8 @@ }, "node_modules/d3-contour": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", "license": "ISC", "dependencies": { "d3-array": "^3.2.0" @@ -2009,6 +3035,8 @@ }, "node_modules/d3-delaunay": { "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "license": "ISC", "dependencies": { "delaunator": "5" @@ -2019,6 +3047,8 @@ }, "node_modules/d3-dispatch": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", "license": "ISC", "engines": { "node": ">=12" @@ -2026,6 +3056,8 @@ }, "node_modules/d3-drag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -2037,6 +3069,8 @@ }, "node_modules/d3-dsv": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", "license": "ISC", "dependencies": { "commander": "7", @@ -2060,6 +3094,8 @@ }, "node_modules/d3-ease": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "license": "BSD-3-Clause", "engines": { "node": ">=12" @@ -2067,6 +3103,8 @@ }, "node_modules/d3-fetch": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", "license": "ISC", "dependencies": { "d3-dsv": "1 - 3" @@ -2077,6 +3115,8 @@ }, "node_modules/d3-force": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -2089,6 +3129,8 @@ }, "node_modules/d3-format": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", "license": "ISC", "engines": { "node": ">=12" @@ -2096,6 +3138,8 @@ }, "node_modules/d3-geo": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", "license": "ISC", "dependencies": { "d3-array": "2.5.0 - 3" @@ -2106,6 +3150,8 @@ }, "node_modules/d3-hierarchy": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", "license": "ISC", "engines": { "node": ">=12" @@ -2113,6 +3159,8 @@ }, "node_modules/d3-interpolate": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -2123,6 +3171,8 @@ }, "node_modules/d3-path": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "license": "ISC", "engines": { "node": ">=12" @@ -2130,6 +3180,8 @@ }, "node_modules/d3-polygon": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", "license": "ISC", "engines": { "node": ">=12" @@ -2137,6 +3189,8 @@ }, "node_modules/d3-quadtree": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", "license": "ISC", "engines": { "node": ">=12" @@ -2144,6 +3198,8 @@ }, "node_modules/d3-random": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", "license": "ISC", "engines": { "node": ">=12" @@ -2151,6 +3207,8 @@ }, "node_modules/d3-scale": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", @@ -2165,6 +3223,8 @@ }, "node_modules/d3-scale-chromatic": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "license": "ISC", "dependencies": { "d3-color": "1 - 3", @@ -2176,6 +3236,8 @@ }, "node_modules/d3-selection": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", "engines": { "node": ">=12" @@ -2183,6 +3245,8 @@ }, "node_modules/d3-shape": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "license": "ISC", "dependencies": { "d3-path": "^3.1.0" @@ -2193,6 +3257,8 @@ }, "node_modules/d3-time": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "license": "ISC", "dependencies": { "d3-array": "2 - 3" @@ -2203,6 +3269,8 @@ }, "node_modules/d3-time-format": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "license": "ISC", "dependencies": { "d3-time": "1 - 3" @@ -2213,6 +3281,8 @@ }, "node_modules/d3-timer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", "engines": { "node": ">=12" @@ -2220,6 +3290,8 @@ }, "node_modules/d3-transition": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", "license": "ISC", "dependencies": { "d3-color": "1 - 3", @@ -2237,6 +3309,8 @@ }, "node_modules/d3-zoom": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -2251,6 +3325,8 @@ }, "node_modules/debug": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2275,6 +3351,8 @@ }, "node_modules/deep-eql": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", "engines": { @@ -2283,12 +3361,16 @@ }, "node_modules/deep-is": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT", "peer": true }, "node_modules/deepmerge": { "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "license": "MIT", "engines": { @@ -2297,6 +3379,8 @@ }, "node_modules/delaunator": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -2304,15 +3388,21 @@ }, "node_modules/devalue": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", "dev": true, "license": "MIT" }, "node_modules/dexie": { "version": "4.0.11", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.11.tgz", + "integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==", "license": "Apache-2.0" }, "node_modules/didyoumean": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, "node_modules/dijkstrajs": { @@ -2323,14 +3413,20 @@ }, "node_modules/dlv": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, "node_modules/doctypes": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", "license": "MIT" }, "node_modules/dunder-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2343,10 +3439,14 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, "node_modules/ejs": { "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" @@ -2359,20 +3459,28 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.155", + "version": "1.5.180", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.180.tgz", + "integrity": "sha512-ED+GEyEh3kYMwt2faNmgMB0b8O5qtATGgR4RmRsIp4T6p7B8vdMbIedYndnvZfsaXvSzegtpfqRMDNCjjiSduA==", "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/emojilib": { "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", "license": "MIT" }, "node_modules/es-define-property": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -2380,6 +3488,8 @@ }, "node_modules/es-errors": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -2387,11 +3497,15 @@ }, "node_modules/es-module-lexer": { "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2402,6 +3516,8 @@ }, "node_modules/es5-ext": { "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -2416,6 +3532,8 @@ }, "node_modules/es6-iterator": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "license": "MIT", "dependencies": { "d": "1", @@ -2425,6 +3543,8 @@ }, "node_modules/es6-symbol": { "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "license": "ISC", "dependencies": { "d": "^1.0.2", @@ -2436,6 +3556,8 @@ }, "node_modules/esbuild": { "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2473,6 +3595,8 @@ }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -2480,6 +3604,8 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "peer": true, @@ -2491,18 +3617,20 @@ } }, "node_modules/eslint": { - "version": "9.27.0", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", + "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.27.0", + "@eslint/js": "9.30.1", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2514,9 +3642,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2552,6 +3680,8 @@ }, "node_modules/eslint-compat-utils": { "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2566,6 +3696,8 @@ }, "node_modules/eslint-plugin-svelte": { "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", "dev": true, "license": "MIT", "dependencies": { @@ -2599,6 +3731,8 @@ }, "node_modules/eslint-plugin-svelte/node_modules/lilconfig": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, "license": "MIT", "engines": { @@ -2607,6 +3741,8 @@ }, "node_modules/eslint-plugin-svelte/node_modules/postcss-load-config": { "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", "dev": true, "license": "MIT", "dependencies": { @@ -2635,6 +3771,8 @@ }, "node_modules/eslint-plugin-svelte/node_modules/postcss-selector-parser": { "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", "dependencies": { @@ -2656,7 +3794,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -2672,7 +3812,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -2685,11 +3827,15 @@ }, "node_modules/esm-env": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "dev": true, "license": "MIT" }, "node_modules/esniff": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", "license": "ISC", "dependencies": { "d": "^1.0.1", @@ -2702,14 +3848,16 @@ } }, "node_modules/espree": { - "version": "10.3.0", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "peer": true, "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2720,6 +3868,8 @@ }, "node_modules/esquery": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", "peer": true, @@ -2731,7 +3881,9 @@ } }, "node_modules/esrap": { - "version": "1.4.6", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", + "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", "dev": true, "license": "MIT", "dependencies": { @@ -2740,6 +3892,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2751,6 +3905,8 @@ }, "node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2759,11 +3915,15 @@ }, "node_modules/estree-walker": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, "license": "MIT" }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2772,6 +3932,8 @@ }, "node_modules/event-emitter": { "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", "license": "MIT", "dependencies": { "d": "1", @@ -2779,7 +3941,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.1", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2788,6 +3952,8 @@ }, "node_modules/ext": { "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "license": "ISC", "dependencies": { "type": "^2.7.2" @@ -2795,12 +3961,16 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT", "peer": true }, "node_modules/fast-glob": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -2815,6 +3985,8 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -2825,25 +3997,33 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/fastq": { "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/fdir": { - "version": "6.4.4", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2857,6 +4037,8 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "peer": true, @@ -2869,13 +4051,17 @@ }, "node_modules/filelist": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2883,6 +4069,8 @@ }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -2893,6 +4081,8 @@ }, "node_modules/fill-range": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -2903,6 +4093,8 @@ }, "node_modules/find-up": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "peer": true, @@ -2919,6 +4111,8 @@ }, "node_modules/flat-cache": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "peer": true, @@ -2932,12 +4126,16 @@ }, "node_modules/flatted": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC", "peer": true }, "node_modules/flowbite": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.5.2.tgz", + "integrity": "sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA==", "dev": true, "license": "MIT", "dependencies": { @@ -2948,6 +4146,8 @@ }, "node_modules/flowbite-datepicker": { "version": "1.3.2", + "resolved": "https://registry.npmjs.org/flowbite-datepicker/-/flowbite-datepicker-1.3.2.tgz", + "integrity": "sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g==", "dev": true, "license": "MIT", "dependencies": { @@ -2957,6 +4157,8 @@ }, "node_modules/flowbite-datepicker/node_modules/@rollup/plugin-node-resolve": { "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", "dev": true, "license": "MIT", "dependencies": { @@ -2980,6 +4182,8 @@ }, "node_modules/flowbite-svelte": { "version": "0.48.6", + "resolved": "https://registry.npmjs.org/flowbite-svelte/-/flowbite-svelte-0.48.6.tgz", + "integrity": "sha512-/PmeR3ipHHvda8vVY9MZlymaRoJsk8VddEeoLzIygfYwJV68ey8gHuQPC1dq9J6NDCTE5+xOPtBiYUtVjCfvZw==", "dev": true, "license": "MIT", "dependencies": { @@ -2994,6 +4198,8 @@ }, "node_modules/flowbite-svelte-icons": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/flowbite-svelte-icons/-/flowbite-svelte-icons-2.1.1.tgz", + "integrity": "sha512-VNNMcekjbM1bQEGgbdGsdYR9mRdTj/L0A5ba0P1tiFv5QB9GvbvJMABJoiD80eqpZUkfR2QVOmiZfgCwHicT/Q==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3003,6 +4209,8 @@ }, "node_modules/flowbite-svelte/node_modules/flowbite": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-3.1.2.tgz", + "integrity": "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3014,6 +4222,8 @@ }, "node_modules/foreground-child": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -3028,6 +4238,8 @@ }, "node_modules/fraction.js": { "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, "license": "MIT", "engines": { @@ -3040,12 +4252,14 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3058,6 +4272,8 @@ }, "node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3065,6 +4281,8 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -3072,6 +4290,8 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3094,6 +4314,8 @@ }, "node_modules/get-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3105,6 +4327,9 @@ }, "node_modules/glob": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -3122,6 +4347,8 @@ }, "node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -3131,7 +4358,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -3139,6 +4368,8 @@ }, "node_modules/glob/node_modules/minimatch": { "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -3149,6 +4380,8 @@ }, "node_modules/globals": { "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "peer": true, @@ -3161,6 +4394,8 @@ }, "node_modules/gopd": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -3171,6 +4406,8 @@ }, "node_modules/handlebars": { "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "license": "MIT", "dependencies": { "minimist": "^1.2.5", @@ -3190,6 +4427,8 @@ }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { "node": ">=8" @@ -3197,6 +4436,8 @@ }, "node_modules/has-symbols": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -3207,6 +4448,8 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3220,6 +4463,8 @@ }, "node_modules/hasown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3230,6 +4475,8 @@ }, "node_modules/he": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "license": "MIT", "bin": { "he": "bin/he" @@ -3237,6 +4484,8 @@ }, "node_modules/highlight.js": { "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", "license": "BSD-3-Clause", "engines": { "node": ">=12.0.0" @@ -3244,6 +4493,8 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -3254,6 +4505,8 @@ }, "node_modules/ignore": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "peer": true, @@ -3263,6 +4516,8 @@ }, "node_modules/import-fresh": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "peer": true, @@ -3279,6 +4534,8 @@ }, "node_modules/import-meta-resolve": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", "dev": true, "license": "MIT", "funding": { @@ -3288,6 +4545,8 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "peer": true, @@ -3297,6 +4556,9 @@ }, "node_modules/inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "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.", "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -3305,10 +4567,14 @@ }, "node_modules/inherits": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/internmap": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "license": "ISC", "engines": { "node": ">=12" @@ -3316,6 +4582,8 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -3326,6 +4594,8 @@ }, "node_modules/is-core-module": { "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -3339,6 +4609,8 @@ }, "node_modules/is-expression": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", "license": "MIT", "dependencies": { "acorn": "^7.1.1", @@ -3347,6 +4619,8 @@ }, "node_modules/is-expression/node_modules/acorn": { "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3357,6 +4631,8 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3364,6 +4640,8 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -3371,6 +4649,8 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -3381,11 +4661,15 @@ }, "node_modules/is-module": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true, "license": "MIT" }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -3393,10 +4677,14 @@ }, "node_modules/is-promise": { "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", "license": "MIT" }, "node_modules/is-reference": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3405,6 +4693,8 @@ }, "node_modules/is-regex": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -3421,14 +4711,20 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -3442,6 +4738,8 @@ }, "node_modules/jake": { "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", "license": "Apache-2.0", "dependencies": { "async": "^3.2.3", @@ -3458,6 +4756,8 @@ }, "node_modules/jiti": { "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -3465,10 +4765,21 @@ }, "node_modules/js-stringify": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "peer": true, @@ -3481,24 +4792,32 @@ }, "node_modules/json-buffer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT", "peer": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT", "peer": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/jstransformer": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", "license": "MIT", "dependencies": { "is-promise": "^2.0.0", @@ -3507,6 +4826,8 @@ }, "node_modules/keyv": { "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "peer": true, @@ -3516,6 +4837,8 @@ }, "node_modules/kleur": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "dev": true, "license": "MIT", "engines": { @@ -3524,11 +4847,15 @@ }, "node_modules/known-css-properties": { "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", "dev": true, "license": "MIT" }, "node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "peer": true, @@ -3542,6 +4869,8 @@ }, "node_modules/light-bolt11-decoder": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz", + "integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==", "license": "MIT", "dependencies": { "@scure/base": "1.1.1" @@ -3549,6 +4878,8 @@ }, "node_modules/light-bolt11-decoder/node_modules/@scure/base": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", "funding": [ { "type": "individual", @@ -3559,6 +4890,8 @@ }, "node_modules/lilconfig": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", "engines": { "node": ">=14" @@ -3569,15 +4902,21 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, "node_modules/locate-character": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "dev": true, "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "peer": true, @@ -3593,27 +4932,39 @@ }, "node_modules/lodash.castarray": { "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "license": "MIT" }, "node_modules/loupe": { - "version": "3.1.3", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", "dev": true, "license": "MIT" }, "node_modules/lru-cache": { "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, "node_modules/magic-string": { "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "license": "MIT", "dependencies": { @@ -3622,6 +4973,8 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -3629,6 +4982,8 @@ }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "license": "MIT", "engines": { "node": ">= 8" @@ -3636,6 +4991,8 @@ }, "node_modules/micromatch": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -3647,6 +5004,8 @@ }, "node_modules/micromatch/node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -3657,6 +5016,8 @@ }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", "license": "MIT", "bin": { "mini-svg-data-uri": "cli.js" @@ -3664,6 +5025,8 @@ }, "node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -3674,6 +5037,8 @@ }, "node_modules/minimist": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3681,6 +5046,8 @@ }, "node_modules/minipass": { "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -3688,6 +5055,8 @@ }, "node_modules/mri": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "dev": true, "license": "MIT", "engines": { @@ -3696,6 +5065,8 @@ }, "node_modules/mrmime": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", "engines": { @@ -3704,10 +5075,14 @@ }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/mz": { "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -3717,6 +5092,8 @@ }, "node_modules/nanoid": { "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -3733,20 +5110,28 @@ }, "node_modules/natural-compare": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/neo-async": { "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, "node_modules/next-tick": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "license": "ISC" }, "node_modules/node-emoji": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.6.0", @@ -3760,6 +5145,8 @@ }, "node_modules/node-gyp-build": { "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "license": "MIT", "bin": { "node-gyp-build": "bin.js", @@ -3769,11 +5156,15 @@ }, "node_modules/node-releases": { "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3781,6 +5172,8 @@ }, "node_modules/normalize-range": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, "license": "MIT", "engines": { @@ -3789,6 +5182,8 @@ }, "node_modules/nostr-tools": { "version": "2.10.4", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz", + "integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==", "license": "Unlicense", "dependencies": { "@noble/ciphers": "^0.5.1", @@ -3812,6 +5207,8 @@ }, "node_modules/nostr-tools/node_modules/@noble/curves": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "license": "MIT", "dependencies": { "@noble/hashes": "1.3.2" @@ -3822,6 +5219,8 @@ }, "node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": { "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "license": "MIT", "engines": { "node": ">= 16" @@ -3832,6 +5231,8 @@ }, "node_modules/nostr-tools/node_modules/@noble/hashes": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", "license": "MIT", "engines": { "node": ">= 16" @@ -3842,6 +5243,8 @@ }, "node_modules/nostr-tools/node_modules/@scure/base": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", "funding": [ { "type": "individual", @@ -3852,11 +5255,15 @@ }, "node_modules/nostr-wasm": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", "license": "MIT", "optional": true }, "node_modules/nunjucks": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", "license": "BSD-2-Clause", "dependencies": { "a-sync-waterfall": "^1.0.0", @@ -3880,6 +5287,8 @@ }, "node_modules/nunjucks/node_modules/commander": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "license": "MIT", "engines": { "node": ">= 6" @@ -3887,6 +5296,8 @@ }, "node_modules/object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3894,6 +5305,8 @@ }, "node_modules/object-hash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "license": "MIT", "engines": { "node": ">= 6" @@ -3901,6 +5314,8 @@ }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" @@ -3908,6 +5323,8 @@ }, "node_modules/optionator": { "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "peer": true, @@ -3925,6 +5342,8 @@ }, "node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "peer": true, @@ -3940,6 +5359,8 @@ }, "node_modules/p-locate": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "peer": true, @@ -3964,10 +5385,14 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "peer": true, @@ -3980,6 +5405,8 @@ }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", "engines": { "node": ">=8" @@ -3987,6 +5414,8 @@ }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" @@ -3994,10 +5423,14 @@ }, "node_modules/path-parse": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -4012,11 +5445,15 @@ }, "node_modules/pathe": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/pathval": { - "version": "2.0.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -4025,10 +5462,14 @@ }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", "engines": { @@ -4040,6 +5481,8 @@ }, "node_modules/pify": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4047,17 +5490,27 @@ }, "node_modules/pirates": { "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "license": "MIT", "engines": { "node": ">= 6" } }, + "node_modules/plantuml-encoder": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/plantuml-encoder/-/plantuml-encoder-1.4.0.tgz", + "integrity": "sha512-sxMwpDw/ySY1WB2CE3+IdMuEcWibJ72DDOsXLkSmEaSzwEUaYBT6DWgOfBiHGCux4q433X6+OEFWjlVqp7gL6g==", + "license": "MIT" + }, "node_modules/playwright": { - "version": "1.52.0", + "version": "1.53.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz", + "integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.52.0" + "playwright-core": "1.53.2" }, "bin": { "playwright": "cli.js" @@ -4070,7 +5523,9 @@ } }, "node_modules/playwright-core": { - "version": "1.52.0", + "version": "1.53.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz", + "integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4080,21 +5535,6 @@ "node": ">=18" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -4105,7 +5545,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -4122,7 +5564,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4132,6 +5574,8 @@ }, "node_modules/postcss-import": { "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -4147,6 +5591,8 @@ }, "node_modules/postcss-js": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -4164,6 +5610,8 @@ }, "node_modules/postcss-load-config": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", "dev": true, "funding": [ { @@ -4205,6 +5653,8 @@ }, "node_modules/postcss-nested": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "funding": [ { "type": "opencollective", @@ -4228,6 +5678,8 @@ }, "node_modules/postcss-nested/node_modules/postcss-selector-parser": { "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -4239,6 +5691,8 @@ }, "node_modules/postcss-safe-parser": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", "dev": true, "license": "MIT", "engines": { @@ -4254,6 +5708,8 @@ }, "node_modules/postcss-scss": { "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", "dev": true, "funding": [ { @@ -4279,6 +5735,8 @@ }, "node_modules/postcss-selector-parser": { "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -4290,10 +5748,14 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "peer": true, @@ -4302,7 +5764,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -4317,6 +5781,8 @@ }, "node_modules/prettier-plugin-svelte": { "version": "3.4.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz", + "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4326,6 +5792,8 @@ }, "node_modules/promise": { "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", "license": "MIT", "dependencies": { "asap": "~2.0.3" @@ -4333,6 +5801,8 @@ }, "node_modules/pug": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", + "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", "license": "MIT", "dependencies": { "pug-code-gen": "^3.0.3", @@ -4347,6 +5817,8 @@ }, "node_modules/pug-attrs": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", "license": "MIT", "dependencies": { "constantinople": "^4.0.1", @@ -4356,6 +5828,8 @@ }, "node_modules/pug-code-gen": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", + "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", "license": "MIT", "dependencies": { "constantinople": "^4.0.1", @@ -4370,10 +5844,14 @@ }, "node_modules/pug-error": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", "license": "MIT" }, "node_modules/pug-filters": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", "license": "MIT", "dependencies": { "constantinople": "^4.0.1", @@ -4385,6 +5863,8 @@ }, "node_modules/pug-lexer": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", "license": "MIT", "dependencies": { "character-parser": "^2.2.0", @@ -4394,6 +5874,8 @@ }, "node_modules/pug-linker": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", "license": "MIT", "dependencies": { "pug-error": "^2.0.0", @@ -4402,6 +5884,8 @@ }, "node_modules/pug-load": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", "license": "MIT", "dependencies": { "object-assign": "^4.1.1", @@ -4410,6 +5894,8 @@ }, "node_modules/pug-parser": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", "license": "MIT", "dependencies": { "pug-error": "^2.0.0", @@ -4418,10 +5904,14 @@ }, "node_modules/pug-runtime": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", "license": "MIT" }, "node_modules/pug-strip-comments": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", "license": "MIT", "dependencies": { "pug-error": "^2.0.0" @@ -4429,10 +5919,14 @@ }, "node_modules/pug-walk": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "peer": true, @@ -4577,6 +6071,8 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "funding": [ { "type": "github", @@ -4595,6 +6091,8 @@ }, "node_modules/read-cache": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -4626,6 +6124,8 @@ }, "node_modules/require-directory": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4639,6 +6139,8 @@ }, "node_modules/resolve": { "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -4657,6 +6159,8 @@ }, "node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "peer": true, @@ -4666,6 +6170,8 @@ }, "node_modules/reusify": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -4674,14 +6180,18 @@ }, "node_modules/robust-predicates": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.40.2", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz", + "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -4691,31 +6201,33 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", + "@rollup/rollup-android-arm-eabi": "4.44.2", + "@rollup/rollup-android-arm64": "4.44.2", + "@rollup/rollup-darwin-arm64": "4.44.2", + "@rollup/rollup-darwin-x64": "4.44.2", + "@rollup/rollup-freebsd-arm64": "4.44.2", + "@rollup/rollup-freebsd-x64": "4.44.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", + "@rollup/rollup-linux-arm-musleabihf": "4.44.2", + "@rollup/rollup-linux-arm64-gnu": "4.44.2", + "@rollup/rollup-linux-arm64-musl": "4.44.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", + "@rollup/rollup-linux-riscv64-gnu": "4.44.2", + "@rollup/rollup-linux-riscv64-musl": "4.44.2", + "@rollup/rollup-linux-s390x-gnu": "4.44.2", + "@rollup/rollup-linux-x64-gnu": "4.44.2", + "@rollup/rollup-linux-x64-musl": "4.44.2", + "@rollup/rollup-win32-arm64-msvc": "4.44.2", + "@rollup/rollup-win32-ia32-msvc": "4.44.2", + "@rollup/rollup-win32-x64-msvc": "4.44.2", "fsevents": "~2.3.2" } }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "funding": [ { "type": "github", @@ -4737,10 +6249,14 @@ }, "node_modules/rw": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause" }, "node_modules/sade": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "dev": true, "license": "MIT", "dependencies": { @@ -4752,10 +6268,14 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/semver": { "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -4773,11 +6293,15 @@ }, "node_modules/set-cookie-parser": { "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "dev": true, "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -4788,6 +6312,8 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" @@ -4795,11 +6321,15 @@ }, "node_modules/siginfo": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/signal-exit": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "engines": { "node": ">=14" @@ -4810,6 +6340,8 @@ }, "node_modules/sirv": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", "dev": true, "license": "MIT", "dependencies": { @@ -4823,6 +6355,8 @@ }, "node_modules/skin-tone": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", "license": "MIT", "dependencies": { "unicode-emoji-modifier-base": "^1.0.0" @@ -4833,6 +6367,8 @@ }, "node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4840,6 +6376,8 @@ }, "node_modules/source-map-js": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4847,16 +6385,22 @@ }, "node_modules/stackback": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/std-env": { "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", "dev": true, "license": "MIT" }, "node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -4870,6 +6414,8 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -4882,6 +6428,8 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -4893,6 +6441,8 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -4903,6 +6453,8 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "peer": true, @@ -4913,8 +6465,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/sucrase": { "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -4934,7 +6501,9 @@ } }, "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4942,6 +6511,8 @@ }, "node_modules/sucrase/node_modules/commander": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "license": "MIT", "engines": { "node": ">= 6" @@ -4949,6 +6520,8 @@ }, "node_modules/sucrase/node_modules/glob": { "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -4967,6 +6540,8 @@ }, "node_modules/sucrase/node_modules/minimatch": { "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -4980,6 +6555,8 @@ }, "node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -4990,6 +6567,8 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4999,7 +6578,9 @@ } }, "node_modules/svelte": { - "version": "5.30.1", + "version": "5.35.4", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.35.4.tgz", + "integrity": "sha512-NUUD+GcV/uvLBANoFwPNtnlkJM77PEkYYH6TChRZnGI1a5UHc9k2Glq7jxGtClfVz2ZhEvpg+c4yS577qM1c6g==", "dev": true, "license": "MIT", "dependencies": { @@ -5012,7 +6593,7 @@ "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", - "esrap": "^1.4.6", + "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -5023,7 +6604,9 @@ } }, "node_modules/svelte-check": { - "version": "4.2.1", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.2.tgz", + "integrity": "sha512-1+31EOYZ7NKN0YDMKusav2hhEoA51GD9Ws6o//0SphMT0ve9mBTsTUEX7OmDMadUP3KjNHsSKtJrqdSaD8CrGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5076,6 +6659,8 @@ }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", + "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", "dev": true, "license": "MIT", "dependencies": { @@ -5102,6 +6687,8 @@ }, "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5117,6 +6704,8 @@ }, "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5128,6 +6717,8 @@ }, "node_modules/svelte-eslint-parser/node_modules/espree": { "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5144,6 +6735,8 @@ }, "node_modules/svelte/node_modules/is-reference": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "dev": true, "license": "MIT", "dependencies": { @@ -5152,6 +6745,8 @@ }, "node_modules/svg.draggable.js": { "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", "dev": true, "license": "MIT", "dependencies": { @@ -5163,6 +6758,8 @@ }, "node_modules/svg.easing.js": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", "dev": true, "license": "MIT", "dependencies": { @@ -5174,6 +6771,8 @@ }, "node_modules/svg.filter.js": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", "dev": true, "license": "MIT", "dependencies": { @@ -5185,11 +6784,15 @@ }, "node_modules/svg.js": { "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==", "dev": true, "license": "MIT" }, "node_modules/svg.pathmorphing.js": { "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", "dev": true, "license": "MIT", "dependencies": { @@ -5201,6 +6804,8 @@ }, "node_modules/svg.resize.js": { "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", "dev": true, "license": "MIT", "dependencies": { @@ -5213,6 +6818,8 @@ }, "node_modules/svg.resize.js/node_modules/svg.select.js": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5224,6 +6831,8 @@ }, "node_modules/svg.select.js": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", "dev": true, "license": "MIT", "dependencies": { @@ -5234,7 +6843,9 @@ } }, "node_modules/tailwind-merge": { - "version": "3.3.0", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", "dev": true, "license": "MIT", "funding": { @@ -5244,6 +6855,8 @@ }, "node_modules/tailwindcss": { "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -5279,6 +6892,8 @@ }, "node_modules/tailwindcss/node_modules/postcss-load-config": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "funding": [ { "type": "opencollective", @@ -5312,6 +6927,8 @@ }, "node_modules/tailwindcss/node_modules/postcss-selector-parser": { "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -5323,6 +6940,8 @@ }, "node_modules/thenify": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -5330,6 +6949,8 @@ }, "node_modules/thenify-all": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -5340,16 +6961,22 @@ }, "node_modules/tinybench": { "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.13", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5364,7 +6991,9 @@ } }, "node_modules/tinypool": { - "version": "1.0.2", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -5373,6 +7002,8 @@ }, "node_modules/tinyrainbow": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -5380,7 +7011,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, "license": "MIT", "engines": { @@ -5389,6 +7022,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -5399,10 +7034,14 @@ }, "node_modules/token-stream": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", "license": "MIT" }, "node_modules/totalist": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, "license": "MIT", "engines": { @@ -5411,27 +7050,39 @@ }, "node_modules/ts-interface-checker": { "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, "node_modules/tseep": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tseep/-/tseep-1.3.1.tgz", + "integrity": "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ==", "license": "MIT" }, "node_modules/tslib": { "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD" }, "node_modules/tstl": { "version": "2.5.16", + "resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.16.tgz", + "integrity": "sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw==", "license": "MIT" }, "node_modules/type": { "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", "license": "ISC" }, "node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "peer": true, @@ -5444,6 +7095,8 @@ }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", "license": "MIT", "dependencies": { "is-typedarray": "^1.0.0" @@ -5451,6 +7104,8 @@ }, "node_modules/typescript": { "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -5463,10 +7118,14 @@ }, "node_modules/typescript-lru-cache": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/typescript-lru-cache/-/typescript-lru-cache-2.0.0.tgz", + "integrity": "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==", "license": "MIT" }, "node_modules/uglify-js": { "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "license": "BSD-2-Clause", "optional": true, "bin": { @@ -5478,11 +7137,15 @@ }, "node_modules/undici-types": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, "node_modules/unicode-emoji-modifier-base": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", "license": "MIT", "engines": { "node": ">=4" @@ -5490,6 +7153,8 @@ }, "node_modules/unxhr": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.2.0.tgz", + "integrity": "sha512-6cGpm8NFXPD9QbSNx0cD2giy7teZ6xOkCUH3U89WKVkL9N9rBrWjlCwhR94Re18ZlAop4MOc3WU1M3Hv/bgpIw==", "license": "MIT", "engines": { "node": ">=8.11" @@ -5497,6 +7162,8 @@ }, "node_modules/update-browserslist-db": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -5526,6 +7193,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -5535,6 +7204,8 @@ }, "node_modules/utf-8-validate": { "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -5546,6 +7217,8 @@ }, "node_modules/utf8-buffer": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utf8-buffer/-/utf8-buffer-1.0.0.tgz", + "integrity": "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg==", "license": "MIT", "engines": { "node": ">=8" @@ -5553,10 +7226,14 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/vite": { "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "license": "MIT", "dependencies": { @@ -5614,15 +7291,17 @@ } }, "node_modules/vite-node": { - "version": "3.1.3", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.4.0", + "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -5634,16 +7313,34 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vitefu": { - "version": "1.0.6", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "dev": true, "license": "MIT", "workspaces": [ "tests/deps/*", - "tests/projects/*" + "tests/projects/*", + "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "peerDependenciesMeta": { "vite": { @@ -5652,30 +7349,34 @@ } }, "node_modules/vitest": { - "version": "3.1.3", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", - "debug": "^4.4.0", + "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", + "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -5691,8 +7392,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, @@ -5722,6 +7423,8 @@ }, "node_modules/void-elements": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5729,6 +7432,8 @@ }, "node_modules/websocket": { "version": "1.0.35", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", + "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", "license": "Apache-2.0", "dependencies": { "bufferutil": "^4.0.1", @@ -5744,6 +7449,8 @@ }, "node_modules/websocket-polyfill": { "version": "0.0.3", + "resolved": "https://registry.npmjs.org/websocket-polyfill/-/websocket-polyfill-0.0.3.tgz", + "integrity": "sha512-pF3kR8Uaoau78MpUmFfzbIRxXj9PeQrCuPepGE6JIsfsJ/o/iXr07Q2iQNzKSSblQJ0FiGWlS64N4pVSm+O3Dg==", "dependencies": { "tstl": "^2.0.7", "websocket": "^1.0.28" @@ -5751,6 +7458,8 @@ }, "node_modules/websocket/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -5758,10 +7467,14 @@ }, "node_modules/websocket/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -5781,6 +7494,8 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -5796,6 +7511,8 @@ }, "node_modules/with": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", "license": "MIT", "dependencies": { "@babel/parser": "^7.9.6", @@ -5809,6 +7526,8 @@ }, "node_modules/word-wrap": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "peer": true, @@ -5818,10 +7537,14 @@ }, "node_modules/wordwrap": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "license": "MIT" }, "node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -5838,6 +7561,8 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -5853,10 +7578,14 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "license": "ISC", "engines": { "node": ">=10" @@ -5864,6 +7593,9 @@ }, "node_modules/yaeti": { "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=0.10.32" @@ -5883,6 +7615,8 @@ }, "node_modules/yargs": { "version": "17.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", + "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", "license": "MIT", "dependencies": { "cliui": "^7.0.2", @@ -5899,6 +7633,8 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "license": "ISC", "engines": { "node": ">=12" @@ -5906,6 +7642,8 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "peer": true, @@ -5918,6 +7656,8 @@ }, "node_modules/zimmerframe": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", "dev": true, "license": "MIT" } diff --git a/package.json b/package.json index 6453ceb..36d63f6 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test": "vitest" }, "dependencies": { - "@nostr-dev-kit/ndk": "2.11.x", + "@nostr-dev-kit/ndk": "^2.14.32", "@nostr-dev-kit/ndk-cache-dexie": "2.5.x", "@popperjs/core": "2.11.x", "@tailwindcss/forms": "0.5.x", @@ -26,6 +26,7 @@ "highlight.js": "^11.11.1", "node-emoji": "^2.2.0", "nostr-tools": "2.10.x", + "plantuml-encoder": "^1.4.0", "qrcode": "^1.5.4" }, "devDependencies": { diff --git a/playwright.config.ts b/playwright.config.ts index dd839c6..cee1e49 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * Read environment variables from file. @@ -12,7 +12,7 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './tests/e2e/', + testDir: "./tests/e2e/", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -22,34 +22,31 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [ - ['list'], - ['html', { outputFolder: './tests/e2e/html-report' }] - ], + reporter: [["list"], ["html", { outputFolder: "./tests/e2e/html-report" }]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", }, /* Configure projects for major browsers */ projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: "webkit", + use: { ...devices["Desktop Safari"] }, }, /* Test against mobile viewports. */ @@ -84,10 +81,10 @@ export default defineConfig({ // testIgnore: '*test-assets', // Glob patterns or regular expressions that match test files. - testMatch: '*.pw.spec.ts', + testMatch: "*.pw.spec.ts", // Folder for test artifacts such as screenshots, videos, traces, etc. - outputDir: './tests/e2e/test-results', + outputDir: "./tests/e2e/test-results", // path to the global setup files. // globalSetup: require.resolve('./global-setup'), @@ -102,5 +99,4 @@ export default defineConfig({ // Maximum time expect() should wait for the condition to be met. timeout: 5000, }, - -}); \ No newline at end of file +}); diff --git a/postcss.config.js b/postcss.config.js index 1105d3a..319ae11 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -2,8 +2,5 @@ import tailwindcss from "tailwindcss"; import autoprefixer from "autoprefixer"; export default { - plugins: [ - tailwindcss(), - autoprefixer(), - ] + plugins: [tailwindcss(), autoprefixer()], }; diff --git a/src/app.css b/src/app.css index 5b9826b..69a6e76 100644 --- a/src/app.css +++ b/src/app.css @@ -1,14 +1,14 @@ -@import './styles/base.css'; -@import './styles/scrollbar.css'; -@import './styles/publications.css'; -@import './styles/visualize.css'; +@import "./styles/base.css"; +@import "./styles/scrollbar.css"; +@import "./styles/publications.css"; +@import "./styles/visualize.css"; @import "./styles/events.css"; @import './styles/asciidoc.css'; /* Custom styles */ @layer base { .leather { - @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-200; + @apply bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100; } .btn-leather.text-xs { @@ -27,8 +27,8 @@ @apply h-4 w-4; } - div[role='tooltip'] button.btn-leather { - @apply hover:text-primary-400 dark:hover:text-primary-500 hover:border-primary-400 dark:hover:border-primary-500 hover:bg-gray-200 dark:hover:bg-gray-700; + div[role="tooltip"] button.btn-leather { + @apply hover:text-primary-600 dark:hover:text-primary-400 hover:border-primary-600 dark:hover:border-primary-400 hover:bg-gray-200 dark:hover:bg-gray-700; } .image-border { @@ -46,11 +46,11 @@ div.card-leather h4, div.card-leather h5, div.card-leather h6 { - @apply text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500; + @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; } div.card-leather .font-thin { - @apply text-gray-900 hover:text-primary-600 dark:text-gray-200 dark:hover:text-primary-200; + @apply text-gray-900 hover:text-primary-700 dark:text-gray-100 dark:hover:text-primary-300; } main { @@ -62,19 +62,19 @@ } /* To scroll columns independently */ - main.publication.blog { - @apply w-full sm:w-auto min-h-full; - } + main.publication.blog { + @apply w-full sm:w-auto min-h-full; + } main.main-leather, article.article-leather { - @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300; + @apply bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100; } div.note-leather, p.note-leather, section.note-leather { - @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300 p-2 rounded; + @apply bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 p-2 rounded; } .edit div.note-leather:hover:not(:has(.note-leather:hover)), @@ -89,7 +89,7 @@ h4.h-leather, h5.h-leather, h6.h-leather { - @apply text-gray-800 dark:text-gray-300; + @apply text-gray-900 dark:text-gray-100; } h1.h-leather { @@ -116,21 +116,21 @@ @apply text-base font-semibold; } - div.modal-leather>div { + div.modal-leather > div { @apply bg-primary-0 dark:bg-primary-950 border-b-[1px] border-primary-100 dark:border-primary-600; } - div.modal-leather>div>h1, - div.modal-leather>div>h2, - div.modal-leather>div>h3, - div.modal-leather>div>h4, - div.modal-leather>div>h5, - div.modal-leather>div>h6 { - @apply text-gray-800 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-300; + div.modal-leather > div > h1, + div.modal-leather > div > h2, + div.modal-leather > div > h3, + div.modal-leather > div > h4, + div.modal-leather > div > h5, + div.modal-leather > div > h6 { + @apply text-gray-900 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-100; } div.modal-leather button { - @apply bg-primary-0 hover:bg-primary-0 dark:bg-primary-950 dark:hover:bg-primary-950 text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500; + @apply bg-primary-0 hover:bg-primary-0 dark:bg-primary-950 dark:hover:bg-primary-950 text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; } /* Navbar */ @@ -143,7 +143,7 @@ } nav.navbar-leather svg { - @apply fill-gray-800 hover:fill-primary-400 dark:fill-gray-300 dark:hover:fill-primary-500; + @apply fill-gray-900 hover:fill-primary-600 dark:fill-gray-100 dark:hover:fill-primary-400; } nav.navbar-leather h1, @@ -152,7 +152,7 @@ nav.navbar-leather h4, nav.navbar-leather h5, nav.navbar-leather h6 { - @apply text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500; + @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; } /* Sidebar */ @@ -177,25 +177,25 @@ @apply bg-primary-0 dark:bg-primary-1000; } - div.textarea-leather>div:nth-child(1), + div.textarea-leather > div:nth-child(1), div.toolbar-leather { @apply border-none; } - div.textarea-leather>div:nth-child(2) { + div.textarea-leather > div:nth-child(2) { @apply bg-primary-0 dark:bg-primary-1000; } div.textarea-leather, div.textarea-leather textarea { - @apply text-gray-800 dark:text-gray-300; + @apply text-gray-900 dark:text-gray-100; } div.tooltip-leather { - @apply text-gray-800 dark:text-gray-300; + @apply text-gray-900 dark:text-gray-100; } - div[role='tooltip'] button.btn-leather .tooltip-leather { + div[role="tooltip"] button.btn-leather .tooltip-leather { @apply bg-primary-100 dark:bg-primary-800; } @@ -216,7 +216,7 @@ /* Utilities can be applied via the @apply directive. */ @layer utilities { .h-leather { - @apply text-gray-800 dark:text-gray-300 pt-4; + @apply text-gray-900 dark:text-gray-100 pt-4; } .h1-leather { @@ -246,11 +246,11 @@ /* Lists */ .ol-leather li a, .ul-leather li a { - @apply text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500; + @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; } .link { - @apply underline cursor-pointer hover:text-primary-400 dark:hover:text-primary-500; + @apply underline cursor-pointer hover:text-primary-600 dark:hover:text-primary-400; } /* Card with transition */ @@ -277,7 +277,6 @@ } @layer components { - /* Legend */ .leather-legend { @apply relative m-4 sm:m-0 sm:absolute sm:top-1 sm:left-1 flex-shrink-0 p-2 rounded; @@ -287,7 +286,7 @@ /* Tooltip */ .tooltip-leather { - @apply fixed p-4 rounded shadow-lg bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300 border border-gray-200 dark:border-gray-700 transition-colors duration-200; + @apply fixed p-4 rounded shadow-lg bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 transition-colors duration-200; max-width: 400px; z-index: 1000; } @@ -378,7 +377,7 @@ } .stemblock { - @apply bg-gray-100 dark:bg-gray-900 p-4 rounded-lg; + @apply bg-gray-200 dark:bg-gray-800 p-4 rounded-lg; } .literalblock { @@ -396,7 +395,6 @@ thead, tbody { - th, td { @apply border border-gray-200 dark:border-gray-700; @@ -426,10 +424,10 @@ padding-left: 1rem; } -.line-ellipsis { - overflow: hidden; - text-overflow: ellipsis; -} + .line-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + } .footnotes li { margin-bottom: 0.5rem; } @@ -495,7 +493,7 @@ input[type="tel"], input[type="url"], textarea { - @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300 border-s-4 border-primary-200 rounded shadow-none px-4 py-2; - @apply focus:border-primary-400 dark:focus:border-primary-500; + @apply bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 border-s-4 border-primary-200 rounded shadow-none px-4 py-2; + @apply focus:border-primary-600 dark:focus:border-primary-400; } -} \ No newline at end of file +} diff --git a/src/app.html b/src/app.html index d025d7c..97127be 100644 --- a/src/app.html +++ b/src/app.html @@ -4,6 +4,37 @@ + + + + + + + + + %sveltekit.head% diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index c46f902..c412cb8 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -1,23 +1,31 @@
@@ -227,11 +339,127 @@ {#each markupButtons as button} {/each} - +
-
+ + +
+
+ { + if (e.key === 'Enter' && mentionSearch.trim() && !isSearching) { + searchMentions(); + } + }} + class="flex-1 rounded-lg border border-gray-300 bg-gray-50 text-gray-900 text-sm focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500 p-2.5" + /> + +
+ + {#if mentionLoading} +
Searching...
+ {:else if mentionResults.length > 0} +
Found {mentionResults.length} results
+
+
    + {#each mentionResults as profile} + + {/each} +
+
+ {:else if mentionSearch.trim()} +
No results found
+ {:else} +
Enter a search term to find users
+ {/if} +
+
+ + + + + +
+ + +
+
+ +
+
+
+ + +
+
+ + + {#if dTagError} +
{dTagError}
+ {/if} +
+
+ +
+ {#if loading} + Publishing... + {/if} + {#if error} +
{error}
+ {/if} + {#if success} +
{success}
+
Relays: {publishedRelays.join(', ')}
+ {#if lastPublishedEventId} +
+ Event ID: {lastPublishedEventId} + +
+ {/if} + {/if} + +
\ No newline at end of file diff --git a/src/lib/components/EventLimitControl.svelte b/src/lib/components/EventLimitControl.svelte index d8c28be..9a32a56 100644 --- a/src/lib/components/EventLimitControl.svelte +++ b/src/lib/components/EventLimitControl.svelte @@ -45,7 +45,7 @@ /> diff --git a/src/lib/components/EventRenderLevelLimit.svelte b/src/lib/components/EventRenderLevelLimit.svelte index 3a7d8a8..9cc08bf 100644 --- a/src/lib/components/EventRenderLevelLimit.svelte +++ b/src/lib/components/EventRenderLevelLimit.svelte @@ -29,10 +29,14 @@
- - + diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte index 36cbdf3..e9dfed2 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -1,204 +1,601 @@
-
+ +
e.key === 'Enter' && searchEvent(true)} + onkeydown={(e: KeyboardEvent) => e.key === "Enter" && handleSearchEvent(true)} + oninput={() => isUserEditing = true} + onblur={() => isUserEditing = false} /> - +
- {#if localError || error} + + {#if showError} {/if} + + {#if showSuccess} + + {/if} + +
{#each Object.entries(relayStatuses) as [relay, status]} - + {/each}
- {#if !foundEvent && Object.values(relayStatuses).some(s => s === 'pending')} -
Searching relays...
- {/if} - {#if !foundEvent && !searching && Object.values(relayStatuses).every(s => s !== 'pending')} -
Event not found on any relay.
+ {#if !foundEvent && hasActiveSearch} +
+ Searching relays... +
{/if}
-
\ No newline at end of file +
diff --git a/src/lib/components/Login.svelte b/src/lib/components/Login.svelte index e0d1171..e24490d 100644 --- a/src/lib/components/Login.svelte +++ b/src/lib/components/Login.svelte @@ -1,21 +1,27 @@ -
{#if $ndkSignedIn} {:else} - + -
- +
+ {#if signInFailed}
{errorMessage} diff --git a/src/lib/components/LoginMenu.svelte b/src/lib/components/LoginMenu.svelte new file mode 100644 index 0000000..a1fa14e --- /dev/null +++ b/src/lib/components/LoginMenu.svelte @@ -0,0 +1,370 @@ + + +
+ {#if !user.signedIn} + +
+ + +
+

Login with...

+ + + +
+
+ {#if result} +
+ {result} + +
+ {/if} +
+ {:else} + +
+ + +
+
+

{user.profile?.displayName || user.profile?.name || (user.npub ? shortenNpub(user.npub) : 'Unknown')}

+
    +
  • + +
  • +
  • + {#if user.loginMethod === 'extension'} + Logged in with extension + {:else if user.loginMethod === 'amber'} + Logged in with Amber + {:else if user.loginMethod === 'npub'} + Logged in with npub + {:else} + Unknown login method + {/if} +
  • +
  • + +
  • +
+
+
+
+
+ {/if} +
+ +{#if showQrCode && qrCodeDataUrl} + +
+
+
+

Scan with Amber

+

Open Amber on your phone and scan this QR code

+
+ Nostr Connect QR Code +
+
+ +
+ + +
+
+
+

1. Open Amber on your phone

+

2. Scan the QR code above

+

3. Approve the connection in Amber

+
+ +
+
+
+{/if} + +{#if showAmberFallback} +
+
+
+

Amber Session Restored

+

+ Your Amber wallet session could not be restored automatically, so you've been switched to read-only mode.
+ You can still browse and read content, but you'll need to reconnect Amber to publish or comment. +

+ + +
+
+
+{/if} \ No newline at end of file diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte index 14ffdcb..465f180 100644 --- a/src/lib/components/LoginModal.svelte +++ b/src/lib/components/LoginModal.svelte @@ -1,6 +1,7 @@ -{#if show} -
-
-
- -
-

Login Required

- -
- - -
-

- You need to be logged in to submit an issue. Your form data will be preserved. -

-
-
- -
- {#if signInFailed} -
- {errorMessage} -
- {/if} -
-
-
+ +

+ You need to be logged in to submit an issue. Your form data will be + preserved. +

+
+
+
+ {#if signInFailed} +
+ {errorMessage} +
+ {/if}
-{/if} \ No newline at end of file +
diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte deleted file mode 100644 index b9b6e14..0000000 --- a/src/lib/components/Modal.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - -{#if showModal} -
- -
-{/if} diff --git a/src/lib/components/Navigation.svelte b/src/lib/components/Navigation.svelte index f78e36e..aa6b7e2 100644 --- a/src/lib/components/Navigation.svelte +++ b/src/lib/components/Navigation.svelte @@ -7,7 +7,7 @@ NavHamburger, NavBrand, } from "flowbite-svelte"; - import Login from "./Login.svelte"; + import LoginMenu from "./LoginMenu.svelte"; let { class: className = "" } = $props(); @@ -19,7 +19,7 @@
- +
diff --git a/src/lib/components/Preview.svelte b/src/lib/components/Preview.svelte index 8a33d01..77e465b 100644 --- a/src/lib/components/Preview.svelte +++ b/src/lib/components/Preview.svelte @@ -1,11 +1,27 @@ - -
-
+
+ +
+ + +
+ +
{#if loading && eventsInView.length === 0} {#each getSkeletonIds() as id} - + {/each} {:else if eventsInView.length > 0} {#each eventsInView as event} {/each} {:else} -
-

No publications found.

+
+

No publications found.

{/if}
+ {#if !loadingMore && !endOfFeed} -
-
{:else if loadingMore} -
+
{:else} -
-

You've reached the end of the feed.

+
+

You've reached the end of the feed.

{/if} -
+
\ No newline at end of file diff --git a/src/lib/components/PublicationHeader.svelte b/src/lib/components/PublicationHeader.svelte index 32a674a..ef55eb6 100644 --- a/src/lib/components/PublicationHeader.svelte +++ b/src/lib/components/PublicationHeader.svelte @@ -1,11 +1,13 @@ {#if title != null && href != null} - - {#if image} -
- + +
+ {#if image && !imageError} +
+ +
+ + Publication cover +
+ {:else} + +
+ {/if}
- {/if} -
- -
- +
+
+
+ + {#if hashtags.length > 0} +
+ {#each hashtags as tag (tag)} + + {/each} +
+ {/if}
{/if} diff --git a/src/lib/components/PublicationSection.svelte b/src/lib/components/PublicationSection.svelte index 6c2586a..fdbd0a5 100644 --- a/src/lib/components/PublicationSection.svelte +++ b/src/lib/components/PublicationSection.svelte @@ -1,11 +1,16 @@ - -
- {#await Promise.all([leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches])} - - {:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches]} +
+ {#await Promise.all( [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches, leafEvent, leafHashtags], )} + + {:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches, resolvedLeafEvent, hashtags]} + {@const contentString = leafContent.toString()} + + + {#each divergingBranches as [branch, depth]} - {@render sectionHeading(getMatchingTags(branch, 'title')[0]?.[1] ?? '', depth)} + {@render sectionHeading( + getMatchingTags(branch, "title")[0]?.[1] ?? "", + depth, + )} {/each} {#if leafTitle} {@const leafDepth = leafHierarchy.length - 1} {@render sectionHeading(leafTitle, leafDepth)} {/if} - {@render contentParagraph(leafContent.toString(), publicationType ?? 'article', false)} + {@render contentParagraph( + contentString, + publicationType ?? "article", + false, + )} + {#if hashtags.length > 0} +
+ {#each hashtags as tag (tag)} + + {/each} +
+ {/if} {/await}
diff --git a/src/lib/components/RelayActions.svelte b/src/lib/components/RelayActions.svelte index 9645f8c..b81d501 100644 --- a/src/lib/components/RelayActions.svelte +++ b/src/lib/components/RelayActions.svelte @@ -1,10 +1,16 @@
- - - {#if $ndkInstance?.activeUser} - - {/if}
{#if foundRelays.length > 0} @@ -133,23 +90,6 @@
{/if} -{#if broadcastSuccess} -
- Event broadcast successfully to: -
- {#each getConnectedRelays() as relay} - - {/each} -
-
-{/if} - -{#if broadcastError} -
- {broadcastError} -
-{/if} -
Found on:
@@ -159,32 +99,32 @@
-{#if showRelayModal} -
-
- -

Relay Search Results

-
- {#each Object.entries({ - 'Standard Relays': standardRelays, - 'User Relays': Array.from($ndkInstance?.pool?.relays.values() || []).map(r => r.url), - 'Fallback Relays': fallbackRelays - }) as [groupName, groupRelays]} - {#if groupRelays.length > 0} -
-

- {groupName} -

- {#each groupRelays as relay} - - {/each} -
- {/if} - {/each} -
-
- -
-
+ +
+ {#each Object.entries( { "Standard Relays": standardRelays, "User Relays": Array.from($ndkInstance?.pool?.relays.values() || []).map((r) => r.url), "Fallback Relays": fallbackRelays }, ) as [groupName, groupRelays]} + {#if groupRelays.length > 0} +
+

+ {groupName} +

+ {#each groupRelays as relay} + + {/each} +
+ {/if} + {/each}
-{/if} \ No newline at end of file +
diff --git a/src/lib/components/RelayDisplay.svelte b/src/lib/components/RelayDisplay.svelte index 1161f7c..f717d42 100644 --- a/src/lib/components/RelayDisplay.svelte +++ b/src/lib/components/RelayDisplay.svelte @@ -1,14 +1,16 @@ -
+
relay icon { (e.target as HTMLImageElement).src = '/favicon.png'; }} + onerror={(e) => { + (e.target as HTMLImageElement).src = "/favicon.png"; + }} /> {relay} {#if showStatus && status} - {#if status === 'pending'} - - - + {#if status === "pending"} + + + - {:else if status === 'found'} + {:else if status === "found"} {:else} {/if} {/if} -
\ No newline at end of file +
diff --git a/src/lib/components/RelayStatus.svelte b/src/lib/components/RelayStatus.svelte new file mode 100644 index 0000000..fa9f51c --- /dev/null +++ b/src/lib/components/RelayStatus.svelte @@ -0,0 +1,167 @@ + + +
+
+

Relay Connection Status

+ +
+ + {#if !$ndkSignedIn} + + Anonymous Mode +

+ You are not signed in. Some relays require authentication and may not be + accessible. Sign in to access all relays. +

+
+ {/if} + +
+ {#each relayStatuses as status} +
+
+
{status.url}
+
+ {getStatusText(status)} +
+
+
+
+ {/each} +
+ + {#if relayStatuses.some((s) => s.requiresAuth && !$ndkSignedIn)} + + Authentication Required +

+ Some relays require authentication. Sign in to access these relays. +

+
+ {/if} +
diff --git a/src/lib/components/Toc.svelte b/src/lib/components/Toc.svelte index 9d433b5..db49b82 100644 --- a/src/lib/components/Toc.svelte +++ b/src/lib/components/Toc.svelte @@ -1,24 +1,28 @@
-

Table of contents

- +

Table of contents

+
diff --git a/src/lib/components/cards/BlogHeader.svelte b/src/lib/components/cards/BlogHeader.svelte index a91d0a4..dcee584 100644 --- a/src/lib/components/cards/BlogHeader.svelte +++ b/src/lib/components/cards/BlogHeader.svelte @@ -1,23 +1,38 @@ {#if title != null} - -
+ +
{@render userBadge(authorPubkey, author)} - {publishedAt()} + {publishedAt()}
- +
+ + + {#if image && active} -
- +
{/if} -
- {#if hashtags} -
- {#each hashtags as tag} - {tag} - {/each} -
+
+ {#each hashtags as tag} + {tag} + {/each} +
{/if}
{#if active} - + {/if}
diff --git a/src/lib/components/cards/ProfileHeader.svelte b/src/lib/components/cards/ProfileHeader.svelte index 206f994..118b5e8 100644 --- a/src/lib/components/cards/ProfileHeader.svelte +++ b/src/lib/components/cards/ProfileHeader.svelte @@ -5,116 +5,204 @@ import { type NostrProfile, toNpub } from "$lib/utils/nostrUtils.ts"; import QrCode from "$components/util/QrCode.svelte"; import CopyToClipboard from "$components/util/CopyToClipboard.svelte"; + import { lnurlpWellKnownUrl, checkCommunity } from "$lib/utils/search_utility"; // @ts-ignore - import { bech32 } from 'https://esm.sh/bech32'; + import { bech32 } from "https://esm.sh/bech32"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; + import { goto } from "$app/navigation"; - const { event, profile, identifiers = [] } = $props<{ event: NDKEvent, profile: NostrProfile, identifiers?: { label: string, value: string, link?: string }[] }>(); + const { + event, + profile, + identifiers = [], + } = $props<{ + event: NDKEvent; + profile: NostrProfile; + identifiers?: { label: string; value: string; link?: string }[]; + }>(); let lnModalOpen = $state(false); let lnurl = $state(null); + let communityStatus = $state(null); onMount(async () => { if (profile?.lud16) { try { // Convert LN address to LNURL - const [name, domain] = profile?.lud16.split('@'); - const url = `https://${domain}/.well-known/lnurlp/${name}`; + const [name, domain] = profile?.lud16.split("@"); + const url = lnurlpWellKnownUrl(domain, name); const words = bech32.toWords(new TextEncoder().encode(url)); - lnurl = bech32.encode('lnurl', words); + lnurl = bech32.encode("lnurl", words); } catch { - console.log('Error converting LN address to LNURL'); + console.log("Error converting LN address to LNURL"); } } }); + + $effect(() => { + if (event?.pubkey) { + checkCommunity(event.pubkey).then((status) => { + communityStatus = status; + }).catch(() => { + communityStatus = false; + }); + } + }); + + function navigateToIdentifier(link: string) { + goto(link); + } {#if profile} - -
- {#if profile.banner} -
- Profile banner { (e.target as HTMLImageElement).style.display = 'none';}} /> -
- {/if} -
- {#if profile.picture} - Profile avatar { (e.target as HTMLImageElement).src = '/favicon.png'; }} /> + +
+ {#if profile.banner} +
+ Profile banner { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> +
{/if} - {@render userBadge(toNpub(event.pubkey) as string, profile.displayName || profile.name || event.pubkey)} -
-
-
-
- {#if profile.name} -
-
Name:
-
{profile.name}
-
- {/if} - {#if profile.displayName} -
-
Display Name:
-
{profile.displayName}
-
- {/if} - {#if profile.about} -
-
About:
-
{profile.about}
-
- {/if} - {#if profile.website} -
-
Website:
-
- {profile.website} -
+
+ {#if profile.picture} + Profile avatar { + (e.target as HTMLImageElement).src = "/favicon.png"; + }} + /> + {/if} +
+ {@render userBadge( + toNpub(event.pubkey) as string, + profile.displayName || + profile.display_name || + profile.name || + event.pubkey, + )} + {#if communityStatus === true} +
+ + +
+ {:else if communityStatus === false} +
{/if} - {#if profile.lud16} -
-
Lightning Address:
-
-
- {/if} - {#if profile.nip05} -
-
NIP-05:
-
{profile.nip05}
-
- {/if} - {#each identifiers as id} -
-
{id.label}:
-
{#if id.link}{id.value}{:else}{id.value}{/if}
-
- {/each} -
+
+
+
+
+
+ {#if profile.name} +
+
Name:
+
{profile.name}
+
+ {/if} + {#if profile.displayName} +
+
Display Name:
+
{profile.displayName}
+
+ {/if} + {#if profile.about} +
+
About:
+
{profile.about}
+
+ {/if} + {#if profile.website} +
+
Website:
+
+ {profile.website} +
+
+ {/if} + {#if profile.lud16} +
+
Lightning Address:
+
+ +
+
+ {/if} + {#if profile.nip05} +
+
NIP-05:
+
{profile.nip05}
+
+ {/if} + {#each identifiers as id} +
+
{id.label}:
+
+ {#if id.link} + + {:else} + {id.value} + {/if} +
+
+ {/each} +
+
-
-
+ - - {#if profile.lud16} -
-
- {@render userBadge(toNpub(event.pubkey) as string, profile?.displayName || profile.name || event.pubkey)} -

{profile.lud16}

-
-
-

Scan the QR code or copy the address

- {#if lnurl} -

- -

- - {:else} -

Couldn't generate address.

- {/if} -
-
- {/if} -
-{/if} \ No newline at end of file + + {#if profile.lud16} +
+
+ {@render userBadge( + toNpub(event.pubkey) as string, + profile?.displayName || profile.name || event.pubkey, + )} +

{profile.lud16}

+
+
+

Scan the QR code or copy the address

+ {#if lnurl} +

+ +

+ + {:else} +

Couldn't generate address.

+ {/if} +
+
+ {/if} +
+{/if} diff --git a/src/lib/components/util/ArticleNav.svelte b/src/lib/components/util/ArticleNav.svelte index a5b8631..1fcc001 100644 --- a/src/lib/components/util/ArticleNav.svelte +++ b/src/lib/components/util/ArticleNav.svelte @@ -1,35 +1,41 @@ - diff --git a/src/lib/components/util/CardActions.svelte b/src/lib/components/util/CardActions.svelte index 6397d17..ef6b45e 100644 --- a/src/lib/components/util/CardActions.svelte +++ b/src/lib/components/util/CardActions.svelte @@ -1,22 +1,27 @@ -
+
- {#if isOpen} - -
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
+ +
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
-
- + {/if} - +
{#if image} -
- Publication cover +
+ Publication cover
{/if}
-

{title || 'Untitled'}

-

by +

{title || "Untitled"}

+

+ by {#if originalAuthor} - {@render userBadge(originalAuthor, author)} + {@render userBadge(originalAuthor, author)} {:else} - {author || 'Unknown'} + {author || "Unknown"} {/if}

{#if version} -

Version: {version}

+

+ Version: {version} +

{/if}
{#if summary}
-

{summary}

+

{summary}

{/if}
-

Index author: {@render userBadge(event.pubkey, author)}

+

+ Index author: {@render userBadge(event.pubkey, author)} +

{#if source} -
Source: {source}
+
+ Source: {source} +
{/if} {#if type}
Publication type: {type}
@@ -202,12 +236,12 @@ {#if identifier}
Identifier: {identifier}
{/if} - View Event Details - +
-
\ No newline at end of file +
diff --git a/src/lib/components/util/ContainingIndexes.svelte b/src/lib/components/util/ContainingIndexes.svelte new file mode 100644 index 0000000..1c7da12 --- /dev/null +++ b/src/lib/components/util/ContainingIndexes.svelte @@ -0,0 +1,105 @@ + + +{#if containingIndexes.length > 0 || loading || error} +
+

+ Containing Publications +

+ + {#if loading} +
+ Loading containing publications... +
+ {:else if error} +
+ {error} +
+ {:else if containingIndexes.length > 0} +
+ {#each containingIndexes.slice(0, 3) as indexEvent} + {@const title = + getMatchingTags(indexEvent, "title")[0]?.[1] || "Untitled"} + + {/each} + {#if containingIndexes.length > 3} + + +{containingIndexes.length - 3} more + + {/if} +
+ {:else} +
+ No containing publications found +
+ {/if} +
+{/if} diff --git a/src/lib/components/util/CopyToClipboard.svelte b/src/lib/components/util/CopyToClipboard.svelte index 600ff65..d848f8c 100644 --- a/src/lib/components/util/CopyToClipboard.svelte +++ b/src/lib/components/util/CopyToClipboard.svelte @@ -1,9 +1,16 @@ - - {/each}
{/if} {#if isModal}
-

+

{#if kind === 30040} Index author: {:else} Author: {/if} - {@render userBadge(event.pubkey, author)} + {@render userBadge(event.pubkey, '')}

-
{#if source !== null} -
Source: {source}
+
+ Source: {source} +
{/if} {#if type !== null}
Publication type: {type}
@@ -106,5 +139,5 @@ {/if} {#if !isModal} - -{/if} \ No newline at end of file + +{/if} diff --git a/src/lib/components/util/Interactions.svelte b/src/lib/components/util/Interactions.svelte index 52e9bab..cd75c18 100644 --- a/src/lib/components/util/Interactions.svelte +++ b/src/lib/components/util/Interactions.svelte @@ -1,15 +1,21 @@ -
- - - - +
+ + + +
- -

Can't like, zap or highlight yet.

-

You should totally check out the discussion though.

-
\ No newline at end of file + +

Can't like, zap or highlight yet.

+

You should totally check out the discussion though.

+
diff --git a/src/lib/components/util/Profile.svelte b/src/lib/components/util/Profile.svelte index 9d75bd5..88ebe4c 100644 --- a/src/lib/components/util/Profile.svelte +++ b/src/lib/components/util/Profile.svelte @@ -1,99 +1,117 @@ -
{#if profile} -
- - {#key username || tag} - -
-
- {#if username} -

{username}

- {#if isNav}

@{tag}

{/if} - {/if} -
    -
  • - -
  • -
  • - - View profile - -
  • - {#if isNav} +
    + + {#key username || tag} + +
    +
    + {#if username} +

    {username}

    + {#if isNav}

    @{tag}

    {/if} + {/if} +
      +
    • + +
    • - {:else} - - {/if} -
    + {/if} +
+
-
- - {/key} -
+ + {/key} +
{/if}
diff --git a/src/lib/components/util/QrCode.svelte b/src/lib/components/util/QrCode.svelte index 616b995..719d9e6 100644 --- a/src/lib/components/util/QrCode.svelte +++ b/src/lib/components/util/QrCode.svelte @@ -1,6 +1,6 @@ + +{#if naddrAddress} + +{/if} \ No newline at end of file diff --git a/src/lib/components/util/ZapOutline.svelte b/src/lib/components/util/ZapOutline.svelte index b5588c0..1f28435 100644 --- a/src/lib/components/util/ZapOutline.svelte +++ b/src/lib/components/util/ZapOutline.svelte @@ -1,6 +1,6 @@ - + diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 7866064..7e78b2a 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -1,23 +1,43 @@ export const wikiKind = 30818; export const indexKind = 30040; -export const zettelKinds = [ 30041, 30818 ]; -export const communityRelay = [ 'wss://theforest.nostr1.com' ]; -export const standardRelays = [ 'wss://thecitadel.nostr1.com', 'wss://theforest.nostr1.com' ]; -export const fallbackRelays = [ - 'wss://purplepag.es', - 'wss://indexer.coracle.social', - 'wss://relay.noswhere.com', - 'wss://relay.damus.io', - 'wss://relay.nostr.band', - 'wss://relay.lumina.rocks', - 'wss://nostr.wine', - 'wss://nostr.land' +export const zettelKinds = [30041, 30818]; +export const communityRelay = "wss://theforest.nostr1.com"; +export const profileRelays = ["wss://profiles.nostr1.com", "wss://aggr.nostr.land", "wss://relay.noswhere.com"]; +export const standardRelays = [ + "wss://thecitadel.nostr1.com", + "wss://theforest.nostr1.com", + "wss://profiles.nostr1.com", + // Removed gitcitadel.nostr1.com as it's causing connection issues + //'wss://thecitadel.gitcitadel.eu', + //'wss://theforest.gitcitadel.eu', +]; + +// Non-auth relays for anonymous users +export const anonymousRelays = [ + "wss://thecitadel.nostr1.com", + "wss://theforest.nostr1.com", + "wss://profiles.nostr1.com", + "wss://freelay.sovbit.host", +]; +export const fallbackRelays = [ + "wss://purplepag.es", + "wss://indexer.coracle.social", + "wss://relay.noswhere.com", + "wss://aggr.nostr.land", + "wss://nostr.land", + "wss://nostr.wine", + "wss://nostr.sovbit.host", + "wss://freelay.sovbit.host", + "wss://nostr21.com", + "wss://greensoul.space", + "wss://relay.damus.io", + "wss://relay.nostr.band", ]; export enum FeedType { - StandardRelays = 'standard', - UserRelays = 'user', + StandardRelays = "standard", + UserRelays = "user", } -export const loginStorageKey = 'alexandria/login/pubkey'; -export const feedTypeStorageKey = 'alexandria/feed/type'; +export const loginStorageKey = "alexandria/login/pubkey"; +export const feedTypeStorageKey = "alexandria/feed/type"; diff --git a/src/lib/data_structures/lazy.ts b/src/lib/data_structures/lazy.ts index d6d6035..6959a60 100644 --- a/src/lib/data_structures/lazy.ts +++ b/src/lib/data_structures/lazy.ts @@ -18,9 +18,9 @@ export class Lazy { /** * Resolves the lazy object and returns the value. - * + * * @returns The resolved value. - * + * * @remarks Lazy object resolution is performed as an atomic operation. If a resolution has * already been requested when this function is invoked, the pending promise from the earlier * invocation is returned. Thus, all calls to this function before it is resolved will depend on @@ -52,4 +52,4 @@ export class Lazy { this.#pendingPromise = null; } } -} \ No newline at end of file +} diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index d616740..e0c56fa 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -1,7 +1,7 @@ import type NDK from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { Lazy } from "./lazy.ts"; -import { findIndexAsync as _findIndexAsync } from '../utils.ts'; +import { findIndexAsync as _findIndexAsync } from "../utils.ts"; enum PublicationTreeNodeType { Branch, @@ -62,7 +62,10 @@ export class PublicationTree implements AsyncIterable { }; this.#nodes = new Map>(); - this.#nodes.set(rootAddress, new Lazy(() => Promise.resolve(this.#root))); + this.#nodes.set( + rootAddress, + new Lazy(() => Promise.resolve(this.#root)), + ); this.#events = new Map(); this.#events.set(rootAddress, rootEvent); @@ -85,7 +88,7 @@ export class PublicationTree implements AsyncIterable { if (!parentNode) { throw new Error( - `PublicationTree: Parent node with address ${parentAddress} not found.` + `PublicationTree: Parent node with address ${parentAddress} not found.`, ); } @@ -116,7 +119,7 @@ export class PublicationTree implements AsyncIterable { if (!parentNode) { throw new Error( - `PublicationTree: Parent node with address ${parentAddress} not found.` + `PublicationTree: Parent node with address ${parentAddress} not found.`, ); } @@ -145,13 +148,15 @@ export class PublicationTree implements AsyncIterable { async getChildAddresses(address: string): Promise> { const node = await this.#nodes.get(address)?.value(); if (!node) { - throw new Error(`PublicationTree: Node with address ${address} not found.`); + throw new Error( + `PublicationTree: Node with address ${address} not found.`, + ); } return Promise.all( - node.children?.map(async child => - (await child.value())?.address ?? null - ) ?? [] + node.children?.map( + async (child) => (await child.value())?.address ?? null, + ) ?? [], ); } /** @@ -163,11 +168,13 @@ export class PublicationTree implements AsyncIterable { async getHierarchy(address: string): Promise { let node = await this.#nodes.get(address)?.value(); if (!node) { - throw new Error(`PublicationTree: Node with address ${address} not found.`); + throw new Error( + `PublicationTree: Node with address ${address} not found.`, + ); } const hierarchy: NDKEvent[] = [this.#events.get(address)!]; - + while (node.parent) { hierarchy.push(this.#events.get(node.parent.address)!); node = node.parent; @@ -187,7 +194,7 @@ export class PublicationTree implements AsyncIterable { // #region Iteration Cursor - #cursor = new class { + #cursor = new (class { target: PublicationTreeNode | null | undefined; #tree: PublicationTree; @@ -199,7 +206,9 @@ export class PublicationTree implements AsyncIterable { async tryMoveTo(address?: string) { if (!address) { const startEvent = await this.#tree.#depthFirstRetrieve(); - this.target = await this.#tree.#nodes.get(startEvent!.tagAddress())?.value(); + this.target = await this.#tree.#nodes + .get(startEvent!.tagAddress()) + ?.value(); } else { this.target = await this.#tree.#nodes.get(address)?.value(); } @@ -224,7 +233,7 @@ export class PublicationTree implements AsyncIterable { if (this.target.children == null || this.target.children.length === 0) { return false; } - + this.target = await this.target.children?.at(0)?.value(); return true; } @@ -234,15 +243,15 @@ export class PublicationTree implements AsyncIterable { console.debug("Cursor: Target node is null or undefined."); return false; } - + if (this.target.type === PublicationTreeNodeType.Leaf) { return false; } - + if (this.target.children == null || this.target.children.length === 0) { return false; } - + this.target = await this.target.children?.at(-1)?.value(); return true; } @@ -260,7 +269,8 @@ export class PublicationTree implements AsyncIterable { } const currentIndex = await siblings.findIndexAsync( - async (sibling: Lazy) => (await sibling.value())?.address === this.target!.address + async (sibling: Lazy) => + (await sibling.value())?.address === this.target!.address, ); if (currentIndex === -1) { @@ -280,25 +290,26 @@ export class PublicationTree implements AsyncIterable { console.debug("Cursor: Target node is null or undefined."); return false; } - + const parent = this.target.parent; const siblings = parent?.children; if (!siblings) { return false; } - + const currentIndex = await siblings.findIndexAsync( - async (sibling: Lazy) => (await sibling.value())?.address === this.target!.address + async (sibling: Lazy) => + (await sibling.value())?.address === this.target!.address, ); if (currentIndex === -1) { return false; } - + if (currentIndex <= 0) { return false; } - + this.target = await siblings.at(currentIndex - 1)?.value(); return true; } @@ -317,7 +328,7 @@ export class PublicationTree implements AsyncIterable { this.target = parent; return true; } - }(this); + })(this); // #endregion @@ -369,7 +380,7 @@ export class PublicationTree implements AsyncIterable { return { done: false, value: event }; } } - + // Based on Raymond Chen's tree traversal algorithm example. // https://devblogs.microsoft.com/oldnewthing/20200106-00/?p=103300 do { @@ -412,17 +423,23 @@ export class PublicationTree implements AsyncIterable { const stack: string[] = [this.#root.address]; let currentNode: PublicationTreeNode | null | undefined = this.#root; - let currentEvent: NDKEvent | null | undefined = this.#events.get(this.#root.address)!; + let currentEvent: NDKEvent | null | undefined = this.#events.get( + this.#root.address, + )!; while (stack.length > 0) { const currentAddress = stack.pop(); currentNode = await this.#nodes.get(currentAddress!)?.value(); if (!currentNode) { - throw new Error(`PublicationTree: Node with address ${currentAddress} not found.`); + throw new Error( + `PublicationTree: Node with address ${currentAddress} not found.`, + ); } currentEvent = this.#events.get(currentAddress!); if (!currentEvent) { - throw new Error(`PublicationTree: Event with address ${currentAddress} not found.`); + throw new Error( + `PublicationTree: Event with address ${currentAddress} not found.`, + ); } // Stop immediately if the target of the search is found. @@ -431,8 +448,8 @@ export class PublicationTree implements AsyncIterable { } const currentChildAddresses = currentEvent.tags - .filter(tag => tag[0] === 'a') - .map(tag => tag[1]); + .filter((tag) => tag[0] === "a") + .map((tag) => tag[1]); // If the current event has no children, it is a leaf. if (currentChildAddresses.length === 0) { @@ -464,34 +481,43 @@ export class PublicationTree implements AsyncIterable { } #addNode(address: string, parentNode: PublicationTreeNode) { - const lazyNode = new Lazy(() => this.#resolveNode(address, parentNode)); + if (this.#nodes.has(address)) { + console.debug( + `[PublicationTree] Node with address ${address} already exists.`, + ); + return; + } + + const lazyNode = new Lazy(() => + this.#resolveNode(address, parentNode), + ); parentNode.children!.push(lazyNode); this.#nodes.set(address, lazyNode); } /** * Resolves a node address into an event, and creates new nodes for its children. - * + * * This method is intended for use as a {@link Lazy} resolver. - * + * * @param address The address of the node to resolve. * @param parentNode The parent node of the node to resolve. * @returns The resolved node. */ async #resolveNode( address: string, - parentNode: PublicationTreeNode + parentNode: PublicationTreeNode, ): Promise { - const [kind, pubkey, dTag] = address.split(':'); + const [kind, pubkey, dTag] = address.split(":"); const event = await this.#ndk.fetchEvent({ kinds: [parseInt(kind)], authors: [pubkey], - '#d': [dTag], + "#d": [dTag], }); if (!event) { console.debug( - `PublicationTree: Event with address ${address} not found.` + `PublicationTree: Event with address ${address} not found.`, ); return { @@ -505,8 +531,10 @@ export class PublicationTree implements AsyncIterable { this.#events.set(address, event); - const childAddresses = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1]); - + const childAddresses = event.tags + .filter((tag) => tag[0] === "a") + .map((tag) => tag[1]); + const node: PublicationTreeNode = { type: this.#getNodeType(event), status: PublicationTreeNodeStatus.Resolved, @@ -523,7 +551,7 @@ export class PublicationTree implements AsyncIterable { } #getNodeType(event: NDKEvent): PublicationTreeNodeType { - if (event.kind === 30040 && event.tags.some(tag => tag[0] === 'a')) { + if (event.kind === 30040 && event.tags.some((tag) => tag[0] === "a")) { return PublicationTreeNodeType.Branch; } @@ -531,4 +559,4 @@ export class PublicationTree implements AsyncIterable { } // #endregion -} \ No newline at end of file +} diff --git a/src/lib/navigator/EventNetwork/Legend.svelte b/src/lib/navigator/EventNetwork/Legend.svelte index 024037f..b553cab 100644 --- a/src/lib/navigator/EventNetwork/Legend.svelte +++ b/src/lib/navigator/EventNetwork/Legend.svelte @@ -1,12 +1,12 @@ -
+

Settings

- @@ -528,50 +551,82 @@ {/if}
- + - - + + - - + +
- - -
diff --git a/src/lib/navigator/EventNetwork/types.ts b/src/lib/navigator/EventNetwork/types.ts index db2d46b..1667a3a 100644 --- a/src/lib/navigator/EventNetwork/types.ts +++ b/src/lib/navigator/EventNetwork/types.ts @@ -1,6 +1,6 @@ /** * Type definitions for the Event Network visualization - * + * * This module defines the core data structures used in the D3 force-directed * graph visualization of Nostr events. */ @@ -12,13 +12,13 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; * Represents the physical properties of a node in the simulation */ export interface SimulationNodeDatum { - index?: number; // Node index in the simulation - x?: number; // X position - y?: number; // Y position - vx?: number; // X velocity - vy?: number; // Y velocity - fx?: number | null; // Fixed X position (when node is pinned) - fy?: number | null; // Fixed Y position (when node is pinned) + index?: number; // Node index in the simulation + x?: number; // X position + y?: number; // Y position + vx?: number; // X velocity + vy?: number; // Y velocity + fx?: number | null; // Fixed X position (when node is pinned) + fy?: number | null; // Fixed Y position (when node is pinned) } /** @@ -26,9 +26,9 @@ export interface SimulationNodeDatum { * Represents connections between nodes */ export interface SimulationLinkDatum { - source: NodeType | string | number; // Source node or identifier - target: NodeType | string | number; // Target node or identifier - index?: number; // Link index in the simulation + source: NodeType | string | number; // Source node or identifier + target: NodeType | string | number; // Target node or identifier + index?: number; // Link index in the simulation } /** @@ -36,17 +36,17 @@ export interface SimulationLinkDatum { * Extends the base simulation node with Nostr event-specific properties */ export interface NetworkNode extends SimulationNodeDatum { - id: string; // Unique identifier (event ID) - event?: NDKEvent; // Reference to the original NDK event - level: number; // Hierarchy level in the network - kind: number; // Nostr event kind (30040 for index, 30041/30818 for content) - title: string; // Event title - content: string; // Event content - author: string; // Author's public key - type: "Index" | "Content"; // Node type classification - naddr?: string; // NIP-19 naddr identifier - nevent?: string; // NIP-19 nevent identifier - isContainer?: boolean; // Whether this node is a container (index) + id: string; // Unique identifier (event ID) + event?: NDKEvent; // Reference to the original NDK event + level: number; // Hierarchy level in the network + kind: number; // Nostr event kind (30040 for index, 30041/30818 for content) + title: string; // Event title + content: string; // Event content + author: string; // Author's public key + type: "Index" | "Content"; // Node type classification + naddr?: string; // NIP-19 naddr identifier + nevent?: string; // NIP-19 nevent identifier + isContainer?: boolean; // Whether this node is a container (index) } /** @@ -54,17 +54,17 @@ export interface NetworkNode extends SimulationNodeDatum { * Extends the base simulation link with event-specific properties */ export interface NetworkLink extends SimulationLinkDatum { - source: NetworkNode; // Source node (overridden to be more specific) - target: NetworkNode; // Target node (overridden to be more specific) - isSequential: boolean; // Whether this link represents a sequential relationship + source: NetworkNode; // Source node (overridden to be more specific) + target: NetworkNode; // Target node (overridden to be more specific) + isSequential: boolean; // Whether this link represents a sequential relationship } /** * Represents the complete graph data for visualization */ export interface GraphData { - nodes: NetworkNode[]; // All nodes in the graph - links: NetworkLink[]; // All links in the graph + nodes: NetworkNode[]; // All nodes in the graph + links: NetworkLink[]; // All links in the graph } /** @@ -72,8 +72,8 @@ export interface GraphData { * Used to track relationships and build the final graph */ export interface GraphState { - nodeMap: Map; // Maps event IDs to nodes - links: NetworkLink[]; // All links in the graph - eventMap: Map; // Maps event IDs to original events - referencedIds: Set; // Set of event IDs referenced by other events + nodeMap: Map; // Maps event IDs to nodes + links: NetworkLink[]; // All links in the graph + eventMap: Map; // Maps event IDs to original events + referencedIds: Set; // Set of event IDs referenced by other events } diff --git a/src/lib/navigator/EventNetwork/utils/forceSimulation.ts b/src/lib/navigator/EventNetwork/utils/forceSimulation.ts index 34731b3..dbcb1e0 100644 --- a/src/lib/navigator/EventNetwork/utils/forceSimulation.ts +++ b/src/lib/navigator/EventNetwork/utils/forceSimulation.ts @@ -1,6 +1,6 @@ /** * D3 Force Simulation Utilities - * + * * This module provides utilities for creating and managing D3 force-directed * graph simulations for the event network visualization. */ @@ -27,18 +27,18 @@ function debug(...args: any[]) { * Provides type safety for simulation operations */ export interface Simulation { - nodes(): NodeType[]; - nodes(nodes: NodeType[]): this; - alpha(): number; - alpha(alpha: number): this; - alphaTarget(): number; - alphaTarget(target: number): this; - restart(): this; - stop(): this; - tick(): this; - on(type: string, listener: (this: this) => void): this; - force(name: string): any; - force(name: string, force: any): this; + nodes(): NodeType[]; + nodes(nodes: NodeType[]): this; + alpha(): number; + alpha(alpha: number): this; + alphaTarget(): number; + alphaTarget(target: number): this; + restart(): this; + stop(): this; + tick(): this; + on(type: string, listener: (this: this) => void): this; + force(name: string): any; + force(name: string, force: any): this; } /** @@ -46,155 +46,173 @@ export interface Simulation { * Provides type safety for drag operations */ export interface D3DragEvent { - active: number; - sourceEvent: any; - subject: Subject; - x: number; - y: number; - dx: number; - dy: number; - identifier: string | number; + active: number; + sourceEvent: any; + subject: Subject; + x: number; + y: number; + dx: number; + dy: number; + identifier: string | number; } /** * Updates a node's velocity by applying a force - * + * * @param node - The node to update * @param deltaVx - Change in x velocity * @param deltaVy - Change in y velocity */ export function updateNodeVelocity( - node: NetworkNode, - deltaVx: number, - deltaVy: number + node: NetworkNode, + deltaVx: number, + deltaVy: number, ) { - debug("Updating node velocity", { - nodeId: node.id, - currentVx: node.vx, - currentVy: node.vy, - deltaVx, - deltaVy - }); - - if (typeof node.vx === "number" && typeof node.vy === "number") { - node.vx = node.vx - deltaVx; - node.vy = node.vy - deltaVy; - debug("New velocity", { nodeId: node.id, vx: node.vx, vy: node.vy }); - } else { - debug("Node velocity not defined", { nodeId: node.id }); - } + debug("Updating node velocity", { + nodeId: node.id, + currentVx: node.vx, + currentVy: node.vy, + deltaVx, + deltaVy, + }); + + if (typeof node.vx === "number" && typeof node.vy === "number") { + node.vx = node.vx - deltaVx; + node.vy = node.vy - deltaVy; + debug("New velocity", { nodeId: node.id, vx: node.vx, vy: node.vy }); + } else { + debug("Node velocity not defined", { nodeId: node.id }); + } } /** * Applies a logarithmic gravity force pulling the node toward the center - * + * * The logarithmic scale ensures that nodes far from the center experience * stronger gravity, preventing them from drifting too far away. - * + * * @param node - The node to apply gravity to * @param centerX - X coordinate of the center * @param centerY - Y coordinate of the center * @param alpha - Current simulation alpha (cooling factor) */ export function applyGlobalLogGravity( - node: NetworkNode, - centerX: number, - centerY: number, - alpha: number, + node: NetworkNode, + centerX: number, + centerY: number, + alpha: number, ) { - const dx = (node.x ?? 0) - centerX; - const dy = (node.y ?? 0) - centerY; - const distance = Math.sqrt(dx * dx + dy * dy); + const dx = (node.x ?? 0) - centerX; + const dy = (node.y ?? 0) - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); - if (distance === 0) return; + if (distance === 0) return; - const force = Math.log(distance + 1) * GRAVITY_STRENGTH * alpha; - updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); + const force = Math.log(distance + 1) * GRAVITY_STRENGTH * alpha; + updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); } /** * Applies gravity between connected nodes - * + * * This creates a cohesive force that pulls connected nodes toward their * collective center of gravity, creating more meaningful clusters. - * + * * @param node - The node to apply connected gravity to * @param links - All links in the network * @param alpha - Current simulation alpha (cooling factor) */ export function applyConnectedGravity( - node: NetworkNode, - links: NetworkLink[], - alpha: number, + node: NetworkNode, + links: NetworkLink[], + alpha: number, ) { - // Find all nodes connected to this node - const connectedNodes = links - .filter(link => link.source.id === node.id || link.target.id === node.id) - .map(link => link.source.id === node.id ? link.target : link.source); + // Find all nodes connected to this node + const connectedNodes = links + .filter((link) => link.source.id === node.id || link.target.id === node.id) + .map((link) => (link.source.id === node.id ? link.target : link.source)); - if (connectedNodes.length === 0) return; + if (connectedNodes.length === 0) return; - // Calculate center of gravity of connected nodes - const cogX = d3.mean(connectedNodes, (n: NetworkNode) => n.x); - const cogY = d3.mean(connectedNodes, (n: NetworkNode) => n.y); + // Calculate center of gravity of connected nodes + const cogX = d3.mean(connectedNodes, (n: NetworkNode) => n.x); + const cogY = d3.mean(connectedNodes, (n: NetworkNode) => n.y); - if (cogX === undefined || cogY === undefined) return; + if (cogX === undefined || cogY === undefined) return; - // Calculate force direction and magnitude - const dx = (node.x ?? 0) - cogX; - const dy = (node.y ?? 0) - cogY; - const distance = Math.sqrt(dx * dx + dy * dy); + // Calculate force direction and magnitude + const dx = (node.x ?? 0) - cogX; + const dy = (node.y ?? 0) - cogY; + const distance = Math.sqrt(dx * dx + dy * dy); - if (distance === 0) return; + if (distance === 0) return; - // Apply force proportional to distance - const force = distance * CONNECTED_GRAVITY_STRENGTH * alpha; - updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); + // Apply force proportional to distance + const force = distance * CONNECTED_GRAVITY_STRENGTH * alpha; + updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); } /** * Sets up drag behavior for nodes - * + * * This enables interactive dragging of nodes in the visualization. - * + * * @param simulation - The D3 force simulation * @param warmupClickEnergy - Alpha target when dragging starts (0-1) * @returns D3 drag behavior configured for the simulation */ export function setupDragHandlers( - simulation: Simulation, - warmupClickEnergy: number = 0.9 + simulation: Simulation, + warmupClickEnergy: number = 0.9, ) { - return d3 - .drag() - .on("start", (event: D3DragEvent, d: NetworkNode) => { - // Warm up simulation if it's cooled down - if (!event.active) { - simulation.alphaTarget(warmupClickEnergy).restart(); - } - // Fix node position at current location - d.fx = d.x; - d.fy = d.y; - }) - .on("drag", (event: D3DragEvent, d: NetworkNode) => { - // Update fixed position to mouse position - d.fx = event.x; - d.fy = event.y; - }) - .on("end", (event: D3DragEvent, d: NetworkNode) => { - // Cool down simulation when drag ends - if (!event.active) { - simulation.alphaTarget(0); - } - // Release fixed position - d.fx = null; - d.fy = null; - }); + return d3 + .drag() + .on( + "start", + ( + event: D3DragEvent, + d: NetworkNode, + ) => { + // Warm up simulation if it's cooled down + if (!event.active) { + simulation.alphaTarget(warmupClickEnergy).restart(); + } + // Fix node position at current location + d.fx = d.x; + d.fy = d.y; + }, + ) + .on( + "drag", + ( + event: D3DragEvent, + d: NetworkNode, + ) => { + // Update fixed position to mouse position + d.fx = event.x; + d.fy = event.y; + }, + ) + .on( + "end", + ( + event: D3DragEvent, + d: NetworkNode, + ) => { + // Cool down simulation when drag ends + if (!event.active) { + simulation.alphaTarget(0); + } + // Release fixed position + d.fx = null; + d.fy = null; + }, + ); } /** * Creates a D3 force simulation for the network - * + * * @param nodes - Array of network nodes * @param links - Array of network links * @param nodeRadius - Radius of node circles @@ -202,34 +220,35 @@ export function setupDragHandlers( * @returns Configured D3 force simulation */ export function createSimulation( - nodes: NetworkNode[], - links: NetworkLink[], - nodeRadius: number, - linkDistance: number + nodes: NetworkNode[], + links: NetworkLink[], + nodeRadius: number, + linkDistance: number, ): Simulation { - debug("Creating simulation", { - nodeCount: nodes.length, - linkCount: links.length, - nodeRadius, - linkDistance - }); - - try { - // Create the simulation with nodes - const simulation = d3 - .forceSimulation(nodes) - .force( - "link", - d3.forceLink(links) - .id((d: NetworkNode) => d.id) - .distance(linkDistance * 0.1) - ) - .force("collide", d3.forceCollide().radius(nodeRadius * 4)); - - debug("Simulation created successfully"); - return simulation; - } catch (error) { - console.error("Error creating simulation:", error); - throw error; - } + debug("Creating simulation", { + nodeCount: nodes.length, + linkCount: links.length, + nodeRadius, + linkDistance, + }); + + try { + // Create the simulation with nodes + const simulation = d3 + .forceSimulation(nodes) + .force( + "link", + d3 + .forceLink(links) + .id((d: NetworkNode) => d.id) + .distance(linkDistance * 0.1), + ) + .force("collide", d3.forceCollide().radius(nodeRadius * 4)); + + debug("Simulation created successfully"); + return simulation; + } catch (error) { + console.error("Error creating simulation:", error); + throw error; + } } diff --git a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts index 236f702..3ab4eca 100644 --- a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts @@ -1,6 +1,6 @@ /** * Network Builder Utilities - * + * * This module provides utilities for building a network graph from Nostr events. * It handles the creation of nodes and links, and the processing of event relationships. */ @@ -9,7 +9,7 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types"; import { nip19 } from "nostr-tools"; import { standardRelays } from "$lib/consts"; -import { getMatchingTags } from '$lib/utils/nostrUtils'; +import { getMatchingTags } from "$lib/utils/nostrUtils"; // Configuration const DEBUG = false; // Set to true to enable debug logging @@ -27,165 +27,175 @@ function debug(...args: any[]) { /** * Creates a NetworkNode from an NDKEvent - * + * * Extracts relevant information from the event and creates a node representation * for the visualization. - * + * * @param event - The Nostr event to convert to a node * @param level - The hierarchy level of the node (default: 0) * @returns A NetworkNode object representing the event */ export function createNetworkNode( - event: NDKEvent, - level: number = 0 + event: NDKEvent, + level: number = 0, ): NetworkNode { - debug("Creating network node", { eventId: event.id, kind: event.kind, level }); - - const isContainer = event.kind === INDEX_EVENT_KIND; - const nodeType = isContainer ? "Index" : "Content"; + debug("Creating network node", { + eventId: event.id, + kind: event.kind, + level, + }); + + const isContainer = event.kind === INDEX_EVENT_KIND; + const nodeType = isContainer ? "Index" : "Content"; + + // Create the base node with essential properties + const node: NetworkNode = { + id: event.id, + event, + isContainer, + level, + title: event.getMatchingTags("title")?.[0]?.[1] || "Untitled", + content: event.content || "", + author: event.pubkey || "", + kind: event.kind || CONTENT_EVENT_KIND, // Default to content event kind if undefined + type: nodeType, + }; - // Create the base node with essential properties - const node: NetworkNode = { + // Add NIP-19 identifiers if possible + if (event.kind && event.pubkey) { + try { + const dTag = event.getMatchingTags("d")?.[0]?.[1] || ""; + + // Create naddr (NIP-19 address) for the event + node.naddr = nip19.naddrEncode({ + pubkey: event.pubkey, + identifier: dTag, + kind: event.kind, + relays: standardRelays, + }); + + // Create nevent (NIP-19 event reference) for the event + node.nevent = nip19.neventEncode({ id: event.id, - event, - isContainer, - level, - title: event.getMatchingTags("title")?.[0]?.[1] || "Untitled", - content: event.content || "", - author: event.pubkey || "", - kind: event.kind || CONTENT_EVENT_KIND, // Default to content event kind if undefined - type: nodeType, - }; - - // Add NIP-19 identifiers if possible - if (event.kind && event.pubkey) { - try { - const dTag = event.getMatchingTags("d")?.[0]?.[1] || ""; - - // Create naddr (NIP-19 address) for the event - node.naddr = nip19.naddrEncode({ - pubkey: event.pubkey, - identifier: dTag, - kind: event.kind, - relays: standardRelays, - }); - - // Create nevent (NIP-19 event reference) for the event - node.nevent = nip19.neventEncode({ - id: event.id, - relays: standardRelays, - kind: event.kind, - }); - } catch (error) { - console.warn("Failed to generate identifiers for node:", error); - } + relays: standardRelays, + kind: event.kind, + }); + } catch (error) { + console.warn("Failed to generate identifiers for node:", error); } + } - return node; + return node; } /** * Creates a map of event IDs to events for quick lookup - * + * * @param events - Array of Nostr events * @returns Map of event IDs to events */ export function createEventMap(events: NDKEvent[]): Map { - debug("Creating event map", { eventCount: events.length }); - - const eventMap = new Map(); - events.forEach((event) => { - if (event.id) { - eventMap.set(event.id, event); - } - }); - - debug("Event map created", { mapSize: eventMap.size }); - return eventMap; + debug("Creating event map", { eventCount: events.length }); + + const eventMap = new Map(); + events.forEach((event) => { + if (event.id) { + eventMap.set(event.id, event); + } + }); + + debug("Event map created", { mapSize: eventMap.size }); + return eventMap; } /** * Extracts an event ID from an 'a' tag - * + * * @param tag - The tag array from a Nostr event * @returns The event ID or null if not found */ export function extractEventIdFromATag(tag: string[]): string | null { - return tag[3] || null; + return tag[3] || null; } /** * Generates a deterministic color for an event based on its ID - * + * * This creates visually distinct colors for different index events * while ensuring the same event always gets the same color. - * + * * @param eventId - The event ID to generate a color for * @returns An HSL color string */ export function getEventColor(eventId: string): string { - // Use first 4 characters of event ID as a hex number - const num = parseInt(eventId.slice(0, 4), 16); - // Convert to a hue value (0-359) - const hue = num % 360; - // Use fixed saturation and lightness for pastel colors - const saturation = 70; - const lightness = 75; - return `hsl(${hue}, ${saturation}%, ${lightness}%)`; + // Use first 4 characters of event ID as a hex number + const num = parseInt(eventId.slice(0, 4), 16); + // Convert to a hue value (0-359) + const hue = num % 360; + // Use fixed saturation and lightness for pastel colors + const saturation = 70; + const lightness = 75; + return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } /** * Initializes the graph state from a set of events - * + * * Creates nodes for all events and identifies referenced events. - * + * * @param events - Array of Nostr events * @returns Initial graph state */ export function initializeGraphState(events: NDKEvent[]): GraphState { - debug("Initializing graph state", { eventCount: events.length }); + debug("Initializing graph state", { eventCount: events.length }); + + const nodeMap = new Map(); + const eventMap = createEventMap(events); + + // Create initial nodes for all events + events.forEach((event) => { + if (!event.id) return; + const node = createNetworkNode(event); + nodeMap.set(event.id, node); + }); + debug("Node map created", { nodeCount: nodeMap.size }); + + // Build set of referenced event IDs to identify root events + const referencedIds = new Set(); + events.forEach((event) => { + // Handle both "a" tags (NIP-62) and "e" tags (legacy) + let tags = getMatchingTags(event, "a"); + if (tags.length === 0) { + tags = getMatchingTags(event, "e"); + } - const nodeMap = new Map(); - const eventMap = createEventMap(events); - - // Create initial nodes for all events - events.forEach((event) => { - if (!event.id) return; - const node = createNetworkNode(event); - nodeMap.set(event.id, node); + debug("Processing tags for event", { + eventId: event.id, + tagCount: tags.length, + tagType: tags.length > 0 ? (getMatchingTags(event, "a").length > 0 ? "a" : "e") : "none" }); - debug("Node map created", { nodeCount: nodeMap.size }); - - // Build set of referenced event IDs to identify root events - const referencedIds = new Set(); - events.forEach((event) => { - const aTags = getMatchingTags(event, "a"); - debug("Processing a-tags for event", { - eventId: event.id, - aTagCount: aTags.length - }); - - aTags.forEach((tag) => { - const id = extractEventIdFromATag(tag); - if (id) referencedIds.add(id); - }); + + tags.forEach((tag) => { + const id = extractEventIdFromATag(tag); + if (id) referencedIds.add(id); }); - debug("Referenced IDs set created", { referencedCount: referencedIds.size }); - - return { - nodeMap, - links: [], - eventMap, - referencedIds, - }; + }); + debug("Referenced IDs set created", { referencedCount: referencedIds.size }); + + return { + nodeMap, + links: [], + eventMap, + referencedIds, + }; } /** * Processes a sequence of nodes referenced by an index event - * + * * Creates links between the index and its content, and between sequential content nodes. * Also processes nested indices recursively up to the maximum level. - * + * * @param sequence - Array of nodes in the sequence * @param indexEvent - The index event referencing the sequence * @param level - Current hierarchy level @@ -193,149 +203,153 @@ export function initializeGraphState(events: NDKEvent[]): GraphState { * @param maxLevel - Maximum hierarchy level to process */ export function processSequence( - sequence: NetworkNode[], - indexEvent: NDKEvent, - level: number, - state: GraphState, - maxLevel: number, + sequence: NetworkNode[], + indexEvent: NDKEvent, + level: number, + state: GraphState, + maxLevel: number, ): void { - // Stop if we've reached max level or have no nodes - if (level >= maxLevel || sequence.length === 0) return; + // Stop if we've reached max level or have no nodes + if (level >= maxLevel || sequence.length === 0) return; + + // Set levels for all nodes in the sequence + sequence.forEach((node) => { + node.level = level + 1; + }); - // Set levels for all nodes in the sequence - sequence.forEach((node) => { - node.level = level + 1; + // Create link from index to first content node + const indexNode = state.nodeMap.get(indexEvent.id); + if (indexNode && sequence[0]) { + state.links.push({ + source: indexNode, + target: sequence[0], + isSequential: true, }); + } - // Create link from index to first content node - const indexNode = state.nodeMap.get(indexEvent.id); - if (indexNode && sequence[0]) { - state.links.push({ - source: indexNode, - target: sequence[0], - isSequential: true, - }); - } + // Create sequential links between content nodes + for (let i = 0; i < sequence.length - 1; i++) { + const currentNode = sequence[i]; + const nextNode = sequence[i + 1]; - // Create sequential links between content nodes - for (let i = 0; i < sequence.length - 1; i++) { - const currentNode = sequence[i]; - const nextNode = sequence[i + 1]; - - state.links.push({ - source: currentNode, - target: nextNode, - isSequential: true, - }); - - // Process nested indices recursively - if (currentNode.isContainer) { - processNestedIndex(currentNode, level + 1, state, maxLevel); - } - } + state.links.push({ + source: currentNode, + target: nextNode, + isSequential: true, + }); - // Process the last node if it's an index - const lastNode = sequence[sequence.length - 1]; - if (lastNode?.isContainer) { - processNestedIndex(lastNode, level + 1, state, maxLevel); + // Process nested indices recursively + if (currentNode.isContainer) { + processNestedIndex(currentNode, level + 1, state, maxLevel); } + } + + // Process the last node if it's an index + const lastNode = sequence[sequence.length - 1]; + if (lastNode?.isContainer) { + processNestedIndex(lastNode, level + 1, state, maxLevel); + } } /** * Processes a nested index node - * + * * @param node - The index node to process * @param level - Current hierarchy level * @param state - Current graph state * @param maxLevel - Maximum hierarchy level to process */ export function processNestedIndex( - node: NetworkNode, - level: number, - state: GraphState, - maxLevel: number, + node: NetworkNode, + level: number, + state: GraphState, + maxLevel: number, ): void { - if (!node.isContainer || level >= maxLevel) return; + if (!node.isContainer || level >= maxLevel) return; - const nestedEvent = state.eventMap.get(node.id); - if (nestedEvent) { - processIndexEvent(nestedEvent, level, state, maxLevel); - } + const nestedEvent = state.eventMap.get(node.id); + if (nestedEvent) { + processIndexEvent(nestedEvent, level, state, maxLevel); + } } /** * Processes an index event and its referenced content - * + * * @param indexEvent - The index event to process * @param level - Current hierarchy level * @param state - Current graph state * @param maxLevel - Maximum hierarchy level to process */ export function processIndexEvent( - indexEvent: NDKEvent, - level: number, - state: GraphState, - maxLevel: number, + indexEvent: NDKEvent, + level: number, + state: GraphState, + maxLevel: number, ): void { - if (level >= maxLevel) return; + if (level >= maxLevel) return; - // Extract the sequence of nodes referenced by this index - const sequence = getMatchingTags(indexEvent, "a") - .map((tag) => extractEventIdFromATag(tag)) - .filter((id): id is string => id !== null) - .map((id) => state.nodeMap.get(id)) - .filter((node): node is NetworkNode => node !== undefined); + // Extract the sequence of nodes referenced by this index + // Handle both "a" tags (NIP-62) and "e" tags (legacy) + let tags = getMatchingTags(indexEvent, "a"); + if (tags.length === 0) { + tags = getMatchingTags(indexEvent, "e"); + } + + const sequence = tags + .map((tag) => extractEventIdFromATag(tag)) + .filter((id): id is string => id !== null) + .map((id) => state.nodeMap.get(id)) + .filter((node): node is NetworkNode => node !== undefined); - processSequence(sequence, indexEvent, level, state, maxLevel); + processSequence(sequence, indexEvent, level, state, maxLevel); } /** * Generates a complete graph from a set of events - * + * * This is the main entry point for building the network visualization. - * + * * @param events - Array of Nostr events * @param maxLevel - Maximum hierarchy level to process * @returns Complete graph data for visualization */ -export function generateGraph( - events: NDKEvent[], - maxLevel: number -): GraphData { - debug("Generating graph", { eventCount: events.length, maxLevel }); - - // Initialize the graph state - const state = initializeGraphState(events); +export function generateGraph(events: NDKEvent[], maxLevel: number): GraphData { + debug("Generating graph", { eventCount: events.length, maxLevel }); - // Find root index events (those not referenced by other events) - const rootIndices = events.filter( - (e) => e.kind === INDEX_EVENT_KIND && e.id && !state.referencedIds.has(e.id) - ); - - debug("Found root indices", { - rootCount: rootIndices.length, - rootIds: rootIndices.map(e => e.id) - }); - - // Process each root index - rootIndices.forEach((rootIndex) => { - debug("Processing root index", { - rootId: rootIndex.id, - aTags: getMatchingTags(rootIndex, "a").length - }); - processIndexEvent(rootIndex, 0, state, maxLevel); - }); + // Initialize the graph state + const state = initializeGraphState(events); - // Create the final graph data - const result = { - nodes: Array.from(state.nodeMap.values()), - links: state.links, - }; - - debug("Graph generation complete", { - nodeCount: result.nodes.length, - linkCount: result.links.length + // Find root index events (those not referenced by other events) + const rootIndices = events.filter( + (e) => + e.kind === INDEX_EVENT_KIND && e.id && !state.referencedIds.has(e.id), + ); + + debug("Found root indices", { + rootCount: rootIndices.length, + rootIds: rootIndices.map((e) => e.id), + }); + + // Process each root index + rootIndices.forEach((rootIndex) => { + debug("Processing root index", { + rootId: rootIndex.id, + aTags: getMatchingTags(rootIndex, "a").length, }); - - return result; + processIndexEvent(rootIndex, 0, state, maxLevel); + }); + + // Create the final graph data + const result = { + nodes: Array.from(state.nodeMap.values()), + links: state.links, + }; + + debug("Graph generation complete", { + nodeCount: result.nodes.length, + linkCount: result.links.length, + }); + + return result; } diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index d5a0f79..7ca1236 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -1,16 +1,283 @@ -import NDK, { NDKNip07Signer, NDKRelay, NDKRelayAuthPolicies, NDKRelaySet, NDKUser } from '@nostr-dev-kit/ndk'; +import NDK, { + NDKNip07Signer, + NDKRelay, + NDKRelayAuthPolicies, + NDKRelaySet, + NDKUser, + NDKEvent, +} from '@nostr-dev-kit/ndk'; import { get, writable, type Writable } from 'svelte/store'; -import { fallbackRelays, FeedType, loginStorageKey, standardRelays } from './consts'; +import { fallbackRelays, FeedType, loginStorageKey, standardRelays, anonymousRelays } from './consts'; import { feedType } from './stores'; +import { userStore } from './stores/userStore'; +import { userPubkey } from '$lib/stores/authStore.Svelte'; export const ndkInstance: Writable = writable(); +export const ndkSignedIn = writable(false); +export const activePubkey = writable(null); +export const inboxRelays = writable([]); +export const outboxRelays = writable([]); -export const ndkSignedIn: Writable = writable(false); +/** + * Custom authentication policy that handles NIP-42 authentication manually + * when the default NDK authentication fails + */ +class CustomRelayAuthPolicy { + private ndk: NDK; + private challenges: Map = new Map(); + + constructor(ndk: NDK) { + this.ndk = ndk; + } + + /** + * Handles authentication for a relay + * @param relay The relay to authenticate with + * @returns Promise that resolves when authentication is complete + */ + async authenticate(relay: NDKRelay): Promise { + if (!this.ndk.signer || !this.ndk.activeUser) { + console.warn( + "[NDK.ts] No signer or active user available for relay authentication", + ); + return; + } + + try { + console.debug(`[NDK.ts] Setting up authentication for ${relay.url}`); + + // Listen for AUTH challenges + relay.on("auth", (challenge: string) => { + console.debug( + `[NDK.ts] Received AUTH challenge from ${relay.url}:`, + challenge, + ); + this.challenges.set(relay.url, challenge); + this.handleAuthChallenge(relay, challenge); + }); + + // Listen for auth-required errors (handle via notice events) + relay.on("notice", (message: string) => { + if (message.includes("auth-required")) { + console.debug(`[NDK.ts] Auth required from ${relay.url}:`, message); + this.handleAuthRequired(relay, message); + } + }); + + // Listen for successful authentication + relay.on("authed", () => { + console.debug(`[NDK.ts] Successfully authenticated to ${relay.url}`); + }); + + // Listen for authentication failures + relay.on("auth:failed", (error: any) => { + console.error( + `[NDK.ts] Authentication failed for ${relay.url}:`, + error, + ); + }); + } catch (error) { + console.error( + `[NDK.ts] Error setting up authentication for ${relay.url}:`, + error, + ); + } + } + + /** + * Handles AUTH challenge from relay + */ + private async handleAuthChallenge( + relay: NDKRelay, + challenge: string, + ): Promise { + try { + if (!this.ndk.signer || !this.ndk.activeUser) { + console.warn("[NDK.ts] No signer available for AUTH challenge"); + return; + } + + // Create NIP-42 authentication event + const authEvent = { + kind: 22242, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["relay", relay.url], + ["challenge", challenge], + ], + content: "", + pubkey: this.ndk.activeUser.pubkey, + }; + + // Create and sign the authentication event using NDKEvent + const authNDKEvent = new NDKEvent(this.ndk, authEvent); + await authNDKEvent.sign(); + + // Send AUTH message to relay using the relay's publish method + await relay.publish(authNDKEvent); + console.debug(`[NDK.ts] Sent AUTH to ${relay.url}`); + } catch (error) { + console.error( + `[NDK.ts] Error handling AUTH challenge for ${relay.url}:`, + error, + ); + } + } + + /** + * Handles auth-required error from relay + */ + private async handleAuthRequired( + relay: NDKRelay, + message: string, + ): Promise { + const challenge = this.challenges.get(relay.url); + if (challenge) { + await this.handleAuthChallenge(relay, challenge); + } else { + console.warn( + `[NDK.ts] Auth required from ${relay.url} but no challenge available`, + ); + } + } +} + +/** + * Checks if the current environment might cause WebSocket protocol downgrade + */ +export function checkEnvironmentForWebSocketDowngrade(): void { + console.debug("[NDK.ts] Environment Check for WebSocket Protocol:"); + + const isLocalhost = + window.location.hostname === "localhost" || + window.location.hostname === "127.0.0.1"; + const isHttp = window.location.protocol === "http:"; + const isHttps = window.location.protocol === "https:"; + + console.debug("[NDK.ts] - Is localhost:", isLocalhost); + console.debug("[NDK.ts] - Protocol:", window.location.protocol); + console.debug("[NDK.ts] - Is HTTP:", isHttp); + console.debug("[NDK.ts] - Is HTTPS:", isHttps); + + if (isLocalhost && isHttp) { + console.warn( + "[NDK.ts] ⚠️ Running on localhost with HTTP - WebSocket downgrade to ws:// is expected", + ); + console.warn("[NDK.ts] This is normal for development environments"); + } else if (isHttp) { + console.error( + "[NDK.ts] ❌ Running on HTTP - WebSocket connections will be insecure", + ); + console.error("[NDK.ts] Consider using HTTPS in production"); + } else if (isHttps) { + console.debug( + "[NDK.ts] ✓ Running on HTTPS - Secure WebSocket connections should work", + ); + } +} + +/** + * Checks WebSocket protocol support and logs diagnostic information + */ +export function checkWebSocketSupport(): void { + console.debug("[NDK.ts] WebSocket Support Diagnostics:"); + console.debug("[NDK.ts] - Protocol:", window.location.protocol); + console.debug("[NDK.ts] - Hostname:", window.location.hostname); + console.debug("[NDK.ts] - Port:", window.location.port); + console.debug("[NDK.ts] - User Agent:", navigator.userAgent); + + // Test if secure WebSocket is supported + try { + const testWs = new WebSocket("wss://echo.websocket.org"); + testWs.onopen = () => { + console.debug("[NDK.ts] ✓ Secure WebSocket (wss://) is supported"); + testWs.close(); + }; + testWs.onerror = () => { + console.warn("[NDK.ts] ✗ Secure WebSocket (wss://) may not be supported"); + }; + } catch (error) { + console.warn("[NDK.ts] ✗ WebSocket test failed:", error); + } +} + +/** + * Tests connection to a relay and returns connection status + * @param relayUrl The relay URL to test + * @param ndk The NDK instance + * @returns Promise that resolves to connection status + */ +export async function testRelayConnection( + relayUrl: string, + ndk: NDK, +): Promise<{ + connected: boolean; + requiresAuth: boolean; + error?: string; + actualUrl?: string; +}> { + return new Promise((resolve) => { + console.debug(`[NDK.ts] Testing connection to: ${relayUrl}`); -export const activePubkey: Writable = writable(null); + // Ensure the URL is using wss:// protocol + const secureUrl = ensureSecureWebSocket(relayUrl); -export const inboxRelays: Writable = writable([]); -export const outboxRelays: Writable = writable([]); + const relay = new NDKRelay(secureUrl, undefined, new NDK()); + let authRequired = false; + let connected = false; + let error: string | undefined; + let actualUrl: string | undefined; + + const timeout = setTimeout(() => { + relay.disconnect(); + resolve({ + connected: false, + requiresAuth: authRequired, + error: "Connection timeout", + actualUrl, + }); + }, 5000); + + relay.on("connect", () => { + console.debug(`[NDK.ts] Connected to ${secureUrl}`); + connected = true; + actualUrl = secureUrl; + clearTimeout(timeout); + relay.disconnect(); + resolve({ + connected: true, + requiresAuth: authRequired, + error, + actualUrl, + }); + }); + + relay.on("notice", (message: string) => { + if (message.includes("auth-required")) { + authRequired = true; + console.debug(`[NDK.ts] ${secureUrl} requires authentication`); + } + }); + + relay.on("disconnect", () => { + if (!connected) { + error = "Connection failed"; + console.error(`[NDK.ts] Failed to connect to ${secureUrl}`); + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: authRequired, + error, + actualUrl, + }); + } + }); + + // Log the actual WebSocket URL being used + console.debug(`[NDK.ts] Attempting connection to: ${secureUrl}`); + relay.connect(); + }); +} /** * Gets the user's pubkey from local storage, if it exists. @@ -47,7 +314,7 @@ export function clearLogin(): void { * @param type The type of relay list to designate. * @returns The constructed key. */ -function getRelayStorageKey(user: NDKUser, type: 'inbox' | 'outbox'): string { +function getRelayStorageKey(user: NDKUser, type: "inbox" | "outbox"): string { return `${loginStorageKey}/${user.pubkey}/${type}`; } @@ -57,14 +324,18 @@ function getRelayStorageKey(user: NDKUser, type: 'inbox' | 'outbox'): string { * @param inboxes The user's inbox relays. * @param outboxes The user's outbox relays. */ -function persistRelays(user: NDKUser, inboxes: Set, outboxes: Set): void { +function persistRelays( + user: NDKUser, + inboxes: Set, + outboxes: Set, +): void { localStorage.setItem( - getRelayStorageKey(user, 'inbox'), - JSON.stringify(Array.from(inboxes).map(relay => relay.url)) + getRelayStorageKey(user, "inbox"), + JSON.stringify(Array.from(inboxes).map((relay) => relay.url)), ); localStorage.setItem( - getRelayStorageKey(user, 'outbox'), - JSON.stringify(Array.from(outboxes).map(relay => relay.url)) + getRelayStorageKey(user, "outbox"), + JSON.stringify(Array.from(outboxes).map((relay) => relay.url)), ); } @@ -76,24 +347,104 @@ function persistRelays(user: NDKUser, inboxes: Set, outboxes: Set, Set] { const inboxes = new Set( - JSON.parse(localStorage.getItem(getRelayStorageKey(user, 'inbox')) ?? '[]') + JSON.parse(localStorage.getItem(getRelayStorageKey(user, "inbox")) ?? "[]"), ); const outboxes = new Set( - JSON.parse(localStorage.getItem(getRelayStorageKey(user, 'outbox')) ?? '[]') + JSON.parse( + localStorage.getItem(getRelayStorageKey(user, "outbox")) ?? "[]", + ), ); return [inboxes, outboxes]; } export function clearPersistedRelays(user: NDKUser): void { - localStorage.removeItem(getRelayStorageKey(user, 'inbox')); - localStorage.removeItem(getRelayStorageKey(user, 'outbox')); + localStorage.removeItem(getRelayStorageKey(user, "inbox")); + localStorage.removeItem(getRelayStorageKey(user, "outbox")); +} + +/** + * Ensures a relay URL uses secure WebSocket protocol + * @param url The relay URL to secure + * @returns The URL with wss:// protocol + */ +function ensureSecureWebSocket(url: string): string { + // Replace ws:// with wss:// if present + const secureUrl = url.replace(/^ws:\/\//, "wss://"); + + if (secureUrl !== url) { + console.warn( + `[NDK.ts] Protocol downgrade detected: ${url} -> ${secureUrl}`, + ); + } + + return secureUrl; +} + +/** + * Creates a relay with proper authentication handling + */ +function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { + console.debug(`[NDK.ts] Creating relay with URL: ${url}`); + + // Ensure the URL is using wss:// protocol + const secureUrl = ensureSecureWebSocket(url); + + // Add connection timeout and error handling + const relay = new NDKRelay( + secureUrl, + NDKRelayAuthPolicies.signIn({ ndk }), + ndk, + ); + + // Set up connection timeout + const connectionTimeout = setTimeout(() => { + console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`); + relay.disconnect(); + }, 10000); // 10 second timeout + + // Set up custom authentication handling only if user is signed in + if (ndk.signer && ndk.activeUser) { + const authPolicy = new CustomRelayAuthPolicy(ndk); + relay.on("connect", () => { + console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + clearTimeout(connectionTimeout); + authPolicy.authenticate(relay); + }); + } else { + relay.on("connect", () => { + console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + clearTimeout(connectionTimeout); + }); + } + + // Add error handling + relay.on("disconnect", () => { + console.debug(`[NDK.ts] Relay disconnected: ${secureUrl}`); + clearTimeout(connectionTimeout); + }); + + return relay; } export function getActiveRelays(ndk: NDK): NDKRelaySet { - return get(feedType) === FeedType.UserRelays + const user = get(userStore); + + // Filter out problematic relays that are known to cause connection issues + const filterProblematicRelays = (relays: string[]) => { + return relays.filter(relay => { + // Filter out gitcitadel.nostr1.com which is causing connection issues + if (relay.includes('gitcitadel.nostr1.com')) { + console.warn(`[NDK.ts] Filtering out problematic relay: ${relay}`); + return false; + } + return true; + }); + }; + + return get(feedType) === FeedType.UserRelays && user.signedIn ? new NDKRelaySet( - new Set(get(inboxRelays).map(relay => new NDKRelay( + new Set(filterProblematicRelays(user.relays.inbox).map(relay => new NDKRelay( relay, NDKRelayAuthPolicies.signIn({ ndk }), ndk, @@ -101,7 +452,7 @@ export function getActiveRelays(ndk: NDK): NDKRelaySet { ndk ) : new NDKRelaySet( - new Set(standardRelays.map(relay => new NDKRelay( + new Set(filterProblematicRelays(standardRelays).map(relay => new NDKRelay( relay, NDKRelayAuthPolicies.signIn({ ndk }), ndk, @@ -117,21 +468,45 @@ export function getActiveRelays(ndk: NDK): NDKRelaySet { */ export function initNdk(): NDK { const startingPubkey = getPersistedLogin(); - const [startingInboxes, _] = startingPubkey != null - ? getPersistedRelays(new NDKUser({ pubkey: startingPubkey })) - : [null, null]; + const [startingInboxes, _] = + startingPubkey != null + ? getPersistedRelays(new NDKUser({ pubkey: startingPubkey })) + : [null, null]; + + // Ensure all relay URLs use secure WebSocket protocol + const secureRelayUrls = ( + startingInboxes != null + ? Array.from(startingInboxes.values()) + : anonymousRelays + ).map(ensureSecureWebSocket); + + console.debug("[NDK.ts] Initializing NDK with relay URLs:", secureRelayUrls); const ndk = new NDK({ autoConnectUserRelays: true, enableOutboxModel: true, - explicitRelayUrls: startingInboxes != null - ? Array.from(startingInboxes.values()) - : standardRelays, + explicitRelayUrls: secureRelayUrls, }); - // TODO: Should we prompt the user to confirm authentication? + // Set up custom authentication policy ndk.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({ ndk }); - ndk.connect().then(() => console.debug("ndk connected")); + + // Connect with better error handling + ndk.connect() + .then(() => { + console.debug("[NDK.ts] NDK connected successfully"); + }) + .catch((error) => { + console.error("[NDK.ts] Failed to connect NDK:", error); + // Try to reconnect after a delay + setTimeout(() => { + console.debug("[NDK.ts] Attempting to reconnect..."); + ndk.connect().catch((retryError) => { + console.error("[NDK.ts] Reconnection failed:", retryError); + }); + }, 5000); + }); + return ndk; } @@ -142,7 +517,9 @@ export function initNdk(): NDK { * @throws If sign-in fails. This may because there is no accessible NIP-07 extension, or because * NDK is unable to fetch the user's profile or relay lists. */ -export async function loginWithExtension(pubkey?: string): Promise { +export async function loginWithExtension( + pubkey?: string, +): Promise { try { const ndk = get(ndkInstance); const signer = new NDKNip07Signer(); @@ -150,12 +527,14 @@ export async function loginWithExtension(pubkey?: string): Promise relay.url)); - outboxRelays.set(Array.from(outboxes ?? persistedOutboxes).map(relay => relay.url)); + inboxRelays.set( + Array.from(inboxes ?? persistedInboxes).map((relay) => relay.url), + ); + outboxRelays.set( + Array.from(outboxes ?? persistedOutboxes).map((relay) => relay.url), + ); persistRelays(signerUser, inboxes, outboxes); @@ -188,7 +571,9 @@ export function logout(user: NDKUser): void { clearLogin(); clearPersistedRelays(user); activePubkey.set(null); + userPubkey.set(null); ndkSignedIn.set(false); + ndkInstance.set(initNdk()); // Re-initialize with anonymous instance } /** @@ -196,17 +581,17 @@ export function logout(user: NDKUser): void { * relay sets. * @returns A tuple of relay sets of the form `[inboxRelays, outboxRelays]`. */ -async function getUserPreferredRelays( +export async function getUserPreferredRelays( ndk: NDK, user: NDKUser, - fallbacks: readonly string[] = fallbackRelays + fallbacks: readonly string[] = fallbackRelays, ): Promise<[Set, Set]> { const relayList = await ndk.fetchEvent( { kinds: [10002], authors: [user.pubkey], }, - { + { groupable: false, skipVerification: false, skipValidation: false, @@ -217,26 +602,39 @@ async function getUserPreferredRelays( const inboxRelays = new Set(); const outboxRelays = new Set(); + // Filter out problematic relays + const filterProblematicRelay = (url: string): boolean => { + if (url.includes('gitcitadel.nostr1.com')) { + console.warn(`[NDK.ts] Filtering out problematic relay from user preferences: ${url}`); + return false; + } + return true; + }; + if (relayList == null) { const relayMap = await window.nostr?.getRelays?.(); Object.entries(relayMap ?? {}).forEach(([url, relayType]) => { - const relay = new NDKRelay(url, NDKRelayAuthPolicies.signIn({ ndk }), ndk); - if (relayType.read) inboxRelays.add(relay); - if (relayType.write) outboxRelays.add(relay); + if (filterProblematicRelay(url)) { + const relay = createRelayWithAuth(url, ndk); + if (relayType.read) inboxRelays.add(relay); + if (relayType.write) outboxRelays.add(relay); + } }); } else { - relayList.tags.forEach(tag => { - switch (tag[0]) { - case 'r': - inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); - break; - case 'w': - outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); - break; - default: - inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); - outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); - break; + relayList.tags.forEach((tag) => { + if (filterProblematicRelay(tag[1])) { + switch (tag[0]) { + case "r": + inboxRelays.add(createRelayWithAuth(tag[1], ndk)); + break; + case "w": + outboxRelays.add(createRelayWithAuth(tag[1], ndk)); + break; + default: + inboxRelays.add(createRelayWithAuth(tag[1], ndk)); + outboxRelays.add(createRelayWithAuth(tag[1], ndk)); + break; + } } }); } diff --git a/src/lib/parser.ts b/src/lib/parser.ts index 860a8c6..b35c86f 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -1,5 +1,5 @@ -import NDK, { NDKEvent } from '@nostr-dev-kit/ndk'; -import asciidoctor from 'asciidoctor'; +import NDK, { NDKEvent } from "@nostr-dev-kit/ndk"; +import asciidoctor from "asciidoctor"; import type { AbstractBlock, AbstractNode, @@ -9,11 +9,11 @@ import type { Extensions, Section, ProcessorOptions, -} from 'asciidoctor'; -import he from 'he'; -import { writable, type Writable } from 'svelte/store'; -import { zettelKinds } from './consts.ts'; -import { getMatchingTags } from '$lib/utils/nostrUtils'; +} from "asciidoctor"; +import he from "he"; +import { writable, type Writable } from "svelte/store"; +import { zettelKinds } from "./consts.ts"; +import { getMatchingTags } from "$lib/utils/nostrUtils"; interface IndexMetadata { authors?: string[]; @@ -28,16 +28,16 @@ interface IndexMetadata { export enum SiblingSearchDirection { Previous, - Next + Next, } export enum InsertLocation { Before, - After + After, } /** - * @classdesc Pharos is an extension of the Asciidoctor class that adds Nostr Knowledge Base (NKB) + * @classdesc Pharos is an extension of the Asciidoctor class that adds Nostr Knowledge Base (NKB) * features to core Asciidoctor functionality. Asciidoctor is used to parse an AsciiDoc document * into an Abstract Syntax Tree (AST), and Phraos generates NKB events from the nodes in that tree. * @class @@ -46,12 +46,12 @@ export enum InsertLocation { export default class Pharos { /** * Key to terminology used in the class: - * + * * Nostr Knowledge Base (NKB) entities: * - Zettel: Bite-sized pieces of text contained within kind 30041 events. * - Index: A kind 30040 event describing a collection of zettels or other Nostr events. * - Event: The generic term for a Nostr event. - * + * * Asciidoctor entities: * - Document: The entirety of an AsciiDoc document. The document title is denoted by a level 0 * header, and the document may contain metadata, such as author and edition, immediately below @@ -112,7 +112,10 @@ export default class Pharos { /** * A map of index IDs to the IDs of the nodes they reference. */ - private indexToChildEventsMap: Map> = new Map>(); + private indexToChildEventsMap: Map> = new Map< + string, + Set + >(); /** * A map of node IDs to the Nostr event IDs of the events they generate. @@ -150,21 +153,47 @@ export default class Pharos { pharos.treeProcessor(treeProcessor, document); }); }); + + // Add advanced extensions for math, PlantUML, BPMN, and TikZ + this.loadAdvancedExtensions(); } - parse(content: string, options?: ProcessorOptions | undefined): void { + /** + * Loads advanced extensions for math, PlantUML, BPMN, and TikZ rendering + */ + private async loadAdvancedExtensions(): Promise { + try { + const { createAdvancedExtensions } = await import( + "./utils/markup/asciidoctorExtensions" + ); + const advancedExtensions = createAdvancedExtensions(); + // Note: Extensions merging might not be available in this version + // We'll handle this in the parse method instead + } catch (error) { + console.warn("Advanced extensions not available:", error); + } + } + parse(content: string, options?: ProcessorOptions | undefined): void { // Ensure the content is valid AsciiDoc and has a header and the doctype book content = ensureAsciiDocHeader(content); - + try { + const mergedAttributes = Object.assign( + {}, + options && typeof options.attributes === "object" + ? options.attributes + : {}, + { "source-highlighter": "highlightjs" }, + ); this.html = this.asciidoctor.convert(content, { - 'extension_registry': this.pharosExtensions, ...options, + extension_registry: this.pharosExtensions, + attributes: mergedAttributes, }) as string | Document | undefined; } catch (error) { console.error(error); - throw new Error('Failed to parse AsciiDoc document.'); + throw new Error("Failed to parse AsciiDoc document."); } } @@ -176,10 +205,10 @@ export default class Pharos { async fetch(event: NDKEvent | string): Promise { let content: string; - if (typeof event === 'string') { + if (typeof event === "string") { const index = await this.ndk.fetchEvent({ ids: [event] }); if (!index) { - throw new Error('Failed to fetch publication.'); + throw new Error("Failed to fetch publication."); } content = await this.getPublicationContent(index); @@ -201,7 +230,7 @@ export default class Pharos { /** * Generates and stores Nostr events from the parsed AsciiDoc document. The events can be * modified via the parser's API and retrieved via the `getEvents()` method. - * @param pubkey The public key (as a hex string) of the user that will sign and publish the + * @param pubkey The public key (as a hex string) of the user that will sign and publish the * events. */ generate(pubkey: string): void { @@ -229,7 +258,7 @@ export default class Pharos { * @returns The HTML content of the converted document. */ getHtml(): string { - return this.html?.toString() || ''; + return this.html?.toString() || ""; } /** @@ -237,7 +266,7 @@ export default class Pharos { * @remarks The root index ID may be used to retrieve metadata or children from the root index. */ getRootIndexId(): string { - return this.normalizeId(this.rootNodeId) ?? ''; + return this.normalizeId(this.rootNodeId) ?? ""; } /** @@ -245,7 +274,7 @@ export default class Pharos { */ getIndexTitle(id: string): string | undefined { const section = this.nodes.get(id) as Section; - const title = section.getTitle() ?? ''; + const title = section.getTitle() ?? ""; return he.decode(title); } @@ -253,16 +282,18 @@ export default class Pharos { * @returns The IDs of any child indices of the index with the given ID. */ getChildIndexIds(id: string): string[] { - return Array.from(this.indexToChildEventsMap.get(id) ?? []) - .filter(id => this.eventToKindMap.get(id) === 30040); + return Array.from(this.indexToChildEventsMap.get(id) ?? []).filter( + (id) => this.eventToKindMap.get(id) === 30040, + ); } /** * @returns The IDs of any child zettels of the index with the given ID. */ getChildZettelIds(id: string): string[] { - return Array.from(this.indexToChildEventsMap.get(id) ?? []) - .filter(id => this.eventToKindMap.get(id) !== 30040); + return Array.from(this.indexToChildEventsMap.get(id) ?? []).filter( + (id) => this.eventToKindMap.get(id) !== 30040, + ); } /** @@ -284,8 +315,8 @@ export default class Pharos { const block = this.nodes.get(normalizedId!) as AbstractBlock; switch (block.getContext()) { - case 'paragraph': - return block.getContent() ?? ''; + case "paragraph": + return block.getContent() ?? ""; } return block.convert(); @@ -301,9 +332,9 @@ export default class Pharos { if (!normalizedId || !this.nodes.has(normalizedId)) { return false; } - + const context = this.eventToContextMap.get(normalizedId); - return context === 'floating_title'; + return context === "floating_title"; } /** @@ -338,7 +369,7 @@ export default class Pharos { getNearestSibling( targetDTag: string, depth: number, - direction: SiblingSearchDirection + direction: SiblingSearchDirection, ): [string | null, string | null] { const eventsAtLevel = this.eventsByLevelMap.get(depth); if (!eventsAtLevel) { @@ -348,13 +379,17 @@ export default class Pharos { const targetIndex = eventsAtLevel.indexOf(targetDTag); if (targetIndex === -1) { - throw new Error(`The event indicated by #d:${targetDTag} does not exist at level ${depth} of the event tree.`); + throw new Error( + `The event indicated by #d:${targetDTag} does not exist at level ${depth} of the event tree.`, + ); } const parentDTag = this.getParent(targetDTag); if (!parentDTag) { - throw new Error(`The event indicated by #d:${targetDTag} does not have a parent.`); + throw new Error( + `The event indicated by #d:${targetDTag} does not have a parent.`, + ); } const grandparentDTag = this.getParent(parentDTag); @@ -372,7 +407,10 @@ export default class Pharos { // If the target is the last node at its level and we're searching for a next sibling, // look among the siblings of the target's parent at the previous level. - if (targetIndex === eventsAtLevel.length - 1 && direction === SiblingSearchDirection.Next) { + if ( + targetIndex === eventsAtLevel.length - 1 && + direction === SiblingSearchDirection.Next + ) { // * Base case: The target is at the last level of the tree and has no subsequent sibling. if (!grandparentDTag) { return [null, null]; @@ -383,10 +421,10 @@ export default class Pharos { // * Base case: There is an adjacent sibling at the same depth as the target. switch (direction) { - case SiblingSearchDirection.Previous: - return [eventsAtLevel[targetIndex - 1], parentDTag]; - case SiblingSearchDirection.Next: - return [eventsAtLevel[targetIndex + 1], parentDTag]; + case SiblingSearchDirection.Previous: + return [eventsAtLevel[targetIndex - 1], parentDTag]; + case SiblingSearchDirection.Next: + return [eventsAtLevel[targetIndex + 1], parentDTag]; } return [null, null]; @@ -401,7 +439,9 @@ export default class Pharos { getParent(dTag: string): string | null { // Check if the event exists in the parser tree. if (!this.eventIds.has(dTag)) { - throw new Error(`The event indicated by #d:${dTag} does not exist in the parser tree.`); + throw new Error( + `The event indicated by #d:${dTag} does not exist in the parser tree.`, + ); } // Iterate through all the index to child mappings. @@ -426,7 +466,11 @@ export default class Pharos { * @remarks Moving the target event within the tree changes the hash of several events, so the * event tree will be regenerated when the consumer next invokes `getEvents()`. */ - moveEvent(targetDTag: string, destinationDTag: string, insertAfter: boolean = false): void { + moveEvent( + targetDTag: string, + destinationDTag: string, + insertAfter: boolean = false, + ): void { const targetEvent = this.events.get(targetDTag); const destinationEvent = this.events.get(destinationDTag); const targetParent = this.getParent(targetDTag); @@ -441,11 +485,15 @@ export default class Pharos { } if (!targetParent) { - throw new Error(`The event indicated by #d:${targetDTag} does not have a parent.`); + throw new Error( + `The event indicated by #d:${targetDTag} does not have a parent.`, + ); } if (!destinationParent) { - throw new Error(`The event indicated by #d:${destinationDTag} does not have a parent.`); + throw new Error( + `The event indicated by #d:${destinationDTag} does not have a parent.`, + ); } // Remove the target from among the children of its current parent. @@ -455,16 +503,22 @@ export default class Pharos { this.indexToChildEventsMap.get(destinationParent)?.delete(targetDTag); // Get the index of the destination event among the children of its parent. - const destinationIndex = Array.from(this.indexToChildEventsMap.get(destinationParent) ?? []) - .indexOf(destinationDTag); + const destinationIndex = Array.from( + this.indexToChildEventsMap.get(destinationParent) ?? [], + ).indexOf(destinationDTag); // Insert next to the index of the destination event, either before or after as specified by // the insertAfter flag. - const destinationChildren = Array.from(this.indexToChildEventsMap.get(destinationParent) ?? []); + const destinationChildren = Array.from( + this.indexToChildEventsMap.get(destinationParent) ?? [], + ); insertAfter ? destinationChildren.splice(destinationIndex + 1, 0, targetDTag) : destinationChildren.splice(destinationIndex, 0, targetDTag); - this.indexToChildEventsMap.set(destinationParent, new Set(destinationChildren)); + this.indexToChildEventsMap.set( + destinationParent, + new Set(destinationChildren), + ); this.shouldUpdateEventTree = true; } @@ -494,7 +548,10 @@ export default class Pharos { * - Each node ID is mapped to an integer event kind that will be used to represent the node. * - Each ID of a node containing children is mapped to the set of IDs of its children. */ - private treeProcessor(treeProcessor: Extensions.TreeProcessor, document: Document) { + private treeProcessor( + treeProcessor: Extensions.TreeProcessor, + document: Document, + ) { this.rootNodeId = this.generateNodeId(document); document.setId(this.rootNodeId); this.nodes.set(this.rootNodeId, document); @@ -510,7 +567,7 @@ export default class Pharos { continue; } - if (block.getContext() === 'section') { + if (block.getContext() === "section") { const children = this.processSection(block as Section); nodeQueue.push(...children); } else { @@ -540,7 +597,7 @@ export default class Pharos { } this.nodes.set(sectionId, section); - this.eventToKindMap.set(sectionId, 30040); // Sections are indexToChildEventsMap by default. + this.eventToKindMap.set(sectionId, 30040); // Sections are indexToChildEventsMap by default. this.indexToChildEventsMap.set(sectionId, new Set()); const parentId = this.normalizeId(section.getParent()?.getId()); @@ -568,7 +625,7 @@ export default class Pharos { // Obtain or generate a unique ID for the block. let blockId = this.normalizeId(block.getId()); if (!blockId) { - blockId = this.generateNodeId(block) ; + blockId = this.generateNodeId(block); block.setId(blockId); } @@ -578,7 +635,7 @@ export default class Pharos { } this.nodes.set(blockId, block); - this.eventToKindMap.set(blockId, 30041); // Blocks are zettels by default. + this.eventToKindMap.set(blockId, 30041); // Blocks are zettels by default. const parentId = this.normalizeId(block.getParent()?.getId()); if (!parentId) { @@ -625,21 +682,24 @@ export default class Pharos { * @remarks This function does a depth-first crawl of the event tree using the relays specified * on the NDK instance. */ - private async getPublicationContent(event: NDKEvent, depth: number = 0): Promise { - let content: string = ''; + private async getPublicationContent( + event: NDKEvent, + depth: number = 0, + ): Promise { + let content: string = ""; // Format title into AsciiDoc header. - const title = getMatchingTags(event, 'title')[0][1]; - let titleLevel = ''; + const title = getMatchingTags(event, "title")[0][1]; + let titleLevel = ""; for (let i = 0; i <= depth; i++) { - titleLevel += '='; + titleLevel += "="; } content += `${titleLevel} ${title}\n\n`; // TODO: Deprecate `e` tags in favor of `a` tags required by NIP-62. - let tags = getMatchingTags(event, 'a'); + let tags = getMatchingTags(event, "a"); if (tags.length === 0) { - tags = getMatchingTags(event, 'e'); + tags = getMatchingTags(event, "e"); } // Base case: The event is a zettel. @@ -650,24 +710,29 @@ export default class Pharos { // Recursive case: The event is an index. const childEvents = await Promise.all( - tags.map(tag => this.ndk.fetchEventFromTag(tag, event)) + tags.map((tag) => this.ndk.fetchEventFromTag(tag, event)), ); // if a blog, save complete events for later - if (getMatchingTags(event, 'type').length > 0 && getMatchingTags(event, 'type')[0][1] === 'blog') { - childEvents.forEach(child => { + if ( + getMatchingTags(event, "type").length > 0 && + getMatchingTags(event, "type")[0][1] === "blog" + ) { + childEvents.forEach((child) => { if (child) { - this.blogEntries.set(getMatchingTags(child, 'd')?.[0]?.[1], child); + this.blogEntries.set(getMatchingTags(child, "d")?.[0]?.[1], child); } - }) + }); } // populate metadata if (event.created_at) { - this.rootIndexMetadata.publicationDate = new Date(event.created_at * 1000).toDateString(); + this.rootIndexMetadata.publicationDate = new Date( + event.created_at * 1000, + ).toDateString(); } - if (getMatchingTags(event, 'image').length > 0) { - this.rootIndexMetadata.coverImage = getMatchingTags(event, 'image')[0][1]; + if (getMatchingTags(event, "image").length > 0) { + this.rootIndexMetadata.coverImage = getMatchingTags(event, "image")[0][1]; } // Michael J - 15 December 2024 - This could be further parallelized by recursively fetching @@ -676,17 +741,19 @@ export default class Pharos { const childContentPromises: Promise[] = []; for (let i = 0; i < childEvents.length; i++) { const childEvent = childEvents[i]; - + if (!childEvent) { console.warn(`NDK could not find event ${tags[i][1]}.`); continue; } - childContentPromises.push(this.getPublicationContent(childEvent, depth + 1)); + childContentPromises.push( + this.getPublicationContent(childEvent, depth + 1), + ); } const childContents = await Promise.all(childContentPromises); - content += childContents.join('\n\n'); + content += childContents.join("\n\n"); return content; } @@ -731,17 +798,17 @@ export default class Pharos { while (nodeIdStack.length > 0) { const nodeId = nodeIdStack.pop(); - - switch (this.eventToKindMap.get(nodeId!)) { - case 30040: - events.push(this.generateIndexEvent(nodeId!, pubkey)); - break; - case 30041: - default: - // Kind 30041 (zettel) is currently the default kind for contentful events. - events.push(this.generateZettelEvent(nodeId!, pubkey)); - break; + switch (this.eventToKindMap.get(nodeId!)) { + case 30040: + events.push(this.generateIndexEvent(nodeId!, pubkey)); + break; + + case 30041: + default: + // Kind 30041 (zettel) is currently the default kind for contentful events. + events.push(this.generateZettelEvent(nodeId!, pubkey)); + break; } } @@ -760,17 +827,14 @@ export default class Pharos { private generateIndexEvent(nodeId: string, pubkey: string): NDKEvent { const title = (this.nodes.get(nodeId)! as AbstractBlock).getTitle(); // TODO: Use a tags as per NIP-62. - const childTags = Array.from(this.indexToChildEventsMap.get(nodeId)!) - .map(id => ['#e', this.eventIds.get(id)!]); + const childTags = Array.from(this.indexToChildEventsMap.get(nodeId)!).map( + (id) => ["#e", this.eventIds.get(id)!], + ); const event = new NDKEvent(this.ndk); event.kind = 30040; - event.content = ''; - event.tags = [ - ['title', title!], - ['#d', nodeId], - ...childTags - ]; + event.content = ""; + event.tags = [["title", title!], ["#d", nodeId], ...childTags]; event.created_at = Date.now(); event.pubkey = pubkey; @@ -782,34 +846,38 @@ export default class Pharos { this.rootIndexMetadata = { authors: document .getAuthors() - .map(author => author.getName()) - .filter(name => name != null), + .map((author) => author.getName()) + .filter((name): name is string => name != null), version: document.getRevisionNumber(), edition: document.getRevisionRemark(), publicationDate: document.getRevisionDate(), }; if (this.rootIndexMetadata.authors) { - event.tags.push(['author', ...this.rootIndexMetadata.authors!]); + event.tags.push(["author", ...this.rootIndexMetadata.authors!]); } if (this.rootIndexMetadata.version || this.rootIndexMetadata.edition) { - event.tags.push( - [ - 'version', - this.rootIndexMetadata.version!, - this.rootIndexMetadata.edition! - ].filter(value => value != null) - ); + const versionTags: string[] = ["version"]; + if (this.rootIndexMetadata.version) { + versionTags.push(this.rootIndexMetadata.version); + } + if (this.rootIndexMetadata.edition) { + versionTags.push(this.rootIndexMetadata.edition); + } + event.tags.push(versionTags); } if (this.rootIndexMetadata.publicationDate) { - event.tags.push(['published_on', this.rootIndexMetadata.publicationDate!]); + event.tags.push([ + "published_on", + this.rootIndexMetadata.publicationDate!, + ]); } } // Event ID generation must be the last step. - const eventId = event.getEventHash(); + const eventId = event.getEventHash(); this.eventIds.set(nodeId, eventId); event.id = eventId; @@ -828,21 +896,21 @@ export default class Pharos { */ private generateZettelEvent(nodeId: string, pubkey: string): NDKEvent { const title = (this.nodes.get(nodeId)! as Block).getTitle(); - const content = (this.nodes.get(nodeId)! as Block).getSource(); // AsciiDoc source content. + const content = (this.nodes.get(nodeId)! as Block).getSource(); // AsciiDoc source content. const event = new NDKEvent(this.ndk); event.kind = 30041; event.content = content!; event.tags = [ - ['title', title!], - ['#d', nodeId], + ["title", title!], + ["#d", nodeId], ...this.extractAndNormalizeWikilinks(content!), ]; event.created_at = Date.now(); event.pubkey = pubkey; // Event ID generation must be the last step. - const eventId = event.getEventHash(); + const eventId = event.getEventHash(); this.eventIds.set(nodeId, eventId); event.id = eventId; @@ -878,173 +946,173 @@ export default class Pharos { const context = block.getContext(); switch (context) { - case 'admonition': - blockNumber = this.contextCounters.get('admonition') ?? 0; - blockId = `${documentId}-admonition-${blockNumber++}`; - this.contextCounters.set('admonition', blockNumber); - break; + case "admonition": + blockNumber = this.contextCounters.get("admonition") ?? 0; + blockId = `${documentId}-admonition-${blockNumber++}`; + this.contextCounters.set("admonition", blockNumber); + break; - case 'audio': - blockNumber = this.contextCounters.get('audio') ?? 0; - blockId = `${documentId}-audio-${blockNumber++}`; - this.contextCounters.set('audio', blockNumber); - break; + case "audio": + blockNumber = this.contextCounters.get("audio") ?? 0; + blockId = `${documentId}-audio-${blockNumber++}`; + this.contextCounters.set("audio", blockNumber); + break; - case 'colist': - blockNumber = this.contextCounters.get('colist') ?? 0; - blockId = `${documentId}-colist-${blockNumber++}`; - this.contextCounters.set('colist', blockNumber); - break; + case "colist": + blockNumber = this.contextCounters.get("colist") ?? 0; + blockId = `${documentId}-colist-${blockNumber++}`; + this.contextCounters.set("colist", blockNumber); + break; - case 'dlist': - blockNumber = this.contextCounters.get('dlist') ?? 0; - blockId = `${documentId}-dlist-${blockNumber++}`; - this.contextCounters.set('dlist', blockNumber); - break; + case "dlist": + blockNumber = this.contextCounters.get("dlist") ?? 0; + blockId = `${documentId}-dlist-${blockNumber++}`; + this.contextCounters.set("dlist", blockNumber); + break; - case 'document': - blockNumber = this.contextCounters.get('document') ?? 0; - blockId = `${documentId}-document-${blockNumber++}`; - this.contextCounters.set('document', blockNumber); - break; + case "document": + blockNumber = this.contextCounters.get("document") ?? 0; + blockId = `${documentId}-document-${blockNumber++}`; + this.contextCounters.set("document", blockNumber); + break; - case 'example': - blockNumber = this.contextCounters.get('example') ?? 0; - blockId = `${documentId}-example-${blockNumber++}`; - this.contextCounters.set('example', blockNumber); - break; + case "example": + blockNumber = this.contextCounters.get("example") ?? 0; + blockId = `${documentId}-example-${blockNumber++}`; + this.contextCounters.set("example", blockNumber); + break; - case 'floating_title': - blockNumber = this.contextCounters.get('floating_title') ?? 0; - blockId = `${documentId}-floating-title-${blockNumber++}`; - this.contextCounters.set('floating_title', blockNumber); - break; + case "floating_title": + blockNumber = this.contextCounters.get("floating_title") ?? 0; + blockId = `${documentId}-floating-title-${blockNumber++}`; + this.contextCounters.set("floating_title", blockNumber); + break; - case 'image': - blockNumber = this.contextCounters.get('image') ?? 0; - blockId = `${documentId}-image-${blockNumber++}`; - this.contextCounters.set('image', blockNumber); - break; + case "image": + blockNumber = this.contextCounters.get("image") ?? 0; + blockId = `${documentId}-image-${blockNumber++}`; + this.contextCounters.set("image", blockNumber); + break; - case 'list_item': - blockNumber = this.contextCounters.get('list_item') ?? 0; - blockId = `${documentId}-list-item-${blockNumber++}`; - this.contextCounters.set('list_item', blockNumber); - break; + case "list_item": + blockNumber = this.contextCounters.get("list_item") ?? 0; + blockId = `${documentId}-list-item-${blockNumber++}`; + this.contextCounters.set("list_item", blockNumber); + break; - case 'listing': - blockNumber = this.contextCounters.get('listing') ?? 0; - blockId = `${documentId}-listing-${blockNumber++}`; - this.contextCounters.set('listing', blockNumber); - break; + case "listing": + blockNumber = this.contextCounters.get("listing") ?? 0; + blockId = `${documentId}-listing-${blockNumber++}`; + this.contextCounters.set("listing", blockNumber); + break; - case 'literal': - blockNumber = this.contextCounters.get('literal') ?? 0; - blockId = `${documentId}-literal-${blockNumber++}`; - this.contextCounters.set('literal', blockNumber); - break; + case "literal": + blockNumber = this.contextCounters.get("literal") ?? 0; + blockId = `${documentId}-literal-${blockNumber++}`; + this.contextCounters.set("literal", blockNumber); + break; - case 'olist': - blockNumber = this.contextCounters.get('olist') ?? 0; - blockId = `${documentId}-olist-${blockNumber++}`; - this.contextCounters.set('olist', blockNumber); - break; + case "olist": + blockNumber = this.contextCounters.get("olist") ?? 0; + blockId = `${documentId}-olist-${blockNumber++}`; + this.contextCounters.set("olist", blockNumber); + break; - case 'open': - blockNumber = this.contextCounters.get('open') ?? 0; - blockId = `${documentId}-open-${blockNumber++}`; - this.contextCounters.set('open', blockNumber); - break; + case "open": + blockNumber = this.contextCounters.get("open") ?? 0; + blockId = `${documentId}-open-${blockNumber++}`; + this.contextCounters.set("open", blockNumber); + break; - case 'page_break': - blockNumber = this.contextCounters.get('page_break') ?? 0; - blockId = `${documentId}-page-break-${blockNumber++}`; - this.contextCounters.set('page_break', blockNumber); - break; + case "page_break": + blockNumber = this.contextCounters.get("page_break") ?? 0; + blockId = `${documentId}-page-break-${blockNumber++}`; + this.contextCounters.set("page_break", blockNumber); + break; - case 'paragraph': - blockNumber = this.contextCounters.get('paragraph') ?? 0; - blockId = `${documentId}-paragraph-${blockNumber++}`; - this.contextCounters.set('paragraph', blockNumber); - break; + case "paragraph": + blockNumber = this.contextCounters.get("paragraph") ?? 0; + blockId = `${documentId}-paragraph-${blockNumber++}`; + this.contextCounters.set("paragraph", blockNumber); + break; - case 'pass': - blockNumber = this.contextCounters.get('pass') ?? 0; - blockId = `${documentId}-pass-${blockNumber++}`; - this.contextCounters.set('pass', blockNumber); - break; + case "pass": + blockNumber = this.contextCounters.get("pass") ?? 0; + blockId = `${documentId}-pass-${blockNumber++}`; + this.contextCounters.set("pass", blockNumber); + break; - case 'preamble': - blockNumber = this.contextCounters.get('preamble') ?? 0; - blockId = `${documentId}-preamble-${blockNumber++}`; - this.contextCounters.set('preamble', blockNumber); - break; + case "preamble": + blockNumber = this.contextCounters.get("preamble") ?? 0; + blockId = `${documentId}-preamble-${blockNumber++}`; + this.contextCounters.set("preamble", blockNumber); + break; - case 'quote': - blockNumber = this.contextCounters.get('quote') ?? 0; - blockId = `${documentId}-quote-${blockNumber++}`; - this.contextCounters.set('quote', blockNumber); - break; + case "quote": + blockNumber = this.contextCounters.get("quote") ?? 0; + blockId = `${documentId}-quote-${blockNumber++}`; + this.contextCounters.set("quote", blockNumber); + break; - case 'section': - blockNumber = this.contextCounters.get('section') ?? 0; - blockId = `${documentId}-section-${blockNumber++}`; - this.contextCounters.set('section', blockNumber); - break; + case "section": + blockNumber = this.contextCounters.get("section") ?? 0; + blockId = `${documentId}-section-${blockNumber++}`; + this.contextCounters.set("section", blockNumber); + break; - case 'sidebar': - blockNumber = this.contextCounters.get('sidebar') ?? 0; - blockId = `${documentId}-sidebar-${blockNumber++}`; - this.contextCounters.set('sidebar', blockNumber); - break; + case "sidebar": + blockNumber = this.contextCounters.get("sidebar") ?? 0; + blockId = `${documentId}-sidebar-${blockNumber++}`; + this.contextCounters.set("sidebar", blockNumber); + break; - case 'table': - blockNumber = this.contextCounters.get('table') ?? 0; - blockId = `${documentId}-table-${blockNumber++}`; - this.contextCounters.set('table', blockNumber); - break; + case "table": + blockNumber = this.contextCounters.get("table") ?? 0; + blockId = `${documentId}-table-${blockNumber++}`; + this.contextCounters.set("table", blockNumber); + break; - case 'table_cell': - blockNumber = this.contextCounters.get('table_cell') ?? 0; - blockId = `${documentId}-table-cell-${blockNumber++}`; - this.contextCounters.set('table_cell', blockNumber); - break; + case "table_cell": + blockNumber = this.contextCounters.get("table_cell") ?? 0; + blockId = `${documentId}-table-cell-${blockNumber++}`; + this.contextCounters.set("table_cell", blockNumber); + break; - case 'thematic_break': - blockNumber = this.contextCounters.get('thematic_break') ?? 0; - blockId = `${documentId}-thematic-break-${blockNumber++}`; - this.contextCounters.set('thematic_break', blockNumber); - break; + case "thematic_break": + blockNumber = this.contextCounters.get("thematic_break") ?? 0; + blockId = `${documentId}-thematic-break-${blockNumber++}`; + this.contextCounters.set("thematic_break", blockNumber); + break; - case 'toc': - blockNumber = this.contextCounters.get('toc') ?? 0; - blockId = `${documentId}-toc-${blockNumber++}`; - this.contextCounters.set('toc', blockNumber); - break; + case "toc": + blockNumber = this.contextCounters.get("toc") ?? 0; + blockId = `${documentId}-toc-${blockNumber++}`; + this.contextCounters.set("toc", blockNumber); + break; - case 'ulist': - blockNumber = this.contextCounters.get('ulist') ?? 0; - blockId = `${documentId}-ulist-${blockNumber++}`; - this.contextCounters.set('ulist', blockNumber); - break; + case "ulist": + blockNumber = this.contextCounters.get("ulist") ?? 0; + blockId = `${documentId}-ulist-${blockNumber++}`; + this.contextCounters.set("ulist", blockNumber); + break; - case 'verse': - blockNumber = this.contextCounters.get('verse') ?? 0; - blockId = `${documentId}-verse-${blockNumber++}`; - this.contextCounters.set('verse', blockNumber); - break; + case "verse": + blockNumber = this.contextCounters.get("verse") ?? 0; + blockId = `${documentId}-verse-${blockNumber++}`; + this.contextCounters.set("verse", blockNumber); + break; - case 'video': - blockNumber = this.contextCounters.get('video') ?? 0; - blockId = `${documentId}-video-${blockNumber++}`; - this.contextCounters.set('video', blockNumber); - break; + case "video": + blockNumber = this.contextCounters.get("video") ?? 0; + blockId = `${documentId}-video-${blockNumber++}`; + this.contextCounters.set("video", blockNumber); + break; - default: - blockNumber = this.contextCounters.get('block') ?? 0; - blockId = `${documentId}-block-${blockNumber++}`; - this.contextCounters.set('block', blockNumber); - break; + default: + blockNumber = this.contextCounters.get("block") ?? 0; + blockId = `${documentId}-block-${blockNumber++}`; + this.contextCounters.set("block", blockNumber); + break; } block.setId(blockId); @@ -1058,24 +1126,25 @@ export default class Pharos { return null; } - return he.decode(input) + return he + .decode(input) .toLowerCase() - .replace(/[_]/g, ' ') // Replace underscores with spaces. + .replace(/[_]/g, " ") // Replace underscores with spaces. .trim() - .replace(/\s+/g, '-') // Replace spaces with dashes. - .replace(/[^a-z0-9\-]/g, ''); // Remove non-alphanumeric characters except dashes. + .replace(/\s+/g, "-") // Replace spaces with dashes. + .replace(/[^a-z0-9\-]/g, ""); // Remove non-alphanumeric characters except dashes. } private updateEventByContext(dTag: string, value: string, context: string) { switch (context) { - case 'document': - case 'section': - this.updateEventTitle(dTag, value); - break; - - default: - this.updateEventBody(dTag, value); - break; + case "document": + case "section": + this.updateEventTitle(dTag, value); + break; + + default: + this.updateEventBody(dTag, value); + break; } } @@ -1107,7 +1176,7 @@ export default class Pharos { while ((match = wikilinkPattern.exec(content)) !== null) { const linkName = match[1]; const normalizedText = this.normalizeId(linkName); - wikilinks.push(['wikilink', normalizedText!]); + wikilinks.push(["wikilink", normalizedText!]); } return wikilinks; @@ -1123,7 +1192,7 @@ export const pharosInstance: Writable = writable(); export const tocUpdate = writable(0); // Whenever you update the publication tree, call: -tocUpdate.update(n => n + 1); +tocUpdate.update((n) => n + 1); function ensureAsciiDocHeader(content: string): string { const lines = content.split(/\r?\n/); @@ -1132,36 +1201,36 @@ function ensureAsciiDocHeader(content: string): string { // Find the first non-empty line as header for (let i = 0; i < lines.length; i++) { - if (lines[i].trim() === '') continue; - if (lines[i].trim().startsWith('=')) { + if (lines[i].trim() === "") continue; + if (lines[i].trim().startsWith("=")) { headerIndex = i; - console.debug('[Pharos] AsciiDoc document header:', lines[i].trim()); + break; } else { - throw new Error('AsciiDoc document is missing a header at the top.'); + throw new Error("AsciiDoc document is missing a header at the top."); } } if (headerIndex === -1) { - throw new Error('AsciiDoc document is missing a header.'); + throw new Error("AsciiDoc document is missing a header."); } // Check for doctype in the next non-empty line after header let nextLine = headerIndex + 1; - while (nextLine < lines.length && lines[nextLine].trim() === '') { + while (nextLine < lines.length && lines[nextLine].trim() === "") { nextLine++; } - if (nextLine < lines.length && lines[nextLine].trim().startsWith(':doctype:')) { + if ( + nextLine < lines.length && + lines[nextLine].trim().startsWith(":doctype:") + ) { hasDoctype = true; } // Insert doctype immediately after header if not present if (!hasDoctype) { - lines.splice(headerIndex + 1, 0, ':doctype: book'); + lines.splice(headerIndex + 1, 0, ":doctype: book"); } - // Log the state of the lines before returning - console.debug('[Pharos] AsciiDoc lines after header/doctype normalization:', lines.slice(0, 5)); - - return lines.join('\n'); + return lines.join("\n"); } diff --git a/src/lib/snippets/PublicationSnippets.svelte b/src/lib/snippets/PublicationSnippets.svelte index 802edfd..3687062 100644 --- a/src/lib/snippets/PublicationSnippets.svelte +++ b/src/lib/snippets/PublicationSnippets.svelte @@ -1,5 +1,5 @@ - @@ -8,13 +8,17 @@ {@const headingLevel = Math.min(depth + 1, 6)} - + {title} {/snippet} -{#snippet contentParagraph(content: string, publicationType: string, isSectionStart: boolean)} -
+{#snippet contentParagraph( + content: string, + publicationType: string, + isSectionStart: boolean, +)} +
{@html content}
{/snippet} diff --git a/src/lib/snippets/UserSnippets.svelte b/src/lib/snippets/UserSnippets.svelte index d8c960e..daab606 100644 --- a/src/lib/snippets/UserSnippets.svelte +++ b/src/lib/snippets/UserSnippets.svelte @@ -1,19 +1,59 @@ {#snippet userBadge(identifier: string, displayText: string | undefined)} - {#if toNpub(identifier)} - {#await createProfileLinkWithVerification(toNpub(identifier) as string, displayText)} - {@html createProfileLink(toNpub(identifier) as string, displayText)} - {:then html} - {@html html} - {:catch} - {@html createProfileLink(toNpub(identifier) as string, displayText)} - {/await} + {@const npub = toNpub(identifier)} + {#if npub} + {#if !displayText || displayText.trim().toLowerCase() === 'unknown'} + {#await getUserMetadata(npub) then profile} + {@const p = profile as NostrProfileWithLegacy} + + + + {:catch} + + + + {/await} + {:else} + {#await createProfileLinkWithVerification(npub as string, displayText)} + + + + {:then html} + + + {@html html.replace(/([\s\S]*<\/a>)/, '').trim()} + + {:catch} + + + + {/await} + {/if} {:else} - {displayText ?? ''} + {displayText ?? ""} {/if} {/snippet} diff --git a/src/lib/stores.ts b/src/lib/stores.ts index e38f0d4..74219db 100644 --- a/src/lib/stores.ts +++ b/src/lib/stores.ts @@ -7,14 +7,13 @@ export let alexandriaKinds = readable([30040, 30041, 30818]); export let feedType = writable(FeedType.StandardRelays); - const defaultVisibility = { toc: false, blog: true, main: true, inner: false, discussion: false, - editing: false + editing: false, }; function createVisibilityStore() { @@ -24,7 +23,7 @@ function createVisibilityStore() { subscribe, set, update, - reset: () => set({ ...defaultVisibility }) + reset: () => set({ ...defaultVisibility }), }; } diff --git a/src/lib/stores/authStore.Svelte.ts b/src/lib/stores/authStore.Svelte.ts new file mode 100644 index 0000000..9337100 --- /dev/null +++ b/src/lib/stores/authStore.Svelte.ts @@ -0,0 +1,11 @@ +import { writable, derived } from 'svelte/store'; + +/** + * Stores the user's public key if logged in, or null otherwise. + */ +export const userPubkey = writable(null); + +/** + * Derived store indicating if the user is logged in. + */ +export const isLoggedIn = derived(userPubkey, ($userPubkey) => !!$userPubkey); \ No newline at end of file diff --git a/src/lib/stores/relayStore.ts b/src/lib/stores/relayStore.ts index 9c7e635..2c038c7 100644 --- a/src/lib/stores/relayStore.ts +++ b/src/lib/stores/relayStore.ts @@ -1,4 +1,4 @@ -import { writable } from 'svelte/store'; +import { writable } from "svelte/store"; // Initialize with empty array, will be populated from user preferences -export const userRelays = writable([]); \ No newline at end of file +export const userRelays = writable([]); diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts new file mode 100644 index 0000000..69e628b --- /dev/null +++ b/src/lib/stores/userStore.ts @@ -0,0 +1,301 @@ +import { writable, get } from 'svelte/store'; +import type { NostrProfile } from '$lib/utils/nostrUtils'; +import type { NDKUser, NDKSigner } from '@nostr-dev-kit/ndk'; +import { NDKNip07Signer, NDKRelayAuthPolicies, NDKRelaySet, NDKRelay } from '@nostr-dev-kit/ndk'; +import { getUserMetadata } from '$lib/utils/nostrUtils'; +import { ndkInstance } from '$lib/ndk'; +import { loginStorageKey, fallbackRelays } from '$lib/consts'; +import { nip19 } from 'nostr-tools'; + +export interface UserState { + pubkey: string | null; + npub: string | null; + profile: NostrProfile | null; + relays: { inbox: string[]; outbox: string[] }; + loginMethod: 'extension' | 'amber' | 'npub' | null; + ndkUser: NDKUser | null; + signer: NDKSigner | null; + signedIn: boolean; +} + +export const userStore = writable({ + pubkey: null, + npub: null, + profile: null, + relays: { inbox: [], outbox: [] }, + loginMethod: null, + ndkUser: null, + signer: null, + signedIn: false, +}); + +// Helper functions for relay management +function getRelayStorageKey(user: NDKUser, type: 'inbox' | 'outbox'): string { + return `${loginStorageKey}/${user.pubkey}/${type}`; +} + +function persistRelays(user: NDKUser, inboxes: Set, outboxes: Set): void { + localStorage.setItem( + getRelayStorageKey(user, 'inbox'), + JSON.stringify(Array.from(inboxes).map(relay => relay.url)) + ); + localStorage.setItem( + getRelayStorageKey(user, 'outbox'), + JSON.stringify(Array.from(outboxes).map(relay => relay.url)) + ); +} + +function getPersistedRelays(user: NDKUser): [Set, Set] { + const inboxes = new Set( + JSON.parse(localStorage.getItem(getRelayStorageKey(user, 'inbox')) ?? '[]') + ); + const outboxes = new Set( + JSON.parse(localStorage.getItem(getRelayStorageKey(user, 'outbox')) ?? '[]') + ); + + return [inboxes, outboxes]; +} + +async function getUserPreferredRelays( + ndk: any, + user: NDKUser, + fallbacks: readonly string[] = fallbackRelays +): Promise<[Set, Set]> { + const relayList = await ndk.fetchEvent( + { + kinds: [10002], + authors: [user.pubkey], + }, + { + groupable: false, + skipVerification: false, + skipValidation: false, + }, + NDKRelaySet.fromRelayUrls(fallbacks, ndk), + ); + + const inboxRelays = new Set(); + const outboxRelays = new Set(); + + if (relayList == null) { + const relayMap = await window.nostr?.getRelays?.(); + Object.entries(relayMap ?? {}).forEach(([url, relayType]: [string, any]) => { + const relay = new NDKRelay(url, NDKRelayAuthPolicies.signIn({ ndk }), ndk); + if (relayType.read) inboxRelays.add(relay); + if (relayType.write) outboxRelays.add(relay); + }); + } else { + relayList.tags.forEach((tag: string[]) => { + switch (tag[0]) { + case 'r': + inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); + break; + case 'w': + outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); + break; + default: + inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); + outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); + break; + } + }); + } + + return [inboxRelays, outboxRelays]; +} + +// --- Unified login/logout helpers --- + +export const loginMethodStorageKey = 'alexandria/login/method'; + +function persistLogin(user: NDKUser, method: 'extension' | 'amber' | 'npub') { + localStorage.setItem(loginStorageKey, user.pubkey); + localStorage.setItem(loginMethodStorageKey, method); +} + +function getPersistedLoginMethod(): 'extension' | 'amber' | 'npub' | null { + return (localStorage.getItem(loginMethodStorageKey) as 'extension' | 'amber' | 'npub') ?? null; +} + +function clearLogin() { + localStorage.removeItem(loginStorageKey); + localStorage.removeItem(loginMethodStorageKey); +} + +/** + * Login with NIP-07 browser extension + */ +export async function loginWithExtension() { + const ndk = get(ndkInstance); + if (!ndk) throw new Error('NDK not initialized'); + // Only clear previous login state after successful login + const signer = new NDKNip07Signer(); + const user = await signer.user(); + const npub = user.npub; + const profile = await getUserMetadata(npub); + // Fetch user's preferred relays + const [persistedInboxes, persistedOutboxes] = getPersistedRelays(user); + for (const relay of persistedInboxes) { + ndk.addExplicitRelay(relay); + } + const [inboxes, outboxes] = await getUserPreferredRelays(ndk, user); + persistRelays(user, inboxes, outboxes); + ndk.signer = signer; + ndk.activeUser = user; + userStore.set({ + pubkey: user.pubkey, + npub, + profile, + relays: { + inbox: Array.from(inboxes ?? persistedInboxes).map(relay => relay.url), + outbox: Array.from(outboxes ?? persistedOutboxes).map(relay => relay.url) + }, + loginMethod: 'extension', + ndkUser: user, + signer, + signedIn: true, + }); + clearLogin(); + localStorage.removeItem('alexandria/logout/flag'); + persistLogin(user, 'extension'); +} + +/** + * Login with Amber (NIP-46) + */ +export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { + const ndk = get(ndkInstance); + if (!ndk) throw new Error('NDK not initialized'); + // Only clear previous login state after successful login + const npub = user.npub; + const profile = await getUserMetadata(npub, true); // Force fresh fetch + const [persistedInboxes, persistedOutboxes] = getPersistedRelays(user); + for (const relay of persistedInboxes) { + ndk.addExplicitRelay(relay); + } + const [inboxes, outboxes] = await getUserPreferredRelays(ndk, user); + persistRelays(user, inboxes, outboxes); + ndk.signer = amberSigner; + ndk.activeUser = user; + userStore.set({ + pubkey: user.pubkey, + npub, + profile, + relays: { + inbox: Array.from(inboxes ?? persistedInboxes).map(relay => relay.url), + outbox: Array.from(outboxes ?? persistedOutboxes).map(relay => relay.url) + }, + loginMethod: 'amber', + ndkUser: user, + signer: amberSigner, + signedIn: true, + }); + clearLogin(); + localStorage.removeItem('alexandria/logout/flag'); + persistLogin(user, 'amber'); +} + +/** + * Login with npub (read-only) + */ +export async function loginWithNpub(pubkeyOrNpub: string) { + const ndk = get(ndkInstance); + if (!ndk) throw new Error('NDK not initialized'); + // Only clear previous login state after successful login + let hexPubkey: string; + if (pubkeyOrNpub.startsWith('npub')) { + try { + hexPubkey = nip19.decode(pubkeyOrNpub).data as string; + } catch (e) { + console.error('Failed to decode hex pubkey from npub:', pubkeyOrNpub, e); + throw e; + } + } else { + hexPubkey = pubkeyOrNpub; + } + let npub: string; + try { + npub = nip19.npubEncode(hexPubkey); + } catch (e) { + console.error('Failed to encode npub from hex pubkey:', hexPubkey, e); + throw e; + } + const user = ndk.getUser({ npub }); + const profile = await getUserMetadata(npub); + ndk.signer = undefined; + ndk.activeUser = user; + userStore.set({ + pubkey: user.pubkey, + npub, + profile, + relays: { inbox: [], outbox: [] }, + loginMethod: 'npub', + ndkUser: user, + signer: null, + signedIn: true, + }); + clearLogin(); + localStorage.removeItem('alexandria/logout/flag'); + persistLogin(user, 'npub'); +} + +/** + * Logout and clear all user state + */ +export function logoutUser() { + console.log('Logging out user...'); + const currentUser = get(userStore); + if (currentUser.ndkUser) { + // Clear persisted relays for the user + localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, 'inbox')); + localStorage.removeItem(getRelayStorageKey(currentUser.ndkUser, 'outbox')); + } + + // Clear all possible login states from localStorage + clearLogin(); + + // Also clear any other potential login keys that might exist + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && (key.includes('login') || key.includes('nostr') || key.includes('user') || key.includes('alexandria') || key === 'pubkey')) { + keysToRemove.push(key); + } + } + + // Specifically target the login storage key + keysToRemove.push('alexandria/login/pubkey'); + keysToRemove.push('alexandria/login/method'); + + keysToRemove.forEach(key => { + console.log('Removing localStorage key:', key); + localStorage.removeItem(key); + }); + + // Clear Amber-specific flags + localStorage.removeItem('alexandria/amber/fallback'); + + // Set a flag to prevent auto-login on next page load + localStorage.setItem('alexandria/logout/flag', 'true'); + + console.log('Cleared all login data from localStorage'); + + userStore.set({ + pubkey: null, + npub: null, + profile: null, + relays: { inbox: [], outbox: [] }, + loginMethod: null, + ndkUser: null, + signer: null, + signedIn: false, + }); + + const ndk = get(ndkInstance); + if (ndk) { + ndk.activeUser = undefined; + ndk.signer = undefined; + } + + console.log('Logout complete'); +} \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index 9b8e84e..e47b037 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -6,4 +6,10 @@ export type Tab = { data?: any; }; -export type TabType = 'welcome' | 'find' | 'article' | 'user' | 'settings' | 'editor'; +export type TabType = + | "welcome" + | "find" + | "article" + | "user" + | "settings" + | "editor"; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 52f5686..b5be33c 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -12,11 +12,11 @@ export function neventEncode(event: NDKEvent, relays: string[]) { } export function naddrEncode(event: NDKEvent, relays: string[]) { - const dTag = getMatchingTags(event, 'd')[0]?.[1]; + const dTag = getMatchingTags(event, "d")[0]?.[1]; if (!dTag) { - throw new Error('Event does not have a d tag'); + throw new Error("Event does not have a d tag"); } - + return nip19.naddrEncode({ identifier: dTag, pubkey: event.pubkey, @@ -110,16 +110,14 @@ export function isElementInViewport(el: string | HTMLElement) { export function filterValidIndexEvents(events: Set): Set { // The filter object supports only limited parameters, so we need to filter out events that // don't respect NKBIP-01. - events.forEach(event => { + events.forEach((event) => { // Index events have no content, and they must have `title`, `d`, and `e` tags. if ( - (event.content != null && event.content.length > 0) - || getMatchingTags(event, 'title').length === 0 - || getMatchingTags(event, 'd').length === 0 - || ( - getMatchingTags(event, 'a').length === 0 - && getMatchingTags(event, 'e').length === 0 - ) + (event.content != null && event.content.length > 0) || + getMatchingTags(event, "title").length === 0 || + getMatchingTags(event, "d").length === 0 || + (getMatchingTags(event, "a").length === 0 && + getMatchingTags(event, "e").length === 0) ) { events.delete(event); } @@ -138,7 +136,7 @@ export function filterValidIndexEvents(events: Set): Set { */ export async function findIndexAsync( array: T[], - predicate: (element: T, index: number, array: T[]) => Promise + predicate: (element: T, index: number, array: T[]) => Promise, ): Promise { for (let i = 0; i < array.length; i++) { if (await predicate(array[i], i, array)) { @@ -152,14 +150,14 @@ export async function findIndexAsync( declare global { interface Array { findIndexAsync( - predicate: (element: T, index: number, array: T[]) => Promise + predicate: (element: T, index: number, array: T[]) => Promise, ): Promise; } } -Array.prototype.findIndexAsync = function( +Array.prototype.findIndexAsync = function ( this: T[], - predicate: (element: T, index: number, array: T[]) => Promise + predicate: (element: T, index: number, array: T[]) => Promise, ): Promise { return findIndexAsync(this, predicate); }; @@ -173,7 +171,7 @@ Array.prototype.findIndexAsync = function( */ export function debounce any>( func: T, - wait: number + wait: number, ): (...args: Parameters) => void { let timeout: ReturnType | undefined; diff --git a/src/lib/utils/community_checker.ts b/src/lib/utils/community_checker.ts new file mode 100644 index 0000000..eaf1dd8 --- /dev/null +++ b/src/lib/utils/community_checker.ts @@ -0,0 +1,86 @@ +import { communityRelay } from '$lib/consts'; +import { RELAY_CONSTANTS, SEARCH_LIMITS } from './search_constants'; + +// Cache for pubkeys with kind 1 events on communityRelay +const communityCache = new Map(); + +/** + * Check if a pubkey has posted to the community relay + */ +export async function checkCommunity(pubkey: string): Promise { + if (communityCache.has(pubkey)) { + return communityCache.get(pubkey)!; + } + + try { + const relayUrl = communityRelay; + const ws = new WebSocket(relayUrl); + return await new Promise((resolve) => { + ws.onopen = () => { + ws.send(JSON.stringify([ + 'REQ', RELAY_CONSTANTS.COMMUNITY_REQUEST_ID, { + kinds: RELAY_CONSTANTS.COMMUNITY_REQUEST_KINDS, + authors: [pubkey], + limit: SEARCH_LIMITS.COMMUNITY_CHECK + } + ])); + }; + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data[0] === 'EVENT' && data[2]?.kind === 1) { + communityCache.set(pubkey, true); + ws.close(); + resolve(true); + } else if (data[0] === 'EOSE') { + communityCache.set(pubkey, false); + ws.close(); + resolve(false); + } + }; + ws.onerror = () => { + communityCache.set(pubkey, false); + ws.close(); + resolve(false); + }; + }); + } catch { + communityCache.set(pubkey, false); + return false; + } +} + +/** + * Check community status for multiple profiles + */ +export async function checkCommunityStatus(profiles: Array<{ pubkey?: string }>): Promise> { + const communityStatus: Record = {}; + + // Run all community checks in parallel with timeout + const checkPromises = profiles.map(async (profile) => { + if (!profile.pubkey) return { pubkey: '', status: false }; + + try { + const status = await Promise.race([ + checkCommunity(profile.pubkey), + new Promise((resolve) => { + setTimeout(() => resolve(false), 2000); // 2 second timeout per check + }) + ]); + return { pubkey: profile.pubkey, status }; + } catch (error) { + console.warn('Community status check failed for', profile.pubkey, error); + return { pubkey: profile.pubkey, status: false }; + } + }); + + // Wait for all checks to complete + const results = await Promise.allSettled(checkPromises); + + for (const result of results) { + if (result.status === 'fulfilled' && result.value.pubkey) { + communityStatus[result.value.pubkey] = result.value.status; + } + } + + return communityStatus; +} \ No newline at end of file diff --git a/src/lib/utils/event_input_utils.ts b/src/lib/utils/event_input_utils.ts new file mode 100644 index 0000000..f248359 --- /dev/null +++ b/src/lib/utils/event_input_utils.ts @@ -0,0 +1,400 @@ +import type { NDKEvent } from './nostrUtils'; +import { get } from 'svelte/store'; +import { ndkInstance } from '$lib/ndk'; +import { NDKEvent as NDKEventClass } from '@nostr-dev-kit/ndk'; +import { EVENT_KINDS } from './search_constants'; + +// ========================= +// Validation +// ========================= + +/** + * Returns true if the event kind requires a d-tag (kinds 30000-39999). + */ +export function requiresDTag(kind: number): boolean { + return kind >= EVENT_KINDS.ADDRESSABLE.MIN && kind <= EVENT_KINDS.ADDRESSABLE.MAX; +} + +/** + * Returns true if the tags array contains at least one d-tag with a non-empty value. + */ +export function hasDTag(tags: [string, string][]): boolean { + return tags.some(([k, v]) => k === 'd' && v && v.trim() !== ''); +} + +/** + * Returns true if the content contains AsciiDoc headers (lines starting with '=' or '=='). + */ +function containsAsciiDocHeaders(content: string): boolean { + return /^={1,}\s+/m.test(content); +} + +/** + * Validates that content does NOT contain AsciiDoc headers (for kind 30023). + * Returns { valid, reason }. + */ +export function validateNotAsciidoc(content: string): { valid: boolean; reason?: string } { + if (containsAsciiDocHeaders(content)) { + return { + valid: false, + reason: 'Kind 30023 must not contain AsciiDoc headers (lines starting with = or ==).', + }; + } + return { valid: true }; +} + +/** + * Validates AsciiDoc content. Must start with '=' and contain at least one '==' section header. + * Returns { valid, reason }. + */ +export function validateAsciiDoc(content: string): { valid: boolean; reason?: string } { + if (!content.trim().startsWith('=')) { + return { valid: false, reason: 'AsciiDoc must start with a document title ("=").' }; + } + if (!/^==\s+/m.test(content)) { + return { valid: false, reason: 'AsciiDoc must contain at least one section header ("==").' }; + } + return { valid: true }; +} + +/** + * Validates that a 30040 event set will be created correctly. + * Returns { valid, reason }. + */ +export function validate30040EventSet(content: string): { valid: boolean; reason?: string } { + // First validate as AsciiDoc + const asciiDocValidation = validateAsciiDoc(content); + if (!asciiDocValidation.valid) { + return asciiDocValidation; + } + + // Check that we have at least one section + const sectionsResult = splitAsciiDocSections(content); + if (sectionsResult.sections.length === 0) { + return { valid: false, reason: '30040 events must contain at least one section.' }; + } + + // Check that we have a document title + const documentTitle = extractAsciiDocDocumentHeader(content); + if (!documentTitle) { + return { valid: false, reason: '30040 events must have a document title (line starting with "=").' }; + } + + // Check that the content will result in an empty 30040 event + // The 30040 event should have empty content, with all content split into 30041 events + if (!content.trim().startsWith('=')) { + return { valid: false, reason: '30040 events must start with a document title ("=").' }; + } + + return { valid: true }; +} + +// ========================= +// Extraction & Normalization +// ========================= + +/** + * Normalize a string for use as a d-tag: lowercase, hyphens, alphanumeric only. + */ +function normalizeDTagValue(header: string): string { + return header + .toLowerCase() + .replace(/[^\p{L}\p{N}]+/gu, '-') + .replace(/^-+|-+$/g, ''); +} + +/** + * Converts a title string to a valid d-tag (lowercase, hyphens, no punctuation). + */ +export function titleToDTag(title: string): string { + return title + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens + .replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens +} + +/** + * Extracts the first AsciiDoc document header (line starting with '= '). + */ +function extractAsciiDocDocumentHeader(content: string): string | null { + const match = content.match(/^=\s+(.+)$/m); + return match ? match[1].trim() : null; +} + +/** + * Extracts all section headers (lines starting with '== '). + */ +function extractAsciiDocSectionHeaders(content: string): string[] { + return Array.from(content.matchAll(/^==\s+(.+)$/gm)).map(m => m[1].trim()); +} + +/** + * Extracts the topmost Markdown # header (line starting with '# '). + */ +function extractMarkdownTopHeader(content: string): string | null { + const match = content.match(/^#\s+(.+)$/m); + return match ? match[1].trim() : null; +} + +/** + * Splits AsciiDoc content into sections at each '==' header. Returns array of section strings. + * Document title (= header) is excluded from sections and only used for the index event title. + * Section headers (==) are discarded from content. + * Text between document header and first section becomes a "Preamble" section. + */ +function splitAsciiDocSections(content: string): { sections: string[]; sectionHeaders: string[]; hasPreamble: boolean } { + const lines = content.split(/\r?\n/); + const sections: string[] = []; + const sectionHeaders: string[] = []; + let current: string[] = []; + let foundFirstSection = false; + let hasPreamble = false; + let preambleContent: string[] = []; + + for (const line of lines) { + // Skip document title lines (= header) + if (/^=\s+/.test(line)) { + continue; + } + + // If we encounter a section header (==) and we have content, start a new section + if (/^==\s+/.test(line)) { + if (current.length > 0) { + sections.push(current.join('\n').trim()); + current = []; + } + + // Extract section header for title tag + const headerMatch = line.match(/^==\s+(.+)$/); + if (headerMatch) { + sectionHeaders.push(headerMatch[1].trim()); + } + + foundFirstSection = true; + } else if (foundFirstSection) { + // Only add lines to current section if we've found the first section + current.push(line); + } else { + // Text before first section becomes preamble + if (line.trim() !== '') { + preambleContent.push(line); + } + } + } + + // Add the last section + if (current.length > 0) { + sections.push(current.join('\n').trim()); + } + + // Add preamble as first section if it exists + if (preambleContent.length > 0) { + sections.unshift(preambleContent.join('\n').trim()); + sectionHeaders.unshift('Preamble'); + hasPreamble = true; + } + + return { sections, sectionHeaders, hasPreamble }; +} + +// ========================= +// Event Construction +// ========================= + +/** + * Returns the current NDK instance from the store. + */ +function getNdk() { + return get(ndkInstance); +} + +/** + * Builds a set of events for a 30040 publication: one 30040 index event and one 30041 event per section. + * Each 30041 gets a d-tag (normalized section header) and a title tag (raw section header). + * The 30040 index event references all 30041s by their d-tag. + */ +export function build30040EventSet( + content: string, + tags: [string, string][], + baseEvent: Partial & { pubkey: string; created_at: number } +): { indexEvent: NDKEvent; sectionEvents: NDKEvent[] } { + console.log('=== build30040EventSet called ==='); + console.log('Input content:', content); + console.log('Input tags:', tags); + console.log('Input baseEvent:', baseEvent); + + const ndk = getNdk(); + console.log('NDK instance:', ndk); + + const sectionsResult = splitAsciiDocSections(content); + const sections = sectionsResult.sections; + const sectionHeaders = sectionsResult.sectionHeaders; + console.log('Sections:', sections); + console.log('Section headers:', sectionHeaders); + + const dTags = sectionHeaders.length === sections.length + ? sectionHeaders.map(normalizeDTagValue) + : sections.map((_, i) => `section${i}`); + console.log('D tags:', dTags); + + const sectionEvents: NDKEvent[] = sections.map((section, i) => { + const header = sectionHeaders[i] || `Section ${i + 1}`; + const dTag = dTags[i]; + console.log(`Creating section ${i}:`, { header, dTag, content: section }); + return new NDKEventClass(ndk, { + kind: 30041, + content: section, + tags: [ + ...tags, + ['d', dTag], + ['title', header], + ], + pubkey: baseEvent.pubkey, + created_at: baseEvent.created_at, + }); + }); + + // Create proper a tags with format: kind:pubkey:d-tag + const aTags = dTags.map(dTag => ['a', `30041:${baseEvent.pubkey}:${dTag}`] as [string, string]); + console.log('A tags:', aTags); + + // Extract document title for the index event + const documentTitle = extractAsciiDocDocumentHeader(content); + const indexDTag = documentTitle ? normalizeDTagValue(documentTitle) : 'index'; + console.log('Index event:', { documentTitle, indexDTag }); + + const indexTags = [ + ...tags, + ['d', indexDTag], + ['title', documentTitle || 'Untitled'], + ...aTags, + ]; + + const indexEvent: NDKEvent = new NDKEventClass(ndk, { + kind: 30040, + content: '', + tags: indexTags, + pubkey: baseEvent.pubkey, + created_at: baseEvent.created_at, + }); + console.log('Final index event:', indexEvent); + console.log('=== build30040EventSet completed ==='); + return { indexEvent, sectionEvents }; +} + +/** + * Returns the appropriate title tag for a given event kind and content. + * - 30041, 30818: AsciiDoc document header (first '= ' line) + * - 30023: Markdown topmost '# ' header + */ +export function getTitleTagForEvent(kind: number, content: string): string | null { + if (kind === 30041 || kind === 30818) { + return extractAsciiDocDocumentHeader(content); + } + if (kind === 30023) { + return extractMarkdownTopHeader(content); + } + return null; +} + +/** + * Returns the appropriate d-tag value for a given event kind and content. + * - 30023: Normalized markdown header + * - 30041, 30818: Normalized AsciiDoc document header + * - 30040: Uses existing d-tag or generates from content + */ +export function getDTagForEvent(kind: number, content: string, existingDTag?: string): string | null { + if (existingDTag && existingDTag.trim() !== '') { + return existingDTag.trim(); + } + + if (kind === 30023) { + const title = extractMarkdownTopHeader(content); + return title ? normalizeDTagValue(title) : null; + } + + if (kind === 30041 || kind === 30818) { + const title = extractAsciiDocDocumentHeader(content); + return title ? normalizeDTagValue(title) : null; + } + + return null; +} + +/** + * Returns a description of what a 30040 event structure should be. + */ +export function get30040EventDescription(): string { + return `30040 events are publication indexes that contain: +- Empty content (metadata only) +- A d-tag for the publication identifier +- A title tag for the publication title +- A tags referencing 30041 content events (one per section) + +The content is split into sections, each published as a separate 30041 event.`; +} + +/** + * Analyzes a 30040 event to determine if it was created correctly. + * Returns { valid, issues } where issues is an array of problems found. + */ +export function analyze30040Event(event: { content: string; tags: [string, string][]; kind: number }): { valid: boolean; issues: string[] } { + const issues: string[] = []; + + // Check if it's actually a 30040 event + if (event.kind !== 30040) { + issues.push('Event is not kind 30040'); + return { valid: false, issues }; + } + + // Check if content is empty (30040 should be metadata only) + if (event.content && event.content.trim() !== '') { + issues.push('30040 events should have empty content (metadata only)'); + issues.push('Content should be split into separate 30041 events'); + } + + // Check for required tags + const hasTitle = event.tags.some(([k, v]) => k === 'title' && v); + const hasDTag = event.tags.some(([k, v]) => k === 'd' && v); + const hasATags = event.tags.some(([k, v]) => k === 'a' && v); + + if (!hasTitle) { + issues.push('Missing title tag'); + } + if (!hasDTag) { + issues.push('Missing d tag'); + } + if (!hasATags) { + issues.push('Missing a tags (should reference 30041 content events)'); + } + + // Check if a tags have the correct format (kind:pubkey:d-tag) + const aTags = event.tags.filter(([k, v]) => k === 'a' && v); + for (const [, value] of aTags) { + if (!value.includes(':')) { + issues.push(`Invalid a tag format: ${value} (should be "kind:pubkey:d-tag")`); + } + } + + return { valid: issues.length === 0, issues }; +} + +/** + * Returns guidance on how to fix incorrect 30040 events. + */ +export function get30040FixGuidance(): string { + return `To fix a 30040 event: + +1. **Content Issue**: 30040 events should have empty content. All content should be split into separate 30041 events. + +2. **Structure**: A proper 30040 event should contain: + - Empty content + - d tag: publication identifier + - title tag: publication title + - a tags: references to 30041 content events (format: "30041:pubkey:d-tag") + +3. **Process**: When creating a 30040 event: + - Write your content with document title (= Title) and sections (== Section) + - The system will automatically split it into one 30040 index event and multiple 30041 content events + - The 30040 will have empty content and reference the 30041s via a tags`; +} \ No newline at end of file diff --git a/src/lib/utils/event_search.ts b/src/lib/utils/event_search.ts new file mode 100644 index 0000000..95605b4 --- /dev/null +++ b/src/lib/utils/event_search.ts @@ -0,0 +1,224 @@ +import { ndkInstance } from "$lib/ndk"; +import { fetchEventWithFallback } from "$lib/utils/nostrUtils"; +import { nip19 } from "$lib/utils/nostrUtils"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import { get } from "svelte/store"; +import { wellKnownUrl, isValidNip05Address } from "./search_utils"; +import { TIMEOUTS, VALIDATION } from "./search_constants"; + +/** + * Search for a single event by ID or filter + */ +export async function searchEvent(query: string): Promise { + // Clean the query and normalize to lowercase + let cleanedQuery = query.replace(/^nostr:/, "").toLowerCase(); + let filterOrId: any = cleanedQuery; + + // If it's a valid hex string, try as event id first, then as pubkey (profile) + if ( + new RegExp(`^[a-f0-9]{${VALIDATION.HEX_LENGTH}}$`, "i").test(cleanedQuery) + ) { + // Try as event id + filterOrId = cleanedQuery; + const eventResult = await fetchEventWithFallback( + get(ndkInstance), + filterOrId, + TIMEOUTS.EVENT_FETCH, + ); + // Always try as pubkey (profile event) as well + const profileFilter = { kinds: [0], authors: [cleanedQuery] }; + const profileEvent = await fetchEventWithFallback( + get(ndkInstance), + profileFilter, + TIMEOUTS.EVENT_FETCH, + ); + // Prefer profile if found and pubkey matches query + if ( + profileEvent && + profileEvent.pubkey.toLowerCase() === cleanedQuery.toLowerCase() + ) { + return profileEvent; + } else if (eventResult) { + return eventResult; + } + } else if ( + new RegExp( + `^(nevent|note|naddr|npub|nprofile)[a-z0-9]{${VALIDATION.MIN_NOSTR_IDENTIFIER_LENGTH},}$`, + "i", + ).test(cleanedQuery) + ) { + try { + const decoded = nip19.decode(cleanedQuery); + if (!decoded) throw new Error("Invalid identifier"); + switch (decoded.type) { + case "nevent": + filterOrId = decoded.data.id; + break; + case "note": + filterOrId = decoded.data; + break; + case "naddr": + filterOrId = { + kinds: [decoded.data.kind], + authors: [decoded.data.pubkey], + "#d": [decoded.data.identifier], + }; + break; + case "nprofile": + filterOrId = { + kinds: [0], + authors: [decoded.data.pubkey], + }; + break; + case "npub": + filterOrId = { + kinds: [0], + authors: [decoded.data], + }; + break; + default: + filterOrId = cleanedQuery; + } + } catch (e) { + console.error("[Search] Invalid Nostr identifier:", cleanedQuery, e); + throw new Error("Invalid Nostr identifier."); + } + } + + try { + const event = await fetchEventWithFallback( + get(ndkInstance), + filterOrId, + TIMEOUTS.EVENT_FETCH, + ); + + if (!event) { + console.warn("[Search] Event not found for filterOrId:", filterOrId); + return null; + } else { + return event; + } + } catch (err) { + console.error("[Search] Error fetching event:", err, "Query:", query); + throw new Error("Error fetching event. Please check the ID and try again."); + } +} + +/** + * Search for NIP-05 address + */ +export async function searchNip05( + nip05Address: string, +): Promise { + // NIP-05 address pattern: user@domain + if (!isValidNip05Address(nip05Address)) { + throw new Error("Invalid NIP-05 address format. Expected: user@domain"); + } + + try { + const [name, domain] = nip05Address.split("@"); + + const res = await fetch(wellKnownUrl(domain, name)); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + const data = await res.json(); + + const pubkey = data.names?.[name]; + if (pubkey) { + const profileFilter = { kinds: [0], authors: [pubkey] }; + const profileEvent = await fetchEventWithFallback( + get(ndkInstance), + profileFilter, + TIMEOUTS.EVENT_FETCH, + ); + if (profileEvent) { + return profileEvent; + } else { + throw new Error( + `No profile found for ${name}@${domain} (pubkey: ${pubkey})`, + ); + } + } else { + throw new Error(`NIP-05 address not found: ${name}@${domain}`); + } + } catch (e) { + console.error( + `[Search] Error resolving NIP-05 address ${nip05Address}:`, + e, + ); + const errorMessage = e instanceof Error ? e.message : String(e); + throw new Error(`Error resolving NIP-05 address: ${errorMessage}`); + } +} + +/** + * Find containing 30040 index events for a given content event + * @param contentEvent The content event to find containers for (30041, 30818, etc.) + * @returns Array of containing 30040 index events + */ +export async function findContainingIndexEvents( + contentEvent: NDKEvent, +): Promise { + // Support all content event kinds that can be contained in indexes + const contentEventKinds = [30041, 30818, 30040, 30023]; + if (!contentEventKinds.includes(contentEvent.kind)) { + return []; + } + + try { + const ndk = get(ndkInstance); + + // Search for 30040 events that reference this content event + // We need to search for events that have an 'a' tag or 'e' tag referencing this event + const contentEventId = contentEvent.id; + const contentEventAddress = contentEvent.tagAddress(); + + // Search for index events that reference this content event + const indexEvents = await ndk.fetchEvents( + { + kinds: [30040], + "#a": [contentEventAddress], + }, + { + groupable: true, + skipVerification: false, + skipValidation: false, + }, + ); + + // Also search for events with 'e' tags (legacy format) + const indexEventsWithETags = await ndk.fetchEvents( + { + kinds: [30040], + "#e": [contentEventId], + }, + { + groupable: true, + skipVerification: false, + skipValidation: false, + }, + ); + + // Combine and deduplicate results + const allIndexEvents = new Set([...indexEvents, ...indexEventsWithETags]); + + // Filter to only include valid index events + const validIndexEvents = Array.from(allIndexEvents).filter((event) => { + // Check if it's a valid index event (has title, d tag, and either a or e tags) + const hasTitle = event.getMatchingTags("title").length > 0; + const hasDTag = event.getMatchingTags("d").length > 0; + const hasATags = event.getMatchingTags("a").length > 0; + const hasETags = event.getMatchingTags("e").length > 0; + + return hasTitle && hasDTag && (hasATags || hasETags); + }); + + return validIndexEvents; + } catch (error) { + console.error("[Search] Error finding containing index events:", error); + return []; + } +} diff --git a/src/lib/utils/indexEventCache.ts b/src/lib/utils/indexEventCache.ts new file mode 100644 index 0000000..bd91fd3 --- /dev/null +++ b/src/lib/utils/indexEventCache.ts @@ -0,0 +1,132 @@ +import type { NDKEvent } from "./nostrUtils"; +import { CACHE_DURATIONS, TIMEOUTS } from './search_constants'; + +export interface IndexEventCacheEntry { + events: NDKEvent[]; + timestamp: number; + relayUrls: string[]; +} + +class IndexEventCache { + private cache: Map = new Map(); + private readonly CACHE_DURATION = CACHE_DURATIONS.INDEX_EVENT_CACHE; + private readonly MAX_CACHE_SIZE = 50; // Maximum number of cached relay combinations + + /** + * Generate a cache key based on relay URLs + */ + private generateKey(relayUrls: string[]): string { + return relayUrls.sort().join('|'); + } + + /** + * Check if a cached entry is still valid + */ + private isExpired(entry: IndexEventCacheEntry): boolean { + return Date.now() - entry.timestamp > this.CACHE_DURATION; + } + + /** + * Get cached index events for a set of relays + */ + get(relayUrls: string[]): NDKEvent[] | null { + const key = this.generateKey(relayUrls); + const entry = this.cache.get(key); + + if (!entry || this.isExpired(entry)) { + if (entry) { + this.cache.delete(key); + } + return null; + } + + console.log(`[IndexEventCache] Using cached index events for ${relayUrls.length} relays`); + return entry.events; + } + + /** + * Store index events in cache + */ + set(relayUrls: string[], events: NDKEvent[]): void { + const key = this.generateKey(relayUrls); + + // Implement LRU eviction if cache is full + if (this.cache.size >= this.MAX_CACHE_SIZE) { + const oldestKey = this.cache.keys().next().value; + if (oldestKey) { + this.cache.delete(oldestKey); + } + } + + this.cache.set(key, { + events, + timestamp: Date.now(), + relayUrls: [...relayUrls] + }); + + console.log(`[IndexEventCache] Cached ${events.length} index events for ${relayUrls.length} relays`); + } + + /** + * Check if index events are cached for a set of relays + */ + has(relayUrls: string[]): boolean { + const key = this.generateKey(relayUrls); + const entry = this.cache.get(key); + return entry !== undefined && !this.isExpired(entry); + } + + /** + * Clear expired entries from cache + */ + cleanup(): void { + const now = Date.now(); + for (const [key, entry] of this.cache.entries()) { + if (this.isExpired(entry)) { + this.cache.delete(key); + } + } + } + + /** + * Clear all cache entries + */ + clear(): void { + this.cache.clear(); + } + + /** + * Get cache size + */ + size(): number { + return this.cache.size; + } + + /** + * Get cache statistics + */ + getStats(): { size: number; totalEvents: number; oldestEntry: number | null } { + let totalEvents = 0; + let oldestTimestamp: number | null = null; + + for (const entry of this.cache.values()) { + totalEvents += entry.events.length; + if (oldestTimestamp === null || entry.timestamp < oldestTimestamp) { + oldestTimestamp = entry.timestamp; + } + } + + return { + size: this.cache.size, + totalEvents, + oldestEntry: oldestTimestamp + }; + } +} + +export const indexEventCache = new IndexEventCache(); + +// Clean up expired entries periodically +setInterval(() => { + indexEventCache.cleanup(); +}, TIMEOUTS.CACHE_CLEANUP); // Check every minute \ No newline at end of file diff --git a/src/lib/utils/markup/MarkupInfo.md b/src/lib/utils/markup/MarkupInfo.md index daa213a..38d78e6 100644 --- a/src/lib/utils/markup/MarkupInfo.md +++ b/src/lib/utils/markup/MarkupInfo.md @@ -6,8 +6,8 @@ Alexandria supports multiple markup formats for different use cases. Below is a The **basic markup parser** follows the [Nostr best-practice guidelines](https://github.com/nostrability/nostrability/issues/146) and supports: -- **Headers:** - - ATX-style: `# H1` through `###### H6` +- **Headers:** + - ATX-style: `# H1` through `###### H6` - Setext-style: `H1\n=====` - **Bold:** `*bold*` or `**bold**` - **Italic:** `_italic_` or `__italic__` @@ -30,7 +30,7 @@ The **advanced markup parser** includes all features of the basic parser, plus: - **Tables:** Pipe-delimited tables with or without headers - **Footnotes:** `[^1]` or `[^Smith]`, which should appear where the footnote shall be placed, and will be displayed as unique, consecutive numbers - **Footnote References:** `[^1]: footnote text` or `[^Smith]: Smith, Adam. 1984 "The Wiggle Mysteries`, which will be listed in order, at the bottom of the event, with back-reference links to the footnote, and text footnote labels appended -- **Wikilinks:** `[[NIP-54]]` will render as a hyperlink and goes to [NIP-54](./wiki?d=nip-54) +- **Wikilinks:** `[[NIP-54]]` will render as a hyperlink and goes to [NIP-54](./events?d=nip-54) ## Publications and Wikis @@ -42,14 +42,89 @@ AsciiDoc supports a much broader set of formatting, semantic, and structural fea - Advanced tables, callouts, admonitions - Cross-references, footnotes, and bibliography - Custom attributes and macros +- **Math rendering** (Asciimath and LaTeX) +- **Diagram rendering** (PlantUML, BPMN, TikZ) - And much more +### Advanced Content Types + +Alexandria supports rendering of advanced content types commonly used in academic, technical, and business documents: + +#### Math Rendering + +Use `[stem]` blocks for mathematical expressions: + +```asciidoc +[stem] +++++ +\frac{\partial f}{\partial x} = \lim_{h \to 0} \frac{f(x + h) - f(x)}{h} +++++ +``` + +Inline math is also supported using `$...$` or `\(...\)` syntax. + +#### PlantUML Diagrams + +PlantUML diagrams are automatically detected and rendered: + +```asciidoc +[source,plantuml] +---- +@startuml +participant User +participant System +User -> System: Login Request +System --> User: Login Response +@enduml +---- +``` + +#### BPMN Diagrams + +BPMN (Business Process Model and Notation) diagrams are supported: + +```asciidoc +[source,bpmn] +---- + + + + + + + + +---- +``` + +#### TikZ Diagrams + +TikZ diagrams for mathematical illustrations: + +```asciidoc +[source,tikz] +---- +\begin{tikzpicture} + \draw[thick,red] (0,0) circle (1cm); + \draw[thick,blue] (2,0) rectangle (3,1); +\end{tikzpicture} +---- +``` + +### Rendering Features + +- **Automatic Detection**: Content types are automatically detected based on syntax +- **Fallback Display**: If rendering fails, the original source code is displayed +- **Source Code**: Click "Show source" to view the original code +- **Responsive Design**: All rendered content is responsive and works on mobile devices + For more information on AsciiDoc, see the [AsciiDoc documentation](https://asciidoc.org/). --- **Note:** + - The markdown parsers are primarily used for comments, issues, and other user-generated content. - Publications and wikis are rendered using AsciiDoc for maximum expressiveness and compatibility. - All URLs are sanitized to remove tracking parameters, and YouTube links are presented in a clean, privacy-friendly format. -- [Here is a test markup file](/tests/integration/markupTestfile.md) that you can use to test out the parser and see how things should be formatted. \ No newline at end of file +- [Here is a test markup file](/tests/integration/markupTestfile.md) that you can use to test out the parser and see how things should be formatted. diff --git a/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts b/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts new file mode 100644 index 0000000..ab417d7 --- /dev/null +++ b/src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts @@ -0,0 +1,371 @@ +import { postProcessAsciidoctorHtml } from "./asciidoctorPostProcessor"; +import plantumlEncoder from "plantuml-encoder"; + +/** + * Unified post-processor for Asciidoctor HTML that handles: + * - Math rendering (Asciimath/Latex, stem blocks) + * - PlantUML diagrams + * - BPMN diagrams + * - TikZ diagrams + */ +export async function postProcessAdvancedAsciidoctorHtml( + html: string, +): Promise { + if (!html) return html; + try { + // First apply the basic post-processing (wikilinks, nostr addresses) + let processedHtml = await postProcessAsciidoctorHtml(html); + // Unified math block processing + processedHtml = fixAllMathBlocks(processedHtml); + // Process PlantUML blocks + processedHtml = processPlantUMLBlocks(processedHtml); + // Process BPMN blocks + processedHtml = processBPMNBlocks(processedHtml); + // Process TikZ blocks + processedHtml = processTikZBlocks(processedHtml); + // After all processing, apply highlight.js if available + if ( + typeof window !== "undefined" && + typeof window.hljs?.highlightAll === "function" + ) { + setTimeout(() => window.hljs!.highlightAll(), 0); + } + if ( + typeof window !== "undefined" && + typeof (window as any).MathJax?.typesetPromise === "function" + ) { + setTimeout(() => (window as any).MathJax.typesetPromise(), 0); + } + return processedHtml; + } catch (error) { + console.error("Error in postProcessAdvancedAsciidoctorHtml:", error); + return html; // Return original HTML if processing fails + } +} + +/** + * Fixes all math blocks for MathJax rendering. + * Now only processes LaTeX within inline code blocks. + */ +function fixAllMathBlocks(html: string): string { + // Unescape \$ to $ for math delimiters + html = html.replace(/\\\$/g, "$"); + + // Process inline code blocks that contain LaTeX + html = html.replace( + /]*class="[^"]*language-[^"]*"[^>]*>([\s\S]*?)<\/code>/g, + (match, codeContent) => { + const trimmedCode = codeContent.trim(); + if (isLaTeXContent(trimmedCode)) { + return `$${trimmedCode}$`; + } + return match; // Return original if not LaTeX + } + ); + + // Also process code blocks without language class + html = html.replace( + /]*>([\s\S]*?)<\/code>/g, + (match, codeContent) => { + const trimmedCode = codeContent.trim(); + if (isLaTeXContent(trimmedCode)) { + return `$${trimmedCode}$`; + } + return match; // Return original if not LaTeX + } + ); + + return html; +} + +/** + * Checks if content contains LaTeX syntax + */ +function isLaTeXContent(content: string): boolean { + const trimmed = content.trim(); + + // Check for common LaTeX patterns + const latexPatterns = [ + /\\[a-zA-Z]+/, // LaTeX commands like \frac, \sum, etc. + /\\[\(\)\[\]]/, // LaTeX delimiters like \(, \), \[, \] + /\\begin\{/, // LaTeX environments + /\\end\{/, // LaTeX environments + /\$\$/, // Display math delimiters + /\$[^$]+\$/, // Inline math delimiters + /\\text\{/, // LaTeX text command + /\\mathrm\{/, // LaTeX mathrm command + /\\mathbf\{/, // LaTeX bold command + /\\mathit\{/, // LaTeX italic command + /\\sqrt/, // Square root + /\\frac/, // Fraction + /\\sum/, // Sum + /\\int/, // Integral + /\\lim/, // Limit + /\\infty/, // Infinity + /\\alpha/, // Greek letters + /\\beta/, + /\\gamma/, + /\\delta/, + /\\theta/, + /\\lambda/, + /\\mu/, + /\\pi/, + /\\sigma/, + /\\phi/, + /\\omega/, + /\\partial/, // Partial derivative + /\\nabla/, // Nabla + /\\cdot/, // Dot product + /\\times/, // Times + /\\div/, // Division + /\\pm/, // Plus-minus + /\\mp/, // Minus-plus + /\\leq/, // Less than or equal + /\\geq/, // Greater than or equal + /\\neq/, // Not equal + /\\approx/, // Approximately equal + /\\equiv/, // Equivalent + /\\propto/, // Proportional + /\\in/, // Element of + /\\notin/, // Not element of + /\\subset/, // Subset + /\\supset/, // Superset + /\\cup/, // Union + /\\cap/, // Intersection + /\\emptyset/, // Empty set + /\\mathbb\{/, // Blackboard bold + /\\mathcal\{/, // Calligraphic + /\\mathfrak\{/, // Fraktur + /\\mathscr\{/, // Script + ]; + + return latexPatterns.some(pattern => pattern.test(trimmed)); +} + +/** + * Processes PlantUML blocks in HTML content + */ +function processPlantUMLBlocks(html: string): string { + // Only match code blocks with class 'language-plantuml' or 'plantuml' + html = html.replace( + /
\s*
\s*
\s*]*class="[^"]*(?:language-plantuml|plantuml)[^"]*"[^>]*>([\s\S]*?)<\/code>\s*<\/pre>\s*<\/div>\s*<\/div>/g,
+    (match, content) => {
+      try {
+        // Unescape HTML for PlantUML server, but escape for 
+        const rawContent = decodeHTMLEntities(content);
+        const encoded = plantumlEncoder.encode(rawContent);
+        const plantUMLUrl = `https://www.plantuml.com/plantuml/svg/${encoded}`;
+        return `
+ PlantUML diagram +
+ + Show PlantUML source + +
+              ${escapeHtml(rawContent)}
+            
+
+
`; + } catch (error) { + console.warn("Failed to process PlantUML block:", error); + return match; + } + }, + ); + // Fallback: match
 blocks whose content starts with @startuml or @start (global, robust)
+  html = html.replace(
+    /
\s*
\s*
([\s\S]*?)<\/pre>\s*<\/div>\s*<\/div>/g,
+    (match, content) => {
+      const lines = content.trim().split("\n");
+      if (
+        lines[0].trim().startsWith("@startuml") ||
+        lines[0].trim().startsWith("@start")
+      ) {
+        try {
+          const rawContent = decodeHTMLEntities(content);
+          const encoded = plantumlEncoder.encode(rawContent);
+          const plantUMLUrl = `https://www.plantuml.com/plantuml/svg/${encoded}`;
+          return `
+ PlantUML diagram +
+ + Show PlantUML source + +
+                ${escapeHtml(rawContent)}
+              
+
+
`; + } catch (error) { + console.warn("Failed to process PlantUML fallback block:", error); + return match; + } + } + return match; + }, + ); + return html; +} + +function decodeHTMLEntities(text: string): string { + const textarea = document.createElement("textarea"); + textarea.innerHTML = text; + return textarea.value; +} + +/** + * Processes BPMN blocks in HTML content + */ +function processBPMNBlocks(html: string): string { + // Only match code blocks with class 'language-bpmn' or 'bpmn' + html = html.replace( + /
\s*
\s*
\s*]*class="[^"]*(?:language-bpmn|bpmn)[^\"]*"[^>]*>([\s\S]*?)<\/code>\s*<\/pre>\s*<\/div>\s*<\/div>/g,
+    (match, content) => {
+      try {
+        return `
+
+
+ + + + BPMN Diagram +
+
+ + Show BPMN source + +
+                ${escapeHtml(content)}
+              
+
+
+
`; + } catch (error) { + console.warn("Failed to process BPMN block:", error); + return match; + } + }, + ); + // Fallback: match
 blocks whose content contains 'bpmn:' or '\s*
\s*
([\s\S]*?)<\/pre>\s*<\/div>\s*<\/div>/g,
+    (match, content) => {
+      const text = content.trim();
+      if (
+        text.includes("bpmn:") ||
+        (text.startsWith("
+            
+
+ + + + BPMN Diagram +
+
+ + Show BPMN source + +
+                  ${escapeHtml(content)}
+                
+
+
+
`; + } catch (error) { + console.warn("Failed to process BPMN fallback block:", error); + return match; + } + } + return match; + }, + ); + return html; +} + +/** + * Processes TikZ blocks in HTML content + */ +function processTikZBlocks(html: string): string { + // Only match code blocks with class 'language-tikz' or 'tikz' + html = html.replace( + /
\s*
\s*
\s*]*class="[^"]*(?:language-tikz|tikz)[^"]*"[^>]*>([\s\S]*?)<\/code>\s*<\/pre>\s*<\/div>\s*<\/div>/g,
+    (match, content) => {
+      try {
+        return `
+
+
+ + + + TikZ Diagram +
+
+ + Show TikZ source + +
+                ${escapeHtml(content)}
+              
+
+
+
`; + } catch (error) { + console.warn("Failed to process TikZ block:", error); + return match; + } + }, + ); + // Fallback: match
 blocks whose content starts with \begin{tikzpicture} or contains tikz
+  html = html.replace(
+    /
\s*
\s*
([\s\S]*?)<\/pre>\s*<\/div>\s*<\/div>/g,
+    (match, content) => {
+      const lines = content.trim().split("\n");
+      if (
+        lines[0].trim().startsWith("\\begin{tikzpicture}") ||
+        content.includes("tikz")
+      ) {
+        try {
+          return `
+
+
+ + + + TikZ Diagram +
+
+ + Show TikZ source + +
+                  ${escapeHtml(content)}
+                
+
+
+
`; + } catch (error) { + console.warn("Failed to process TikZ fallback block:", error); + return match; + } + } + return match; + }, + ); + return html; +} + +/** + * Escapes HTML characters for safe display + */ +function escapeHtml(text: string): string { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} diff --git a/src/lib/utils/markup/advancedMarkupParser.ts b/src/lib/utils/markup/advancedMarkupParser.ts index 9273857..2e4721f 100644 --- a/src/lib/utils/markup/advancedMarkupParser.ts +++ b/src/lib/utils/markup/advancedMarkupParser.ts @@ -1,13 +1,29 @@ -import { parseBasicmarkup } from './basicMarkupParser'; -import hljs from 'highlight.js'; -import 'highlight.js/lib/common'; // Import common languages -import 'highlight.js/styles/github-dark.css'; // Dark theme only +import { parseBasicmarkup } from "./basicMarkupParser"; +import hljs from "highlight.js"; +import "highlight.js/lib/common"; // Import common languages +import "highlight.js/styles/github-dark.css"; // Dark theme only // Register common languages hljs.configure({ - ignoreUnescapedHTML: true + ignoreUnescapedHTML: true, }); +// Escapes HTML characters for safe display +function escapeHtml(text: string): string { + const div = typeof document !== 'undefined' ? document.createElement('div') : null; + if (div) { + div.textContent = text; + return div.innerHTML; + } + // Fallback for non-browser environments + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + // Regular expressions for advanced markup elements const HEADING_REGEX = /^(#{1,6})\s+(.+)$/gm; const ALTERNATE_HEADING_REGEX = /^([^\n]+)\n(=+|-+)\n/gm; @@ -17,18 +33,28 @@ const FOOTNOTE_REFERENCE_REGEX = /\[\^([^\]]+)\]/g; const FOOTNOTE_DEFINITION_REGEX = /^\[\^([^\]]+)\]:\s*(.+)$/gm; const CODE_BLOCK_REGEX = /^```(\w*)$/; +// LaTeX math regex patterns +const INLINE_MATH_REGEX = /\$([^$\n]+?)\$/g; +const DISPLAY_MATH_REGEX = /\$\$([\s\S]*?)\$\$/g; +const LATEX_BLOCK_REGEX = /\\\[([\s\S]*?)\\\]/g; +const LATEX_INLINE_REGEX = /\\\(([^)]+?)\\\)/g; +// Add regex for LaTeX display math environments (e.g., \begin{pmatrix}...\end{pmatrix}) +// Improved regex: match optional whitespace/linebreaks before and after, and allow for indented environments +const LATEX_ENV_BLOCK_REGEX = + /(?:^|\n)\s*\\begin\{([a-zA-Z*]+)\}([\s\S]*?)\\end\{\1\}\s*(?=\n|$)/gm; + /** * Process headings (both styles) */ function processHeadings(content: string): string { // Tailwind classes for each heading level const headingClasses = [ - 'text-4xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h1 - 'text-3xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h2 - 'text-2xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h3 - 'text-xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h4 - 'text-lg font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h5 - 'text-base font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-300', // h6 + "text-4xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h1 + "text-3xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h2 + "text-2xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h3 + "text-xl font-bold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h4 + "text-lg font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h5 + "text-base font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-300", // h6 ]; // Process ATX-style headings (# Heading) @@ -39,11 +65,14 @@ function processHeadings(content: string): string { }); // Process Setext-style headings (Heading\n====) - processedContent = processedContent.replace(ALTERNATE_HEADING_REGEX, (_, text, level) => { - const headingLevel = level[0] === '=' ? 1 : 2; - const classes = headingClasses[headingLevel - 1]; - return `${text.trim()}`; - }); + processedContent = processedContent.replace( + ALTERNATE_HEADING_REGEX, + (_, text, level) => { + const headingLevel = level[0] === "=" ? 1 : 2; + const classes = headingClasses[headingLevel - 1]; + return `${text.trim()}`; + }, + ); return processedContent; } @@ -53,29 +82,30 @@ function processHeadings(content: string): string { */ function processTables(content: string): string { try { - if (!content) return ''; - + if (!content) return ""; + return content.replace(/^\|(.*(?:\n\|.*)*)/gm, (match) => { try { // Split into rows and clean up - const rows = match.split('\n').filter(row => row.trim()); + const rows = match.split("\n").filter((row) => row.trim()); if (rows.length < 1) return match; // Helper to process a row into cells const processCells = (row: string): string[] => { return row - .split('|') + .split("|") .slice(1, -1) // Remove empty cells from start/end - .map(cell => cell.trim()); + .map((cell) => cell.trim()); }; // Check if second row is a delimiter row (only hyphens) - const hasHeader = rows.length > 1 && rows[1].trim().match(/^\|[-\s|]+\|$/); - + const hasHeader = + rows.length > 1 && rows[1].trim().match(/^\|[-\s|]+\|$/); + // Extract header and body rows let headerCells: string[] = []; let bodyRows: string[] = []; - + if (hasHeader) { // If we have a header, first row is header, skip delimiter, rest is body headerCells = processCells(rows[0]); @@ -91,33 +121,33 @@ function processTables(content: string): string { // Add header if exists if (hasHeader) { - html += '\n\n'; - headerCells.forEach(cell => { + html += "\n\n"; + headerCells.forEach((cell) => { html += `${cell}\n`; }); - html += '\n\n'; + html += "\n\n"; } // Add body - html += '\n'; - bodyRows.forEach(row => { + html += "\n"; + bodyRows.forEach((row) => { const cells = processCells(row); - html += '\n'; - cells.forEach(cell => { + html += "\n"; + cells.forEach((cell) => { html += `${cell}\n`; }); - html += '\n'; + html += "\n"; }); - html += '\n\n
'; + html += "\n\n
"; return html; } catch (e: unknown) { - console.error('Error processing table row:', e); + console.error("Error processing table row:", e); return match; } }); } catch (e: unknown) { - console.error('Error in processTables:', e); + console.error("Error in processTables:", e); return content; } } @@ -126,8 +156,9 @@ function processTables(content: string): string { * Process horizontal rules */ function processHorizontalRules(content: string): string { - return content.replace(HORIZONTAL_RULE_REGEX, - '
' + return content.replace( + HORIZONTAL_RULE_REGEX, + '
', ); } @@ -136,7 +167,7 @@ function processHorizontalRules(content: string): string { */ function processFootnotes(content: string): string { try { - if (!content) return ''; + if (!content) return ""; // Collect all footnote definitions (but do not remove them from the text yet) const footnotes = new Map(); @@ -146,48 +177,57 @@ function processFootnotes(content: string): string { }); // Remove all footnote definition lines from the main content - let processedContent = content.replace(FOOTNOTE_DEFINITION_REGEX, ''); + let processedContent = content.replace(FOOTNOTE_DEFINITION_REGEX, ""); // Track all references to each footnote - const referenceOrder: { id: string, refNum: number, label: string }[] = []; + const referenceOrder: { id: string; refNum: number; label: string }[] = []; const referenceMap = new Map(); // id -> [refNum, ...] let globalRefNum = 1; - processedContent = processedContent.replace(FOOTNOTE_REFERENCE_REGEX, (match, id) => { - if (!footnotes.has(id)) { - console.warn(`Footnote reference [^${id}] found but no definition exists`); - return match; - } - const refNum = globalRefNum++; - if (!referenceMap.has(id)) referenceMap.set(id, []); - referenceMap.get(id)!.push(refNum); - referenceOrder.push({ id, refNum, label: id }); - return `[${refNum}]`; - }); + processedContent = processedContent.replace( + FOOTNOTE_REFERENCE_REGEX, + (match, id) => { + if (!footnotes.has(id)) { + console.warn( + `Footnote reference [^${id}] found but no definition exists`, + ); + return match; + } + const refNum = globalRefNum++; + if (!referenceMap.has(id)) referenceMap.set(id, []); + referenceMap.get(id)!.push(refNum); + referenceOrder.push({ id, refNum, label: id }); + return `[${refNum}]`; + }, + ); // Only render footnotes section if there are actual definitions and at least one reference if (footnotes.size > 0 && referenceOrder.length > 0) { - processedContent += '\n\n

Footnotes

\n
    \n'; + processedContent += + '\n\n

    Footnotes

    \n
      \n'; // Only include each unique footnote once, in order of first reference const seen = new Set(); for (const { id, label } of referenceOrder) { if (seen.has(id)) continue; seen.add(id); - const text = footnotes.get(id) || ''; + const text = footnotes.get(id) || ""; // List of backrefs for this footnote const refs = referenceMap.get(id) || []; - const backrefs = refs.map((num, i) => - `↩${num}` - ).join(' '); + const backrefs = refs + .map( + (num, i) => + `↩${num}`, + ) + .join(" "); // If label is not a number, show it after all backrefs - const labelSuffix = isNaN(Number(label)) ? ` ${label}` : ''; + const labelSuffix = isNaN(Number(label)) ? ` ${label}` : ""; processedContent += `
    1. ${text} ${backrefs}${labelSuffix}
    2. \n`; } - processedContent += '
    '; + processedContent += "
"; } return processedContent; } catch (error) { - console.error('Error processing footnotes:', error); + console.error("Error processing footnotes:", error); return content; } } @@ -198,15 +238,15 @@ function processFootnotes(content: string): string { function processBlockquotes(content: string): string { // Match blockquotes that might span multiple lines const blockquoteRegex = /^>[ \t]?(.+(?:\n>[ \t]?.+)*)/gm; - + return content.replace(blockquoteRegex, (match) => { // Remove the '>' prefix from each line and preserve line breaks const text = match - .split('\n') - .map(line => line.replace(/^>[ \t]?/, '')) - .join('\n') + .split("\n") + .map((line) => line.replace(/^>[ \t]?/, "")) + .join("\n") .trim(); - + return `
${text}
`; }); } @@ -214,20 +254,23 @@ function processBlockquotes(content: string): string { /** * Process code blocks by finding consecutive code lines and preserving their content */ -function processCodeBlocks(text: string): { text: string; blocks: Map } { - const lines = text.split('\n'); +function processCodeBlocks(text: string): { + text: string; + blocks: Map; +} { + const lines = text.split("\n"); const processedLines: string[] = []; const blocks = new Map(); let inCodeBlock = false; let currentCode: string[] = []; - let currentLanguage = ''; + let currentLanguage = ""; let blockCount = 0; let lastWasCodeBlock = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const codeBlockStart = line.match(CODE_BLOCK_REGEX); - + if (codeBlockStart) { if (!inCodeBlock) { // Starting a new code block @@ -239,36 +282,39 @@ function processCodeBlocks(text: string): { text: string; blocks: Map 0) { blockCount++; const id = `CODE_BLOCK_${blockCount}`; - const code = currentCode.join('\n'); - + const code = currentCode.join("\n"); + // Try to format JSON if specified let formattedCode = code; - if (currentLanguage.toLowerCase() === 'json') { + if (currentLanguage.toLowerCase() === "json") { try { formattedCode = JSON.stringify(JSON.parse(code), null, 2); } catch (e: unknown) { formattedCode = code; } } - - blocks.set(id, JSON.stringify({ - code: formattedCode, - language: currentLanguage, - raw: true - })); - processedLines.push(''); + + blocks.set( + id, + JSON.stringify({ + code: formattedCode, + language: currentLanguage, + raw: true, + }), + ); + processedLines.push(""); processedLines.push(id); - processedLines.push(''); + processedLines.push(""); } return { - text: processedLines.join('\n'), - blocks + text: processedLines.join("\n"), + blocks, }; } @@ -312,22 +361,22 @@ function processCodeBlocks(text: string): { text: string; blocks: Map): string { let result = text; - + for (const [id, blockData] of blocks) { try { const { code, language } = JSON.parse(blockData); - + let html; if (language && hljs.getLanguage(language)) { try { const highlighted = hljs.highlight(code, { language, - ignoreIllegals: true + ignoreIllegals: true, }).value; html = `
${highlighted}
`; } catch (e: unknown) { - console.warn('Failed to highlight code block:', e); - html = `
${code}
`; + console.warn("Failed to highlight code block:", e); + html = `
${code}
`; } } else { html = `
${code}
`; @@ -335,55 +384,346 @@ function restoreCodeBlocks(text: string, blocks: Map): string { result = result.replace(id, html); } catch (e: unknown) { - console.error('Error restoring code block:', e); - result = result.replace(id, '
Error processing code block
'); + console.error("Error restoring code block:", e); + result = result.replace( + id, + '
Error processing code block
', + ); } } return result; } +/** + * Process $...$ and $$...$$ math blocks: render as LaTeX if recognized, otherwise as AsciiMath + * This must run BEFORE any paragraph or inline code formatting. + */ +function processDollarMath(content: string): string { + // Display math: $$...$$ (multi-line, not empty) + content = content.replace(/\$\$([\s\S]*?\S[\s\S]*?)\$\$/g, (match, expr) => { + if (isLaTeXContent(expr)) { + return `
$$${expr}$$
`; + } else { + // Strip all $ or $$ from AsciiMath + const clean = expr.replace(/\$+/g, '').trim(); + return `
${clean}
`; + } + }); + // Inline math: $...$ (not empty, not just whitespace) + content = content.replace(/\$([^\s$][^$\n]*?)\$/g, (match, expr) => { + if (isLaTeXContent(expr)) { + return `$${expr}$`; + } else { + const clean = expr.replace(/\$+/g, '').trim(); + return `${clean}`; + } + }); + return content; +} + +/** + * Process LaTeX math expressions only within inline code blocks + */ +function processMathExpressions(content: string): string { + // Only process LaTeX within inline code blocks (backticks) + return content.replace(INLINE_CODE_REGEX, (match, code) => { + const trimmedCode = code.trim(); + + // Check for unsupported LaTeX environments (like tabular) first + if (/\\begin\{tabular\}|\\\\begin\{tabular\}/.test(trimmedCode)) { + return `
+

+ Unrendered, as it is LaTeX typesetting, not a formula: +

+
+          ${escapeHtml(trimmedCode)}
+        
+
`; + } + + // Check if the code contains LaTeX syntax + if (isLaTeXContent(trimmedCode)) { + // Detect LaTeX display math (\\[...\\]) + if (/^\\\[[\s\S]*\\\]$/.test(trimmedCode)) { + // Remove the delimiters for rendering + const inner = trimmedCode.replace(/^\\\[|\\\]$/g, ''); + return `
$$${inner}$$
`; + } + // Detect display math ($$...$$) + if (/^\$\$[\s\S]*\$\$$/.test(trimmedCode)) { + // Remove the delimiters for rendering + const inner = trimmedCode.replace(/^\$\$|\$\$$/g, ''); + return `
$$${inner}$$
`; + } + // Detect inline math ($...$) + if (/^\$[\s\S]*\$$/.test(trimmedCode)) { + // Remove the delimiters for rendering + const inner = trimmedCode.replace(/^\$|\$$/g, ''); + return `$${inner}$`; + } + // Default to inline math for any other LaTeX content + return `$${trimmedCode}$`; + } else { + // Check for edge cases that should remain as code, not math + // These patterns indicate code that contains dollar signs but is not math + const codePatterns = [ + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=/, // Variable assignment like "const price =" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/, // Function call like "echo(" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\{/, // Object literal like "const obj = {" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\[/, // Array literal like "const arr = [" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*!&|^~]/, // Operator like "const x = 1 +" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Two identifiers like "const price = amount" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]/, // Number like "const x = 1" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]/, // Complex expression like "const price = amount +" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Three identifiers like "const price = amount + tax" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]/, // Two identifiers and number like "const price = amount + 1" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]/, // Identifier, number, operator like "const x = 1 +" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Identifier, number, identifier like "const x = 1 + y" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[0-9]/, // Identifier, number, number like "const x = 1 + 2" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Complex like "const x = 1 + y" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Complex like "const x = 1 + 2" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]/, // Very complex like "const x = 1 + y +" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Very complex like "const x = 1 + y + z" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Very complex like "const x = 1 + y + 2" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]\s*[+\-*/%=<>!&|^~]/, // Very complex like "const x = 1 + 2 +" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Very complex like "const x = 1 + 2 + y" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Very complex like "const x = 1 + 2 + 3" + // Additional patterns for JavaScript template literals and other code + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*`/, // Template literal assignment like "const str = `" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*'/, // String assignment like "const str = '" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*"/, // String assignment like "const str = \"" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]/, // Number assignment like "const x = 1" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Variable assignment like "const x = y" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[+\-*/%=<>!&|^~]/, // Assignment with operator like "const x = +" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]/, // Assignment with variable and operator like "const x = y +" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Assignment with two variables and operator like "const x = y + z" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]\s*[+\-*/%=<>!&|^~]/, // Assignment with number and operator like "const x = 1 +" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Assignment with number, operator, variable like "const x = 1 + y" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Assignment with variable, operator, number like "const x = y + 1" + /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Assignment with number, operator, number like "const x = 1 + 2" + ]; + + // If it matches code patterns, treat as regular code + if (codePatterns.some(pattern => pattern.test(trimmedCode))) { + const escapedCode = trimmedCode + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + return `${escapedCode}`; + } + + // Return as regular inline code + const escapedCode = trimmedCode + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + return `${escapedCode}`; + } + }); +} + +/** + * Checks if content contains LaTeX syntax + */ +function isLaTeXContent(content: string): boolean { + const trimmed = content.trim(); + + // Check for simple math expressions first (like AsciiMath) + if (/^\$[^$]+\$$/.test(trimmed)) { + return true; + } + + // Check for display math + if (/^\$\$[\s\S]*\$\$$/.test(trimmed)) { + return true; + } + + // Check for LaTeX display math + if (/^\\\[[\s\S]*\\\]$/.test(trimmed)) { + return true; + } + + // Check for LaTeX environments with double backslashes (like tabular) + if (/\\\\begin\{[^}]+\}/.test(trimmed) || /\\\\end\{[^}]+\}/.test(trimmed)) { + return true; + } + + // Check for common LaTeX patterns + const latexPatterns = [ + /\\[a-zA-Z]+/, // LaTeX commands like \frac, \sum, etc. + /\\\\[a-zA-Z]+/, // LaTeX commands with double backslashes like \\frac, \\sum, etc. + /\\[\(\)\[\]]/, // LaTeX delimiters like \(, \), \[, \] + /\\\\[\(\)\[\]]/, // LaTeX delimiters with double backslashes like \\(, \\), \\[, \\] + /\\\[[\s\S]*?\\\]/, // LaTeX display math \[ ... \] + /\\\\\[[\s\S]*?\\\\\]/, // LaTeX display math with double backslashes \\[ ... \\] + /\\begin\{/, // LaTeX environments + /\\\\begin\{/, // LaTeX environments with double backslashes + /\\end\{/, // LaTeX environments + /\\\\end\{/, // LaTeX environments with double backslashes + /\\begin\{array\}/, // LaTeX array environment + /\\\\begin\{array\}/, // LaTeX array environment with double backslashes + /\\end\{array\}/, + /\\\\end\{array\}/, + /\\begin\{matrix\}/, // LaTeX matrix environment + /\\\\begin\{matrix\}/, // LaTeX matrix environment with double backslashes + /\\end\{matrix\}/, + /\\\\end\{matrix\}/, + /\\begin\{bmatrix\}/, // LaTeX bmatrix environment + /\\\\begin\{bmatrix\}/, // LaTeX bmatrix environment with double backslashes + /\\end\{bmatrix\}/, + /\\\\end\{bmatrix\}/, + /\\begin\{pmatrix\}/, // LaTeX pmatrix environment + /\\\\begin\{pmatrix\}/, // LaTeX pmatrix environment with double backslashes + /\\end\{pmatrix\}/, + /\\\\end\{pmatrix\}/, + /\\begin\{tabular\}/, // LaTeX tabular environment + /\\\\begin\{tabular\}/, // LaTeX tabular environment with double backslashes + /\\end\{tabular\}/, + /\\\\end\{tabular\}/, + /\$\$/, // Display math delimiters + /\$[^$]+\$/, // Inline math delimiters + /\\text\{/, // LaTeX text command + /\\\\text\{/, // LaTeX text command with double backslashes + /\\mathrm\{/, // LaTeX mathrm command + /\\\\mathrm\{/, // LaTeX mathrm command with double backslashes + /\\mathbf\{/, // LaTeX bold command + /\\\\mathbf\{/, // LaTeX bold command with double backslashes + /\\mathit\{/, // LaTeX italic command + /\\\\mathit\{/, // LaTeX italic command with double backslashes + /\\sqrt/, // Square root + /\\\\sqrt/, // Square root with double backslashes + /\\frac/, // Fraction + /\\\\frac/, // Fraction with double backslashes + /\\sum/, // Sum + /\\\\sum/, // Sum with double backslashes + /\\int/, // Integral + /\\\\int/, // Integral with double backslashes + /\\lim/, // Limit + /\\\\lim/, // Limit with double backslashes + /\\infty/, // Infinity + /\\\\infty/, // Infinity with double backslashes + /\\alpha/, // Greek letters + /\\\\alpha/, // Greek letters with double backslashes + /\\beta/, + /\\\\beta/, + /\\gamma/, + /\\\\gamma/, + /\\delta/, + /\\\\delta/, + /\\theta/, + /\\\\theta/, + /\\lambda/, + /\\\\lambda/, + /\\mu/, + /\\\\mu/, + /\\pi/, + /\\\\pi/, + /\\sigma/, + /\\\\sigma/, + /\\phi/, + /\\\\phi/, + /\\omega/, + /\\\\omega/, + /\\partial/, // Partial derivative + /\\\\partial/, // Partial derivative with double backslashes + /\\nabla/, // Nabla + /\\\\nabla/, // Nabla with double backslashes + /\\cdot/, // Dot product + /\\\\cdot/, // Dot product with double backslashes + /\\times/, // Times + /\\\\times/, // Times with double backslashes + /\\div/, // Division + /\\\\div/, // Division with double backslashes + /\\pm/, // Plus-minus + /\\\\pm/, // Plus-minus with double backslashes + /\\mp/, // Minus-plus + /\\\\mp/, // Minus-plus with double backslashes + /\\leq/, // Less than or equal + /\\\\leq/, // Less than or equal with double backslashes + /\\geq/, // Greater than or equal + /\\\\geq/, // Greater than or equal with double backslashes + /\\neq/, // Not equal + /\\\\neq/, // Not equal with double backslashes + /\\approx/, // Approximately equal + /\\\\approx/, // Approximately equal with double backslashes + /\\equiv/, // Equivalent + /\\\\equiv/, // Equivalent with double backslashes + /\\propto/, // Proportional + /\\\\propto/, // Proportional with double backslashes + /\\in/, // Element of + /\\\\in/, // Element of with double backslashes + /\\notin/, // Not element of + /\\\\notin/, // Not element of with double backslashes + /\\subset/, // Subset + /\\\\subset/, // Subset with double backslashes + /\\supset/, // Superset + /\\\\supset/, // Superset with double backslashes + /\\cup/, // Union + /\\\\cup/, // Union with double backslashes + /\\cap/, // Intersection + /\\\\cap/, // Intersection with double backslashes + /\\emptyset/, // Empty set + /\\\\emptyset/, // Empty set with double backslashes + /\\mathbb\{/, // Blackboard bold + /\\\\mathbb\{/, // Blackboard bold with double backslashes + /\\mathcal\{/, // Calligraphic + /\\\\mathcal\{/, // Calligraphic with double backslashes + /\\mathfrak\{/, // Fraktur + /\\\\mathfrak\{/, // Fraktur with double backslashes + /\\mathscr\{/, // Script + /\\\\mathscr\{/, // Script with double backslashes + ]; + + return latexPatterns.some(pattern => pattern.test(trimmed)); +} + /** * Parse markup text with advanced formatting */ export async function parseAdvancedmarkup(text: string): Promise { - if (!text) return ''; - + if (!text) return ""; + try { // Step 1: Extract and save code blocks first const { text: withoutCode, blocks } = processCodeBlocks(text); let processedText = withoutCode; - // Step 2: Process block-level elements + // Step 2: Process $...$ and $$...$$ math blocks (LaTeX or AsciiMath) + processedText = processDollarMath(processedText); + + // Step 3: Process LaTeX math expressions ONLY within inline code blocks (legacy support) + processedText = processMathExpressions(processedText); + + // Step 4: Process block-level elements (tables, blockquotes, headings, horizontal rules) processedText = processTables(processedText); processedText = processBlockquotes(processedText); processedText = processHeadings(processedText); processedText = processHorizontalRules(processedText); - // Process inline elements - processedText = processedText.replace(INLINE_CODE_REGEX, (_, code) => { - const escapedCode = code - .trim() - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - return `${escapedCode}`; - }); - - // Process footnotes (only references, not definitions) + // Step 5: Process footnotes (only references, not definitions) processedText = processFootnotes(processedText); - // Process basic markup (which will also handle Nostr identifiers) + // Step 6: Process basic markup (which will also handle Nostr identifiers) + // This includes paragraphs, inline code, links, lists, etc. processedText = await parseBasicmarkup(processedText); - // Step 3: Restore code blocks + // Step 7: Restore code blocks processedText = restoreCodeBlocks(processedText, blocks); return processedText; } catch (e: unknown) { - console.error('Error in parseAdvancedmarkup:', e); - return `
Error processing markup: ${(e as Error)?.message ?? 'Unknown error'}
`; + console.error("Error in parseAdvancedmarkup:", e); + return `
Error processing markup: ${(e as Error)?.message ?? "Unknown error"}
`; } -} \ No newline at end of file +} diff --git a/src/lib/utils/markup/asciidoctorExtensions.ts b/src/lib/utils/markup/asciidoctorExtensions.ts new file mode 100644 index 0000000..5e7be35 --- /dev/null +++ b/src/lib/utils/markup/asciidoctorExtensions.ts @@ -0,0 +1,213 @@ +import { renderTikZ } from "./tikzRenderer"; +import asciidoctor from "asciidoctor"; + +// Simple math rendering using MathJax CDN +function renderMath(content: string): string { + return `
+
${content}
+ +
`; +} + +// Simple PlantUML rendering using PlantUML server +function renderPlantUML(content: string): string { + // Encode content for PlantUML server + const encoded = btoa(unescape(encodeURIComponent(content))); + const plantUMLUrl = `https://www.plantuml.com/plantuml/svg/${encoded}`; + + return `PlantUML diagram`; +} + +/** + * Creates Asciidoctor extensions for advanced content rendering + * including Asciimath/Latex, PlantUML, BPMN, and TikZ + */ +export function createAdvancedExtensions(): any { + const Asciidoctor = asciidoctor(); + const extensions = Asciidoctor.Extensions.create(); + + // Math rendering extension (Asciimath/Latex) + extensions.treeProcessor(function (this: any) { + const dsl = this; + dsl.process(function (this: any, document: any) { + const treeProcessor = this; + processMathBlocks(treeProcessor, document); + }); + }); + + // PlantUML rendering extension + extensions.treeProcessor(function (this: any) { + const dsl = this; + dsl.process(function (this: any, document: any) { + const treeProcessor = this; + processPlantUMLBlocks(treeProcessor, document); + }); + }); + + // TikZ rendering extension + extensions.treeProcessor(function (this: any) { + const dsl = this; + dsl.process(function (this: any, document: any) { + const treeProcessor = this; + processTikZBlocks(treeProcessor, document); + }); + }); + + // --- NEW: Support [plantuml], [tikz], [bpmn] as source blocks --- + // Helper to register a block for a given name and treat it as a source block + function registerDiagramBlock(name: string) { + extensions.block(name, function (this: any) { + const self = this; + self.process(function (parent: any, reader: any, attrs: any) { + // Read the block content + const lines = reader.getLines(); + // Create a source block with the correct language and lang attributes + const block = self.createBlock(parent, "source", lines, { + ...attrs, + language: name, + lang: name, + style: "source", + role: name, + }); + block.setAttribute("language", name); + block.setAttribute("lang", name); + block.setAttribute("style", "source"); + block.setAttribute("role", name); + block.setOption("source", true); + block.setOption("listing", true); + block.setStyle("source"); + return block; + }); + }); + } + registerDiagramBlock("plantuml"); + registerDiagramBlock("tikz"); + registerDiagramBlock("bpmn"); + // --- END NEW --- + + return extensions; +} + +/** + * Processes math blocks (stem blocks) and converts them to rendered HTML + */ +function processMathBlocks(treeProcessor: any, document: any): void { + const blocks = document.getBlocks(); + for (const block of blocks) { + if (block.getContext() === "stem") { + const content = block.getContent(); + if (content) { + try { + // Output as a single div with delimiters for MathJax + const rendered = `
$$${content}$$
`; + block.setContent(rendered); + } catch (error) { + console.warn("Failed to render math:", error); + } + } + } + // Inline math: context 'inline' and style 'stem' or 'latexmath' + if ( + block.getContext() === "inline" && + (block.getStyle() === "stem" || block.getStyle() === "latexmath") + ) { + const content = block.getContent(); + if (content) { + try { + const rendered = `$${content}$`; + block.setContent(rendered); + } catch (error) { + console.warn("Failed to render inline math:", error); + } + } + } + } +} + +/** + * Processes PlantUML blocks and converts them to rendered SVG + */ +function processPlantUMLBlocks(treeProcessor: any, document: any): void { + const blocks = document.getBlocks(); + + for (const block of blocks) { + if (block.getContext() === "listing" && isPlantUMLBlock(block)) { + const content = block.getContent(); + if (content) { + try { + // Use simple PlantUML rendering + const rendered = renderPlantUML(content); + + // Replace the block content with the image + block.setContent(rendered); + } catch (error) { + console.warn("Failed to render PlantUML:", error); + // Keep original content if rendering fails + } + } + } + } +} + +/** + * Processes TikZ blocks and converts them to rendered SVG + */ +function processTikZBlocks(treeProcessor: any, document: any): void { + const blocks = document.getBlocks(); + + for (const block of blocks) { + if (block.getContext() === "listing" && isTikZBlock(block)) { + const content = block.getContent(); + if (content) { + try { + // Render TikZ to SVG + const svg = renderTikZ(content); + + // Replace the block content with the SVG + block.setContent(svg); + } catch (error) { + console.warn("Failed to render TikZ:", error); + // Keep original content if rendering fails + } + } + } + } +} + +/** + * Checks if a block contains PlantUML content + */ +function isPlantUMLBlock(block: any): boolean { + const content = block.getContent() || ""; + const lines = content.split("\n"); + + // Check for PlantUML indicators + return lines.some( + (line: string) => + line.trim().startsWith("@startuml") || + line.trim().startsWith("@start") || + line.includes("plantuml") || + line.includes("uml"), + ); +} + +/** + * Checks if a block contains TikZ content + */ +function isTikZBlock(block: any): boolean { + const content = block.getContent() || ""; + const lines = content.split("\n"); + + // Check for TikZ indicators + return lines.some( + (line: string) => + line.trim().startsWith("\\begin{tikzpicture}") || + line.trim().startsWith("\\tikz") || + line.includes("tikzpicture") || + line.includes("tikz"), + ); +} diff --git a/src/lib/utils/markup/asciidoctorPostProcessor.ts b/src/lib/utils/markup/asciidoctorPostProcessor.ts new file mode 100644 index 0000000..8664c02 --- /dev/null +++ b/src/lib/utils/markup/asciidoctorPostProcessor.ts @@ -0,0 +1,136 @@ +import { processNostrIdentifiers } from "../nostrUtils"; + +/** + * Normalizes a string for use as a d-tag by converting to lowercase, + * replacing non-alphanumeric characters with dashes, and removing + * leading/trailing dashes. + */ +function normalizeDTag(input: string): string { + return input + .toLowerCase() + .replace(/[^\p{L}\p{N}]/gu, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); +} + +/** + * Replaces wikilinks in the format [[target]] or [[target|display]] with + * clickable links to the events page. + */ +function replaceWikilinks(html: string): string { + // [[target page]] or [[target page|display text]] + return html.replace( + /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, + (_match, target, label) => { + const normalized = normalizeDTag(target.trim()); + const display = (label || target).trim(); + const url = `./events?d=${normalized}`; + // Output as a clickable with the [[display]] format and matching link colors + return `${display}`; + }, + ); +} + +/** + * Replaces AsciiDoctor-generated empty anchor tags with clickable wikilink-style tags. + */ +function replaceAsciiDocAnchors(html: string): string { + return html.replace( + /<\/a>/g, + (_match, id) => { + const normalized = normalizeDTag(id.trim()); + const url = `./events?d=${normalized}`; + return `${id}`; + } + ); +} + +/** + * Processes nostr addresses in HTML content, but skips addresses that are + * already within hyperlink tags. + */ +async function processNostrAddresses(html: string): Promise { + // Helper to check if a match is within an existing tag + function isWithinLink(text: string, index: number): boolean { + // Look backwards from the match position to find the nearest tag + const before = text.slice(0, index); + const lastOpenTag = before.lastIndexOf(""); + + // If we find an opening tag after the last closing tag, we're inside a link + return lastOpenTag > lastCloseTag; + } + + // Process nostr addresses that are not within existing links + const nostrPattern = + /nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/g; + let processedHtml = html; + + // Find all nostr addresses + const matches = Array.from(processedHtml.matchAll(nostrPattern)); + + // Process them in reverse order to avoid index shifting issues + for (let i = matches.length - 1; i >= 0; i--) { + const match = matches[i]; + const [fullMatch] = match; + const matchIndex = match.index ?? 0; + + // Skip if already within a link + if (isWithinLink(processedHtml, matchIndex)) { + continue; + } + + // Process the nostr identifier + const processedMatch = await processNostrIdentifiers(fullMatch); + + // Replace the match in the HTML + processedHtml = + processedHtml.slice(0, matchIndex) + + processedMatch + + processedHtml.slice(matchIndex + fullMatch.length); + } + + return processedHtml; +} + +/** + * Fixes AsciiDoctor stem blocks for MathJax rendering. + * Joins split spans and wraps content in $$...$$ for block math. + */ +function fixStemBlocks(html: string): string { + // Replace
$...$
+ // with
$$...$$
+ return html.replace( + /
\s*
\s*\$<\/span>([\s\S]*?)\$<\/span>\s*<\/div>\s*<\/div>/g, + (_match, mathContent) => { + // Remove any extra tags inside mathContent + const cleanMath = mathContent.replace(/<\/?span[^>]*>/g, "").trim(); + return `
$$${cleanMath}$$
`; + }, + ); +} + +/** + * Post-processes asciidoctor HTML output to add wikilink and nostr address rendering. + * This function should be called after asciidoctor.convert() to enhance the HTML output. + */ +export async function postProcessAsciidoctorHtml( + html: string, +): Promise { + if (!html) return html; + + try { + // First process AsciiDoctor-generated anchors + let processedHtml = replaceAsciiDocAnchors(html); + // Then process wikilinks in [[...]] format (if any remain) + processedHtml = replaceWikilinks(processedHtml); + // Then process nostr addresses (but not those already in links) + processedHtml = await processNostrAddresses(processedHtml); + processedHtml = fixStemBlocks(processedHtml); // Fix math blocks for MathJax + + return processedHtml; + } catch (error) { + console.error("Error in postProcessAsciidoctorHtml:", error); + return html; // Return original HTML if processing fails + } +} diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index d0d6ff1..e7a1d74 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -1,6 +1,6 @@ -import { processNostrIdentifiers } from '../nostrUtils'; -import * as emoji from 'node-emoji'; -import { nip19 } from 'nostr-tools'; +import { processNostrIdentifiers } from "../nostrUtils"; +import * as emoji from "node-emoji"; +import { nip19 } from "nostr-tools"; /* Regex constants for basic markup parsing */ @@ -23,37 +23,42 @@ const DIRECT_LINK = /(?"]+)(?!["'])/g; const IMAGE_EXTENSIONS = /\.(jpg|jpeg|gif|png|webp|svg)$/i; const VIDEO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp4|webm|mov|avi)(?:[^\s<]*)?/i; const AUDIO_URL_REGEX = /https?:\/\/[^\s<]+\.(?:mp3|wav|ogg|m4a)(?:[^\s<]*)?/i; -const YOUTUBE_URL_REGEX = /https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})(?:[^\s<]*)?/i; +const YOUTUBE_URL_REGEX = + /https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})(?:[^\s<]*)?/i; // Add this helper function near the top: function replaceAlexandriaNostrLinks(text: string): string { // Regex for Alexandria/localhost URLs - const alexandriaPattern = /^https?:\/\/((next-)?alexandria\.gitcitadel\.(eu|com)|localhost(:\d+)?)/i; + const alexandriaPattern = + /^https?:\/\/((next-)?alexandria\.gitcitadel\.(eu|com)|localhost(:\d+)?)/i; // Regex for bech32 Nostr identifiers const bech32Pattern = /(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/; // Regex for 64-char hex const hexPattern = /\b[a-fA-F0-9]{64}\b/; // 1. Alexandria/localhost markup links - text = text.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (match, _label, url) => { - if (alexandriaPattern.test(url)) { - if (/[?&]d=/.test(url)) return match; - const hexMatch = url.match(hexPattern); - if (hexMatch) { - try { - const nevent = nip19.neventEncode({ id: hexMatch[0] }); - return `nostr:${nevent}`; - } catch { - return match; + text = text.replace( + /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, + (match, _label, url) => { + if (alexandriaPattern.test(url)) { + if (/[?&]d=/.test(url)) return match; + const hexMatch = url.match(hexPattern); + if (hexMatch) { + try { + const nevent = nip19.neventEncode({ id: hexMatch[0] }); + return `nostr:${nevent}`; + } catch { + return match; + } + } + const bech32Match = url.match(bech32Pattern); + if (bech32Match) { + return `nostr:${bech32Match[0]}`; } } - const bech32Match = url.match(bech32Pattern); - if (bech32Match) { - return `nostr:${bech32Match[0]}`; - } - } - return match; - }); + return match; + }, + ); // 2. Alexandria/localhost bare URLs and non-Alexandria/localhost URLs with Nostr identifiers text = text.replace(/https?:\/\/[^\s)\]]+/g, (url) => { @@ -96,12 +101,18 @@ function replaceAlexandriaNostrLinks(text: string): string { // Utility to strip tracking parameters from URLs function stripTrackingParams(url: string): string { // List of tracking params to remove - const trackingParams = [/^utm_/i, /^fbclid$/i, /^gclid$/i, /^tracking$/i, /^ref$/i]; + const trackingParams = [ + /^utm_/i, + /^fbclid$/i, + /^gclid$/i, + /^tracking$/i, + /^ref$/i, + ]; try { // Absolute URL if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) { const parsed = new URL(url); - trackingParams.forEach(pattern => { + trackingParams.forEach((pattern) => { for (const key of Array.from(parsed.searchParams.keys())) { if (pattern.test(key)) { parsed.searchParams.delete(key); @@ -109,19 +120,24 @@ function stripTrackingParams(url: string): string { } }); const queryString = parsed.searchParams.toString(); - return parsed.origin + parsed.pathname + (queryString ? '?' + queryString : '') + (parsed.hash || ''); + return ( + parsed.origin + + parsed.pathname + + (queryString ? "?" + queryString : "") + + (parsed.hash || "") + ); } else { // Relative URL: parse query string manually - const [path, queryAndHash = ''] = url.split('?'); - const [query = '', hash = ''] = queryAndHash.split('#'); + const [path, queryAndHash = ""] = url.split("?"); + const [query = "", hash = ""] = queryAndHash.split("#"); if (!query) return url; - const params = query.split('&').filter(Boolean); - const filtered = params.filter(param => { - const [key] = param.split('='); - return !trackingParams.some(pattern => pattern.test(key)); + const params = query.split("&").filter(Boolean); + const filtered = params.filter((param) => { + const [key] = param.split("="); + return !trackingParams.some((pattern) => pattern.test(key)); }); - const queryString = filtered.length ? '?' + filtered.join('&') : ''; - const hashString = hash ? '#' + hash : ''; + const queryString = filtered.length ? "?" + filtered.join("&") : ""; + const hashString = hash ? "#" + hash : ""; return path + queryString + hashString; } } catch { @@ -132,38 +148,45 @@ function stripTrackingParams(url: string): string { function normalizeDTag(input: string): string { return input .toLowerCase() - .replace(/[^\p{L}\p{N}]/gu, '-') - .replace(/-+/g, '-') - .replace(/^-|-$/g, ''); + .replace(/[^\p{L}\p{N}]/gu, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); } function replaceWikilinks(text: string): string { // [[target page]] or [[target page|display text]] - return text.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_match, target, label) => { - const normalized = normalizeDTag(target.trim()); - const display = (label || target).trim(); - const url = `./wiki?d=${normalized}`; - // Output as a clickable with the [[display]] format and matching link colors - return `${display}`; - }); + return text.replace( + /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, + (_match, target, label) => { + const normalized = normalizeDTag(target.trim()); + const display = (label || target).trim(); + const url = `./events?d=${normalized}`; + // Output as a clickable with the [[display]] format and matching link colors + return `${display}`; + }, + ); } -function renderListGroup(lines: string[], typeHint?: 'ol' | 'ul'): string { - function parseList(start: number, indent: number, type: 'ol' | 'ul'): [string, number] { - let html = ''; +function renderListGroup(lines: string[], typeHint?: "ol" | "ul"): string { + function parseList( + start: number, + indent: number, + type: "ol" | "ul", + ): [string, number] { + let html = ""; let i = start; - html += `<${type} class="${type === 'ol' ? 'list-decimal' : 'list-disc'} ml-6 mb-2">`; + html += `<${type} class="${type === "ol" ? "list-decimal" : "list-disc"} ml-6 mb-2">`; while (i < lines.length) { const line = lines[i]; const match = line.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+(.*)$/); if (!match) break; - const lineIndent = match[1].replace(/\t/g, ' ').length; + const lineIndent = match[1].replace(/\t/g, " ").length; const isOrdered = /\d+\./.test(match[2]); - const itemType = isOrdered ? 'ol' : 'ul'; + const itemType = isOrdered ? "ol" : "ul"; if (lineIndent > indent) { // Nested list const [nestedHtml, consumed] = parseList(i, lineIndent, itemType); - html = html.replace(/<\/li>$/, '') + nestedHtml + ''; + html = html.replace(/<\/li>$/, "") + nestedHtml + ""; i = consumed; continue; } @@ -175,35 +198,39 @@ function renderListGroup(lines: string[], typeHint?: 'ol' | 'ul'): string { if (i + 1 < lines.length) { const nextMatch = lines[i + 1].match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/); if (nextMatch) { - const nextIndent = nextMatch[1].replace(/\t/g, ' ').length; - const nextType = /\d+\./.test(nextMatch[2]) ? 'ol' : 'ul'; + const nextIndent = nextMatch[1].replace(/\t/g, " ").length; + const nextType = /\d+\./.test(nextMatch[2]) ? "ol" : "ul"; if (nextIndent > lineIndent) { - const [nestedHtml, consumed] = parseList(i + 1, nextIndent, nextType); + const [nestedHtml, consumed] = parseList( + i + 1, + nextIndent, + nextType, + ); html += nestedHtml; i = consumed - 1; } } } - html += ''; + html += ""; i++; } html += ``; return [html, i]; } - if (!lines.length) return ''; + if (!lines.length) return ""; const firstLine = lines[0]; const match = firstLine.match(/^([ \t]*)([*+-]|\d+\.)[ \t]+/); - const indent = match ? match[1].replace(/\t/g, ' ').length : 0; - const type = typeHint || (match && /\d+\./.test(match[2]) ? 'ol' : 'ul'); + const indent = match ? match[1].replace(/\t/g, " ").length : 0; + const type = typeHint || (match && /\d+\./.test(match[2]) ? "ol" : "ul"); const [html] = parseList(0, indent, type); return html; } function processBasicFormatting(content: string): string { - if (!content) return ''; - + if (!content) return ""; + let processedText = content; - + try { // Sanitize Alexandria Nostr links before further processing processedText = replaceAlexandriaNostrLinks(processedText); @@ -214,17 +241,17 @@ function processBasicFormatting(content: string): string { if (YOUTUBE_URL_REGEX.test(url)) { const videoId = extractYouTubeVideoId(url); if (videoId) { - return ``; + return ``; } } if (VIDEO_URL_REGEX.test(url)) { - return ``; + return ``; } if (AUDIO_URL_REGEX.test(url)) { - return ``; + return ``; } // Only render if the url ends with a direct image extension - if (IMAGE_EXTENSIONS.test(url.split('?')[0])) { + if (IMAGE_EXTENSIONS.test(url.split("?")[0])) { return `${alt}`; } // Otherwise, render as a clickable link @@ -232,19 +259,21 @@ function processBasicFormatting(content: string): string { }); // Process markup links - processedText = processedText.replace(MARKUP_LINK, (match, text, url) => - `${text}` + processedText = processedText.replace( + MARKUP_LINK, + (match, text, url) => + `${text}`, ); // Process WebSocket URLs - processedText = processedText.replace(WSS_URL, match => { + processedText = processedText.replace(WSS_URL, (match) => { // Remove 'wss://' from the start and any trailing slashes - const cleanUrl = match.slice(6).replace(/\/+$/, ''); + const cleanUrl = match.slice(6).replace(/\/+$/, ""); return `${match}`; }); // Process direct media URLs and auto-link all URLs - processedText = processedText.replace(DIRECT_LINK, match => { + processedText = processedText.replace(DIRECT_LINK, (match) => { const clean = stripTrackingParams(match); if (YOUTUBE_URL_REGEX.test(clean)) { const videoId = extractYouTubeVideoId(clean); @@ -259,30 +288,36 @@ function processBasicFormatting(content: string): string { return ``; } // Only render if the url ends with a direct image extension - if (IMAGE_EXTENSIONS.test(clean.split('?')[0])) { + if (IMAGE_EXTENSIONS.test(clean.split("?")[0])) { return `Embedded media`; } // Otherwise, render as a clickable link return `${clean}`; }); - + // Process text formatting - processedText = processedText.replace(BOLD_REGEX, '$2'); - processedText = processedText.replace(ITALIC_REGEX, match => { - const text = match.replace(/^_+|_+$/g, ''); + processedText = processedText.replace(BOLD_REGEX, "$2"); + processedText = processedText.replace(ITALIC_REGEX, (match) => { + const text = match.replace(/^_+|_+$/g, ""); return `${text}`; }); - processedText = processedText.replace(STRIKETHROUGH_REGEX, (match, doubleText, singleText) => { - const text = doubleText || singleText; - return `${text}`; - }); + processedText = processedText.replace( + STRIKETHROUGH_REGEX, + (match, doubleText, singleText) => { + const text = doubleText || singleText; + return `${text}`; + }, + ); // Process hashtags - processedText = processedText.replace(HASHTAG_REGEX, '#$1'); + processedText = processedText.replace( + HASHTAG_REGEX, + '#$1', + ); // --- Improved List Grouping and Parsing --- - const lines = processedText.split('\n'); - let output = ''; + const lines = processedText.split("\n"); + let output = ""; let buffer: string[] = []; let inList = false; for (let i = 0; i < lines.length; i++) { @@ -294,23 +329,22 @@ function processBasicFormatting(content: string): string { if (inList) { const firstLine = buffer[0]; const isOrdered = /^\s*\d+\.\s+/.test(firstLine); - output += renderListGroup(buffer, isOrdered ? 'ol' : 'ul'); + output += renderListGroup(buffer, isOrdered ? "ol" : "ul"); buffer = []; inList = false; } - output += (output && !output.endsWith('\n') ? '\n' : '') + line + '\n'; + output += (output && !output.endsWith("\n") ? "\n" : "") + line + "\n"; } } if (buffer.length) { const firstLine = buffer[0]; const isOrdered = /^\s*\d+\.\s+/.test(firstLine); - output += renderListGroup(buffer, isOrdered ? 'ol' : 'ul'); + output += renderListGroup(buffer, isOrdered ? "ol" : "ul"); } processedText = output; // --- End Improved List Grouping and Parsing --- - } catch (e: unknown) { - console.error('Error in processBasicFormatting:', e); + console.error("Error in processBasicFormatting:", e); } return processedText; @@ -318,61 +352,72 @@ function processBasicFormatting(content: string): string { // Helper function to extract YouTube video ID function extractYouTubeVideoId(url: string): string | null { - const match = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})/); + const match = url.match( + /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/|youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})/, + ); return match ? match[1] : null; } function processBlockquotes(content: string): string { try { - if (!content) return ''; - - return content.replace(BLOCKQUOTE_REGEX, match => { - const lines = match.split('\n').map(line => { - return line.replace(/^[ \t]*>[ \t]?/, '').trim(); + if (!content) return ""; + + return content.replace(BLOCKQUOTE_REGEX, (match) => { + const lines = match.split("\n").map((line) => { + return line.replace(/^[ \t]*>[ \t]?/, "").trim(); }); - - return `
${ - lines.join('\n') - }
`; + + return `
${lines.join( + "\n", + )}
`; }); } catch (e: unknown) { - console.error('Error in processBlockquotes:', e); + console.error("Error in processBlockquotes:", e); return content; } } function processEmojiShortcuts(content: string): string { try { - return emoji.emojify(content, { fallback: (name: string) => { - const emojiChar = emoji.get(name); - return emojiChar || `:${name}:`; - }}); + return emoji.emojify(content, { + fallback: (name: string) => { + const emojiChar = emoji.get(name); + return emojiChar || `:${name}:`; + }, + }); } catch (e: unknown) { - console.error('Error in processEmojiShortcuts:', e); + console.error("Error in processEmojiShortcuts:", e); return content; } } export async function parseBasicmarkup(text: string): Promise { - if (!text) return ''; - + if (!text) return ""; + try { // Process basic text formatting first let processedText = processBasicFormatting(text); // Process emoji shortcuts processedText = processEmojiShortcuts(processedText); - + // Process blockquotes processedText = processBlockquotes(processedText); - + // Process paragraphs - split by double newlines and wrap in p tags + // Skip wrapping if content already contains block-level elements processedText = processedText .split(/\n\n+/) - .map(para => para.trim()) - .filter(para => para.length > 0) - .map(para => `

${para}

`) - .join('\n'); + .map((para) => para.trim()) + .filter((para) => para.length > 0) + .map((para) => { + // Skip wrapping if para already contains block-level elements or math blocks + if (/(]*class=["'][^"']*math-block[^"']*["'])|<(div|h[1-6]|blockquote|table|pre|ul|ol|hr)/i.test(para)) { + return para; + } + return `

${para}

`; + }) + .join("\n"); // Process Nostr identifiers last processedText = await processNostrIdentifiers(processedText); @@ -382,7 +427,7 @@ export async function parseBasicmarkup(text: string): Promise { return processedText; } catch (e: unknown) { - console.error('Error in parseBasicmarkup:', e); - return `
Error processing markup: ${(e as Error)?.message ?? 'Unknown error'}
`; + console.error("Error in parseBasicmarkup:", e); + return `
Error processing markup: ${(e as Error)?.message ?? "Unknown error"}
`; } -} \ No newline at end of file +} diff --git a/src/lib/utils/markup/tikzRenderer.ts b/src/lib/utils/markup/tikzRenderer.ts new file mode 100644 index 0000000..3e194b6 --- /dev/null +++ b/src/lib/utils/markup/tikzRenderer.ts @@ -0,0 +1,60 @@ +/** + * TikZ renderer using node-tikzjax + * Converts TikZ LaTeX code to SVG for browser rendering + */ + +// We'll use a simple approach for now since node-tikzjax might not be available +// This is a placeholder implementation that can be enhanced later + +export function renderTikZ(tikzCode: string): string { + try { + // For now, we'll create a simple SVG placeholder + // In a full implementation, this would use node-tikzjax or similar library + + // Extract TikZ content and create a basic SVG + const svgContent = createBasicSVG(tikzCode); + + return svgContent; + } catch (error) { + console.error("Failed to render TikZ:", error); + return `
+

TikZ Rendering Error

+

Failed to render TikZ diagram. Original code:

+
${tikzCode}
+
`; + } +} + +/** + * Creates a basic SVG placeholder for TikZ content + * This is a temporary implementation until proper TikZ rendering is available + */ +function createBasicSVG(tikzCode: string): string { + // Create a simple SVG with the TikZ code as text + const width = 400; + const height = 300; + + return ` + + + TikZ Diagram + + + (Rendering not yet implemented) + + +
+
${escapeHtml(tikzCode)}
+
+
+
`; +} + +/** + * Escapes HTML characters for safe display + */ +function escapeHtml(text: string): string { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} diff --git a/src/lib/utils/mime.ts b/src/lib/utils/mime.ts index 28f744e..979c294 100644 --- a/src/lib/utils/mime.ts +++ b/src/lib/utils/mime.ts @@ -1,3 +1,5 @@ +import { EVENT_KINDS } from './search_constants'; + /** * Determine the type of Nostr event based on its kind number * Following NIP specification for kind ranges: @@ -6,22 +8,25 @@ * - Addressable: 30000-39999 (latest per d-tag stored) * - Regular: all other kinds (stored by relays) */ -export function getEventType(kind: number): 'regular' | 'replaceable' | 'ephemeral' | 'addressable' { +export function getEventType( + kind: number, +): "regular" | "replaceable" | "ephemeral" | "addressable" { // Check special ranges first - if (kind >= 30000 && kind < 40000) { - return 'addressable'; + if (kind >= EVENT_KINDS.ADDRESSABLE.MIN && kind < EVENT_KINDS.ADDRESSABLE.MAX) { + return "addressable"; } - - if (kind >= 20000 && kind < 30000) { - return 'ephemeral'; + + if (kind >= EVENT_KINDS.PARAMETERIZED_REPLACEABLE.MIN && kind < EVENT_KINDS.PARAMETERIZED_REPLACEABLE.MAX) { + return "ephemeral"; } - - if ((kind >= 10000 && kind < 20000) || kind === 0 || kind === 3) { - return 'replaceable'; + + if ((kind >= EVENT_KINDS.REPLACEABLE.MIN && kind < EVENT_KINDS.REPLACEABLE.MAX) || + EVENT_KINDS.REPLACEABLE.SPECIFIC.includes(kind as 0 | 3)) { + return "replaceable"; } - + // Everything else is regular - return 'regular'; + return "regular"; } /** @@ -36,9 +41,10 @@ export function getMimeTags(kind: number): [string, string][] { // Determine replaceability based on event type const eventType = getEventType(kind); - const replaceability = (eventType === 'replaceable' || eventType === 'addressable') - ? "replaceable" - : "nonreplaceable"; + const replaceability = + eventType === "replaceable" || eventType === "addressable" + ? "replaceable" + : "nonreplaceable"; switch (kind) { // Short text note @@ -93,4 +99,4 @@ export function getMimeTags(kind: number): [string, string][] { } return [mTag, MTag]; -} \ No newline at end of file +} diff --git a/src/lib/utils/nostrEventService.ts b/src/lib/utils/nostrEventService.ts new file mode 100644 index 0000000..6a41c12 --- /dev/null +++ b/src/lib/utils/nostrEventService.ts @@ -0,0 +1,431 @@ +import { nip19 } from "nostr-tools"; +import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils"; +import { standardRelays, fallbackRelays } from "$lib/consts"; +import { userRelays } from "$lib/stores/relayStore"; +import { get } from "svelte/store"; +import { goto } from "$app/navigation"; +import type { NDKEvent } from "./nostrUtils"; +import { EVENT_KINDS, TIME_CONSTANTS, TIMEOUTS } from './search_constants'; + +export interface RootEventInfo { + rootId: string; + rootPubkey: string; + rootRelay: string; + rootKind: number; + rootAddress: string; + rootIValue: string; + rootIRelay: string; + isRootA: boolean; + isRootI: boolean; +} + +export interface ParentEventInfo { + parentId: string; + parentPubkey: string; + parentRelay: string; + parentKind: number; + parentAddress: string; +} + +export interface EventPublishResult { + success: boolean; + relay?: string; + eventId?: string; + error?: string; +} + +/** + * Helper function to find a tag by its first element + */ +function findTag(tags: string[][], tagName: string): string[] | undefined { + return tags?.find((t: string[]) => t[0] === tagName); +} + +/** + * Helper function to get tag value safely + */ +function getTagValue(tags: string[][], tagName: string, index: number = 1): string { + const tag = findTag(tags, tagName); + return tag?.[index] || ''; +} + +/** + * Helper function to create a tag array + */ +function createTag(name: string, ...values: (string | number)[]): string[] { + return [name, ...values.map(v => String(v))]; +} + +/** + * Helper function to add tags to an array + */ +function addTags(tags: string[][], ...newTags: string[][]): void { + tags.push(...newTags); +} + +/** + * Extract root event information from parent event tags + */ +export function extractRootEventInfo(parent: NDKEvent): RootEventInfo { + const rootInfo: RootEventInfo = { + rootId: parent.id, + rootPubkey: getPubkeyString(parent.pubkey), + rootRelay: getRelayString(parent.relay), + rootKind: parent.kind || 1, + rootAddress: '', + rootIValue: '', + rootIRelay: '', + isRootA: false, + isRootI: false, + }; + + if (!parent.tags) return rootInfo; + + const rootE = findTag(parent.tags, 'E'); + const rootA = findTag(parent.tags, 'A'); + const rootI = findTag(parent.tags, 'I'); + + rootInfo.isRootA = !!rootA; + rootInfo.isRootI = !!rootI; + + if (rootE) { + rootInfo.rootId = rootE[1]; + rootInfo.rootRelay = getRelayString(rootE[2]); + rootInfo.rootPubkey = getPubkeyString(rootE[3] || rootInfo.rootPubkey); + rootInfo.rootKind = Number(getTagValue(parent.tags, 'K')) || rootInfo.rootKind; + } else if (rootA) { + rootInfo.rootAddress = rootA[1]; + rootInfo.rootRelay = getRelayString(rootA[2]); + rootInfo.rootPubkey = getPubkeyString(getTagValue(parent.tags, 'P') || rootInfo.rootPubkey); + rootInfo.rootKind = Number(getTagValue(parent.tags, 'K')) || rootInfo.rootKind; + } else if (rootI) { + rootInfo.rootIValue = rootI[1]; + rootInfo.rootIRelay = getRelayString(rootI[2]); + rootInfo.rootKind = Number(getTagValue(parent.tags, 'K')) || rootInfo.rootKind; + } + + return rootInfo; +} + +/** + * Extract parent event information + */ +export function extractParentEventInfo(parent: NDKEvent): ParentEventInfo { + const dTag = getTagValue(parent.tags || [], 'd'); + const parentAddress = dTag ? `${parent.kind}:${getPubkeyString(parent.pubkey)}:${dTag}` : ''; + + return { + parentId: parent.id, + parentPubkey: getPubkeyString(parent.pubkey), + parentRelay: getRelayString(parent.relay), + parentKind: parent.kind || 1, + parentAddress, + }; +} + +/** + * Build root scope tags for NIP-22 threading + */ +function buildRootScopeTags(rootInfo: RootEventInfo, parentInfo: ParentEventInfo): string[][] { + const tags: string[][] = []; + + if (rootInfo.rootAddress) { + const tagType = rootInfo.isRootA ? 'A' : rootInfo.isRootI ? 'I' : 'E'; + addTags(tags, createTag(tagType, rootInfo.rootAddress || rootInfo.rootId, rootInfo.rootRelay)); + } else if (rootInfo.rootIValue) { + addTags(tags, createTag('I', rootInfo.rootIValue, rootInfo.rootIRelay)); + } else { + addTags(tags, createTag('E', rootInfo.rootId, rootInfo.rootRelay)); + } + + addTags(tags, createTag('K', rootInfo.rootKind)); + + if (rootInfo.rootPubkey && !rootInfo.rootIValue) { + addTags(tags, createTag('P', rootInfo.rootPubkey, rootInfo.rootRelay)); + } + + return tags; +} + +/** + * Build parent scope tags for NIP-22 threading + */ +function buildParentScopeTags(parent: NDKEvent, parentInfo: ParentEventInfo, rootInfo: RootEventInfo): string[][] { + const tags: string[][] = []; + + if (parentInfo.parentAddress) { + const tagType = rootInfo.isRootA ? 'a' : rootInfo.isRootI ? 'i' : 'e'; + addTags(tags, createTag(tagType, parentInfo.parentAddress, parentInfo.parentRelay)); + } + + addTags( + tags, + createTag('e', parent.id, parentInfo.parentRelay), + createTag('k', parentInfo.parentKind), + createTag('p', parentInfo.parentPubkey, parentInfo.parentRelay) + ); + + return tags; +} + +/** + * Build tags for a reply event based on parent and root information + */ +export function buildReplyTags( + parent: NDKEvent, + rootInfo: RootEventInfo, + parentInfo: ParentEventInfo, + kind: number +): string[][] { + const tags: string[][] = []; + + const isParentReplaceable = parentInfo.parentKind >= EVENT_KINDS.ADDRESSABLE.MIN && parentInfo.parentKind < EVENT_KINDS.ADDRESSABLE.MAX; + const isParentComment = parentInfo.parentKind === EVENT_KINDS.COMMENT; + const isReplyToComment = isParentComment && rootInfo.rootId !== parent.id; + + if (kind === 1) { + // Kind 1 replies use simple e/p tags + addTags( + tags, + createTag('e', parent.id, parentInfo.parentRelay, 'root'), + createTag('p', parentInfo.parentPubkey) + ); + + // Add address for replaceable events + if (isParentReplaceable) { + const dTag = getTagValue(parent.tags || [], 'd'); + if (dTag) { + const parentAddress = `${parentInfo.parentKind}:${parentInfo.parentPubkey}:${dTag}`; + addTags(tags, createTag('a', parentAddress, '', 'root')); + } + } + } else { + // Kind 1111 (comment) uses NIP-22 threading format + if (isParentReplaceable) { + const dTag = getTagValue(parent.tags || [], 'd'); + if (dTag) { + const parentAddress = `${parentInfo.parentKind}:${parentInfo.parentPubkey}:${dTag}`; + + if (isReplyToComment) { + // Root scope (uppercase) - use the original article + addTags( + tags, + createTag('A', parentAddress, parentInfo.parentRelay), + createTag('K', rootInfo.rootKind), + createTag('P', rootInfo.rootPubkey, rootInfo.rootRelay) + ); + // Parent scope (lowercase) - the comment we're replying to + addTags( + tags, + createTag('e', parent.id, parentInfo.parentRelay), + createTag('k', parentInfo.parentKind), + createTag('p', parentInfo.parentPubkey, parentInfo.parentRelay) + ); + } else { + // Top-level comment - root and parent are the same + addTags( + tags, + createTag('A', parentAddress, parentInfo.parentRelay), + createTag('K', rootInfo.rootKind), + createTag('P', rootInfo.rootPubkey, rootInfo.rootRelay), + createTag('a', parentAddress, parentInfo.parentRelay), + createTag('e', parent.id, parentInfo.parentRelay), + createTag('k', parentInfo.parentKind), + createTag('p', parentInfo.parentPubkey, parentInfo.parentRelay) + ); + } + } else { + // Fallback to E/e tags if no d-tag found + if (isReplyToComment) { + addTags( + tags, + createTag('E', rootInfo.rootId, rootInfo.rootRelay), + createTag('K', rootInfo.rootKind), + createTag('P', rootInfo.rootPubkey, rootInfo.rootRelay), + createTag('e', parent.id, parentInfo.parentRelay), + createTag('k', parentInfo.parentKind), + createTag('p', parentInfo.parentPubkey, parentInfo.parentRelay) + ); + } else { + addTags( + tags, + createTag('E', parent.id, rootInfo.rootRelay), + createTag('K', rootInfo.rootKind), + createTag('P', rootInfo.rootPubkey, rootInfo.rootRelay), + createTag('e', parent.id, parentInfo.parentRelay), + createTag('k', parentInfo.parentKind), + createTag('p', parentInfo.parentPubkey, parentInfo.parentRelay) + ); + } + } + } else { + // For regular events, use E/e tags + if (isReplyToComment) { + // Reply to a comment - distinguish root from parent + addTags(tags, ...buildRootScopeTags(rootInfo, parentInfo)); + addTags( + tags, + createTag('e', parent.id, parentInfo.parentRelay), + createTag('k', parentInfo.parentKind), + createTag('p', parentInfo.parentPubkey, parentInfo.parentRelay) + ); + } else { + // Top-level comment or regular event + addTags(tags, ...buildRootScopeTags(rootInfo, parentInfo)); + addTags(tags, ...buildParentScopeTags(parent, parentInfo, rootInfo)); + } + } + } + + return tags; +} + +/** + * Create and sign a Nostr event + */ +export async function createSignedEvent( + content: string, + pubkey: string, + kind: number, + tags: string[][] +): Promise<{ id: string; sig: string; event: any }> { + const prefixedContent = prefixNostrAddresses(content); + + const eventToSign = { + kind: Number(kind), + created_at: Number(Math.floor(Date.now() / TIME_CONSTANTS.UNIX_TIMESTAMP_FACTOR)), + tags: tags.map(tag => [String(tag[0]), String(tag[1]), String(tag[2] || ''), String(tag[3] || '')]), + content: String(prefixedContent), + pubkey: pubkey, + }; + + let sig, id; + if (typeof window !== 'undefined' && window.nostr && window.nostr.signEvent) { + const signed = await window.nostr.signEvent(eventToSign); + sig = signed.sig as string; + id = 'id' in signed ? signed.id as string : getEventHash(eventToSign); + } else { + id = getEventHash(eventToSign); + sig = await signEvent(eventToSign); + } + + return { + id, + sig, + event: { + ...eventToSign, + id, + sig, + } + }; +} + +/** + * Publish event to a single relay + */ +async function publishToRelay(relayUrl: string, signedEvent: any): Promise { + const ws = new WebSocket(relayUrl); + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + ws.close(); + reject(new Error("Timeout")); + }, TIMEOUTS.GENERAL); + + ws.onopen = () => { + ws.send(JSON.stringify(["EVENT", signedEvent])); + }; + + ws.onmessage = (e) => { + const [type, id, ok, message] = JSON.parse(e.data); + if (type === "OK" && id === signedEvent.id) { + clearTimeout(timeout); + if (ok) { + ws.close(); + resolve(); + } else { + ws.close(); + reject(new Error(message)); + } + } + }; + + ws.onerror = () => { + clearTimeout(timeout); + ws.close(); + reject(new Error("WebSocket error")); + }; + }); +} + +/** + * Publish event to relays + */ +export async function publishEvent( + signedEvent: any, + useOtherRelays = false, + useFallbackRelays = false, + userRelayPreference = false +): Promise { + // Determine which relays to use + let relays = userRelayPreference ? get(userRelays) : standardRelays; + if (useOtherRelays) { + relays = userRelayPreference ? standardRelays : get(userRelays); + } + if (useFallbackRelays) { + relays = fallbackRelays; + } + + // Try to publish to relays + for (const relayUrl of relays) { + try { + await publishToRelay(relayUrl, signedEvent); + return { + success: true, + relay: relayUrl, + eventId: signedEvent.id + }; + } catch (e) { + console.error(`Failed to publish to ${relayUrl}:`, e); + } + } + + return { + success: false, + error: "Failed to publish to any relays" + }; +} + +/** + * Navigate to the published event + */ +export function navigateToEvent(eventId: string): void { + try { + // Validate that eventId is a valid hex string + if (!/^[0-9a-fA-F]{64}$/.test(eventId)) { + console.warn('Invalid event ID format:', eventId); + return; + } + + const nevent = nip19.neventEncode({ id: eventId }); + goto(`/events?id=${nevent}`); + } catch (error) { + console.error('Failed to encode event ID for navigation:', eventId, error); + } +} + +// Helper functions to ensure relay and pubkey are always strings +function getRelayString(relay: any): string { + if (!relay) return ''; + if (typeof relay === 'string') return relay; + if (typeof relay.url === 'string') return relay.url; + return ''; +} + +function getPubkeyString(pubkey: any): string { + if (!pubkey) return ''; + if (typeof pubkey === 'string') return pubkey; + if (typeof pubkey.hex === 'function') return pubkey.hex(); + if (typeof pubkey.pubkey === 'string') return pubkey.pubkey; + return ''; +} \ No newline at end of file diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 9d80b1c..e670316 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -1,22 +1,28 @@ -import { get } from 'svelte/store'; -import { nip19 } from 'nostr-tools'; -import { ndkInstance } from '$lib/ndk'; -import { npubCache } from './npubCache'; +import { get } from "svelte/store"; +import { nip19 } from "nostr-tools"; +import { ndkInstance } from "$lib/ndk"; +import { npubCache } from "./npubCache"; import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type { NDKFilter, NDKKind } from "@nostr-dev-kit/ndk"; -import { standardRelays, fallbackRelays } from "$lib/consts"; -import { NDKRelaySet as NDKRelaySetFromNDK } from '@nostr-dev-kit/ndk'; -import { sha256 } from '@noble/hashes/sha256'; -import { schnorr } from '@noble/curves/secp256k1'; -import { bytesToHex } from '@noble/hashes/utils'; +import { standardRelays, fallbackRelays, anonymousRelays } from "$lib/consts"; +import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk"; +import { sha256 } from "@noble/hashes/sha256"; +import { schnorr } from "@noble/curves/secp256k1"; +import { bytesToHex } from "@noble/hashes/utils"; +import { wellKnownUrl } from "./search_utility"; +import { TIMEOUTS, VALIDATION } from './search_constants'; -const badgeCheckSvg = '' +const badgeCheckSvg = + ''; -const graduationCapSvg = ''; +const graduationCapSvg = + ''; // Regular expressions for Nostr identifiers - match the entire identifier including any prefix -export const NOSTR_PROFILE_REGEX = /(?': '>', - '"': '"', - "'": ''' + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", }; - return text.replace(/[&<>"']/g, char => htmlEscapes[char]); + return text.replace(/[&<>"']/g, (char) => htmlEscapes[char]); } /** * Get user metadata for a nostr identifier (npub or nprofile) */ -export async function getUserMetadata(identifier: string): Promise { +export async function getUserMetadata(identifier: string, force = false): Promise { // Remove nostr: prefix if present const cleanId = identifier.replace(/^nostr:/, ''); - if (npubCache.has(cleanId)) { + if (!force && npubCache.has(cleanId)) { return npubCache.get(cleanId)!; } @@ -71,29 +77,35 @@ export async function getUserMetadata(identifier: string): Promise // Handle different identifier types let pubkey: string; - if (decoded.type === 'npub') { + if (decoded.type === "npub") { pubkey = decoded.data; - } else if (decoded.type === 'nprofile') { + } else if (decoded.type === "nprofile") { pubkey = decoded.data.pubkey; } else { npubCache.set(cleanId, fallback); return fallback; } - const profileEvent = await fetchEventWithFallback(ndk, { kinds: [0], authors: [pubkey] }); - const profile = profileEvent && profileEvent.content ? JSON.parse(profileEvent.content) : null; + const profileEvent = await fetchEventWithFallback(ndk, { + kinds: [0], + authors: [pubkey], + }); + const profile = + profileEvent && profileEvent.content + ? JSON.parse(profileEvent.content) + : null; const metadata: NostrProfile = { name: profile?.name || fallback.name, - displayName: profile?.displayName, + displayName: profile?.displayName || profile?.display_name, nip05: profile?.nip05, - picture: profile?.image, + picture: profile?.picture || profile?.image, about: profile?.about, banner: profile?.banner, website: profile?.website, - lud16: profile?.lud16 + lud16: profile?.lud16, }; - + npubCache.set(cleanId, metadata); return metadata; } catch (e) { @@ -105,27 +117,34 @@ export async function getUserMetadata(identifier: string): Promise /** * Create a profile link element */ -export function createProfileLink(identifier: string, displayText: string | undefined): string { - const cleanId = identifier.replace(/^nostr:/, ''); +export function createProfileLink( + identifier: string, + displayText: string | undefined, +): string { + const cleanId = identifier.replace(/^nostr:/, ""); const escapedId = escapeHtml(cleanId); const defaultText = `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}`; const escapedText = escapeHtml(displayText || defaultText); - return `@${escapedText}`; + // Remove target="_blank" for internal navigation + return `@${escapedText}`; } /** * Create a profile link element with a NIP-05 verification indicator. */ -export async function createProfileLinkWithVerification(identifier: string, displayText: string | undefined): Promise { +export async function createProfileLinkWithVerification( + identifier: string, + displayText: string | undefined, +): Promise { const ndk = get(ndkInstance) as NDK; if (!ndk) { return createProfileLink(identifier, displayText); } - const cleanId = identifier.replace(/^nostr:/, ''); + const cleanId = identifier.replace(/^nostr:/, ""); const escapedId = escapeHtml(cleanId); - const isNpub = cleanId.startsWith('npub'); + const isNpub = cleanId.startsWith("npub"); let user: NDKUser; if (isNpub) { @@ -134,19 +153,37 @@ export async function createProfileLinkWithVerification(identifier: string, disp user = ndk.getUser({ pubkey: cleanId }); } - const userRelays = Array.from(ndk.pool?.relays.values() || []).map(r => r.url); + const userRelays = Array.from(ndk.pool?.relays.values() || []).map( + (r) => r.url, + ); + + // Filter out problematic relays + const filterProblematicRelays = (relays: string[]) => { + return relays.filter(relay => { + if (relay.includes('gitcitadel.nostr1.com')) { + console.info(`[nostrUtils.ts] Filtering out problematic relay: ${relay}`); + return false; + } + return true; + }); + }; + const allRelays = [ ...standardRelays, ...userRelays, - ...fallbackRelays + ...fallbackRelays, ].filter((url, idx, arr) => arr.indexOf(url) === idx); - const relaySet = NDKRelaySetFromNDK.fromRelayUrls(allRelays, ndk); + + const filteredRelays = filterProblematicRelays(allRelays); + const relaySet = NDKRelaySetFromNDK.fromRelayUrls(filteredRelays, ndk); const profileEvent = await ndk.fetchEvent( { kinds: [0], authors: [user.pubkey] }, undefined, - relaySet + relaySet, ); - const profile = profileEvent?.content ? JSON.parse(profileEvent.content) : null; + const profile = profileEvent?.content + ? JSON.parse(profileEvent.content) + : null; const nip05 = profile?.nip05; if (!nip05) { @@ -155,39 +192,45 @@ export async function createProfileLinkWithVerification(identifier: string, disp const defaultText = `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}`; const escapedText = escapeHtml(displayText || defaultText); - const displayIdentifier = profile?.displayName ?? profile?.name ?? escapedText; + const displayIdentifier = + profile?.displayName ?? + profile?.display_name ?? + profile?.name ?? + escapedText; const isVerified = await user.validateNip05(nip05); - + if (!isVerified) { return createProfileLink(identifier, displayText); } - + // TODO: Make this work with an enum in case we add more types. - const type = nip05.endsWith('edu') ? 'edu' : 'standard'; + const type = nip05.endsWith("edu") ? "edu" : "standard"; switch (type) { case 'edu': - return `@${displayIdentifier}${graduationCapSvg}`; + return `@${displayIdentifier}${graduationCapSvg}`; case 'standard': - return `@${displayIdentifier}${badgeCheckSvg}`; + return `@${displayIdentifier}${badgeCheckSvg}`; } } /** * Create a note link element */ function createNoteLink(identifier: string): string { - const cleanId = identifier.replace(/^nostr:/, ''); + const cleanId = identifier.replace(/^nostr:/, ""); const shortId = `${cleanId.slice(0, 12)}...${cleanId.slice(-8)}`; const escapedId = escapeHtml(cleanId); const escapedText = escapeHtml(shortId); - - return `${escapedText}`; + + return `${escapedText}`; } /** * Process Nostr identifiers in text */ -export async function processNostrIdentifiers(content: string): Promise { +export async function processNostrIdentifiers( + content: string, +): Promise { let processedContent = content; // Helper to check if a match is part of a URL @@ -206,8 +249,8 @@ export async function processNostrIdentifiers(content: string): Promise continue; // skip if part of a URL } let identifier = fullMatch; - if (!identifier.startsWith('nostr:')) { - identifier = 'nostr:' + identifier; + if (!identifier.startsWith("nostr:")) { + identifier = "nostr:" + identifier; } const metadata = await getUserMetadata(identifier); const displayText = metadata.displayName || metadata.name; @@ -224,8 +267,8 @@ export async function processNostrIdentifiers(content: string): Promise continue; // skip if part of a URL } let identifier = fullMatch; - if (!identifier.startsWith('nostr:')) { - identifier = 'nostr:' + identifier; + if (!identifier.startsWith("nostr:")) { + identifier = "nostr:" + identifier; } const link = createNoteLink(identifier); processedContent = processedContent.replace(fullMatch, link); @@ -236,19 +279,69 @@ export async function processNostrIdentifiers(content: string): Promise export async function getNpubFromNip05(nip05: string): Promise { try { - const ndk = get(ndkInstance); - if (!ndk) { - console.error('NDK not initialized'); + // Parse the NIP-05 address + const [name, domain] = nip05.split('@'); + if (!name || !domain) { + console.error('[getNpubFromNip05] Invalid NIP-05 format:', nip05); return null; } + + // Fetch the well-known.json file with timeout and CORS handling + const url = wellKnownUrl(domain, name); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout - const user = await ndk.getUser({ nip05 }); - if (!user || !user.npub) { + try { + const response = await fetch(url, { + signal: controller.signal, + mode: 'cors', + headers: { + 'Accept': 'application/json' + } + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + console.error('[getNpubFromNip05] HTTP error:', response.status, response.statusText); + return null; + } + + const data = await response.json(); + + // Try exact match first + let pubkey = data.names?.[name]; + + // If not found, try case-insensitive search + if (!pubkey && data.names) { + const names = Object.keys(data.names); + const matchingName = names.find(n => n.toLowerCase() === name.toLowerCase()); + if (matchingName) { + pubkey = data.names[matchingName]; + console.log(`[getNpubFromNip05] Found case-insensitive match: ${name} -> ${matchingName}`); + } + } + + if (!pubkey) { + console.error('[getNpubFromNip05] No pubkey found for name:', name); + return null; + } + + // Convert pubkey to npub + const npub = nip19.npubEncode(pubkey); + return npub; + } catch (fetchError: unknown) { + clearTimeout(timeoutId); + if (fetchError instanceof Error && fetchError.name === 'AbortError') { + console.warn('[getNpubFromNip05] Request timeout for:', url); + } else { + console.warn('[getNpubFromNip05] CORS or network error for:', url); + } return null; } - return user.npub; } catch (error) { - console.error('Error getting npub from nip05:', error); + console.error("[getNpubFromNip05] Error getting npub from nip05:", error); return null; } } @@ -256,9 +349,9 @@ export async function getNpubFromNip05(nip05: string): Promise { /** * Generic utility function to add a timeout to any promise * Can be used in two ways: - * 1. Method style: promise.withTimeout(5000) - * 2. Function style: withTimeout(promise, 5000) - * + * 1. Method style: promise.withTimeout(TIMEOUTS.GENERAL) + * 2. Function style: withTimeout(promise, TIMEOUTS.GENERAL) + * * @param thisOrPromise Either the promise to timeout (function style) or the 'this' context (method style) * @param timeoutMsOrPromise Timeout duration in milliseconds (function style) or the promise (method style) * @returns The promise result if completed before timeout, otherwise throws an error @@ -266,28 +359,28 @@ export async function getNpubFromNip05(nip05: string): Promise { */ export function withTimeout( thisOrPromise: Promise | number, - timeoutMsOrPromise?: number | Promise + timeoutMsOrPromise?: number | Promise, ): Promise { // Handle method-style call (promise.withTimeout(5000)) - if (typeof thisOrPromise === 'number') { + if (typeof thisOrPromise === "number") { const timeoutMs = thisOrPromise; const promise = timeoutMsOrPromise as Promise; return Promise.race([ promise, - new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), timeoutMs) - ) + new Promise((_, reject) => + setTimeout(() => reject(new Error("Timeout")), timeoutMs), + ), ]); } - + // Handle function-style call (withTimeout(promise, 5000)) const promise = thisOrPromise; const timeoutMs = timeoutMsOrPromise as number; return Promise.race([ promise, - new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), timeoutMs) - ) + new Promise((_, reject) => + setTimeout(() => reject(new Error("Timeout")), timeoutMs), + ), ]); } @@ -298,7 +391,10 @@ declare global { } } -Promise.prototype.withTimeout = function(this: Promise, timeoutMs: number): Promise { +Promise.prototype.withTimeout = function ( + this: Promise, + timeoutMs: number, +): Promise { return withTimeout(timeoutMs, this); }; @@ -311,20 +407,25 @@ Promise.prototype.withTimeout = function(this: Promise, timeoutMs: number) export async function fetchEventWithFallback( ndk: NDK, filterOrId: string | NDKFilter, - timeoutMs: number = 3000 + timeoutMs: number = 3000, ): Promise { // Get user relays if logged in - const userRelays = ndk.activeUser ? - Array.from(ndk.pool?.relays.values() || []) - .filter(r => r.status === 1) // Only use connected relays - .map(r => r.url) : - []; - + const userRelays = ndk.activeUser + ? Array.from(ndk.pool?.relays.values() || []) + .filter((r) => r.status === 1) // Only use connected relays + .map((r) => r.url) + .filter(url => !url.includes('gitcitadel.nostr1.com')) // Filter out problematic relay + : []; + + // Determine which relays to use based on user authentication status + const isSignedIn = ndk.signer && ndk.activeUser; + const primaryRelays = isSignedIn ? standardRelays : anonymousRelays; + // Create three relay sets in priority order const relaySets = [ - NDKRelaySetFromNDK.fromRelayUrls(standardRelays, ndk), // 1. Standard relays - NDKRelaySetFromNDK.fromRelayUrls(userRelays, ndk), // 2. User relays (if logged in) - NDKRelaySetFromNDK.fromRelayUrls(fallbackRelays, ndk) // 3. fallback relays (last resort) + NDKRelaySetFromNDK.fromRelayUrls(primaryRelays, ndk), // 1. Primary relays (auth or anonymous) + NDKRelaySetFromNDK.fromRelayUrls(userRelays, ndk), // 2. User relays (if logged in) + NDKRelaySetFromNDK.fromRelayUrls(fallbackRelays, ndk), // 3. fallback relays (last resort) ]; try { @@ -332,47 +433,75 @@ export async function fetchEventWithFallback( const triedRelaySets: string[] = []; // Helper function to try fetching from a relay set - async function tryFetchFromRelaySet(relaySet: NDKRelaySetFromNDK, setName: string): Promise { + async function tryFetchFromRelaySet( + relaySet: NDKRelaySetFromNDK, + setName: string, + ): Promise { if (relaySet.relays.size === 0) return null; triedRelaySets.push(setName); - - if (typeof filterOrId === 'string' && /^[0-9a-f]{64}$/i.test(filterOrId)) { - return await ndk.fetchEvent({ ids: [filterOrId] }, undefined, relaySet).withTimeout(timeoutMs); + + if ( + typeof filterOrId === "string" && + new RegExp(`^[0-9a-f]{${VALIDATION.HEX_LENGTH}}$`, 'i').test(filterOrId) + ) { + return await ndk + .fetchEvent({ ids: [filterOrId] }, undefined, relaySet) + .withTimeout(timeoutMs); } else { - const filter = typeof filterOrId === 'string' ? { ids: [filterOrId] } : filterOrId; - const results = await ndk.fetchEvents(filter, undefined, relaySet).withTimeout(timeoutMs); - return results instanceof Set ? Array.from(results)[0] as NDKEvent : null; + const filter = + typeof filterOrId === "string" ? { ids: [filterOrId] } : filterOrId; + const results = await ndk + .fetchEvents(filter, undefined, relaySet) + .withTimeout(timeoutMs); + return results instanceof Set + ? (Array.from(results)[0] as NDKEvent) + : null; } } // Try each relay set in order for (const [index, relaySet] of relaySets.entries()) { - const setName = index === 0 ? 'standard relays' : - index === 1 ? 'user relays' : - 'fallback relays'; - + const setName = + index === 0 + ? isSignedIn + ? "standard relays" + : "anonymous relays" + : index === 1 + ? "user relays" + : "fallback relays"; + found = await tryFetchFromRelaySet(relaySet, setName); if (found) break; } if (!found) { const timeoutSeconds = timeoutMs / 1000; - const relayUrls = relaySets.map((set, i) => { - const setName = i === 0 ? 'standard relays' : - i === 1 ? 'user relays' : - 'fallback relays'; - const urls = Array.from(set.relays).map(r => r.url); - return urls.length > 0 ? `${setName} (${urls.join(', ')})` : null; - }).filter(Boolean).join(', then '); - - console.warn(`Event not found after ${timeoutSeconds}s timeout. Tried ${relayUrls}. Some relays may be offline or slow.`); + const relayUrls = relaySets + .map((set, i) => { + const setName = + i === 0 + ? isSignedIn + ? "standard relays" + : "anonymous relays" + : i === 1 + ? "user relays" + : "fallback relays"; + const urls = Array.from(set.relays).map((r) => r.url); + return urls.length > 0 ? `${setName} (${urls.join(", ")})` : null; + }) + .filter(Boolean) + .join(", then "); + + console.warn( + `Event not found after ${timeoutSeconds}s timeout. Tried ${relayUrls}. Some relays may be offline or slow.`, + ); return null; } // Always wrap as NDKEvent return found instanceof NDKEvent ? found : new NDKEvent(ndk, found); } catch (err) { - console.error('Error in fetchEventWithFallback:', err); + console.error("Error in fetchEventWithFallback:", err); return null; } } @@ -383,10 +512,10 @@ export async function fetchEventWithFallback( export function toNpub(pubkey: string | undefined): string | null { if (!pubkey) return null; try { - if (/^[a-f0-9]{64}$/i.test(pubkey)) { + if (new RegExp(`^[a-f0-9]{${VALIDATION.HEX_LENGTH}}$`, 'i').test(pubkey)) { return nip19.npubEncode(pubkey); } - if (pubkey.startsWith('npub1')) return pubkey; + if (pubkey.startsWith("npub1")) return pubkey; return null; } catch { return null; @@ -428,7 +557,7 @@ export function getEventHash(event: { event.created_at, event.kind, event.tags, - event.content + event.content, ]); return bytesToHex(sha256(serialized)); } @@ -443,4 +572,80 @@ export async function signEvent(event: { const id = getEventHash(event); const sig = await schnorr.sign(id, event.pubkey); return bytesToHex(sig); -} \ No newline at end of file +} + +/** + * Prefixes Nostr addresses (npub, nprofile, nevent, naddr, note, etc.) with "nostr:" + * if they are not already prefixed and are not part of a hyperlink + */ +export function prefixNostrAddresses(content: string): string { + // Regex to match Nostr addresses that are not already prefixed with "nostr:" + // and are not part of a markdown link or HTML link + // Must be followed by at least 20 alphanumeric characters to be considered an address + const nostrAddressPattern = /\b(npub|nprofile|nevent|naddr|note)[a-zA-Z0-9]{20,}\b/g; + + return content.replace(nostrAddressPattern, (match, offset) => { + // Check if this match is part of a markdown link [text](url) + const beforeMatch = content.substring(0, offset); + const afterMatch = content.substring(offset + match.length); + + // Check if it's part of a markdown link + const beforeBrackets = beforeMatch.lastIndexOf('['); + const afterParens = afterMatch.indexOf(')'); + + if (beforeBrackets !== -1 && afterParens !== -1) { + const textBeforeBrackets = beforeMatch.substring(0, beforeBrackets); + const lastOpenBracket = textBeforeBrackets.lastIndexOf('['); + const lastCloseBracket = textBeforeBrackets.lastIndexOf(']'); + + // If we have [text] before this, it might be a markdown link + if (lastOpenBracket !== -1 && lastCloseBracket > lastOpenBracket) { + return match; // Don't prefix if it's part of a markdown link + } + } + + // Check if it's part of an HTML link + const beforeHref = beforeMatch.lastIndexOf('href='); + if (beforeHref !== -1) { + const afterHref = afterMatch.indexOf('"'); + if (afterHref !== -1) { + return match; // Don't prefix if it's part of an HTML link + } + } + + // Check if it's already prefixed with "nostr:" + const beforeNostr = beforeMatch.lastIndexOf('nostr:'); + if (beforeNostr !== -1) { + const textAfterNostr = beforeMatch.substring(beforeNostr + 6); + if (!textAfterNostr.includes(' ')) { + return match; // Already prefixed + } + } + + // Additional check: ensure it's actually a valid Nostr address format + // The part after the prefix should be a valid bech32 string + const addressPart = match.substring(4); // Remove npub, nprofile, etc. + if (addressPart.length < 20) { + return match; // Too short to be a valid address + } + + // Check if it looks like a valid bech32 string (alphanumeric, no special chars) + if (!/^[a-zA-Z0-9]+$/.test(addressPart)) { + return match; // Not a valid bech32 format + } + + // Additional check: ensure the word before is not a common word that would indicate + // this is just a general reference, not an actual address + const wordBefore = beforeMatch.match(/\b(\w+)\s*$/); + if (wordBefore) { + const beforeWord = wordBefore[1].toLowerCase(); + const commonWords = ['the', 'a', 'an', 'this', 'that', 'my', 'your', 'his', 'her', 'their', 'our']; + if (commonWords.includes(beforeWord)) { + return match; // Likely just a general reference, not an actual address + } + } + + // Prefix with "nostr:" + return `nostr:${match}`; + }); +} diff --git a/src/lib/utils/npubCache.ts b/src/lib/utils/npubCache.ts index c99f879..4fc4405 100644 --- a/src/lib/utils/npubCache.ts +++ b/src/lib/utils/npubCache.ts @@ -1,4 +1,4 @@ -import type { NostrProfile } from './nostrUtils'; +import type { NostrProfile } from "./nostrUtils"; export type NpubMetadata = NostrProfile; @@ -48,4 +48,4 @@ class NpubCache { } } -export const npubCache = new NpubCache(); \ No newline at end of file +export const npubCache = new NpubCache(); diff --git a/src/lib/utils/profile_search.ts b/src/lib/utils/profile_search.ts new file mode 100644 index 0000000..37b9e42 --- /dev/null +++ b/src/lib/utils/profile_search.ts @@ -0,0 +1,328 @@ +import { ndkInstance } from '$lib/ndk'; +import { getUserMetadata, getNpubFromNip05 } from '$lib/utils/nostrUtils'; +import { NDKRelaySet, NDKEvent } from '@nostr-dev-kit/ndk'; +import { searchCache } from '$lib/utils/searchCache'; +import { standardRelays, fallbackRelays } from '$lib/consts'; +import { get } from 'svelte/store'; +import type { NostrProfile, ProfileSearchResult } from './search_types'; +import { fieldMatches, nip05Matches, normalizeSearchTerm, COMMON_DOMAINS, createProfileFromEvent } from './search_utils'; +import { checkCommunityStatus } from './community_checker'; +import { TIMEOUTS } from './search_constants'; + +/** + * Search for profiles by various criteria (display name, name, NIP-05, npub) + */ +export async function searchProfiles(searchTerm: string): Promise { + const normalizedSearchTerm = normalizeSearchTerm(searchTerm); + + console.log('searchProfiles called with:', searchTerm, 'normalized:', normalizedSearchTerm); + + // Check cache first + const cachedResult = searchCache.get('profile', normalizedSearchTerm); + if (cachedResult) { + console.log('Found cached result for:', normalizedSearchTerm); + const profiles = cachedResult.events.map(event => { + try { + const profileData = JSON.parse(event.content); + return createProfileFromEvent(event, profileData); + } catch { + return null; + } + }).filter(Boolean) as NostrProfile[]; + + console.log('Cached profiles found:', profiles.length); + return { profiles, Status: {} }; + } + + const ndk = get(ndkInstance); + if (!ndk) { + console.error('NDK not initialized'); + throw new Error('NDK not initialized'); + } + + console.log('NDK initialized, starting search logic'); + + let foundProfiles: NostrProfile[] = []; + + try { + // Check if it's a valid npub/nprofile first + if (normalizedSearchTerm.startsWith('npub') || normalizedSearchTerm.startsWith('nprofile')) { + try { + const metadata = await getUserMetadata(normalizedSearchTerm); + if (metadata) { + foundProfiles = [metadata]; + } + } catch (error) { + console.error('Error fetching metadata for npub:', error); + } + } else if (normalizedSearchTerm.includes('@')) { + // Check if it's a NIP-05 address - normalize it properly + const normalizedNip05 = normalizedSearchTerm.toLowerCase(); + try { + const npub = await getNpubFromNip05(normalizedNip05); + if (npub) { + const metadata = await getUserMetadata(npub); + const profile: NostrProfile = { + ...metadata, + pubkey: npub + }; + foundProfiles = [profile]; + } + } catch (e) { + console.error('[Search] NIP-05 lookup failed:', e); + } + } else { + // Try NIP-05 search first (faster than relay search) + console.log('Starting NIP-05 search for:', normalizedSearchTerm); + foundProfiles = await searchNip05Domains(normalizedSearchTerm, ndk); + console.log('NIP-05 search completed, found:', foundProfiles.length, 'profiles'); + + // If no NIP-05 results, try quick relay search + if (foundProfiles.length === 0) { + console.log('No NIP-05 results, trying quick relay search'); + foundProfiles = await quickRelaySearch(normalizedSearchTerm, ndk); + console.log('Quick relay search completed, found:', foundProfiles.length, 'profiles'); + } + } + + // Cache the results + if (foundProfiles.length > 0) { + const events = foundProfiles.map(profile => { + const event = new NDKEvent(ndk); + event.content = JSON.stringify(profile); + event.pubkey = profile.pubkey || ''; + return event; + }); + + const result = { + events, + secondOrder: [], + tTagEvents: [], + eventIds: new Set(), + addresses: new Set(), + searchType: 'profile', + searchTerm: normalizedSearchTerm + }; + searchCache.set('profile', normalizedSearchTerm, result); + } + + console.log('Search completed, found profiles:', foundProfiles.length); + return { profiles: foundProfiles, Status: {} }; + + } catch (error) { + console.error('Error searching profiles:', error); + return { profiles: [], Status: {} }; + } +} + +/** + * Search for NIP-05 addresses across common domains + */ +async function searchNip05Domains(searchTerm: string, ndk: any): Promise { + const foundProfiles: NostrProfile[] = []; + + // Enhanced list of common domains for NIP-05 lookups + // Prioritize gitcitadel.com since we know it has profiles + const commonDomains = [ + 'gitcitadel.com', // Prioritize this domain + 'theforest.nostr1.com', + 'nostr1.com', + 'nostr.land', + 'sovbit.host', + 'damus.io', + 'snort.social', + 'iris.to', + 'coracle.social', + 'nostr.band', + 'nostr.wine', + 'purplepag.es', + 'relay.noswhere.com', + 'aggr.nostr.land', + 'nostr.sovbit.host', + 'freelay.sovbit.host', + 'nostr21.com', + 'greensoul.space', + 'relay.damus.io', + 'relay.nostr.band' + ]; + + // Normalize the search term for NIP-05 lookup + const normalizedSearchTerm = searchTerm.toLowerCase().trim(); + console.log('NIP-05 search: normalized search term:', normalizedSearchTerm); + + // Try gitcitadel.com first with extra debugging + const gitcitadelAddress = `${normalizedSearchTerm}@gitcitadel.com`; + console.log('NIP-05 search: trying gitcitadel.com first:', gitcitadelAddress); + try { + const npub = await getNpubFromNip05(gitcitadelAddress); + if (npub) { + console.log('NIP-05 search: SUCCESS! found npub for gitcitadel.com:', npub); + const metadata = await getUserMetadata(npub); + const profile: NostrProfile = { + ...metadata, + pubkey: npub + }; + console.log('NIP-05 search: created profile for gitcitadel.com:', profile); + foundProfiles.push(profile); + return foundProfiles; // Return immediately if we found it on gitcitadel.com + } else { + console.log('NIP-05 search: no npub found for gitcitadel.com'); + } + } catch (e) { + console.log('NIP-05 search: error for gitcitadel.com:', e); + } + + // If gitcitadel.com didn't work, try other domains + console.log('NIP-05 search: gitcitadel.com failed, trying other domains...'); + const otherDomains = commonDomains.filter(domain => domain !== 'gitcitadel.com'); + + // Search all other domains in parallel with timeout + const searchPromises = otherDomains.map(async (domain) => { + const nip05Address = `${normalizedSearchTerm}@${domain}`; + console.log('NIP-05 search: trying address:', nip05Address); + try { + const npub = await getNpubFromNip05(nip05Address); + if (npub) { + console.log('NIP-05 search: found npub for', nip05Address, ':', npub); + const metadata = await getUserMetadata(npub); + const profile: NostrProfile = { + ...metadata, + pubkey: npub + }; + console.log('NIP-05 search: created profile for', nip05Address, ':', profile); + return profile; + } else { + console.log('NIP-05 search: no npub found for', nip05Address); + } + } catch (e) { + console.log('NIP-05 search: error for', nip05Address, ':', e); + // Continue to next domain + } + return null; + }); + + // Wait for all searches with timeout + const results = await Promise.allSettled(searchPromises); + + for (const result of results) { + if (result.status === 'fulfilled' && result.value) { + foundProfiles.push(result.value); + } + } + + console.log('NIP-05 search: total profiles found:', foundProfiles.length); + return foundProfiles; +} + +/** + * Quick relay search with short timeout + */ +async function quickRelaySearch(searchTerm: string, ndk: any): Promise { + console.log('quickRelaySearch called with:', searchTerm); + const foundProfiles: NostrProfile[] = []; + + // Normalize the search term for relay search + const normalizedSearchTerm = normalizeSearchTerm(searchTerm); + console.log('Normalized search term for relay search:', normalizedSearchTerm); + + // Use all profile relays for better coverage + const quickRelayUrls = [...standardRelays, ...fallbackRelays]; // Use all available relays + console.log('Using all relays for search:', quickRelayUrls); + + // Create relay sets for parallel search + const relaySets = quickRelayUrls.map(url => { + try { + return NDKRelaySet.fromRelayUrls([url], ndk); + } catch (e) { + console.warn(`Failed to create relay set for ${url}:`, e); + return null; + } + }).filter(Boolean); + + // Search all relays in parallel with short timeout + const searchPromises = relaySets.map(async (relaySet, index) => { + if (!relaySet) return []; + + return new Promise((resolve) => { + const foundInRelay: NostrProfile[] = []; + let eventCount = 0; + + console.log(`Starting search on relay ${index + 1}: ${quickRelayUrls[index]}`); + + const sub = ndk.subscribe( + { kinds: [0] }, + { closeOnEose: true, relaySet } + ); + + sub.on('event', (event: NDKEvent) => { + eventCount++; + try { + if (!event.content) return; + const profileData = JSON.parse(event.content); + const displayName = profileData.displayName || profileData.display_name || ''; + const display_name = profileData.display_name || ''; + const name = profileData.name || ''; + const nip05 = profileData.nip05 || ''; + const about = profileData.about || ''; + + // Check if any field matches the search term using normalized comparison + const matchesDisplayName = fieldMatches(displayName, normalizedSearchTerm); + const matchesDisplay_name = fieldMatches(display_name, normalizedSearchTerm); + const matchesName = fieldMatches(name, normalizedSearchTerm); + const matchesNip05 = nip05Matches(nip05, normalizedSearchTerm); + const matchesAbout = fieldMatches(about, normalizedSearchTerm); + + if (matchesDisplayName || matchesDisplay_name || matchesName || matchesNip05 || matchesAbout) { + console.log(`Found matching profile on relay ${index + 1}:`, { + name: profileData.name, + display_name: profileData.display_name, + nip05: profileData.nip05, + pubkey: event.pubkey, + searchTerm: normalizedSearchTerm + }); + const profile = createProfileFromEvent(event, profileData); + + // Check if we already have this profile in this relay + const existingIndex = foundInRelay.findIndex(p => p.pubkey === event.pubkey); + if (existingIndex === -1) { + foundInRelay.push(profile); + } + } + } catch (e) { + // Invalid JSON or other error, skip + } + }); + + sub.on('eose', () => { + console.log(`Relay ${index + 1} (${quickRelayUrls[index]}) search completed, processed ${eventCount} events, found ${foundInRelay.length} matches`); + resolve(foundInRelay); + }); + + // Short timeout for quick search + setTimeout(() => { + console.log(`Relay ${index + 1} (${quickRelayUrls[index]}) search timed out after 1.5s, processed ${eventCount} events, found ${foundInRelay.length} matches`); + sub.stop(); + resolve(foundInRelay); + }, 1500); // 1.5 second timeout per relay + }); + }); + + // Wait for all searches to complete + const results = await Promise.allSettled(searchPromises); + + // Combine and deduplicate results + const allProfiles: Record = {}; + + for (const result of results) { + if (result.status === 'fulfilled') { + for (const profile of result.value) { + if (profile.pubkey) { + allProfiles[profile.pubkey] = profile; + } + } + } + } + + console.log(`Total unique profiles found: ${Object.keys(allProfiles).length}`); + return Object.values(allProfiles); +} \ No newline at end of file diff --git a/src/lib/utils/relayDiagnostics.ts b/src/lib/utils/relayDiagnostics.ts new file mode 100644 index 0000000..49a2874 --- /dev/null +++ b/src/lib/utils/relayDiagnostics.ts @@ -0,0 +1,141 @@ +import { standardRelays, anonymousRelays, fallbackRelays } from '$lib/consts'; +import NDK from '@nostr-dev-kit/ndk'; +import { TIMEOUTS } from './search_constants'; + +export interface RelayDiagnostic { + url: string; + connected: boolean; + requiresAuth: boolean; + error?: string; + responseTime?: number; +} + +/** + * Tests connection to a single relay + */ +export async function testRelay(url: string): Promise { + const startTime = Date.now(); + + return new Promise((resolve) => { + const ws = new WebSocket(url); + let resolved = false; + + const timeout = setTimeout(() => { + if (!resolved) { + resolved = true; + ws.close(); + resolve({ + url, + connected: false, + requiresAuth: false, + error: 'Connection timeout', + responseTime: Date.now() - startTime, + }); + } + }, TIMEOUTS.RELAY_DIAGNOSTICS); + + ws.onopen = () => { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + ws.close(); + resolve({ + url, + connected: true, + requiresAuth: false, + responseTime: Date.now() - startTime, + }); + } + }; + + ws.onerror = () => { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + resolve({ + url, + connected: false, + requiresAuth: false, + error: 'WebSocket error', + responseTime: Date.now() - startTime, + }); + } + }; + + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data[0] === 'NOTICE' && data[1]?.includes('auth-required')) { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + ws.close(); + resolve({ + url, + connected: true, + requiresAuth: true, + responseTime: Date.now() - startTime, + }); + } + } + }; + }); +} + +/** + * Tests all relays and returns diagnostic information + */ +export async function testAllRelays(): Promise { + const allRelays = [...new Set([...standardRelays, ...anonymousRelays, ...fallbackRelays])]; + + console.log('[RelayDiagnostics] Testing', allRelays.length, 'relays...'); + + const results = await Promise.allSettled( + allRelays.map(url => testRelay(url)) + ); + + return results.map((result, index) => { + if (result.status === 'fulfilled') { + return result.value; + } else { + return { + url: allRelays[index], + connected: false, + requiresAuth: false, + error: 'Test failed', + }; + } + }); +} + +/** + * Gets working relays from diagnostic results + */ +export function getWorkingRelays(diagnostics: RelayDiagnostic[]): string[] { + return diagnostics + .filter(d => d.connected) + .map(d => d.url); +} + +/** + * Logs relay diagnostic results to console + */ +export function logRelayDiagnostics(diagnostics: RelayDiagnostic[]): void { + console.group('[RelayDiagnostics] Results'); + + const working = diagnostics.filter(d => d.connected); + const failed = diagnostics.filter(d => !d.connected); + + console.log(`✅ Working relays (${working.length}):`); + working.forEach(d => { + console.log(` - ${d.url}${d.requiresAuth ? ' (requires auth)' : ''}${d.responseTime ? ` (${d.responseTime}ms)` : ''}`); + }); + + if (failed.length > 0) { + console.log(`❌ Failed relays (${failed.length}):`); + failed.forEach(d => { + console.log(` - ${d.url}: ${d.error || 'Unknown error'}`); + }); + } + + console.groupEnd(); +} \ No newline at end of file diff --git a/src/lib/utils/searchCache.ts b/src/lib/utils/searchCache.ts new file mode 100644 index 0000000..0fabd5b --- /dev/null +++ b/src/lib/utils/searchCache.ts @@ -0,0 +1,105 @@ +import type { NDKEvent } from "./nostrUtils"; +import { CACHE_DURATIONS, TIMEOUTS } from './search_constants'; + +export interface SearchResult { + events: NDKEvent[]; + secondOrder: NDKEvent[]; + tTagEvents: NDKEvent[]; + eventIds: Set; + addresses: Set; + searchType: string; + searchTerm: string; + timestamp: number; +} + +class SearchCache { + private cache: Map = new Map(); + private readonly CACHE_DURATION = CACHE_DURATIONS.SEARCH_CACHE; + + /** + * Generate a cache key for a search + */ + private generateKey(searchType: string, searchTerm: string): string { + if (!searchTerm) { + return `${searchType}:`; + } + return `${searchType}:${searchTerm.toLowerCase().trim()}`; + } + + /** + * Check if a cached result is still valid + */ + private isExpired(result: SearchResult): boolean { + return Date.now() - result.timestamp > this.CACHE_DURATION; + } + + /** + * Get cached search results + */ + get(searchType: string, searchTerm: string): SearchResult | null { + const key = this.generateKey(searchType, searchTerm); + const result = this.cache.get(key); + + if (!result || this.isExpired(result)) { + if (result) { + this.cache.delete(key); + } + return null; + } + + return result; + } + + /** + * Store search results in cache + */ + set(searchType: string, searchTerm: string, result: Omit): void { + const key = this.generateKey(searchType, searchTerm); + this.cache.set(key, { + ...result, + timestamp: Date.now() + }); + } + + /** + * Check if a search result is cached and valid + */ + has(searchType: string, searchTerm: string): boolean { + const key = this.generateKey(searchType, searchTerm); + const result = this.cache.get(key); + return result !== undefined && !this.isExpired(result); + } + + /** + * Clear expired entries from cache + */ + cleanup(): void { + const now = Date.now(); + for (const [key, result] of this.cache.entries()) { + if (this.isExpired(result)) { + this.cache.delete(key); + } + } + } + + /** + * Clear all cache entries + */ + clear(): void { + this.cache.clear(); + } + + /** + * Get cache size + */ + size(): number { + return this.cache.size; + } +} + +export const searchCache = new SearchCache(); + +// Clean up expired entries periodically +setInterval(() => { + searchCache.cleanup(); +}, TIMEOUTS.CACHE_CLEANUP); // Check every minute \ No newline at end of file diff --git a/src/lib/utils/search_constants.ts b/src/lib/utils/search_constants.ts new file mode 100644 index 0000000..577c53d --- /dev/null +++ b/src/lib/utils/search_constants.ts @@ -0,0 +1,124 @@ +/** + * Search and Event Utility Constants + * + * This file centralizes all magic numbers used throughout the search and event utilities + * to improve maintainability and reduce code duplication. + */ + +// Timeout constants (in milliseconds) +export const TIMEOUTS = { + /** Default timeout for event fetching operations */ + EVENT_FETCH: 10000, + + /** Timeout for profile search operations */ + PROFILE_SEARCH: 15000, + + /** Timeout for subscription search operations */ + SUBSCRIPTION_SEARCH: 10000, + + /** Timeout for second-order search operations */ + SECOND_ORDER_SEARCH: 5000, + + /** Timeout for relay diagnostics */ + RELAY_DIAGNOSTICS: 5000, + + /** Timeout for general operations */ + GENERAL: 5000, + + /** Cache cleanup interval */ + CACHE_CLEANUP: 60000, +} as const; + +// Cache duration constants (in milliseconds) +export const CACHE_DURATIONS = { + /** Default cache duration for search results */ + SEARCH_CACHE: 5 * 60 * 1000, // 5 minutes + + /** Cache duration for index events */ + INDEX_EVENT_CACHE: 10 * 60 * 1000, // 10 minutes +} as const; + +// Search limits +export const SEARCH_LIMITS = { + /** Limit for specific profile searches (npub, NIP-05) */ + SPECIFIC_PROFILE: 10, + + /** Limit for general profile searches */ + GENERAL_PROFILE: 500, + + /** Limit for community relay checks */ + COMMUNITY_CHECK: 1, + + /** Limit for second-order search results */ + SECOND_ORDER_RESULTS: 100, +} as const; + +// Nostr event kind ranges +export const EVENT_KINDS = { + /** Replaceable event kinds (0, 3, 10000-19999) */ + REPLACEABLE: { + MIN: 0, + MAX: 19999, + SPECIFIC: [0, 3], + }, + + /** Parameterized replaceable event kinds (20000-29999) */ + PARAMETERIZED_REPLACEABLE: { + MIN: 20000, + MAX: 29999, + }, + + /** Addressable event kinds (30000-39999) */ + ADDRESSABLE: { + MIN: 30000, + MAX: 39999, + }, + + /** Comment event kind */ + COMMENT: 1111, + + /** Text note event kind */ + TEXT_NOTE: 1, + + /** Profile metadata event kind */ + PROFILE_METADATA: 0, +} as const; + +// Relay-specific constants +export const RELAY_CONSTANTS = { + /** Request ID for community relay checks */ + COMMUNITY_REQUEST_ID: 'alexandria-forest', + + /** Default relay request kinds for community checks */ + COMMUNITY_REQUEST_KINDS: [1], +} as const; + +// Time constants +export const TIME_CONSTANTS = { + /** Unix timestamp conversion factor (seconds to milliseconds) */ + UNIX_TIMESTAMP_FACTOR: 1000, + + /** Current timestamp in seconds */ + CURRENT_TIMESTAMP: Math.floor(Date.now() / 1000), +} as const; + +// Validation constants +export const VALIDATION = { + /** Hex string length for event IDs and pubkeys */ + HEX_LENGTH: 64, + + /** Minimum length for Nostr identifiers */ + MIN_NOSTR_IDENTIFIER_LENGTH: 4, +} as const; + +// HTTP status codes +export const HTTP_STATUS = { + /** OK status code */ + OK: 200, + + /** Not found status code */ + NOT_FOUND: 404, + + /** Internal server error status code */ + INTERNAL_SERVER_ERROR: 500, +} as const; \ No newline at end of file diff --git a/src/lib/utils/search_types.ts b/src/lib/utils/search_types.ts new file mode 100644 index 0000000..df75ffc --- /dev/null +++ b/src/lib/utils/search_types.ts @@ -0,0 +1,69 @@ +import { NDKEvent } from '@nostr-dev-kit/ndk'; + +/** + * Extended NostrProfile interface for search results + */ +export interface NostrProfile { + name?: string; + displayName?: string; + nip05?: string; + picture?: string; + about?: string; + banner?: string; + website?: string; + lud16?: string; + pubkey?: string; +} + +/** + * Search result interface for subscription-based searches + */ +export interface SearchResult { + events: NDKEvent[]; + secondOrder: NDKEvent[]; + tTagEvents: NDKEvent[]; + eventIds: Set; + addresses: Set; + searchType: string; + searchTerm: string; +} + +/** + * Profile search result interface + */ +export interface ProfileSearchResult { + profiles: NostrProfile[]; + Status: Record; +} + +/** + * Search subscription type + */ +export type SearchSubscriptionType = 'd' | 't' | 'n'; + +/** + * Search filter configuration + */ +export interface SearchFilter { + filter: any; + subscriptionType: string; +} + +/** + * Second-order search parameters + */ +export interface SecondOrderSearchParams { + searchType: 'n' | 'd'; + firstOrderEvents: NDKEvent[]; + eventIds?: Set; + addresses?: Set; + targetPubkey?: string; +} + +/** + * Search callback functions + */ +export interface SearchCallbacks { + onSecondOrderUpdate?: (result: SearchResult) => void; + onSubscriptionCreated?: (sub: any) => void; +} \ No newline at end of file diff --git a/src/lib/utils/search_utility.ts b/src/lib/utils/search_utility.ts new file mode 100644 index 0000000..a44395a --- /dev/null +++ b/src/lib/utils/search_utility.ts @@ -0,0 +1,25 @@ +// Re-export all search functionality from modular files +export * from './search_types'; +export * from './search_utils'; +export * from './community_checker'; +export * from './profile_search'; +export * from './event_search'; +export * from './subscription_search'; +export * from './search_constants'; + +// Legacy exports for backward compatibility +export { searchProfiles } from './profile_search'; +export { searchBySubscription } from './subscription_search'; +export { searchEvent, searchNip05 } from './event_search'; +export { checkCommunity } from './community_checker'; +export { + wellKnownUrl, + lnurlpWellKnownUrl, + isValidNip05Address, + normalizeSearchTerm, + fieldMatches, + nip05Matches, + COMMON_DOMAINS, + isEmojiReaction, + createProfileFromEvent +} from './search_utils'; \ No newline at end of file diff --git a/src/lib/utils/search_utils.ts b/src/lib/utils/search_utils.ts new file mode 100644 index 0000000..5a5d6ac --- /dev/null +++ b/src/lib/utils/search_utils.ts @@ -0,0 +1,104 @@ +/** + * Generate well-known NIP-05 URL + */ +export function wellKnownUrl(domain: string, name: string): string { + return `https://${domain}/.well-known/nostr.json?name=${name}`; +} + +/** + * Generate well-known LNURLp URL for Lightning Network addresses + */ +export function lnurlpWellKnownUrl(domain: string, name: string): string { + return `https://${domain}/.well-known/lnurlp/${name}`; +} + +/** + * Validate NIP-05 address format + */ +export function isValidNip05Address(address: string): boolean { + return /^[a-z0-9._-]+@[a-z0-9.-]+$/i.test(address); +} + +/** + * Helper function to normalize search terms + */ +export function normalizeSearchTerm(term: string): string { + return term.toLowerCase().replace(/\s+/g, ''); +} + +/** + * Helper function to check if a profile field matches the search term + */ +export function fieldMatches(field: string, searchTerm: string): boolean { + if (!field) return false; + const fieldLower = field.toLowerCase(); + const fieldNormalized = fieldLower.replace(/\s+/g, ''); + const searchTermLower = searchTerm.toLowerCase(); + const normalizedSearchTerm = normalizeSearchTerm(searchTerm); + + // Check exact match + if (fieldLower === searchTermLower) return true; + if (fieldNormalized === normalizedSearchTerm) return true; + + // Check if field contains the search term + if (fieldLower.includes(searchTermLower)) return true; + if (fieldNormalized.includes(normalizedSearchTerm)) return true; + + // Check individual words (handle spaces in display names) + const words = fieldLower.split(/\s+/); + return words.some(word => word.includes(searchTermLower)); +} + +/** + * Helper function to check if NIP-05 address matches the search term + */ +export function nip05Matches(nip05: string, searchTerm: string): boolean { + if (!nip05) return false; + const nip05Lower = nip05.toLowerCase(); + const searchTermLower = searchTerm.toLowerCase(); + const normalizedSearchTerm = normalizeSearchTerm(searchTerm); + + // Check if the part before @ contains the search term + const atIndex = nip05Lower.indexOf('@'); + if (atIndex !== -1) { + const localPart = nip05Lower.substring(0, atIndex); + const localPartNormalized = localPart.replace(/\s+/g, ''); + return localPart.includes(searchTermLower) || localPartNormalized.includes(normalizedSearchTerm); + } + return false; +} + +/** + * Common domains for NIP-05 lookups + */ +export const COMMON_DOMAINS = [ + 'gitcitadel.com', + 'theforest.nostr1.com', + 'nostr1.com', + 'nostr.land', + 'sovbit.host' +] as const; + +/** + * Check if an event is an emoji reaction (kind 7) + */ +export function isEmojiReaction(event: any): boolean { + return event.kind === 7; +} + +/** + * Create a profile object from event data + */ +export function createProfileFromEvent(event: any, profileData: any): any { + return { + name: profileData.name, + displayName: profileData.displayName || profileData.display_name, + nip05: profileData.nip05, + picture: profileData.picture, + about: profileData.about, + banner: profileData.banner, + website: profileData.website, + lud16: profileData.lud16, + pubkey: event.pubkey + }; +} \ No newline at end of file diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts new file mode 100644 index 0000000..46cc49f --- /dev/null +++ b/src/lib/utils/subscription_search.ts @@ -0,0 +1,656 @@ +import { ndkInstance } from '$lib/ndk'; +import { getMatchingTags, getNpubFromNip05 } from '$lib/utils/nostrUtils'; +import { nip19 } from '$lib/utils/nostrUtils'; +import { NDKRelaySet, NDKEvent } from '@nostr-dev-kit/ndk'; +import { searchCache } from '$lib/utils/searchCache'; +import { communityRelay, profileRelays } from '$lib/consts'; +import { get } from 'svelte/store'; +import type { SearchResult, SearchSubscriptionType, SearchFilter, SearchCallbacks, SecondOrderSearchParams } from './search_types'; +import { fieldMatches, nip05Matches, normalizeSearchTerm, COMMON_DOMAINS, isEmojiReaction } from './search_utils'; +import { TIMEOUTS, SEARCH_LIMITS } from './search_constants'; + +/** + * Search for events by subscription type (d, t, n) + */ +export async function searchBySubscription( + searchType: SearchSubscriptionType, + searchTerm: string, + callbacks?: SearchCallbacks, + abortSignal?: AbortSignal +): Promise { + const normalizedSearchTerm = searchTerm.toLowerCase().trim(); + + console.log("subscription_search: Starting search:", { searchType, searchTerm, normalizedSearchTerm }); + + // Check cache first + const cachedResult = searchCache.get(searchType, normalizedSearchTerm); + if (cachedResult) { + console.log("subscription_search: Found cached result:", cachedResult); + return cachedResult; + } + + const ndk = get(ndkInstance); + if (!ndk) { + console.error("subscription_search: NDK not initialized"); + throw new Error('NDK not initialized'); + } + + console.log("subscription_search: NDK initialized, creating search state"); + const searchState = createSearchState(); + const cleanup = createCleanupFunction(searchState); + + // Set a timeout to force completion after subscription search timeout + searchState.timeoutId = setTimeout(() => { + console.log("subscription_search: Search timeout reached"); + cleanup(); + }, TIMEOUTS.SUBSCRIPTION_SEARCH); + + // Check for abort signal + if (abortSignal?.aborted) { + console.log("subscription_search: Search aborted"); + cleanup(); + throw new Error('Search cancelled'); + } + + const searchFilter = await createSearchFilter(searchType, normalizedSearchTerm); + console.log("subscription_search: Created search filter:", searchFilter); + const primaryRelaySet = createPrimaryRelaySet(searchType, ndk); + console.log("subscription_search: Created primary relay set with", primaryRelaySet.relays.size, "relays"); + + // Phase 1: Search primary relay + if (primaryRelaySet.relays.size > 0) { + try { + console.log("subscription_search: Searching primary relay with filter:", searchFilter.filter); + const primaryEvents = await ndk.fetchEvents( + searchFilter.filter, + { closeOnEose: true }, + primaryRelaySet + ); + + console.log("subscription_search: Primary relay returned", primaryEvents.size, "events"); + processPrimaryRelayResults(primaryEvents, searchType, searchFilter.subscriptionType, normalizedSearchTerm, searchState, abortSignal, cleanup); + + // If we found results from primary relay, return them immediately + if (hasResults(searchState, searchType)) { + console.log("subscription_search: Found results from primary relay, returning immediately"); + const immediateResult = createSearchResult(searchState, searchType, normalizedSearchTerm); + searchCache.set(searchType, normalizedSearchTerm, immediateResult); + + // Start Phase 2 in background for additional results + searchOtherRelaysInBackground(searchType, searchFilter, searchState, callbacks, abortSignal, cleanup); + + return immediateResult; + } else { + console.log("subscription_search: No results from primary relay, continuing to Phase 2"); + } + } catch (error) { + console.error(`subscription_search: Error searching primary relay:`, error); + } + } else { + console.log("subscription_search: No primary relays available, skipping Phase 1"); + } + + // Always do Phase 2: Search all other relays in parallel + return searchOtherRelaysInBackground(searchType, searchFilter, searchState, callbacks, abortSignal, cleanup); +} + +/** + * Create search state object + */ +function createSearchState() { + return { + timeoutId: null as ReturnType | null, + firstOrderEvents: [] as NDKEvent[], + secondOrderEvents: [] as NDKEvent[], + tTagEvents: [] as NDKEvent[], + eventIds: new Set(), + eventAddresses: new Set(), + foundProfiles: [] as NDKEvent[], + isCompleted: false, + currentSubscription: null as any + }; +} + +/** + * Create cleanup function + */ +function createCleanupFunction(searchState: any) { + return () => { + if (searchState.timeoutId) { + clearTimeout(searchState.timeoutId); + searchState.timeoutId = null; + } + if (searchState.currentSubscription) { + try { + searchState.currentSubscription.stop(); + } catch (e) { + console.warn('Error stopping subscription:', e); + } + searchState.currentSubscription = null; + } + }; +} + +/** + * Create search filter based on search type + */ +async function createSearchFilter(searchType: SearchSubscriptionType, normalizedSearchTerm: string): Promise { + console.log("subscription_search: Creating search filter for:", { searchType, normalizedSearchTerm }); + + switch (searchType) { + case 'd': + const dFilter = { + filter: { "#d": [normalizedSearchTerm] }, + subscriptionType: 'd-tag' + }; + console.log("subscription_search: Created d-tag filter:", dFilter); + return dFilter; + case 't': + const tFilter = { + filter: { "#t": [normalizedSearchTerm] }, + subscriptionType: 't-tag' + }; + console.log("subscription_search: Created t-tag filter:", tFilter); + return tFilter; + case 'n': + const nFilter = await createProfileSearchFilter(normalizedSearchTerm); + console.log("subscription_search: Created profile filter:", nFilter); + return nFilter; + default: + throw new Error(`Unknown search type: ${searchType}`); + } +} + +/** + * Create profile search filter + */ +async function createProfileSearchFilter(normalizedSearchTerm: string): Promise { + // For npub searches, try to decode the search term first + try { + const decoded = nip19.decode(normalizedSearchTerm); + if (decoded && decoded.type === 'npub') { + return { + filter: { kinds: [0], authors: [decoded.data], limit: SEARCH_LIMITS.SPECIFIC_PROFILE }, + subscriptionType: 'npub-specific' + }; + } + } catch (e) { + // Not a valid npub, continue with other strategies + } + + // Try NIP-05 lookup first + try { + for (const domain of COMMON_DOMAINS) { + const nip05Address = `${normalizedSearchTerm}@${domain}`; + try { + const npub = await getNpubFromNip05(nip05Address); + if (npub) { + return { + filter: { kinds: [0], authors: [npub], limit: SEARCH_LIMITS.SPECIFIC_PROFILE }, + subscriptionType: 'nip05-found' + }; + } + } catch (e) { + // Continue to next domain + } + } + } catch (e) { + // Fallback to reasonable profile search + } + + return { + filter: { kinds: [0], limit: SEARCH_LIMITS.GENERAL_PROFILE }, + subscriptionType: 'profile' + }; +} + +/** + * Create primary relay set based on search type + */ +function createPrimaryRelaySet(searchType: SearchSubscriptionType, ndk: any): NDKRelaySet { + if (searchType === 'n') { + // For profile searches, use profile relays first + const profileRelaySet = Array.from(ndk.pool.relays.values()).filter((relay: any) => + profileRelays.some(profileRelay => relay.url === profileRelay || relay.url === profileRelay + '/') + ); + return new NDKRelaySet(new Set(profileRelaySet) as any, ndk); + } else { + // For other searches, use community relay first + const communityRelaySet = Array.from(ndk.pool.relays.values()).filter((relay: any) => + relay.url === communityRelay || relay.url === communityRelay + '/' + ); + return new NDKRelaySet(new Set(communityRelaySet) as any, ndk); + } +} + +/** + * Process primary relay results + */ +function processPrimaryRelayResults( + events: Set, + searchType: SearchSubscriptionType, + subscriptionType: string, + normalizedSearchTerm: string, + searchState: any, + abortSignal?: AbortSignal, + cleanup?: () => void +) { + console.log("subscription_search: Processing", events.size, "events from primary relay"); + + for (const event of events) { + // Check for abort signal + if (abortSignal?.aborted) { + cleanup?.(); + throw new Error('Search cancelled'); + } + + try { + if (searchType === 'n') { + processProfileEvent(event, subscriptionType, normalizedSearchTerm, searchState); + } else { + processContentEvent(event, searchType, searchState); + } + } catch (e) { + console.warn("subscription_search: Error processing event:", e); + // Invalid JSON or other error, skip + } + } + + console.log("subscription_search: Processed events - firstOrder:", searchState.firstOrderEvents.length, "profiles:", searchState.foundProfiles.length, "tTag:", searchState.tTagEvents.length); +} + +/** + * Process profile event + */ +function processProfileEvent(event: NDKEvent, subscriptionType: string, normalizedSearchTerm: string, searchState: any) { + if (!event.content) return; + + // If this is a specific npub search or NIP-05 found search, include all matching events + if (subscriptionType === 'npub-specific' || subscriptionType === 'nip05-found') { + searchState.foundProfiles.push(event); + return; + } + + // For general profile searches, filter by content + const profileData = JSON.parse(event.content); + const displayName = profileData.display_name || profileData.displayName || ''; + const name = profileData.name || ''; + const nip05 = profileData.nip05 || ''; + const username = profileData.username || ''; + const about = profileData.about || ''; + const bio = profileData.bio || ''; + const description = profileData.description || ''; + + const matchesDisplayName = fieldMatches(displayName, normalizedSearchTerm); + const matchesName = fieldMatches(name, normalizedSearchTerm); + const matchesNip05 = nip05Matches(nip05, normalizedSearchTerm); + const matchesUsername = fieldMatches(username, normalizedSearchTerm); + const matchesAbout = fieldMatches(about, normalizedSearchTerm); + const matchesBio = fieldMatches(bio, normalizedSearchTerm); + const matchesDescription = fieldMatches(description, normalizedSearchTerm); + + if (matchesDisplayName || matchesName || matchesNip05 || matchesUsername || matchesAbout || matchesBio || matchesDescription) { + searchState.foundProfiles.push(event); + } +} + +/** + * Process content event + */ +function processContentEvent(event: NDKEvent, searchType: SearchSubscriptionType, searchState: any) { + if (isEmojiReaction(event)) return; // Skip emoji reactions + + if (searchType === 'd') { + console.log("subscription_search: Processing d-tag event:", { id: event.id, kind: event.kind, pubkey: event.pubkey }); + searchState.firstOrderEvents.push(event); + + // Collect event IDs and addresses for second-order search + if (event.id) { + searchState.eventIds.add(event.id); + } + // Handle both "a" tags (NIP-62) and "e" tags (legacy) + let tags = getMatchingTags(event, "a"); + if (tags.length === 0) { + tags = getMatchingTags(event, "e"); + } + + tags.forEach((tag: string[]) => { + if (tag[1]) { + searchState.eventAddresses.add(tag[1]); + } + }); + } else if (searchType === 't') { + searchState.tTagEvents.push(event); + } +} + +/** + * Check if search state has results + */ +function hasResults(searchState: any, searchType: SearchSubscriptionType): boolean { + if (searchType === 'n') { + return searchState.foundProfiles.length > 0; + } else if (searchType === 'd') { + return searchState.firstOrderEvents.length > 0; + } else if (searchType === 't') { + return searchState.tTagEvents.length > 0; + } + return false; +} + +/** + * Create search result from state + */ +function createSearchResult(searchState: any, searchType: SearchSubscriptionType, normalizedSearchTerm: string): SearchResult { + return { + events: searchType === 'n' ? searchState.foundProfiles : searchType === 't' ? searchState.tTagEvents : searchState.firstOrderEvents, + secondOrder: [], + tTagEvents: [], + eventIds: searchState.eventIds, + addresses: searchState.eventAddresses, + searchType: searchType, + searchTerm: normalizedSearchTerm + }; +} + +/** + * Search other relays in background + */ +async function searchOtherRelaysInBackground( + searchType: SearchSubscriptionType, + searchFilter: SearchFilter, + searchState: any, + callbacks?: SearchCallbacks, + abortSignal?: AbortSignal, + cleanup?: () => void +): Promise { + const ndk = get(ndkInstance); + + const otherRelays = new NDKRelaySet( + new Set(Array.from(ndk.pool.relays.values()).filter((relay: any) => { + if (searchType === 'n') { + // For profile searches, exclude profile relays from fallback search + return !profileRelays.some(profileRelay => relay.url === profileRelay || relay.url === profileRelay + '/'); + } else { + // For other searches, exclude community relay from fallback search + return relay.url !== communityRelay && relay.url !== communityRelay + '/'; + } + })), + ndk + ); + + // Subscribe to events from other relays + const sub = ndk.subscribe( + searchFilter.filter, + { closeOnEose: true }, + otherRelays + ); + + // Store the subscription for cleanup + searchState.currentSubscription = sub; + + // Notify the component about the subscription for cleanup + if (callbacks?.onSubscriptionCreated) { + callbacks.onSubscriptionCreated(sub); + } + + sub.on('event', (event: NDKEvent) => { + try { + if (searchType === 'n') { + processProfileEvent(event, searchFilter.subscriptionType, searchState.normalizedSearchTerm, searchState); + } else { + processContentEvent(event, searchType, searchState); + } + } catch (e) { + // Invalid JSON or other error, skip + } + }); + + return new Promise((resolve) => { + sub.on('eose', () => { + const result = processEoseResults(searchType, searchState, searchFilter, callbacks); + searchCache.set(searchType, searchState.normalizedSearchTerm, result); + cleanup?.(); + resolve(result); + }); + }); +} + +/** + * Process EOSE results + */ +function processEoseResults( + searchType: SearchSubscriptionType, + searchState: any, + searchFilter: SearchFilter, + callbacks?: SearchCallbacks +): SearchResult { + if (searchType === 'n') { + return processProfileEoseResults(searchState, searchFilter, callbacks); + } else if (searchType === 'd') { + return processContentEoseResults(searchState, searchType); + } else if (searchType === 't') { + return processTTagEoseResults(searchState); + } + + return createEmptySearchResult(searchType, searchState.normalizedSearchTerm); +} + +/** + * Process profile EOSE results + */ +function processProfileEoseResults(searchState: any, searchFilter: SearchFilter, callbacks?: SearchCallbacks): SearchResult { + if (searchState.foundProfiles.length === 0) { + return createEmptySearchResult('n', searchState.normalizedSearchTerm); + } + + // Deduplicate by pubkey, keep only newest + const deduped: Record = {}; + for (const event of searchState.foundProfiles) { + const pubkey = event.pubkey; + const created_at = event.created_at || 0; + if (!deduped[pubkey] || deduped[pubkey].created_at < created_at) { + deduped[pubkey] = { event, created_at }; + } + } + + // Sort by creation time (newest first) and take only the most recent profiles + const dedupedProfiles = Object.values(deduped) + .sort((a, b) => b.created_at - a.created_at) + .map(x => x.event); + + // Perform second-order search for npub searches + if (searchFilter.subscriptionType === 'npub-specific' || searchFilter.subscriptionType === 'nip05-found') { + const targetPubkey = dedupedProfiles[0]?.pubkey; + if (targetPubkey) { + performSecondOrderSearchInBackground('n', dedupedProfiles, new Set(), new Set(), targetPubkey, callbacks); + } + } else if (searchFilter.subscriptionType === 'profile') { + // For general profile searches, perform second-order search for each found profile + for (const profile of dedupedProfiles) { + if (profile.pubkey) { + performSecondOrderSearchInBackground('n', dedupedProfiles, new Set(), new Set(), profile.pubkey, callbacks); + } + } + } + + return { + events: dedupedProfiles, + secondOrder: [], + tTagEvents: [], + eventIds: new Set(dedupedProfiles.map(p => p.id)), + addresses: new Set(), + searchType: 'n', + searchTerm: searchState.normalizedSearchTerm + }; +} + +/** + * Process content EOSE results + */ +function processContentEoseResults(searchState: any, searchType: SearchSubscriptionType): SearchResult { + if (searchState.firstOrderEvents.length === 0) { + return createEmptySearchResult(searchType, searchState.normalizedSearchTerm); + } + + // Deduplicate by kind, pubkey, and d-tag, keep only newest event for each combination + const deduped: Record = {}; + for (const event of searchState.firstOrderEvents) { + const dTag = getMatchingTags(event, 'd')[0]?.[1] || ''; + const key = `${event.kind}:${event.pubkey}:${dTag}`; + const created_at = event.created_at || 0; + if (!deduped[key] || deduped[key].created_at < created_at) { + deduped[key] = { event, created_at }; + } + } + const dedupedEvents = Object.values(deduped).map(x => x.event); + + // Perform second-order search for d-tag searches + if (dedupedEvents.length > 0) { + performSecondOrderSearchInBackground('d', dedupedEvents, searchState.eventIds, searchState.eventAddresses); + } + + return { + events: dedupedEvents, + secondOrder: [], + tTagEvents: [], + eventIds: searchState.eventIds, + addresses: searchState.eventAddresses, + searchType: searchType, + searchTerm: searchState.normalizedSearchTerm + }; +} + +/** + * Process t-tag EOSE results + */ +function processTTagEoseResults(searchState: any): SearchResult { + if (searchState.tTagEvents.length === 0) { + return createEmptySearchResult('t', searchState.normalizedSearchTerm); + } + + return { + events: searchState.tTagEvents, + secondOrder: [], + tTagEvents: [], + eventIds: new Set(), + addresses: new Set(), + searchType: 't', + searchTerm: searchState.normalizedSearchTerm + }; +} + +/** + * Create empty search result + */ +function createEmptySearchResult(searchType: SearchSubscriptionType, searchTerm: string): SearchResult { + return { + events: [], + secondOrder: [], + tTagEvents: [], + eventIds: new Set(), + addresses: new Set(), + searchType: searchType, + searchTerm: searchTerm + }; +} + +/** + * Perform second-order search in background + */ +async function performSecondOrderSearchInBackground( + searchType: 'n' | 'd', + firstOrderEvents: NDKEvent[], + eventIds: Set = new Set(), + addresses: Set = new Set(), + targetPubkey?: string, + callbacks?: SearchCallbacks +) { + try { + const ndk = get(ndkInstance); + let allSecondOrderEvents: NDKEvent[] = []; + + // Set a timeout for second-order search + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Second-order search timeout')), TIMEOUTS.SECOND_ORDER_SEARCH); + }); + + const searchPromise = (async () => { + if (searchType === 'n' && targetPubkey) { + // Search for events that mention this pubkey via p-tags + const pTagFilter = { '#p': [targetPubkey] }; + const pTagEvents = await ndk.fetchEvents( + pTagFilter, + { closeOnEose: true }, + new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), + ); + // Filter out emoji reactions + const filteredEvents = Array.from(pTagEvents).filter(event => !isEmojiReaction(event)); + allSecondOrderEvents = [...allSecondOrderEvents, ...filteredEvents]; + } else if (searchType === 'd') { + // Parallel fetch for #e and #a tag events + const relaySet = new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk); + const [eTagEvents, aTagEvents] = await Promise.all([ + eventIds.size > 0 + ? ndk.fetchEvents( + { '#e': Array.from(eventIds) }, + { closeOnEose: true }, + relaySet + ) + : Promise.resolve([]), + addresses.size > 0 + ? ndk.fetchEvents( + { '#a': Array.from(addresses) }, + { closeOnEose: true }, + relaySet + ) + : Promise.resolve([]), + ]); + // Filter out emoji reactions + const filteredETagEvents = Array.from(eTagEvents).filter(event => !isEmojiReaction(event)); + const filteredATagEvents = Array.from(aTagEvents).filter(event => !isEmojiReaction(event)); + allSecondOrderEvents = [...allSecondOrderEvents, ...filteredETagEvents, ...filteredATagEvents]; + } + + // Deduplicate by event ID + const uniqueSecondOrder = new Map(); + allSecondOrderEvents.forEach(event => { + if (event.id) { + uniqueSecondOrder.set(event.id, event); + } + }); + + let deduplicatedSecondOrder = Array.from(uniqueSecondOrder.values()); + + // Remove any events already in first order + const firstOrderIds = new Set(firstOrderEvents.map(e => e.id)); + deduplicatedSecondOrder = deduplicatedSecondOrder.filter(e => !firstOrderIds.has(e.id)); + + // Sort by creation date (newest first) and limit to newest results + const sortedSecondOrder = deduplicatedSecondOrder + .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) + .slice(0, SEARCH_LIMITS.SECOND_ORDER_RESULTS); + + // Update the search results with second-order events + const result: SearchResult = { + events: firstOrderEvents, + secondOrder: sortedSecondOrder, + tTagEvents: [], + eventIds: searchType === 'n' ? new Set(firstOrderEvents.map(p => p.id)) : eventIds, + addresses: searchType === 'n' ? new Set() : addresses, + searchType: searchType, + searchTerm: '' // This will be set by the caller + }; + + // Notify UI of updated results + if (callbacks?.onSecondOrderUpdate) { + callbacks.onSecondOrderUpdate(result); + } + })(); + + // Race between search and timeout + await Promise.race([searchPromise, timeoutPromise]); + } catch (err) { + console.error(`[Search] Error in second-order ${searchType}-tag search:`, err); + } +} \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 89660e5..9cb3197 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -7,12 +7,13 @@ import { HammerSolid } from "flowbite-svelte-icons"; // Get standard metadata for OpenGraph tags - let title = 'Library of Alexandria'; + let title = "Library of Alexandria"; let currentUrl = $page.url.href; - + // Get default image and summary for the Alexandria website - let image = '/screenshots/old_books.jpg'; - let summary = 'Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.'; + let image = "/screenshots/old_books.jpg"; + let summary = + "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; onMount(() => { const rect = document.body.getBoundingClientRect(); @@ -23,24 +24,24 @@ {title} - - + + - - - + + + - - + + - - - + + + -
- +
+
diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 915324a..d4102db 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,32 +1,100 @@ import { feedTypeStorageKey } from '$lib/consts'; import { FeedType } from '$lib/consts'; -import { getPersistedLogin, initNdk, loginWithExtension, ndkInstance } from '$lib/ndk'; +import { getPersistedLogin, initNdk, ndkInstance } from '$lib/ndk'; +import { loginWithExtension, loginWithAmber, loginWithNpub } from '$lib/stores/userStore'; +import { loginMethodStorageKey } from '$lib/stores/userStore'; import Pharos, { pharosInstance } from '$lib/parser'; import { feedType } from '$lib/stores'; import type { LayoutLoad } from './$types'; +import { get } from 'svelte/store'; export const ssr = false; export const load: LayoutLoad = () => { - const initialFeedType = localStorage.getItem(feedTypeStorageKey) as FeedType - ?? FeedType.StandardRelays; + const initialFeedType = + (localStorage.getItem(feedTypeStorageKey) as FeedType) ?? + FeedType.StandardRelays; feedType.set(initialFeedType); const ndk = initNdk(); ndkInstance.set(ndk); try { - // Michael J - 18 Jan 2025 - This will not work server-side, since the NIP-07 extension is only - // available in the browser, and the flags for persistent login are saved in the browser's - // local storage. If SSR is ever enabled, move this code block to run client-side. const pubkey = getPersistedLogin(); - if (pubkey) { - // Michael J - 27 Jan 2025 - We don't await this call; it will run in the background and - // update Svelte stores to propagate data. - loginWithExtension(pubkey); + const loginMethod = localStorage.getItem(loginMethodStorageKey); + const logoutFlag = localStorage.getItem('alexandria/logout/flag'); + console.log('Layout load - persisted pubkey:', pubkey); + console.log('Layout load - persisted login method:', loginMethod); + console.log('Layout load - logout flag:', logoutFlag); + console.log('All localStorage keys:', Object.keys(localStorage)); + + if (pubkey && loginMethod && !logoutFlag) { + if (loginMethod === 'extension') { + console.log('Restoring extension login...'); + loginWithExtension(); + } else if (loginMethod === 'amber') { + // Attempt to restore Amber (NIP-46) session from localStorage + const relay = 'wss://relay.nsec.app'; + const localNsec = localStorage.getItem('amber/nsec'); + if (localNsec) { + import('@nostr-dev-kit/ndk').then(async ({ NDKNip46Signer, default: NDK }) => { + const ndk = get(ndkInstance); + try { + const amberSigner = NDKNip46Signer.nostrconnect(ndk, relay, localNsec, { + name: 'Alexandria', + perms: 'sign_event:1;sign_event:4', + }); + // Try to reconnect (blockUntilReady will resolve if Amber is running and session is valid) + await amberSigner.blockUntilReady(); + const user = await amberSigner.user(); + await loginWithAmber(amberSigner, user); + console.log('Amber session restored.'); + } catch (err) { + // If reconnection fails, automatically fallback to npub-only mode + console.warn('Amber session could not be restored. Falling back to npub-only mode.'); + try { + // Set the flag first, before login + localStorage.setItem('alexandria/amber/fallback', '1'); + console.log('Set fallback flag in localStorage'); + + // Small delay to ensure flag is set + await new Promise(resolve => setTimeout(resolve, 100)); + + await loginWithNpub(pubkey); + console.log('Successfully fell back to npub-only mode.'); + } catch (fallbackErr) { + console.error('Failed to fallback to npub-only mode:', fallbackErr); + } + } + }); + } else { + // No session data, automatically fallback to npub-only mode + console.log('No Amber session data found. Falling back to npub-only mode.'); + + // Set the flag first, before login + localStorage.setItem('alexandria/amber/fallback', '1'); + console.log('Set fallback flag in localStorage'); + + // Small delay to ensure flag is set + setTimeout(async () => { + try { + await loginWithNpub(pubkey); + console.log('Successfully fell back to npub-only mode.'); + } catch (fallbackErr) { + console.error('Failed to fallback to npub-only mode:', fallbackErr); + } + }, 100); + } + } else if (loginMethod === 'npub') { + console.log('Restoring npub login...'); + loginWithNpub(pubkey); + } + } else if (logoutFlag) { + console.log('Skipping auto-login due to logout flag'); + localStorage.removeItem('alexandria/logout/flag'); } } catch (e) { - console.warn(`Failed to login with extension: ${e}\n\nContinuing with anonymous session.`); + console.warn(`Failed to restore login: ${e}\n\nContinuing with anonymous session.`); } const parser = new Pharos(ndk); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index d3cee5c..21a723d 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,66 +1,38 @@ - - - - - Pardon our dust! The publication view is currently using an experimental loader, and may be unstable. - + + + + Pardon our dust! The publication view is currently using an experimental + loader, and may be unstable. +
- {#if !$ndkSignedIn} - - {:else} -
- - - -
  • - Alexandria's Relays -
  • -
  • - Your Relays -
  • -
    -
    - {#if $feedType === FeedType.StandardRelays} - - {:else if $feedType === FeedType.UserRelays} - - {/if} - {/if} +
    + +
    +
    diff --git a/src/routes/[...catchall]/+page.svelte b/src/routes/[...catchall]/+page.svelte index 18be414..0224b3d 100644 --- a/src/routes/[...catchall]/+page.svelte +++ b/src/routes/[...catchall]/+page.svelte @@ -1,13 +1,23 @@ -
    +

    404 - Page Not Found

    -

    The page you are looking for does not exist or has been moved.

    +

    The page you are looking for does not exist or has been moved.

    - - + +
    diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index a6badf3..caabdcb 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -1,6 +1,8 @@ -
    -
    - Contact GitCitadel - +
    +
    + Contact GitCitadel +

    - Make sure that you follow us on GitHub and Geyserfund. + Make sure that you follow us on GitHub and Geyserfund.

    - You can contact us on Nostr {@render userBadge("npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz", "GitCitadel")} or you can view submitted issues on the Alexandria repo page. + You can contact us on Nostr {@render userBadge( + "npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz", + "GitCitadel", + )} or you can view submitted issues on the Alexandria repo page.

    - - Submit an issue - + + Submit an issue +

    - If you are logged into the Alexandria web application (using the button at the top-right of the window), then you can use the form, below, to submit an issue, that will appear on our repo page. + If you are logged into the Alexandria web application (using the button at + the top-right of the window), then you can use the form, below, to submit + an issue, that will appear on our repo page.

    - +
    - +
    -
    +
    -
    -
      +
      +
      • -
      - +
      - {#if activeTab === 'write'} + {#if activeTab === "write"}
      {:else} -
      - - - + + + + - - + + {#if rootIndexId} - + {/if} {/if} diff --git a/src/routes/publication/+error.svelte b/src/routes/publication/+error.svelte index 2cbb819..9d0d347 100644 --- a/src/routes/publication/+error.svelte +++ b/src/routes/publication/+error.svelte @@ -1,29 +1,37 @@ -
      -
      - - - Failed to load publication. - +
      + + Failed to load publication.
      -

      - Alexandria failed to find one or more of the events comprising this publication. +

      + Alexandria failed to find one or more of the events comprising this + publication.

      -

      +

      {page.error?.message}

      -
      - -
      diff --git a/src/routes/publication/+page.svelte b/src/routes/publication/+page.svelte index 48b156c..5612115 100644 --- a/src/routes/publication/+page.svelte +++ b/src/routes/publication/+page.svelte @@ -68,6 +68,7 @@ {#await data.waitable} {:then} + {@const debugInfo = console.debug(`[Publication Page] Data loaded, rendering Publication component with publicationType: ${data.publicationType}, rootAddress: ${data.indexEvent.tagAddress()}`)} { const filter = decodeNaddr(id); - + // Handle the case where filter is null (decoding error) if (filter === null) { // If we can't decode the naddr, try using the raw ID @@ -46,14 +46,14 @@ async function fetchEventById(ndk: any, id: string): Promise { throw error(404, `Failed to fetch publication root event.\n${err}`); } } - + const hasFilter = Object.keys(filter).length > 0; - + try { - const event = await (hasFilter ? - ndk.fetchEvent(filter) : - ndk.fetchEvent(id)); - + const event = await (hasFilter + ? ndk.fetchEvent(filter) + : ndk.fetchEvent(id)); + if (!event) { throw new Error(`Event not found for ID: ${id}`); } @@ -69,11 +69,11 @@ async function fetchEventById(ndk: any, id: string): Promise { async function fetchEventByDTag(ndk: any, dTag: string): Promise { try { const event = await ndk.fetchEvent( - { '#d': [dTag] }, - { closeOnEose: false }, - getActiveRelays(ndk) + { "#d": [dTag] }, + { closeOnEose: false }, + getActiveRelays(ndk), ); - + if (!event) { throw new Error(`Event not found for d tag: ${dTag}`); } @@ -83,21 +83,27 @@ async function fetchEventByDTag(ndk: any, dTag: string): Promise { } } -export const load: Load = async ({ url, parent }: { url: URL; parent: () => Promise }) => { - const id = url.searchParams.get('id'); - const dTag = url.searchParams.get('d'); +export const load: Load = async ({ + url, + parent, +}: { + url: URL; + parent: () => Promise; +}) => { + const id = url.searchParams.get("id"); + const dTag = url.searchParams.get("d"); const { ndk, parser } = await parent(); - + if (!id && !dTag) { - throw error(400, 'No publication root event ID or d tag provided.'); + throw error(400, "No publication root event ID or d tag provided."); } - + // Fetch the event based on available parameters - const indexEvent = id + const indexEvent = id ? await fetchEventById(ndk, id) : await fetchEventByDTag(ndk, dTag!); - - const publicationType = getMatchingTags(indexEvent, 'type')[0]?.[1]; + + const publicationType = getMatchingTags(indexEvent, "type")[0]?.[1]; const fetchPromise = parser.fetch(indexEvent); return { diff --git a/src/routes/start/+page.svelte b/src/routes/start/+page.svelte index 05d0776..8a26909 100644 --- a/src/routes/start/+page.svelte +++ b/src/routes/start/+page.svelte @@ -1,5 +1,6 @@