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 095e9ca..983a123 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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,384 @@ "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-x64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "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" ], @@ -163,7 +558,7 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { "node": ">=12" @@ -171,6 +566,8 @@ }, "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,6 +689,8 @@ }, "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, @@ -286,42 +699,66 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", + "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.14.0", + "@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/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": { + "@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", @@ -571,6 +1041,7 @@ "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", "@noble/hashes": "^1.5.0", @@ -590,6 +1061,8 @@ }, "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", @@ -601,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", @@ -619,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" ], @@ -751,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/" @@ -763,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", @@ -775,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" @@ -785,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" @@ -795,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" @@ -805,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/" @@ -812,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", @@ -823,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" @@ -833,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/" @@ -840,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" @@ -850,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": { @@ -858,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": { @@ -869,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": { @@ -883,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": { @@ -890,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": { @@ -905,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" @@ -914,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": { @@ -941,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": { @@ -957,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" @@ -967,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", @@ -978,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": { @@ -1022,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": { @@ -1035,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": { @@ -1043,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": { @@ -1062,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": { @@ -1080,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": { @@ -1098,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": { @@ -1116,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": { @@ -1129,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": { @@ -1157,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": { @@ -1175,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": { @@ -1198,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": { @@ -1205,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": { @@ -1246,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" }, @@ -1264,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" }, @@ -1277,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": { @@ -1290,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": { @@ -1297,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": { @@ -1308,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" }, @@ -1333,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": { @@ -1358,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": { @@ -1378,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": { @@ -1386,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, @@ -1402,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" @@ -1409,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" @@ -1422,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", @@ -1437,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" @@ -1447,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": { @@ -1461,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": { @@ -1479,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", @@ -1503,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": { @@ -1515,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": [ { @@ -1555,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": { @@ -1563,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" @@ -1573,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": { @@ -1583,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" @@ -1592,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", @@ -1601,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" @@ -1610,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": [ { @@ -1628,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" }, @@ -1642,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": { @@ -1653,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": { @@ -1661,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", @@ -1672,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", @@ -1686,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, @@ -1704,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": [ { @@ -1729,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": { @@ -1740,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", @@ -1759,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" @@ -1766,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" @@ -1773,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": { @@ -1817,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", @@ -1826,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": { @@ -1834,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" @@ -1844,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" @@ -1855,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", @@ -1872,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": { @@ -1880,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", @@ -1892,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" @@ -1902,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", @@ -1913,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", @@ -1952,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" @@ -1962,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" @@ -1969,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", @@ -1983,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" @@ -1993,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" @@ -2000,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" @@ -2010,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" @@ -2020,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" @@ -2027,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", @@ -2038,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", @@ -2061,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" @@ -2068,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" @@ -2078,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", @@ -2090,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" @@ -2097,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" @@ -2107,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" @@ -2114,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" @@ -2124,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" @@ -2131,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" @@ -2138,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" @@ -2145,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" @@ -2152,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", @@ -2166,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", @@ -2177,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" @@ -2184,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" @@ -2194,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" @@ -2204,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" @@ -2214,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" @@ -2221,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", @@ -2238,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", @@ -2252,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" @@ -2276,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": { @@ -2284,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": { @@ -2298,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" @@ -2305,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": { @@ -2324,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", @@ -2344,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" @@ -2360,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" @@ -2381,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" @@ -2388,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" @@ -2403,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": { @@ -2417,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", @@ -2426,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", @@ -2437,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", @@ -2474,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" @@ -2481,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, @@ -2492,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", @@ -2515,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", @@ -2553,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": { @@ -2567,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": { @@ -2600,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": { @@ -2608,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": { @@ -2636,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": { @@ -2657,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, @@ -2673,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, @@ -2686,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", @@ -2703,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" @@ -2721,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, @@ -2732,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": { @@ -2741,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": { @@ -2752,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": { @@ -2760,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": { @@ -2773,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", @@ -2780,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": { @@ -2789,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" @@ -2796,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", @@ -2816,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" @@ -2826,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": { @@ -2858,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, @@ -2870,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" @@ -2884,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" @@ -2894,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" @@ -2904,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, @@ -2920,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, @@ -2933,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": { @@ -2949,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": { @@ -2958,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": { @@ -2981,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": { @@ -2995,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": { @@ -3004,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": { @@ -3015,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", @@ -3029,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": { @@ -3041,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, @@ -3059,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" @@ -3066,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.*" @@ -3073,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", @@ -3095,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", @@ -3106,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", @@ -3123,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" @@ -3132,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" @@ -3140,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" @@ -3150,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, @@ -3162,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" @@ -3172,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", @@ -3191,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" @@ -3198,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" @@ -3208,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" @@ -3221,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" @@ -3231,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" @@ -3238,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" @@ -3245,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" @@ -3255,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, @@ -3264,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, @@ -3280,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": { @@ -3289,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, @@ -3298,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", @@ -3306,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" @@ -3317,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" @@ -3327,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" @@ -3340,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", @@ -3348,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" @@ -3358,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" @@ -3365,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" @@ -3372,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" @@ -3382,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" @@ -3394,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": { @@ -3406,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", @@ -3422,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" @@ -3443,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", @@ -3459,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" @@ -3466,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, @@ -3482,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", @@ -3508,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, @@ -3517,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": { @@ -3525,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, @@ -3543,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" @@ -3550,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", @@ -3560,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" @@ -3570,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, @@ -3594,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": { @@ -3623,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" @@ -3630,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" @@ -3637,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", @@ -3648,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" @@ -3658,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" @@ -3665,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" @@ -3675,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" @@ -3682,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" @@ -3689,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": { @@ -3697,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": { @@ -3705,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", @@ -3718,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", @@ -3734,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", @@ -3761,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", @@ -3770,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" @@ -3782,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": { @@ -3790,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", @@ -3813,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" @@ -3823,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" @@ -3833,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" @@ -3843,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", @@ -3853,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", @@ -3881,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" @@ -3888,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" @@ -3895,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" @@ -3902,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" @@ -3909,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, @@ -3926,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, @@ -3941,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, @@ -3965,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, @@ -3981,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" @@ -3988,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" @@ -3995,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", @@ -4013,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": { @@ -4026,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": { @@ -4041,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" @@ -4048,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" @@ -4071,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": { @@ -4081,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", @@ -4106,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", @@ -4123,7 +5564,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4133,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", @@ -4148,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" @@ -4165,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": [ { @@ -4206,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", @@ -4229,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", @@ -4240,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": { @@ -4255,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": [ { @@ -4280,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", @@ -4291,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, @@ -4303,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": { @@ -4318,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": { @@ -4327,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" @@ -4334,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", @@ -4348,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", @@ -4357,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", @@ -4371,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", @@ -4386,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", @@ -4395,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", @@ -4403,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", @@ -4411,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", @@ -4419,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" @@ -4430,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, @@ -4578,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", @@ -4596,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" @@ -4627,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" @@ -4640,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", @@ -4658,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, @@ -4667,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", @@ -4675,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" @@ -4692,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", @@ -4738,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": { @@ -4753,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": { @@ -4774,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" @@ -4789,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" @@ -4796,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" @@ -4811,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": { @@ -4824,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" @@ -4834,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" @@ -4841,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" @@ -4848,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", @@ -4871,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", @@ -4883,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" @@ -4894,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" @@ -4904,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, @@ -4914,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", @@ -4935,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" @@ -4943,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" @@ -4950,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", @@ -4968,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" @@ -4981,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" @@ -4991,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" @@ -5000,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": { @@ -5013,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", @@ -5024,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": { @@ -5077,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": { @@ -5103,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": { @@ -5118,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": { @@ -5129,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": { @@ -5145,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": { @@ -5153,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": { @@ -5164,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": { @@ -5175,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": { @@ -5186,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": { @@ -5202,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": { @@ -5214,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": { @@ -5225,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": { @@ -5235,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": { @@ -5245,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", @@ -5280,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", @@ -5313,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", @@ -5324,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" @@ -5331,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" @@ -5341,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": { @@ -5365,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": { @@ -5374,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": { @@ -5381,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": { @@ -5390,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" @@ -5400,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": { @@ -5412,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, @@ -5445,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" @@ -5452,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": { @@ -5464,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": { @@ -5479,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" @@ -5491,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" @@ -5498,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": [ { @@ -5527,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, @@ -5536,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": { @@ -5547,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" @@ -5554,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": { @@ -5615,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" @@ -5635,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": { @@ -5653,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": { @@ -5692,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": "*" }, @@ -5723,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" @@ -5730,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", @@ -5745,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" @@ -5752,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" @@ -5759,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" @@ -5782,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": { @@ -5797,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", @@ -5810,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, @@ -5819,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", @@ -5839,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", @@ -5854,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" @@ -5865,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" @@ -5884,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", @@ -5900,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" @@ -5907,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, @@ -5919,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 cb2a5d9..1d57d44 100644 --- a/package.json +++ b/package.json @@ -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 21e1a48..e6cac91 100644 --- a/src/app.css +++ b/src/app.css @@ -1,13 +1,13 @@ -@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"; /* 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 { @@ -26,8 +26,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 { @@ -45,11 +45,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 { @@ -61,19 +61,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)), @@ -88,7 +88,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 { @@ -115,21 +115,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 */ @@ -142,7 +142,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, @@ -151,7 +151,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 */ @@ -176,25 +176,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; } @@ -215,7 +215,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 { @@ -245,11 +245,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 */ @@ -276,7 +276,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; @@ -286,7 +285,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; } @@ -377,7 +376,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 { @@ -395,7 +394,6 @@ thead, tbody { - th, td { @apply border border-gray-200 dark:border-gray-700; @@ -425,10 +423,10 @@ padding-left: 1rem; } -.line-ellipsis { - overflow: hidden; - text-overflow: ellipsis; -} + .line-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + } .footnotes li { margin-bottom: 0.5rem; } @@ -494,7 +492,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 ab37d19..eacb2de 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -1,65 +1,135 @@
@@ -231,11 +333,126 @@ {#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} +
+
    + {#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..9f3fa3f 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -1,177 +1,548 @@
-
+ +
e.key === 'Enter' && searchEvent(true)} + onkeydown={(e: KeyboardEvent) => e.key === "Enter" && handleSearchEvent(true)} /> - +
- {#if localError || error} + + {#if showError} {/if}
{/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 new file mode 100644 index 0000000..e24490d --- /dev/null +++ b/src/lib/components/Login.svelte @@ -0,0 +1,78 @@ + + +
+ {#if $ndkSignedIn} + + {:else} + + +
+ + {#if signInFailed} +
+ {errorMessage} +
+ {/if} + +
+
+ {/if} +
diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte index f09ec13..465f180 100644 --- a/src/lib/components/LoginModal.svelte +++ b/src/lib/components/LoginModal.svelte @@ -1,5 +1,5 @@ -{#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/Preview.svelte b/src/lib/components/Preview.svelte index bfdeb51..77e465b 100644 --- a/src/lib/components/Preview.svelte +++ b/src/lib/components/Preview.svelte @@ -1,9 +1,24 @@ - -
-
+
{#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} -
diff --git a/src/lib/components/PublicationHeader.svelte b/src/lib/components/PublicationHeader.svelte index b575794..140b3d9 100644 --- a/src/lib/components/PublicationHeader.svelte +++ b/src/lib/components/PublicationHeader.svelte @@ -1,12 +1,13 @@ {#if title != null && href != null} - + {#if image} -
- - -
- {@render userBadge(event.pubkey, '')} +
+ + +
+ {@render userBadge(event.pubkey, '')} +
-
{/if} -
+
- +
diff --git a/src/lib/components/PublicationSection.svelte b/src/lib/components/PublicationSection.svelte index 6c2586a..7d60996 100644 --- a/src/lib/components/PublicationSection.svelte +++ b/src/lib/components/PublicationSection.svelte @@ -1,11 +1,15 @@ - -
- {#await Promise.all([leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches])} - +
+ {#await Promise.all( [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches], )} + {:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches]} + {@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, + )} {/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 9f3e10e..9b55a28 100644 --- a/src/lib/components/cards/BlogHeader.svelte +++ b/src/lib/components/cards/BlogHeader.svelte @@ -1,24 +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..b633360 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 839e374..ef6b45e 100644 --- a/src/lib/components/util/CardActions.svelte +++ b/src/lib/components/util/CardActions.svelte @@ -1,18 +1,19 @@ -
+
- {#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}
@@ -206,12 +236,12 @@ {#if identifier}
Identifier: {identifier}
{/if} - View Event Details - +
-
\ No newline at end of file +
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 @@ - - - - - +
+ + + +
- -

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 b9f236c..88ebe4c 100644 --- a/src/lib/components/util/Profile.svelte +++ b/src/lib/components/util/Profile.svelte @@ -1,94 +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} +
      +
    • + +
    • - {/if} -
    + {#if isNav} +
  • + +
  • + {:else} + + {/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..b1e4b3c 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 profileRelay = "wss://profiles.nostr1.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..e05db37 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,169 @@ 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, + }); - // Create the base node with essential properties - const node: NetworkNode = { + 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, + }; + + // 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 }); - - 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("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) => { + const aTags = getMatchingTags(event, "a"); + debug("Processing a-tags for event", { + eventId: event.id, + aTagCount: aTags.length, }); - 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); - }); + + aTags.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 +197,147 @@ 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 + 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); - 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); - - // 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); - }); +export function generateGraph(events: NDKEvent[], maxLevel: number): GraphData { + debug("Generating graph", { eventCount: events.length, maxLevel }); + + // Initialize the graph state + const state = initializeGraphState(events); + + // 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), + ); - // 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 + 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 9625aef..2e77c61 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -1,10 +1,283 @@ -import NDK, { 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([]); + +/** + * 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}`); + + // Ensure the URL is using wss:// protocol + const secureUrl = ensureSecureWebSocket(relayUrl); + + 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. @@ -41,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}`; } @@ -51,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)), ); } @@ -70,18 +347,84 @@ 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 { @@ -112,24 +455,114 @@ 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; } +/** + * Signs in with a NIP-07 browser extension, and determines the user's preferred inbox and outbox + * relays. + * @returns The user's profile, if it is available. + * @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 { + try { + const ndk = get(ndkInstance); + const signer = new NDKNip07Signer(); + const signerUser = await signer.user(); + + // TODO: Handle changing pubkeys. + if (pubkey && signerUser.pubkey !== pubkey) { + console.debug("[NDK.ts] Switching pubkeys from last login."); + } + + activePubkey.set(signerUser.pubkey); + userPubkey.set(signerUser.pubkey); + + const [persistedInboxes, persistedOutboxes] = + getPersistedRelays(signerUser); + for (const relay of persistedInboxes) { + ndk.addExplicitRelay(relay); + } + + const user = ndk.getUser({ pubkey: signerUser.pubkey }); + const [inboxes, outboxes] = await getUserPreferredRelays(ndk, user); + + inboxRelays.set( + Array.from(inboxes ?? persistedInboxes).map((relay) => relay.url), + ); + outboxRelays.set( + Array.from(outboxes ?? persistedOutboxes).map((relay) => relay.url), + ); + + persistRelays(signerUser, inboxes, outboxes); + + ndk.signer = signer; + ndk.activeUser = user; + + ndkInstance.set(ndk); + ndkSignedIn.set(true); + + return user; + } catch (e) { + throw new Error(`Failed to sign in with NIP-07 extension: ${e}`); + } +} + +/** + * Handles logging out a user. + * @param user The user to log out. + */ +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 +} + /** * Fetches the user's NIP-65 relay list, if one can be found, and returns the inbox and outbox * relay sets. @@ -138,14 +571,14 @@ export function initNdk(): NDK { 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, @@ -159,22 +592,22 @@ export async function getUserPreferredRelays( if (relayList == null) { const relayMap = await window.nostr?.getRelays?.(); Object.entries(relayMap ?? {}).forEach(([url, relayType]) => { - const relay = new NDKRelay(url, NDKRelayAuthPolicies.signIn({ ndk }), ndk); + const relay = createRelayWithAuth(url, ndk); if (relayType.read) inboxRelays.add(relay); if (relayType.write) outboxRelays.add(relay); }); } else { - relayList.tags.forEach(tag => { + relayList.tags.forEach((tag) => { switch (tag[0]) { - case 'r': - inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); + case "r": + inboxRelays.add(createRelayWithAuth(tag[1], ndk)); break; - case 'w': - outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); + case "w": + outboxRelays.add(createRelayWithAuth(tag[1], ndk)); break; default: - inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); - outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); + 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 7fc5632..daab606 100644 --- a/src/lib/snippets/UserSnippets.svelte +++ b/src/lib/snippets/UserSnippets.svelte @@ -54,6 +54,6 @@ {/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 new file mode 100644 index 0000000..2c038c7 --- /dev/null +++ b/src/lib/stores/relayStore.ts @@ -0,0 +1,4 @@ +import { writable } from "svelte/store"; + +// Initialize with empty array, will be populated from user preferences +export const userRelays = writable([]); 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..6f906e7 --- /dev/null +++ b/src/lib/utils/community_checker.ts @@ -0,0 +1,65 @@ +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 = {}; + + for (const profile of profiles) { + if (profile.pubkey) { + communityStatus[profile.pubkey] = await checkCommunity(profile.pubkey); + } + } + + 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..98c0fe0 --- /dev/null +++ b/src/lib/utils/event_search.ts @@ -0,0 +1,143 @@ +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}`); + } +} \ No newline at end of file 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..8a59d8e --- /dev/null +++ b/src/lib/utils/nostrEventService.ts @@ -0,0 +1,421 @@ +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 { + const nevent = nip19.neventEncode({ id: eventId }); + goto(`/events?id=${nevent}`); +} + +// 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 faa001b..dd272d4 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]); } /** @@ -71,29 +77,35 @@ export async function getUserMetadata(identifier: string, force = false): Promis // 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?.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,8 +117,11 @@ export async function getUserMetadata(identifier: string, force = false): Promis /** * 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); @@ -118,15 +133,18 @@ export function createProfileLink(identifier: string, displayText: string | unde /** * 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) { @@ -135,19 +153,23 @@ 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, + ); const allRelays = [ ...standardRelays, ...userRelays, - ...fallbackRelays + ...fallbackRelays, ].filter((url, idx, arr) => arr.indexOf(url) === idx); const relaySet = NDKRelaySetFromNDK.fromRelayUrls(allRelays, 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) { @@ -156,16 +178,20 @@ 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}`; @@ -177,18 +203,20 @@ export async function createProfileLinkWithVerification(identifier: string, disp * 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 @@ -207,8 +235,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; @@ -225,8 +253,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); @@ -237,19 +265,35 @@ 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 + const url = wellKnownUrl(domain, name); - const user = await ndk.getUser({ nip05 }); - if (!user || !user.npub) { + const response = await fetch(url); + if (!response.ok) { + console.error('[getNpubFromNip05] HTTP error:', response.status, response.statusText); return null; } - return user.npub; + + const data = await response.json(); + + const pubkey = data.names?.[name]; + 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 (error) { - console.error('Error getting npub from nip05:', error); + console.error("[getNpubFromNip05] Error getting npub from nip05:", error); return null; } } @@ -257,9 +301,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 @@ -267,28 +311,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), + ), ]); } @@ -299,7 +343,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); }; @@ -312,20 +359,24 @@ 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) + : []; + + // 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 { @@ -333,47 +384,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; } } @@ -384,10 +463,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; @@ -429,7 +508,7 @@ export function getEventHash(event: { event.created_at, event.kind, event.tags, - event.content + event.content, ]); return bytesToHex(sha256(serialized)); } @@ -444,4 +523,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..29dc408 --- /dev/null +++ b/src/lib/utils/profile_search.ts @@ -0,0 +1,233 @@ +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 { communityRelay, profileRelay } 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 = searchTerm.toLowerCase().trim(); + + // Check cache first + const cachedResult = searchCache.get('profile', normalizedSearchTerm); + if (cachedResult) { + const profiles = cachedResult.events.map(event => { + try { + const profileData = JSON.parse(event.content); + return createProfileFromEvent(event, profileData); + } catch { + return null; + } + }).filter(Boolean) as NostrProfile[]; + + const communityStatus = await checkCommunityStatus(profiles); + return { profiles, Status: communityStatus }; + } + + const ndk = get(ndkInstance); + if (!ndk) { + throw new Error('NDK not initialized'); + } + + let foundProfiles: NostrProfile[] = []; + let timeoutId: ReturnType | null = null; + + // Set a timeout to force completion after profile search timeout + timeoutId = setTimeout(() => { + if (foundProfiles.length === 0) { + // Timeout reached, but no need to log this + } + }, TIMEOUTS.PROFILE_SEARCH); + + 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 + try { + const npub = await getNpubFromNip05(normalizedSearchTerm); + 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); + // If NIP-05 lookup fails, continue with regular search + } + } else { + // Try searching for NIP-05 addresses that match the search term + foundProfiles = await searchNip05Domains(normalizedSearchTerm, ndk); + + // If no NIP-05 results found, search for profiles across relays + if (foundProfiles.length === 0) { + foundProfiles = await searchProfilesAcrossRelays(normalizedSearchTerm, ndk); + } + } + + // Wait for search to complete or timeout + await new Promise((resolve) => { + const checkComplete = () => { + if (timeoutId === null || foundProfiles.length > 0) { + resolve(); + } else { + setTimeout(checkComplete, 100); + } + }; + checkComplete(); + }); + + // 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); + } + + // Check community status for all profiles + const communityStatus = await checkCommunityStatus(foundProfiles); + return { profiles: foundProfiles, Status: communityStatus }; + + } catch (error) { + console.error('Error searching profiles:', error); + return { profiles: [], Status: {} }; + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } + } +} + +/** + * Search for NIP-05 addresses across common domains + */ +async function searchNip05Domains(searchTerm: string, ndk: any): Promise { + try { + for (const domain of COMMON_DOMAINS) { + const nip05Address = `${searchTerm}@${domain}`; + try { + const npub = await getNpubFromNip05(nip05Address); + if (npub) { + const metadata = await getUserMetadata(npub); + const profile: NostrProfile = { + ...metadata, + pubkey: npub + }; + return [profile]; + } + } catch (e) { + // Continue to next domain + } + } + } catch (e) { + console.error('[Search] NIP-05 domain search failed:', e); + } + return []; +} + +/** + * Search for profiles across relays + */ +async function searchProfilesAcrossRelays(searchTerm: string, ndk: any): Promise { + const foundProfiles: NostrProfile[] = []; + + // Prioritize community relays for better search results + const allRelays = Array.from(ndk.pool.relays.values()) as any[]; + const prioritizedRelays = new Set([ + ...allRelays.filter((relay: any) => relay.url === communityRelay), + ...allRelays.filter((relay: any) => relay.url !== communityRelay) + ]); + const relaySet = new NDKRelaySet(prioritizedRelays as any, ndk); + + // Subscribe to profile events + const sub = ndk.subscribe( + { kinds: [0] }, + { closeOnEose: true }, + relaySet + ); + + return new Promise((resolve) => { + sub.on('event', (event: NDKEvent) => { + 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 + const matchesDisplayName = fieldMatches(displayName, searchTerm); + const matchesDisplay_name = fieldMatches(display_name, searchTerm); + const matchesName = fieldMatches(name, searchTerm); + const matchesNip05 = nip05Matches(nip05, searchTerm); + const matchesAbout = fieldMatches(about, searchTerm); + + if (matchesDisplayName || matchesDisplay_name || matchesName || matchesNip05 || matchesAbout) { + const profile = createProfileFromEvent(event, profileData); + + // Check if we already have this profile + const existingIndex = foundProfiles.findIndex(p => p.pubkey === event.pubkey); + if (existingIndex === -1) { + foundProfiles.push(profile); + } + } + } catch (e) { + // Invalid JSON or other error, skip + } + }); + + sub.on('eose', () => { + if (foundProfiles.length > 0) { + // Deduplicate by pubkey, keep only newest + const deduped: Record = {}; + for (const profile of foundProfiles) { + const pubkey = profile.pubkey; + if (pubkey) { + // We don't have created_at from getUserMetadata, so just keep the first one + if (!deduped[pubkey]) { + deduped[pubkey] = { profile, created_at: 0 }; + } + } + } + const dedupedProfiles = Object.values(deduped).map(x => x.profile); + resolve(dedupedProfiles); + } else { + resolve([]); + } + }); + }); +} \ 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..2fa927a --- /dev/null +++ b/src/lib/utils/search_constants.ts @@ -0,0 +1,121 @@ +/** + * 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: 30000, + + /** 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..30f4dda --- /dev/null +++ b/src/lib/utils/subscription_search.ts @@ -0,0 +1,651 @@ +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, profileRelay } 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 relay first + const profileRelays = Array.from(ndk.pool.relays.values()).filter((relay: any) => + relay.url === profileRelay || relay.url === profileRelay + '/' + ); + return new NDKRelaySet(new Set(profileRelays) as any, ndk); + } else { + // For other searches, use community relay first + const communityRelays = Array.from(ndk.pool.relays.values()).filter((relay: any) => + relay.url === communityRelay || relay.url === communityRelay + '/' + ); + return new NDKRelaySet(new Set(communityRelays) 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); + } + const aTags = getMatchingTags(event, "a"); + aTags.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 : searchState.firstOrderEvents, + secondOrder: [], + tTagEvents: searchType === 't' ? searchState.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 relay from fallback search + return 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: [], + secondOrder: [], + tTagEvents: searchState.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[] = []; + + 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') { + // Search for events that reference the original events via e-tags and a-tags + + // Search for events that reference the original events via e-tags + if (eventIds.size > 0) { + const eTagFilter = { "#e": Array.from(eventIds) }; + const eTagEvents = await ndk.fetchEvents( + eTagFilter, + { closeOnEose: true }, + new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), + ); + + // Filter out emoji reactions + const filteredETagEvents = Array.from(eTagEvents).filter(event => !isEmojiReaction(event)); + allSecondOrderEvents = [...allSecondOrderEvents, ...filteredETagEvents]; + } + + // Search for events that reference the original events via a-tags + if (addresses.size > 0) { + const aTagFilter = { "#a": Array.from(addresses) }; + const aTagEvents = await ndk.fetchEvents( + aTagFilter, + { closeOnEose: true }, + new NDKRelaySet(new Set(Array.from(ndk.pool.relays.values())), ndk), + ); + + // Filter out emoji reactions + const filteredATagEvents = Array.from(aTagEvents).filter(event => !isEmojiReaction(event)); + allSecondOrderEvents = [...allSecondOrderEvents, ...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); + } + + } 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 df15c76..d4102db 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -11,8 +11,9 @@ 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(); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 96d070b..27c3693 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,8 +1,14 @@ - - - - - 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 !user.signedIn} - - {:else} -
- - + {/if} + + + {#if $ndkSignedIn}
  • - Alexandria's Relays + Alexandria's Relays
  • - Your Relays + Your Relays
  • -
    - {#if $feedType === FeedType.StandardRelays} - - {:else if $feedType === FeedType.UserRelays} - {/if} +
    + {#if !$ndkSignedIn} + + {:else if $feedType === FeedType.StandardRelays} + + {:else if $feedType === FeedType.UserRelays} + {/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 b5b7c09..caabdcb 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -2,6 +2,7 @@ import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { Heading, Img, P, A } from "flowbite-svelte"; import { goto } from '$app/navigation'; + import RelayStatus from "$lib/components/RelayStatus.svelte"; // Get the git tag version from environment variables const appVersion = import.meta.env.APP_VERSION || "development"; @@ -16,7 +17,7 @@ > {#if isVersionKnown} Version: {appVersion} {/if} @@ -44,11 +45,18 @@

    - We are easiest to contact over our Nostr address {@render userBadge("npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz", "GitCitadel")}. Or, you can visit us on our homepage and find out more about us, and the many projects we are working on.

    + +
    + +
    diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte index df9453e..90539ae 100644 --- a/src/routes/contact/+page.svelte +++ b/src/routes/contact/+page.svelte @@ -1,105 +1,107 @@ - -
    -
    - 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.ts b/src/routes/publication/+page.ts index b100f70..b3d0885 100644 --- a/src/routes/publication/+page.ts +++ b/src/routes/publication/+page.ts @@ -1,28 +1,28 @@ -import { error } from '@sveltejs/kit'; -import type { Load } from '@sveltejs/kit'; -import type { NDKEvent } from '@nostr-dev-kit/ndk'; -import { nip19 } from 'nostr-tools'; -import { getActiveRelays } from '$lib/ndk'; -import { getMatchingTags } from '$lib/utils/nostrUtils'; +import { error } from "@sveltejs/kit"; +import type { Load } from "@sveltejs/kit"; +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import { nip19 } from "nostr-tools"; +import { getActiveRelays } from "$lib/ndk"; +import { getMatchingTags } from "$lib/utils/nostrUtils"; /** * Decodes an naddr identifier and returns a filter object */ function decodeNaddr(id: string) { try { - if (!id.startsWith('naddr')) return {}; - + if (!id.startsWith("naddr")) return {}; + const decoded = nip19.decode(id); - if (decoded.type !== 'naddr') return {}; - + if (decoded.type !== "naddr") return {}; + const data = decoded.data; return { kinds: [data.kind], authors: [data.pubkey], - '#d': [data.identifier] + "#d": [data.identifier], }; } catch (e) { - console.error('Failed to decode naddr:', e); + console.error("Failed to decode naddr:", e); return null; } } @@ -32,7 +32,7 @@ function decodeNaddr(id: string) { */ async function fetchEventById(ndk: any, id: string): Promise { 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 04177de..8a26909 100644 --- a/src/routes/start/+page.svelte +++ b/src/routes/start/+page.svelte @@ -55,10 +55,9 @@ Each content section (30041 or 30818) is also a level in the table of contents, which can be accessed from the floating icon top-left in the reading view. This allows for navigation within the publication. - Publications of type "blog" have a ToC which emphasizes that each entry - is a blog post. - - (This functionality has been temporarily disabled, but the TOC is visible.) + Publications of type "blog" have a ToC which emphasizes that each entry is + a blog post. (This functionality has been temporarily disabled, but the + TOC is visible.)

      diff --git a/src/routes/visualize/+page.svelte b/src/routes/visualize/+page.svelte index 629199f..ba2901f 100644 --- a/src/routes/visualize/+page.svelte +++ b/src/routes/visualize/+page.svelte @@ -11,12 +11,12 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { filterValidIndexEvents } from "$lib/utils"; import { networkFetchLimit } from "$lib/state"; - + // Configuration const DEBUG = false; // Set to true to enable debug logging const INDEX_EVENT_KIND = 30040; const CONTENT_EVENT_KINDS = [30041, 30818]; - + /** * Debug logging function that only logs when DEBUG is true */ @@ -34,7 +34,7 @@ /** * Fetches events from the Nostr network - * + * * This function fetches index events and their referenced content events, * filters them according to NIP-62, and combines them for visualization. */ @@ -47,9 +47,9 @@ // Step 1: Fetch index events debug(`Fetching index events (kind ${INDEX_EVENT_KIND})`); const indexEvents = await $ndkInstance.fetchEvents( - { - kinds: [INDEX_EVENT_KIND], - limit: $networkFetchLimit + { + kinds: [INDEX_EVENT_KIND], + limit: $networkFetchLimit, }, { groupable: true, @@ -68,7 +68,7 @@ validIndexEvents.forEach((event) => { const aTags = event.getMatchingTags("a"); debug(`Event ${event.id} has ${aTags.length} a-tags`); - + aTags.forEach((tag) => { const eventId = tag[3]; if (eventId) { @@ -79,7 +79,9 @@ debug("Content event IDs to fetch:", contentEventIds.size); // Step 4: Fetch the referenced content events - debug(`Fetching content events (kinds ${CONTENT_EVENT_KINDS.join(', ')})`); + debug( + `Fetching content events (kinds ${CONTENT_EVENT_KINDS.join(", ")})`, + ); const contentEvents = await $ndkInstance.fetchEvents( { kinds: CONTENT_EVENT_KINDS, @@ -104,7 +106,6 @@ } } - // Fetch events when component mounts onMount(() => { debug("Component mounted"); @@ -123,7 +124,7 @@
      - + {:else if error}
      - + {:else} diff --git a/src/styles/base.css b/src/styles/base.css index e655206..943b334 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -3,7 +3,7 @@ @tailwind utilities; @layer components { - body { - @apply bg-primary-0 dark:bg-primary-1000; - } -} \ No newline at end of file + body { + @apply bg-primary-0 dark:bg-primary-1000; + } +} diff --git a/src/styles/events.css b/src/styles/events.css index 9e8c202..3c61536 100644 --- a/src/styles/events.css +++ b/src/styles/events.css @@ -1,5 +1,5 @@ @layer components { - canvas.qr-code { - @apply block mx-auto my-4; - } -} \ No newline at end of file + canvas.qr-code { + @apply block mx-auto my-4; + } +} diff --git a/src/styles/publications.css b/src/styles/publications.css index f5c643c..71b70b6 100644 --- a/src/styles/publications.css +++ b/src/styles/publications.css @@ -1,288 +1,288 @@ @layer components { - /* AsciiDoc content */ - .publication-leather p a { - @apply underline hover:text-primary-500 dark:hover:text-primary-400; - } - - .publication-leather section p { - @apply w-full; - } - - .publication-leather section p table { - @apply w-full table-fixed space-x-2 space-y-2; - } - - .publication-leather section p table td { - @apply p-2; - } - - .publication-leather section p table td .content:has(> .imageblock) { - @apply flex flex-col items-center; - } - - .publication-leather .imageblock { - @apply flex flex-col space-y-2; - } - - .publication-leather .imageblock .content { - @apply flex justify-center; - } - .publication-leather .imageblock .title { - @apply text-center; - } - - .publication-leather .imageblock.left .content { - @apply justify-start; - } - .publication-leather .imageblock.left .title { - @apply text-left; - } - - .publication-leather .imageblock.right .content { - @apply justify-end; - } - .publication-leather .imageblock.right .title { - @apply text-right; - } - - .publication-leather section p table td .literalblock { - @apply my-2 p-2 border rounded border-gray-400 dark:border-gray-600; - } - - .publication-leather .literalblock pre { - @apply p-3 text-wrap break-words; - } - - .publication-leather .listingblock pre { - @apply overflow-x-auto; - } - - /* lists */ - .publication-leather .ulist ul { - @apply space-y-1 list-disc list-inside; - } - - .publication-leather .olist ol { - @apply space-y-1 list-inside; - } - - .publication-leather ol.arabic { - @apply list-decimal; - } - - .publication-leather ol.loweralpha { - @apply list-lower-alpha; - } - - .publication-leather ol.upperalpha { - @apply list-upper-alpha; - } - - .publication-leather li ol, - .publication-leather li ul { - @apply ps-5 my-2; - } - - .audioblock .title, - .imageblock .title, - .literalblock .title, - .tableblock .title, - .videoblock .title, - .olist .title, - .ulist .title { - @apply my-2 font-thin text-lg; - } - - .publication-leather li p { - @apply inline; - } - - /* blockquote; prose and poetry quotes */ - .publication-leather .quoteblock, - .publication-leather .verseblock { - @apply p-4 my-4 border-s-4 rounded border-primary-300 bg-primary-50 dark:border-primary-500 dark:bg-primary-700; - } - - .publication-leather .verseblock pre.content { - @apply text-base font-sans overflow-x-scroll py-1; - } - - .publication-leather .attribution { - @apply mt-3 italic clear-both; - } - - .publication-leather cite { - @apply text-sm; - } - - .leading-normal.first-letter\:text-7xl .quoteblock { - min-height: 108px; - } - - /* admonition */ - .publication-leather .admonitionblock .title { - @apply font-semibold; - } - - .publication-leather .admonitionblock table { - @apply w-full border-collapse; - } - - .publication-leather .admonitionblock tr { - @apply flex flex-col border-none; - } - - .publication-leather .admonitionblock td { - @apply border-none; - } - - .publication-leather .admonitionblock p:has(code) { - @apply my-3; - } - - .publication-leather .admonitionblock { - @apply rounded overflow-hidden border; - } - - .publication-leather .admonitionblock .icon, - .publication-leather .admonitionblock .content { - @apply p-4; - } - - .publication-leather .admonitionblock .content { - @apply pt-0; - } - - .publication-leather .admonitionblock.tip { - @apply rounded overflow-hidden border border-success-100 dark:border-success-800; - } - - .publication-leather .admonitionblock.tip .icon, - .publication-leather .admonitionblock.tip .content { - @apply bg-success-100 dark:bg-success-800; - } - - .publication-leather .admonitionblock.note { - @apply rounded overflow-hidden border border-info-100 dark:border-info-700; - } - - .publication-leather .admonitionblock.note .icon, - .publication-leather .admonitionblock.note .content { - @apply bg-info-100 dark:bg-info-800; - } - - .publication-leather .admonitionblock.important { - @apply rounded overflow-hidden border border-primary-200 dark:border-primary-700; - } - - .publication-leather .admonitionblock.important .icon, - .publication-leather .admonitionblock.important .content { - @apply bg-primary-200 dark:bg-primary-700; - } - - .publication-leather .admonitionblock.caution { - @apply rounded overflow-hidden border border-warning-200 dark:border-warning-700; - } - - .publication-leather .admonitionblock.caution .icon, - .publication-leather .admonitionblock.caution .content { - @apply bg-warning-200 dark:bg-warning-700; - } - - .publication-leather .admonitionblock.warning { - @apply rounded overflow-hidden border border-danger-200 dark:border-danger-800; - } - - .publication-leather .admonitionblock.warning .icon, - .publication-leather .admonitionblock.warning .content { - @apply bg-danger-200 dark:bg-danger-800; - } - - /* listingblock, literalblock */ - .publication-leather .listingblock, - .publication-leather .literalblock { - @apply p-4 rounded bg-highlight dark:bg-primary-700; - } - - .publication-leather .sidebarblock .title, - .publication-leather .listingblock .title, - .publication-leather .literalblock .title { - @apply font-semibold mb-1; - } - - /* sidebar */ - .publication-leather .sidebarblock { - @apply p-4 rounded bg-info-100 dark:bg-info-800; - } - - /* video */ - .videoblock .content { - @apply w-full aspect-video; - } - - .videoblock .content iframe, - .videoblock .content video { - @apply w-full h-full; - } - - /* audio */ - .audioblock .content { - @apply my-3; - } - - .audioblock .content audio { - @apply w-full; - } - - .coverImage { - @apply max-h-[230px] overflow-hidden; - } - - .coverImage.depth-0 { - @apply max-h-[460px] overflow-hidden; - } - - .coverImage img { - @apply object-contain w-full; - } - - .coverImage.depth-0 img { - @apply m-auto w-auto; - } - - /** blog */ - @screen lg { - @media (hover: hover) { - .blog .discreet .card-leather:not(:hover) { - @apply bg-primary-50 dark:bg-primary-1000 opacity-75 transition duration-500 ease-in-out ; - } - .blog .discreet .group { - @apply bg-transparent; - } - } - } - - /* Discrete headers */ - h3.discrete, - h4.discrete, - h5.discrete, - h6.discrete { - @apply text-gray-800 dark:text-gray-300; - } - - h3.discrete { - @apply text-2xl font-bold; - } - - h4.discrete { - @apply text-xl font-bold; - } - - h5.discrete { - @apply text-lg font-semibold; - } - - h6.discrete { - @apply text-base font-semibold; - } -} \ No newline at end of file + /* AsciiDoc content */ + .publication-leather p a { + @apply underline hover:text-primary-600 dark:hover:text-primary-400; + } + + .publication-leather section p { + @apply w-full; + } + + .publication-leather section p table { + @apply w-full table-fixed space-x-2 space-y-2; + } + + .publication-leather section p table td { + @apply p-2; + } + + .publication-leather section p table td .content:has(> .imageblock) { + @apply flex flex-col items-center; + } + + .publication-leather .imageblock { + @apply flex flex-col space-y-2; + } + + .publication-leather .imageblock .content { + @apply flex justify-center; + } + .publication-leather .imageblock .title { + @apply text-center; + } + + .publication-leather .imageblock.left .content { + @apply justify-start; + } + .publication-leather .imageblock.left .title { + @apply text-left; + } + + .publication-leather .imageblock.right .content { + @apply justify-end; + } + .publication-leather .imageblock.right .title { + @apply text-right; + } + + .publication-leather section p table td .literalblock { + @apply my-2 p-2 border rounded border-gray-400 dark:border-gray-600; + } + + .publication-leather .literalblock pre { + @apply p-3 text-wrap break-words; + } + + .publication-leather .listingblock pre { + @apply overflow-x-auto; + } + + /* lists */ + .publication-leather .ulist ul { + @apply space-y-1 list-disc list-inside; + } + + .publication-leather .olist ol { + @apply space-y-1 list-inside; + } + + .publication-leather ol.arabic { + @apply list-decimal; + } + + .publication-leather ol.loweralpha { + @apply list-lower-alpha; + } + + .publication-leather ol.upperalpha { + @apply list-upper-alpha; + } + + .publication-leather li ol, + .publication-leather li ul { + @apply ps-5 my-2; + } + + .audioblock .title, + .imageblock .title, + .literalblock .title, + .tableblock .title, + .videoblock .title, + .olist .title, + .ulist .title { + @apply my-2 font-thin text-lg; + } + + .publication-leather li p { + @apply inline; + } + + /* blockquote; prose and poetry quotes */ + .publication-leather .quoteblock, + .publication-leather .verseblock { + @apply p-4 my-4 border-s-4 rounded border-primary-300 bg-primary-50 dark:border-primary-500 dark:bg-primary-700; + } + + .publication-leather .verseblock pre.content { + @apply text-base font-sans overflow-x-scroll py-1; + } + + .publication-leather .attribution { + @apply mt-3 italic clear-both; + } + + .publication-leather cite { + @apply text-sm; + } + + .leading-normal.first-letter\:text-7xl .quoteblock { + min-height: 108px; + } + + /* admonition */ + .publication-leather .admonitionblock .title { + @apply font-semibold; + } + + .publication-leather .admonitionblock table { + @apply w-full border-collapse; + } + + .publication-leather .admonitionblock tr { + @apply flex flex-col border-none; + } + + .publication-leather .admonitionblock td { + @apply border-none; + } + + .publication-leather .admonitionblock p:has(code) { + @apply my-3; + } + + .publication-leather .admonitionblock { + @apply rounded overflow-hidden border; + } + + .publication-leather .admonitionblock .icon, + .publication-leather .admonitionblock .content { + @apply p-4; + } + + .publication-leather .admonitionblock .content { + @apply pt-0; + } + + .publication-leather .admonitionblock.tip { + @apply rounded overflow-hidden border border-success-100 dark:border-success-800; + } + + .publication-leather .admonitionblock.tip .icon, + .publication-leather .admonitionblock.tip .content { + @apply bg-success-100 dark:bg-success-800; + } + + .publication-leather .admonitionblock.note { + @apply rounded overflow-hidden border border-info-100 dark:border-info-700; + } + + .publication-leather .admonitionblock.note .icon, + .publication-leather .admonitionblock.note .content { + @apply bg-info-100 dark:bg-info-800; + } + + .publication-leather .admonitionblock.important { + @apply rounded overflow-hidden border border-primary-200 dark:border-primary-700; + } + + .publication-leather .admonitionblock.important .icon, + .publication-leather .admonitionblock.important .content { + @apply bg-primary-200 dark:bg-primary-700; + } + + .publication-leather .admonitionblock.caution { + @apply rounded overflow-hidden border border-warning-200 dark:border-warning-700; + } + + .publication-leather .admonitionblock.caution .icon, + .publication-leather .admonitionblock.caution .content { + @apply bg-warning-200 dark:bg-warning-700; + } + + .publication-leather .admonitionblock.warning { + @apply rounded overflow-hidden border border-danger-200 dark:border-danger-800; + } + + .publication-leather .admonitionblock.warning .icon, + .publication-leather .admonitionblock.warning .content { + @apply bg-danger-200 dark:bg-danger-800; + } + + /* listingblock, literalblock */ + .publication-leather .listingblock, + .publication-leather .literalblock { + @apply p-4 rounded bg-highlight dark:bg-primary-700; + } + + .publication-leather .sidebarblock .title, + .publication-leather .listingblock .title, + .publication-leather .literalblock .title { + @apply font-semibold mb-1; + } + + /* sidebar */ + .publication-leather .sidebarblock { + @apply p-4 rounded bg-info-100 dark:bg-info-800; + } + + /* video */ + .videoblock .content { + @apply w-full aspect-video; + } + + .videoblock .content iframe, + .videoblock .content video { + @apply w-full h-full; + } + + /* audio */ + .audioblock .content { + @apply my-3; + } + + .audioblock .content audio { + @apply w-full; + } + + .coverImage { + @apply max-h-[230px] overflow-hidden; + } + + .coverImage.depth-0 { + @apply max-h-[460px] overflow-hidden; + } + + .coverImage img { + @apply object-contain w-full; + } + + .coverImage.depth-0 img { + @apply m-auto w-auto; + } + + /** blog */ + @screen lg { + @media (hover: hover) { + .blog .discreet .card-leather:not(:hover) { + @apply bg-primary-50 dark:bg-primary-1000 opacity-75 transition duration-500 ease-in-out; + } + .blog .discreet .group { + @apply bg-transparent; + } + } + } + + /* Discrete headers */ + h3.discrete, + h4.discrete, + h5.discrete, + h6.discrete { + @apply text-gray-800 dark:text-gray-300; + } + + h3.discrete { + @apply text-2xl font-bold; + } + + h4.discrete { + @apply text-xl font-bold; + } + + h5.discrete { + @apply text-lg font-semibold; + } + + h6.discrete { + @apply text-base font-semibold; + } +} diff --git a/src/styles/scrollbar.css b/src/styles/scrollbar.css index 8d2735d..4691a9b 100644 --- a/src/styles/scrollbar.css +++ b/src/styles/scrollbar.css @@ -1,20 +1,20 @@ @layer components { - /* Global scrollbar styles */ - * { - scrollbar-color: rgba(87, 66, 41, 0.8) transparent; /* Transparent track, default scrollbar thumb */ - } + /* Global scrollbar styles */ + * { + scrollbar-color: rgba(87, 66, 41, 0.8) transparent; /* Transparent track, default scrollbar thumb */ + } - /* Webkit Browsers (Chrome, Safari, Edge) */ - *::-webkit-scrollbar { - width: 12px; /* Thin scrollbar */ - } + /* Webkit Browsers (Chrome, Safari, Edge) */ + *::-webkit-scrollbar { + width: 12px; /* Thin scrollbar */ + } - *::-webkit-scrollbar-track { - background: transparent; /* Fully transparent track */ - } + *::-webkit-scrollbar-track { + background: transparent; /* Fully transparent track */ + } - *::-webkit-scrollbar-thumb { - @apply bg-primary-500 dark:bg-primary-600 hover:bg-primary-600 dark:hover:bg-primary-800;; - border-radius: 6px; /* Rounded scrollbar */ - } -} \ No newline at end of file + *::-webkit-scrollbar-thumb { + @apply bg-primary-500 dark:bg-primary-600 hover:bg-primary-600 dark:hover:bg-primary-800; + border-radius: 6px; /* Rounded scrollbar */ + } +} diff --git a/src/styles/visualize.css b/src/styles/visualize.css index 1ff732d..a2f8374 100644 --- a/src/styles/visualize.css +++ b/src/styles/visualize.css @@ -1,112 +1,112 @@ @layer components { - /* Legend styles - specific to visualization */ - .legend-list { - @apply list-disc mt-2 space-y-2 text-gray-800 dark:text-gray-300; - } - - .legend-item { - @apply flex items-center; - } - - .legend-icon { - @apply relative w-6 h-6 mr-2; - } - - .legend-circle { - @apply absolute inset-0 rounded-full border-2 border-black; - } - - .legend-circle.content { - @apply bg-gray-700 dark:bg-gray-300; - background-color: #d6c1a8; - } - - .legend-circle.content { - background-color: var(--content-color, #d6c1a8); - } - - :global(.dark) .legend-circle.content { - background-color: var(--content-color-dark, #FFFFFF); - } - - .legend-letter { - @apply absolute inset-0 flex items-center justify-center text-black text-xs font-bold; - } - - .legend-text { - @apply text-sm; - } - - /* Network visualization styles - specific to visualization */ - .network-container { - @apply flex flex-col w-full h-[calc(100vh-138px)] min-h-[400px] max-h-[900px]; - } - - .network-svg-container { - @apply relative sm:h-[100%]; - } - - .network-svg { - @apply w-full sm:h-[100%] border; - @apply border border-primary-200 has-[:hover]:border-primary-700 dark:bg-primary-1000 dark:border-primary-800 dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500 rounded; - } - - .network-error { - @apply w-full p-4 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 rounded-lg mb-4; - } - - .network-error-title { - @apply font-bold text-lg; - } - - .network-error-retry { - @apply mt-2 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700; - } - - .network-debug { - @apply mt-4 text-sm text-gray-500; - } - - /* Zoom controls */ - .network-controls { - @apply absolute bottom-4 right-4 flex flex-col gap-2 z-10; - } - - .network-control-button { - @apply bg-white; - } - - /* Tooltip styles - specific to visualization tooltips */ - .tooltip-close-btn { - @apply absolute top-2 right-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 + /* Legend styles - specific to visualization */ + .legend-list { + @apply list-disc mt-2 space-y-2 text-gray-800 dark:text-gray-300; + } + + .legend-item { + @apply flex items-center; + } + + .legend-icon { + @apply relative w-6 h-6 mr-2; + } + + .legend-circle { + @apply absolute inset-0 rounded-full border-2 border-black; + } + + .legend-circle.content { + @apply bg-gray-700 dark:bg-gray-300; + background-color: #d6c1a8; + } + + .legend-circle.content { + background-color: var(--content-color, #d6c1a8); + } + + :global(.dark) .legend-circle.content { + background-color: var(--content-color-dark, #ffffff); + } + + .legend-letter { + @apply absolute inset-0 flex items-center justify-center text-black text-xs font-bold; + } + + .legend-text { + @apply text-sm; + } + + /* Network visualization styles - specific to visualization */ + .network-container { + @apply flex flex-col w-full h-[calc(100vh-138px)] min-h-[400px] max-h-[900px]; + } + + .network-svg-container { + @apply relative sm:h-[100%]; + } + + .network-svg { + @apply w-full sm:h-[100%] border; + @apply border border-primary-200 has-[:hover]:border-primary-700 dark:bg-primary-1000 dark:border-primary-800 dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500 rounded; + } + + .network-error { + @apply w-full p-4 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 rounded-lg mb-4; + } + + .network-error-title { + @apply font-bold text-lg; + } + + .network-error-retry { + @apply mt-2 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700; + } + + .network-debug { + @apply mt-4 text-sm text-gray-500; + } + + /* Zoom controls */ + .network-controls { + @apply absolute bottom-4 right-4 flex flex-col gap-2 z-10; + } + + .network-control-button { + @apply bg-white; + } + + /* Tooltip styles - specific to visualization tooltips */ + .tooltip-close-btn { + @apply absolute top-2 right-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-full p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200; - } + } - .tooltip-content { - @apply space-y-2 pr-6; - } + .tooltip-content { + @apply space-y-2 pr-6; + } - .tooltip-title { - @apply font-bold text-base; - } + .tooltip-title { + @apply font-bold text-base; + } - .tooltip-title-link { - @apply text-gray-800 hover:text-blue-600 dark:text-gray-200 dark:hover:text-blue-400; - } + .tooltip-title-link { + @apply text-gray-800 hover:text-blue-600 dark:text-gray-200 dark:hover:text-blue-400; + } - .tooltip-metadata { - @apply text-gray-600 dark:text-gray-400 text-sm; - } + .tooltip-metadata { + @apply text-gray-600 dark:text-gray-400 text-sm; + } - .tooltip-summary { - @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto max-h-40; - } + .tooltip-summary { + @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto max-h-40; + } - .tooltip-content-preview { - @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto max-h-40; - } + .tooltip-content-preview { + @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto max-h-40; + } - .tooltip-help-text { - @apply mt-2 text-xs text-gray-500 dark:text-gray-400 italic; - } + .tooltip-help-text { + @apply mt-2 text-xs text-gray-500 dark:text-gray-400 italic; + } } diff --git a/src/types/d3.d.ts b/src/types/d3.d.ts index 3d230f5..2b12771 100644 --- a/src/types/d3.d.ts +++ b/src/types/d3.d.ts @@ -1,19 +1,19 @@ /** * Type declarations for D3.js and related modules - * + * * These declarations allow TypeScript to recognize D3 imports without requiring * detailed type definitions. For a project requiring more type safety, consider * using the @types/d3 package and its related sub-packages. */ // Core D3 library -declare module 'd3'; +declare module "d3"; // Force simulation module for graph layouts -declare module 'd3-force'; +declare module "d3-force"; // DOM selection and manipulation module -declare module 'd3-selection'; +declare module "d3-selection"; // Drag behavior module -declare module 'd3-drag'; +declare module "d3-drag"; diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000..4e2e76a --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,5 @@ +interface Window { + hljs?: { + highlightAll: () => void; + }; +} diff --git a/src/types/plantuml-encoder.d.ts b/src/types/plantuml-encoder.d.ts new file mode 100644 index 0000000..0e6c137 --- /dev/null +++ b/src/types/plantuml-encoder.d.ts @@ -0,0 +1,5 @@ +declare module "plantuml-encoder" { + export function encode(text: string): string; + const _default: { encode: typeof encode }; + export default _default; +} diff --git a/tailwind.config.cjs b/tailwind.config.cjs index e28c2eb..5bd3b5f 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -12,110 +12,110 @@ const config = { theme: { extend: { colors: { - highlight: '#f9f6f1', + highlight: "#f9f6f1", primary: { - 0: '#efe6dc', - 50: '#decdb9', - 100: '#d6c1a8', - 200: '#c6a885', - 300: '#b58f62', - 400: '#ad8351', - 500: '#c6a885', - 600: '#795c39', - 700: '#564a3e', - 800: '#3c352c', - 900: '#2a241c', - 950: '#1d1812', - 1000: '#15110d', + 0: "#efe6dc", + 50: "#decdb9", + 100: "#d6c1a8", + 200: "#c6a885", + 300: "#b58f62", + 400: "#ad8351", + 500: "#c6a885", + 600: "#795c39", + 700: "#564a3e", + 800: "#3c352c", + 900: "#2a241c", + 950: "#1d1812", + 1000: "#15110d", }, success: { - 50: '#e3f2e7', - 100: '#c7e6cf', - 200: '#a2d4ae', - 300: '#7dbf8e', - 400: '#5ea571', - 500: '#4e8e5f', - 600: '#3e744c', - 700: '#305b3b', - 800: '#22412a', - 900: '#15281b', + 50: "#e3f2e7", + 100: "#c7e6cf", + 200: "#a2d4ae", + 300: "#7dbf8e", + 400: "#5ea571", + 500: "#4e8e5f", + 600: "#3e744c", + 700: "#305b3b", + 800: "#22412a", + 900: "#15281b", }, info: { - 50: '#e7eff6', - 100: '#c5d9ea', - 200: '#9fbfdb', - 300: '#7aa5cc', - 400: '#5e90be', - 500: '#4779a5', - 600: '#365d80', - 700: '#27445d', - 800: '#192b3a', - 900: '#0d161f', + 50: "#e7eff6", + 100: "#c5d9ea", + 200: "#9fbfdb", + 300: "#7aa5cc", + 400: "#5e90be", + 500: "#4779a5", + 600: "#365d80", + 700: "#27445d", + 800: "#192b3a", + 900: "#0d161f", }, warning: { - 50: '#fef4e6', - 100: '#fde4bf', - 200: '#fcd18e', - 300: '#fbbc5c', - 400: '#f9aa33', - 500: '#f7971b', - 600: '#c97a14', - 700: '#9a5c0e', - 800: '#6c3e08', - 900: '#3e2404', + 50: "#fef4e6", + 100: "#fde4bf", + 200: "#fcd18e", + 300: "#fbbc5c", + 400: "#f9aa33", + 500: "#f7971b", + 600: "#c97a14", + 700: "#9a5c0e", + 800: "#6c3e08", + 900: "#3e2404", }, danger: { - 50: '#fbeaea', - 100: '#f5cccc', - 200: '#eba5a5', - 300: '#e17e7e', - 400: '#d96060', - 500: '#c94848', - 600: '#a53939', - 700: '#7c2b2b', - 800: '#521c1c', - 900: '#2b0e0e', + 50: "#fbeaea", + 100: "#f5cccc", + 200: "#eba5a5", + 300: "#e17e7e", + 400: "#d96060", + 500: "#c94848", + 600: "#a53939", + 700: "#7c2b2b", + 800: "#521c1c", + 900: "#2b0e0e", }, }, listStyleType: { - 'upper-alpha': 'upper-alpha', // Uppercase letters - 'lower-alpha': 'lower-alpha', // Lowercase letters + "upper-alpha": "upper-alpha", // Uppercase letters + "lower-alpha": "lower-alpha", // Lowercase letters }, flexGrow: { - '1': '1', - '2': '2', - '3': '3', + 1: "1", + 2: "2", + 3: "3", }, hueRotate: { - 20: '20deg', - } + 20: "20deg", + }, }, }, plugins: [ flowbite(), - plugin(function({ addUtilities, matchUtilities }) { + plugin(function ({ addUtilities, matchUtilities }) { addUtilities({ - '.content-visibility-auto': { - 'content-visibility': 'auto', + ".content-visibility-auto": { + "content-visibility": "auto", }, - '.contain-size': { - contain: 'size', + ".contain-size": { + contain: "size", }, }); matchUtilities({ - 'contain-intrinsic-w-*': value => ({ + "contain-intrinsic-w-*": (value) => ({ width: value, }), - 'contain-intrinsic-h-*': value => ({ + "contain-intrinsic-h-*": (value) => ({ height: value, - }) + }), }); - }) + }), ], - darkMode: 'class', + darkMode: "class", }; module.exports = config; diff --git a/test_data/AsciidocFiles/SimpleTest.adoc b/test_data/AsciidocFiles/SimpleTest.adoc new file mode 100644 index 0000000..144a1c9 --- /dev/null +++ b/test_data/AsciidocFiles/SimpleTest.adoc @@ -0,0 +1,73 @@ += Simple Advanced Rendering Test + +This is a simple test document to verify that Alexandria's advanced rendering features are working correctly. + +== Math Test + +Here's a simple math expression: + +[stem] +++++ +E = mc^2 +++++ + +And a more complex one: + +[stem] +++++ +\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi} +++++ + +== PlantUML Test + +A simple sequence diagram: + +[source,plantuml] +---- +@startuml +participant User +participant System +User -> System: Hello +System --> User: Hi there! +@enduml +---- + +== BPMN Test + +A simple BPMN process: + +[source,bpmn] +---- + + + + + + + + + + +---- + +== TikZ Test + +A simple TikZ diagram: + +[source,tikz] +---- +\begin{tikzpicture} + \draw[thick,red] (0,0) circle (1cm); + \draw[thick,blue] (2,0) rectangle (3,1); +\end{tikzpicture} +---- + +== Conclusion + +If you can see: +1. Rendered math expressions +2. A PlantUML diagram +3. A BPMN diagram placeholder with source +4. A TikZ diagram placeholder with source + +Then the advanced rendering is working correctly! \ No newline at end of file diff --git a/test_data/LaTeXtestfile.json b/test_data/LaTeXtestfile.json new file mode 100644 index 0000000..079226d --- /dev/null +++ b/test_data/LaTeXtestfile.json @@ -0,0 +1,34 @@ +{ + "created_at": 1752150799, + "content": "# This is a test file for writing mathematical formulas in #NostrMarkup\n\nThis document covers the rendering of formulas in TeX/LaTeX and AsciiMath notation, or some combination of those within the same page. It is meant to be rendered by clients utilizing MathJax.\n\nIf you want the entire document to be rendered as mathematics, place the entire thing in a back-tick code-block, but know that this makes the document slower to load, it is harder to format the prose, and the result is less legible. It also doesn't increase portability, as it's easy to export markup as LaTeX files, or as PDFs, with the formulas rendered.\n\nThe general idea, is that anything placed within `single back-ticks` is inline code, and inline-code will all be scanned for typical mathematics statements and rendered with best-effort. (For more precise rendering, use AsciiDoc.) We will not render text that is not marked as inline code, as mathematical formulas, as that is prose.\n\nIf you want the TeX to be blended into the surrounding text, wrap the text within single `$`. Otherwise, use double `$$` symbols, for display math, and it will appear on its own line.\n\n## TeX Examples\n\nInline equation: `$\\sqrt{x}$`\n\nSame equation, in the display mode: `$$\\sqrt{x}$$`\n\nSomething more complex, inline: `$\\mathbb{N} = \\{ a \\in \\mathbb{Z} : a > 0 \\}$`\n\nSomething complex, in display mode: `$$P \\left( A=2 \\, \\middle| \\, \\dfrac{A^2}{B}>4 \\right)$$`\n\nAnother example of `$$\\prod_{i=1}^{n} x_i - 1$$` inline formulas.\n\nFunction example: \n`$$\nf(x)=\n\\begin{cases}\n1/d_{ij} & \\quad \\text{when $d_{ij} \\leq 160$}\\\\ \n0 & \\quad \\text{otherwise}\n\\end{cases}\n$$`\n\nAnd a matrix:\n`$$\nM = \n\\begin{bmatrix}\n\\frac{5}{6} & \\frac{1}{6} & 0 \\\\[0.3em]\n\\frac{5}{6} & 0 & \\frac{1}{6} \\\\[0.3em]\n0 & \\frac{5}{6} & \\frac{1}{6}\n\\end{bmatrix}\n$$`\n\nLaTeX ypesetting won't be rendered. Use NostrMarkup delimeter tables for this sort of thing.\n\n`\\\\begin{tabular}{|c|c|c|l|r|}\n\\\\hline\n\\\\multicolumn{3}{|l|}{test} & A & B \\\\\\\\\n\\\\hline\n1 & 2 & 3 & 4 & 5 \\\\\\\\\n\\\\hline\n\\\\end{tabular}`\n\nWe also recognize common LaTeX statements:\n\n`\\[\n\\begin{array}{ccccc}\n1 & 2 & 3 & 4 & 5 \\\\\n\\end{array}\n\\]`\n\n`\\[ x^n + y^n = z^n \\]`\n\n`\\sqrt{x^2+1}`\n\nGreek letters are a snap: `$\\Psi$`, `$\\psi$`, `$\\Phi$`, `$\\phi$`. \n\nEquations within text are easy--- A well known Maxwell thermodynamic relation is `$\\left.{\\partial T \\over \\partial P}\\right|_{s} = \\left.{\\partial v \\over \\partial s}\\right|_{P}$`.\n\nYou can also set aside equations like so: `\\begin{eqnarray} du &=& T\\ ds -P\\ dv, \\qquad \\mbox{first law.}\\label{fl}\\\\ ds &\\ge& {\\delta q \\over T}.\\qquad \\qquad \\mbox{second law.} \\label{sl} \\end {eqnarray}`\n\n## And some good ole Asciimath\n\nAsciimath doesn't use `$` or `$$` delimiters, but we are using it to make mathy stuff easier to find. If you want it inline, include it inline. If you want it on a separate line, put a hard-return before and after.\n\nInline text example here `$E=mc^2$` and another `$1/(x+1)$`; very simple.\n\nDisplaying on a separate line:\n\n`$$sum_(k=1)^n k = 1+2+ cdots +n=(n(n+1))/2$$`\n\n`$$int_0^1 x^2 dx$$`\n\n`$$x = (-6 +- sqrt((-6)^2 - 4 (1)(4)))/(2 xx 1)$$`\n\n`$$|x|= {(x , if x ge 0 text(,)),(-x , if x <0.):}$$`\n\nDisplaying with wider spacing:\n\n`$a=3, \\ \\ \\ b=-3,\\ \\ $` and `$ \\ \\ c=2$`.\n\nThus `$(a+b)(c+b)=0$`.\n\nDisplaying with indentations:\n\nUsing the quadratic formula, the roots of `$x^2-6x+4=0$` are\n\n`$$x = (-6 +- sqrt((-6)^2 - 4 (1)(4)))/(2 xx 1)$$`\n\n`$$ \\ \\ = (-6 +- sqrt(36 - 16))/2$$`\n\n`$$ \\ \\ =(-6 +- sqrt(20))/2$$`\n\n`$$ \\ \\ = -0.8 or 2.2 \\ \\ \\ $$` to 1 decimal place.\n\nAdvanced alignment and matrices looks like this:\n\nA `$3xx3$` matrix, `$$((1,2,3),(4,5,6),(7,8,9))$$` and a `$2xx1$` matrix, or vector, `$$((1),(0))$$`.\n\nThe outer brackets determine the delimiters e.g. `$|(a,b),(c,d)|=ad-bc$`.\n\nA general `$m xx n$` matrix `$$((a_(11), cdots , a_(1n)),(vdots, ddots, vdots),(a_(m1), cdots , a_(mn)))$$`\n\n## Mixed Examples\n\nHere are some examples mixing LaTeX and AsciiMath:\n\n- LaTeX inline: `$\\frac{1}{2}$` vs AsciiMath inline: `$1/2$`\n- LaTeX display: `$$\\sum_{i=1}^n x_i$$` vs AsciiMath display: `$$sum_(i=1)^n x_i$$`\n- LaTeX matrix: `$$\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}$$` vs AsciiMath matrix: `$$((a,b),(c,d))$$`\n\n## Edge Cases\n\n- Empty math: `$$`\n- Just delimiters: `$ $`\n- Dollar signs in text: The price is $10.50\n- Currency: `$19.99`\n- Shell command: `echo \"Price: $100\"`\n- JavaScript template: `const price = \\`$${amount}\\``\n- CSS with dollar signs: `color: $primary-color`\n\nThis document should demonstrate that:\n1. LaTeX is processed within inline code blocks with proper delimiters\n2. AsciiMath is processed within inline code blocks with proper delimiters\n3. Regular code blocks remain unchanged\n4. Mixed content is handled correctly\n5. Edge cases are handled gracefully", + "tags": [ + [ + "t", + "test" + ], + [ + "t", + "Asciimath" + ], + [ + "t", + "TeX" + ], + [ + "t", + "LaTeX" + ], + [ + "d", + "this-is-a-test-file-for-writing-mathematical-formulas-in-nostrmarkup" + ], + [ + "title", + "This is a test file for writing mathematical formulas in #NostrMarkup" + ] + ], + "kind": 30023, + "pubkey": "fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1", + "id": "91be487e67cb68cfe3c7e965a654642b7bcedecb68340523a8c1b865b21fa5dc", + "sig": "59b7f87fe2c2d318152cf5b4796580f79a26936d515a816ddcb89b89ba337992eaa3d50896d3bde345d25be99c9caa3a237d476abeb8537589256cbcceeb2e75" +} \ No newline at end of file diff --git a/test_data/LaTeXtestfile.md b/test_data/LaTeXtestfile.md new file mode 100644 index 0000000..3c2e7e8 --- /dev/null +++ b/test_data/LaTeXtestfile.md @@ -0,0 +1,135 @@ +# This is a testfile for writing mathematic formulas in NostrMarkup + +This document covers the rendering of formulas in TeX/LaTeX and AsciiMath notation, or some combination of those within the same page. It is meant to be rendered by clients utilizing MathJax. + +If you want the entire document to be rendered as mathematics, place the entire thing in a backtick-codeblock, but know that this makes the document slower to load, it is harder to format the prose, and the result is less legible. It also doesn't increase portability, as it's easy to export markup as LaTeX files, or as PDFs, with the formulas rendered. + +The general idea, is that anything placed within `single backticks` is inline code, and inline-code will all be scanned for typical mathematics statements and rendered with best-effort. (For more precise rendering, use Asciidoc.) We will not render text that is not marked as inline code, as mathematical formulas, as that is prose. + +If you want the TeX to be blended into the surrounding text, wrap the text within single `$`. Otherwise, use double `$$` symbols, for display math, and it will appear on its own line. + +## TeX Examples + +Inline equation: `$\sqrt{x}$` + +Same equation, in the display mode: `$$\sqrt{x}$$` + +Something more complex, inline: `$\mathbb{N} = \{ a \in \mathbb{Z} : a > 0 \}$` + +Something complex, in display mode: `$$P \left( A=2 \, \middle| \, \dfrac{A^2}{B}>4 \right)$$` + +Another example of `$$\prod_{i=1}^{n} x_i - 1$$` inline formulas. + +Function example: +`$$ +f(x)= +\begin{cases} +1/d_{ij} & \quad \text{when $d_{ij} \leq 160$}\\ +0 & \quad \text{otherwise} +\end{cases} +$$` + +And a matrix: +`$$ +M = +\begin{bmatrix} +\frac{5}{6} & \frac{1}{6} & 0 \\[0.3em] +\frac{5}{6} & 0 & \frac{1}{6} \\[0.3em] +0 & \frac{5}{6} & \frac{1}{6} +\end{bmatrix} +$$` + +LaTeX ypesetting won't be rendered. Use NostrMarkup delimeter tables for this sort of thing. + +`\\begin{tabular}{|c|c|c|l|r|} +\\hline +\\multicolumn{3}{|l|}{test} & A & B \\\\ +\\hline +1 & 2 & 3 & 4 & 5 \\\\ +\\hline +\\end{tabular}` + +We also recognize common LaTeX statements: + +`\[ +\begin{array}{ccccc} +1 & 2 & 3 & 4 & 5 \\ +\end{array} +\]` + +`\[ x^n + y^n = z^n \]` + +`\sqrt{x^2+1}` + +Greek letters are a snap: `$\Psi$`, `$\psi$`, `$\Phi$`, `$\phi$`. + +Equations within text are easy--- A well known Maxwell thermodynamic relation is `$\left.{\partial T \over \partial P}\right|_{s} = \left.{\partial v \over \partial s}\right|_{P}$`. + +You can also set aside equations like so: `\begin{eqnarray} du &=& T\ ds -P\ dv, \qquad \mbox{first law.}\label{fl}\\ ds &\ge& {\delta q \over T}.\qquad \qquad \mbox{second law.} \label{sl} \end {eqnarray}` + +## And some good ole Asciimath + +Asciimath doesn't use `$` or `$$` delimiters, but we are using it to make mathy stuff easier to find. If you want it inline, include it inline. If you want it on a separate line, put a hard-return before and after. + +Inline text example here `$E=mc^2$` and another `$1/(x+1)$`; very simple. + +Displaying on a separate line: + +`$$sum_(k=1)^n k = 1+2+ cdots +n=(n(n+1))/2$$` + +`$$int_0^1 x^2 dx$$` + +`$$x = (-6 +- sqrt((-6)^2 - 4 (1)(4)))/(2 xx 1)$$` + +`$$|x|= {(x , if x ge 0 text(,)),(-x , if x <0.):}$$` + +Displaying with wider spacing: + +`$a=3, \ \ \ b=-3,\ \ $` and `$ \ \ c=2$`. + +Thus `$(a+b)(c+b)=0$`. + +Displaying with indentations: + +Using the quadratic formula, the roots of `$x^2-6x+4=0$` are + +`$$x = (-6 +- sqrt((-6)^2 - 4 (1)(4)))/(2 xx 1)$$` + +`$$ \ \ = (-6 +- sqrt(36 - 16))/2$$` + +`$$ \ \ =(-6 +- sqrt(20))/2$$` + +`$$ \ \ = -0.8 or 2.2 \ \ \ $$` to 1 decimal place. + +Advanced alignment and matrices looks like this: + +A `$3xx3$` matrix, `$$((1,2,3),(4,5,6),(7,8,9))$$` and a `$2xx1$` matrix, or vector, `$$((1),(0))$$`. + +The outer brackets determine the delimiters e.g. `$|(a,b),(c,d)|=ad-bc$`. + +A general `$m xx n$` matrix `$$((a_(11), cdots , a_(1n)),(vdots, ddots, vdots),(a_(m1), cdots , a_(mn)))$$` + +## Mixed Examples + +Here are some examples mixing LaTeX and AsciiMath: + +- LaTeX inline: `$\frac{1}{2}$` vs AsciiMath inline: `$1/2$` +- LaTeX display: `$$\sum_{i=1}^n x_i$$` vs AsciiMath display: `$$sum_(i=1)^n x_i$$` +- LaTeX matrix: `$$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$$` vs AsciiMath matrix: `$$((a,b),(c,d))$$` + +## Edge Cases + +- Empty math: `$$` +- Just delimiters: `$ $` +- Dollar signs in text: The price is $10.50 +- Currency: `$19.99` +- Shell command: `echo "Price: $100"` +- JavaScript template: `const price = \`$${amount}\`` +- CSS with dollar signs: `color: $primary-color` + +This document should demonstrate that: +1. LaTeX is processed within inline code blocks with proper delimiters +2. AsciiMath is processed within inline code blocks with proper delimiters +3. Regular code blocks remain unchanged +4. Mixed content is handled correctly +5. Edge cases are handled gracefully diff --git a/tests/e2e/example.pw.spec.ts b/tests/e2e/example.pw.spec.ts index 54a906a..b60fe7c 100644 --- a/tests/e2e/example.pw.spec.ts +++ b/tests/e2e/example.pw.spec.ts @@ -1,18 +1,20 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); +test("has title", async ({ page }) => { + await page.goto("https://playwright.dev/"); // Expect a title "to contain" a substring. await expect(page).toHaveTitle(/Playwright/); }); -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); +test("get started link", async ({ page }) => { + await page.goto("https://playwright.dev/"); // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); + await page.getByRole("link", { name: "Get started" }).click(); // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Installation" }), + ).toBeVisible(); }); diff --git a/tests/integration/markupIntegration.test.ts b/tests/integration/markupIntegration.test.ts index b4de512..a834593 100644 --- a/tests/integration/markupIntegration.test.ts +++ b/tests/integration/markupIntegration.test.ts @@ -1,42 +1,50 @@ -import { describe, it, expect } from 'vitest'; -import { parseBasicmarkup } from '../../src/lib/utils/markup/basicMarkupParser'; -import { parseAdvancedmarkup } from '../../src/lib/utils/markup/advancedMarkupParser'; -import { readFileSync } from 'fs'; -import { join } from 'path'; +import { describe, it, expect } from "vitest"; +import { parseBasicmarkup } from "../../src/lib/utils/markup/basicMarkupParser"; +import { parseAdvancedmarkup } from "../../src/lib/utils/markup/advancedMarkupParser"; +import { readFileSync } from "fs"; +import { join } from "path"; -const testFilePath = join(__dirname, './markupTestfile.md'); -const md = readFileSync(testFilePath, 'utf-8'); +const testFilePath = join(__dirname, "./markupTestfile.md"); +const md = readFileSync(testFilePath, "utf-8"); -describe('Markup Integration Test', () => { - it('parses markupTestfile.md with the basic parser', async () => { +describe("Markup Integration Test", () => { + it("parses markupTestfile.md with the basic parser", async () => { const output = await parseBasicmarkup(md); - // Headers (should be present as text, not

      tags) - expect(output).toContain('This is a test'); - expect(output).toContain('============'); - expect(output).toContain('### Disclaimer'); + // Headers (should be present as raw text, not HTML tags) + expect(output).toContain("This is a test"); + expect(output).toContain("# This is a test"); + expect(output).toContain("### Disclaimer"); // Unordered list - expect(output).toContain(']*>.*]*>/s); // Blockquotes - expect(output).toContain(''); + expect(output).toContain( + '
      ', + ); // Images - expect(output).toMatch(/]+src="https:\/\/upload\.wikimedia\.org\/wikipedia\/commons\/f\/f1\/Heart_coraz%C3%B3n\.svg"/); + expect(output).toMatch( + /]+src="https:\/\/upload\.wikimedia\.org\/wikipedia\/commons\/f\/f1\/Heart_coraz%C3%B3n\.svg"/, + ); // Links - expect(output).toMatch(/]+href="https:\/\/github.com\/nostrability\/nostrability\/issues\/146"/); + expect(output).toMatch( + /]+href="https:\/\/github.com\/nostrability\/nostrability\/issues\/146"/, + ); // Hashtags - expect(output).toContain('text-primary-600'); + expect(output).toContain("text-primary-600"); // Nostr identifiers (should be Alexandria links) - expect(output).toContain('./events?id=npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z'); + expect(output).toContain( + "./events?id=npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z", + ); // Wikilinks - expect(output).toContain('wikilink'); + expect(output).toContain("wikilink"); // YouTube iframe expect(output).toMatch(/]+youtube/); // Tracking token removal: should not contain utm_, fbclid, or gclid in any link @@ -44,42 +52,50 @@ describe('Markup Integration Test', () => { expect(output).not.toMatch(/fbclid/); expect(output).not.toMatch(/gclid/); // Horizontal rule (should be present as --- in basic) - expect(output).toContain('---'); + expect(output).toContain("---"); // Footnote references (should be present as [^1] in basic) - expect(output).toContain('[^1]'); + expect(output).toContain("[^1]"); // Table (should be present as | Syntax | Description | in basic) - expect(output).toContain('| Syntax | Description |'); + expect(output).toContain("| Syntax | Description |"); }); - it('parses markupTestfile.md with the advanced parser', async () => { + it("parses markupTestfile.md with the advanced parser", async () => { const output = await parseAdvancedmarkup(md); // Headers - expect(output).toContain(']*>.*]*>/s); // Blockquotes - expect(output).toContain(']*>.*leather min-h-full w-full flex flex-col items-center.*<\/code>/s); + expect(output).toMatch( + /]*>.*leather min-h-full w-full flex flex-col items-center.*<\/code>/s, + ); // Images - expect(output).toMatch(/]+src="https:\/\/upload\.wikimedia\.org\/wikipedia\/commons\/f\/f1\/Heart_coraz%C3%B3n\.svg"/); + expect(output).toMatch( + /]+src="https:\/\/upload\.wikimedia\.org\/wikipedia\/commons\/f\/f1\/Heart_coraz%C3%B3n\.svg"/, + ); // Links - expect(output).toMatch(/]+href="https:\/\/github.com\/nostrability\/nostrability\/issues\/146"/); + expect(output).toMatch( + /]+href="https:\/\/github.com\/nostrability\/nostrability\/issues\/146"/, + ); // Hashtags - expect(output).toContain('text-primary-600'); + expect(output).toContain("text-primary-600"); // Nostr identifiers (should be Alexandria links) - expect(output).toContain('./events?id=npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z'); + expect(output).toContain( + "./events?id=npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z", + ); // Wikilinks - expect(output).toContain('wikilink'); + expect(output).toContain("wikilink"); // YouTube iframe expect(output).toMatch(/]+youtube/); // Tracking token removal: should not contain utm_, fbclid, or gclid in any link @@ -87,13 +103,13 @@ describe('Markup Integration Test', () => { expect(output).not.toMatch(/fbclid/); expect(output).not.toMatch(/gclid/); // Horizontal rule - expect(output).toContain('/); // Table - expect(output).toContain(' lines of > important information > with a second[^2] footnote. -[^2]: This is a "Test" of a longer footnote-reference, placed inline, including some punctuation. 1984. +> [^2]: This is a "Test" of a longer footnote-reference, placed inline, including some punctuation. 1984. -This is a youtube link +This is a youtube link https://www.youtube.com/watch?v=9aqVxNCpx9s And here is a link with tracking tokens: https://arstechnica.com/science/2019/07/new-data-may-extend-norse-occupancy-in-north-america/?fbclid=IwAR1LOW3BebaMLinfkWFtFpzkLFi48jKNF7P6DV2Ux2r3lnT6Lqj6eiiOZNU This is an unordered list: -* but -* not -* really + +- but +- not +- really This is an unordered list with nesting: -* but - * not - * really -* but - * yes, - * really - + +- but + - not + - really +- but + - yes, + - really + ## More testing An ordered list: + 1. first 2. second 3. third Let's nest that: -1. first - 2. second indented -3. third - 4. fourth indented - 5. fifth indented even more - 6. sixth under the fourth - 7. seventh under the sixth -8. eighth under the third + +1. first 2. second indented +2. third 4. fourth indented 5. fifth indented even more 6. sixth under the fourth 7. seventh under the sixth +3. eighth under the third This is ordered and unordered mixed: -1. first - 2. second indented -3. third - * make this a bullet point - 4. fourth indented even more - * second bullet point + +1. first 2. second indented +2. third + - make this a bullet point 4. fourth indented even more + - second bullet point Here is a horizontal rule: @@ -130,13 +132,31 @@ in a code block You can even use a multi-line code block, with a json tag. -```json +````json { -"created_at":1745038670,"content":"# This is a test\n\nIt is _only_ a test. I just wanted to see if the *markup* renders correctly on the page, even if I use **two asterisks** for bold text.[^1]\n\nnpub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z wrote this. That's the same person as nostr:npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z. That is a different person from npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz.\n\n> This is important information\n\n> This is multiple\n> lines of\n> important information\n> with a second[^2] footnote.\n\n* but\n* not\n* really\n\n## More testing\n\n1. first\n2. second\n3. third\n\nHere is a horizontal rule:\n\n---\n\nThis is an implementation of [Nostr-flavored markup](github.com/nostrability/nostrability/issues/146 ) for #gitstuff issue notes.\n\nYou can even include `code inline` or\n\n```\nin a code block\n```\n\nYou can even use a \n\n```json\nmultiline of json block\n```\n\n\n![Nostr logo](https://user-images.githubusercontent.com/99301796/219900773-d6d02038-e2a0-4334-9f28-c14d40ab6fe7.png)\n\n[^1]: this is a footnote\n[^2]: so is this","tags":[["subject","test"],["alt","git repository issue: test"],["a","30617:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:Alexandria","","root"],["p","fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1"],["t","gitstuff"]],"kind":1621,"pubkey":"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319","id":"e78a689369511fdb3c36b990380c2d8db2b5e62f13f6b836e93ef5a09611afe8","sig":"7a2b3a6f6f61b6ea04de1fe873e46d40f2a220f02cdae004342430aa1df67647a9589459382f22576c651b3d09811546bbd79564cf472deaff032f137e94a865" + "created_at": 1745038670, + "content": "# This is a test\n\nIt is _only_ a test. I just wanted to see if the *markup* renders correctly on the page, even if I use **two asterisks** for bold text.[^1]\n\nnpub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z wrote this. That's the same person as nostr:npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z. That is a different person from npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz.\n\n> This is important information\n\n> This is multiple\n> lines of\n> important information\n> with a second[^2] footnote.\n\n* but\n* not\n* really\n\n## More testing\n\n1. first\n2. second\n3. third\n\nHere is a horizontal rule:\n\n---\n\nThis is an implementation of [Nostr-flavored markup](github.com/nostrability/nostrability/issues/146 ) for #gitstuff issue notes.\n\nYou can even include `code inline` or\n\n```\nin a code block\n```\n\nYou can even use a \n\n```json\nmultiline of json block\n```\n\n\n![Nostr logo](https://user-images.githubusercontent.com/99301796/219900773-d6d02038-e2a0-4334-9f28-c14d40ab6fe7.png)\n\n[^1]: this is a footnote\n[^2]: so is this", + "tags": [ + ["subject", "test"], + ["alt", "git repository issue: test"], + [ + "a", + "30617:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:Alexandria", + "", + "root" + ], + ["p", "fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1"], + ["t", "gitstuff"] + ], + "kind": 1621, + "pubkey": "dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319", + "id": "e78a689369511fdb3c36b990380c2d8db2b5e62f13f6b836e93ef5a09611afe8", + "sig": "7a2b3a6f6f61b6ea04de1fe873e46d40f2a220f02cdae004342430aa1df67647a9589459382f22576c651b3d09811546bbd79564cf472deaff032f137e94a865" } -``` +```` C or C++: + ```cpp bool getBit(int num, int i) { return ((num & (1< { - it('parses headers (ATX and Setext)', async () => { - const input = '# H1\nText\n\nH2\n====\n'; +describe("Advanced Markup Parser", () => { + it("parses headers (ATX and Setext)", async () => { + const input = "# H1\nText\n\nH2\n====\n"; const output = await parseAdvancedmarkup(input); - expect(stripWS(output)).toContain('H1'); - expect(stripWS(output)).toContain('H2'); + expect(stripWS(output)).toContain("H1"); + expect(stripWS(output)).toContain("H2"); }); - it('parses bold, italic, and strikethrough', async () => { - const input = '*bold* **bold** _italic_ __italic__ ~strikethrough~ ~~strikethrough~~'; + it("parses bold, italic, and strikethrough", async () => { + const input = + "*bold* **bold** _italic_ __italic__ ~strikethrough~ ~~strikethrough~~"; const output = await parseAdvancedmarkup(input); - expect(output).toContain('bold'); - expect(output).toContain('italic'); + expect(output).toContain("bold"); + expect(output).toContain("italic"); expect(output).toContain('strikethrough'); }); - it('parses blockquotes', async () => { - const input = '> quote'; + it("parses blockquotes", async () => { + const input = "> quote"; const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '> quote\n> quote'; + it("parses multi-line blockquotes", async () => { + const input = "> quote\n> quote"; const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '* a\n* b'; + it("parses unordered lists", async () => { + const input = "* a\n* b"; const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '1. one\n2. two'; + it("parses ordered lists", async () => { + const input = "1. one\n2. two"; const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '[link](https://example.com) ![alt](https://img.com/x.png)'; + it("parses links and images", async () => { + const input = "[link](https://example.com) ![alt](https://img.com/x.png)"; const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '#hashtag'; + it("parses hashtags", async () => { + const input = "#hashtag"; const output = await parseAdvancedmarkup(input); - expect(output).toContain('text-primary-600'); - expect(output).toContain('#hashtag'); + expect(output).toContain("text-primary-600"); + expect(output).toContain("#hashtag"); }); - it('parses nostr identifiers', async () => { - const input = 'npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'; + it("parses nostr identifiers", async () => { + const input = + "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"; const output = await parseAdvancedmarkup(input); - expect(output).toContain('./events?id=npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'); + expect(output).toContain( + "./events?id=npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", + ); }); - it('parses emoji shortcodes', async () => { - const input = 'hello :smile:'; + it("parses emoji shortcodes", async () => { + const input = "hello :smile:"; const output = await parseAdvancedmarkup(input); expect(output).toMatch(/😄|:smile:/); }); - it('parses wikilinks', async () => { - const input = '[[Test Page|display]]'; + it("parses wikilinks", async () => { + const input = "[[Test Page|display]]"; const output = await parseAdvancedmarkup(input); - expect(output).toContain('wikilink'); - expect(output).toContain('display'); + expect(output).toContain("wikilink"); + expect(output).toContain("display"); }); - it('parses tables (with and without headers)', async () => { + it("parses tables (with and without headers)", async () => { const input = `| Syntax | Description |\n|--------|-------------|\n| Header | Title |\n| Paragraph | Text |\n\n| a | b |\n| c | d |`; const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '```js\nconsole.log(1);\n```\n```\nno lang\n```'; + it("parses code blocks (with and without language)", async () => { + const input = "```js\nconsole.log(1);\n```\n```\nno lang\n```"; const output = await parseAdvancedmarkup(input); - const textOnly = output.replace(/<[^>]+>/g, ''); - expect(output).toContain(']+>/g, ""); + expect(output).toContain(" { - const input = '---'; + it("parses horizontal rules", async () => { + const input = "---"; const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = 'Here is a footnote[^1].\n\n[^1]: This is the footnote.'; + it("parses footnotes (references and section)", async () => { + const input = "Here is a footnote[^1].\n\n[^1]: This is the footnote."; const output = await parseAdvancedmarkup(input); - expect(output).toContain('Footnotes'); - expect(output).toContain('This is the footnote'); - expect(output).toContain('fn-1'); + expect(output).toContain("Footnotes"); + expect(output).toContain("This is the footnote"); + expect(output).toContain("fn-1"); }); -}); \ No newline at end of file + + it("parses unordered lists with '-' as bullet", async () => { + const input = "- item one\n- item two\n - nested item\n- item three"; + const output = await parseAdvancedmarkup(input); + expect(output).toContain(" { - it('parses ATX and Setext headers', async () => { - const input = '# H1\nText\n\nH2\n====\n'; +describe("Basic Markup Parser", () => { + it("parses ATX and Setext headers", async () => { + const input = "# H1\nText\n\nH2\n====\n"; const output = await parseBasicmarkup(input); - expect(stripWS(output)).toContain('H1'); - expect(stripWS(output)).toContain('H2'); + expect(stripWS(output)).toContain("H1"); + expect(stripWS(output)).toContain("H2"); }); - it('parses bold, italic, and strikethrough', async () => { - const input = '*bold* **bold** _italic_ __italic__ ~strikethrough~ ~~strikethrough~~'; + it("parses bold, italic, and strikethrough", async () => { + const input = + "*bold* **bold** _italic_ __italic__ ~strikethrough~ ~~strikethrough~~"; const output = await parseBasicmarkup(input); - expect(output).toContain('bold'); - expect(output).toContain('italic'); + expect(output).toContain("bold"); + expect(output).toContain("italic"); expect(output).toContain('strikethrough'); }); - it('parses blockquotes', async () => { - const input = '> quote'; + it("parses blockquotes", async () => { + const input = "> quote"; const output = await parseBasicmarkup(input); - expect(output).toContain(' { - const input = '> quote\n> quote'; + it("parses multi-line blockquotes", async () => { + const input = "> quote\n> quote"; const output = await parseBasicmarkup(input); - expect(output).toContain(' { - const input = '* a\n* b'; + it("parses unordered lists", async () => { + const input = "* a\n* b"; const output = await parseBasicmarkup(input); - expect(output).toContain(' { - const input = '1. one\n2. two'; + it("parses ordered lists", async () => { + const input = "1. one\n2. two"; const output = await parseBasicmarkup(input); - expect(output).toContain(' { - const input = '[link](https://example.com) ![alt](https://img.com/x.png)'; + it("parses links and images", async () => { + const input = "[link](https://example.com) ![alt](https://img.com/x.png)"; const output = await parseBasicmarkup(input); - expect(output).toContain(' { - const input = '#hashtag'; + it("parses hashtags", async () => { + const input = "#hashtag"; const output = await parseBasicmarkup(input); - expect(output).toContain('text-primary-600'); - expect(output).toContain('#hashtag'); + expect(output).toContain("text-primary-600"); + expect(output).toContain("#hashtag"); }); - it('parses nostr identifiers', async () => { - const input = 'npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'; + it("parses nostr identifiers", async () => { + const input = + "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"; const output = await parseBasicmarkup(input); - expect(output).toContain('./events?id=npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'); + expect(output).toContain( + "./events?id=npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", + ); }); - it('parses emoji shortcodes', async () => { - const input = 'hello :smile:'; + it("parses emoji shortcodes", async () => { + const input = "hello :smile:"; const output = await parseBasicmarkup(input); expect(output).toMatch(/😄|:smile:/); }); - it('parses wikilinks', async () => { - const input = '[[Test Page|display]]'; + it("parses wikilinks", async () => { + const input = "[[Test Page|display]]"; const output = await parseBasicmarkup(input); - expect(output).toContain('wikilink'); - expect(output).toContain('display'); + expect(output).toContain("wikilink"); + expect(output).toContain("display"); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/latexRendering.test.ts b/tests/unit/latexRendering.test.ts new file mode 100644 index 0000000..667cd0d --- /dev/null +++ b/tests/unit/latexRendering.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest"; +import { parseAdvancedmarkup } from "../../src/lib/utils/markup/advancedMarkupParser"; +import { readFileSync } from "fs"; +import { join } from "path"; + +describe("LaTeX and AsciiMath Rendering in Inline Code Blocks", () => { + const jsonPath = join(__dirname, "../../test_data/LaTeXtestfile.json"); + const raw = readFileSync(jsonPath, "utf-8"); + // Extract the markdown content field from the JSON event + const content = JSON.parse(raw).content; + + it('renders LaTeX inline and display math correctly', async () => { + const html = await parseAdvancedmarkup(content); + // Test basic LaTeX examples from the test document + expect(html).toMatch(/\$\\sqrt\{x\}\$<\/span>/); + expect(html).toMatch(/
      \$\$\\sqrt\{x\}\$\$<\/div>/); + expect(html).toMatch(/\$\\mathbb\{N\} = \\{ a \\in \\mathbb\{Z\} : a > 0 \\}\$<\/span>/); + expect(html).toMatch(/
      \$\$P \\left\( A=2 \\, \\middle\| \\, \\dfrac\{A\^2\}\{B\}>4 \\right\)\$\$<\/div>/); + }); + + it('renders AsciiMath inline and display math correctly', async () => { + const html = await parseAdvancedmarkup(content); + // Test AsciiMath examples + expect(html).toMatch(/\$E=mc\^2\$<\/span>/); + expect(html).toMatch(/
      \$\$sum_\(k=1\)\^n k = 1\+2\+ cdots \+n=\(n\(n\+1\)\)\/2\$\$<\/div>/); + expect(html).toMatch(/
      \$\$int_0\^1 x\^2 dx\$\$<\/div>/); + }); + + it('renders LaTeX array and matrix environments as math', async () => { + const html = await parseAdvancedmarkup(content); + // Test array and matrix environments + expect(html).toMatch(/
      \$\$[\s\S]*\\begin\{array\}\{ccccc\}[\s\S]*\\end\{array\}[\s\S]*\$\$<\/div>/); + expect(html).toMatch(/
      \$\$[\s\S]*\\begin\{bmatrix\}[\s\S]*\\end\{bmatrix\}[\s\S]*\$\$<\/div>/); + }); + + it('handles unsupported LaTeX environments gracefully', async () => { + const html = await parseAdvancedmarkup(content); + // Should show a message and plaintext for tabular + expect(html).toMatch(/
      /); + expect(html).toMatch(/Unrendered, as it is LaTeX typesetting, not a formula:/); + expect(html).toMatch(/\\\\begin\{tabular\}/); + }); + + it('renders mixed LaTeX and AsciiMath correctly', async () => { + const html = await parseAdvancedmarkup(content); + // Test mixed content + expect(html).toMatch(/\$\\frac\{1\}\{2\}\$<\/span>/); + expect(html).toMatch(/\$1\/2\$<\/span>/); + expect(html).toMatch(/
      \$\$\\sum_\{i=1\}\^n x_i\$\$<\/div>/); + expect(html).toMatch(/
      \$\$sum_\(i=1\)\^n x_i\$\$<\/div>/); + }); + + it('handles edge cases and regular code blocks', async () => { + const html = await parseAdvancedmarkup(content); + // Test regular code blocks (should remain as code, not math) + expect(html).toMatch(/]*>\$19\.99<\/code>/); + expect(html).toMatch(/]*>echo "Price: \$100"<\/code>/); + expect(html).toMatch(/]*>const price = \\`\$\$\{amount\}\\`<\/code>/); + expect(html).toMatch(/]*>color: \$primary-color<\/code>/); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index 61e619b..dfbd6e3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,16 +5,20 @@ import { execSync } from "child_process"; // Function to get the latest git tag function getAppVersionString() { // if running in ci context, we can assume the package has been properly versioned - if (process.env.ALEXANDIRA_IS_CI_BUILD && process.env.npm_package_version && process.env.npm_package_version.trim() !== '') { + if ( + process.env.ALEXANDIRA_IS_CI_BUILD && + process.env.npm_package_version && + process.env.npm_package_version.trim() !== "" + ) { return process.env.npm_package_version; } - + try { // Get the latest git tag, assuming git is installed and tagged branch is available - const tag = execSync('git describe --tags --abbrev=0').toString().trim(); + const tag = execSync("git describe --tags --abbrev=0").toString().trim(); return tag; } catch (error) { - return 'development'; + return "development"; } } @@ -22,20 +26,20 @@ export default defineConfig({ plugins: [sveltekit()], resolve: { alias: { - $lib: './src/lib', - $components: './src/components' - } + $lib: "./src/lib", + $components: "./src/components", + }, }, build: { rollupOptions: { - external: ['bech32'] - } + external: ["bech32"], + }, }, test: { - include: ['./tests/unit/**/*.test.ts', './tests/integration/**/*.test.ts'] + include: ["./tests/unit/**/*.test.ts", "./tests/integration/**/*.test.ts"], }, define: { // Expose the app version as a global variable - 'import.meta.env.APP_VERSION': JSON.stringify(getAppVersionString()) - } + "import.meta.env.APP_VERSION": JSON.stringify(getAppVersionString()), + }, });