diff --git a/README.md b/README.md index 274657e..3273302 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,31 @@ # Alexandria Alexandria is a reader and writer for curated publications, including e-books. -For a thorough introduction, please refer to our [project documention](https://next-alexandria.gitcitadel.eu/publication?d=gitcitadel-project-documentation-by-stella-v-1), viewable on Alexandria, or to the Alexandria [About page](https://next-alexandria.gitcitadel.eu/about). +For a thorough introduction, please refer to our +[project documention](https://next-alexandria.gitcitadel.eu/publication?d=gitcitadel-project-documentation-by-stella-v-1), +viewable on Alexandria, or to the Alexandria +[About page](https://next-alexandria.gitcitadel.eu/about). -It also contains a [universal event viewer](https://next-alexandria.gitcitadel.eu/events), with which you can search our relays, some aggregator relays, and your own relay list, to find and view event data. +It also contains a +[universal event viewer](https://next-alexandria.gitcitadel.eu/events), with +which you can search our relays, some aggregator relays, and your own relay +list, to find and view event data. ## Issues and Patches -If you would like to suggest a feature or report a bug, please use the [Alexandria Contact page](https://next-alexandria.gitcitadel.eu/contact). +If you would like to suggest a feature or report a bug, please use the +[Alexandria Contact page](https://next-alexandria.gitcitadel.eu/contact). -You can also contact us [on Nostr](https://next-alexandria.gitcitadel.eu/events?id=nprofile1qqsggm4l0xs23qfjwnkfwf6fqcs66s3lz637gaxhl4nwd2vtle8rnfqprfmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdaks5zhueg), directly. +You can also contact us +[on Nostr](https://next-alexandria.gitcitadel.eu/events?id=nprofile1qqsggm4l0xs23qfjwnkfwf6fqcs66s3lz637gaxhl4nwd2vtle8rnfqprfmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdaks5zhueg), +directly. ## Developing -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. +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: @@ -43,7 +55,8 @@ 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: @@ -71,7 +84,8 @@ deno task preview ## 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: @@ -87,9 +101,11 @@ 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. +To run the Vitest suite we've built, install the program locally and run the +tests. ```bash npm run test @@ -103,4 +119,8 @@ 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). +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.lock b/deno.lock index 7083352..ceb1ed5 100644 --- a/deno.lock +++ b/deno.lock @@ -1,96 +1,22 @@ { "version": "5", "specifiers": { - "npm:@noble/curves@^1.9.4": "1.9.4", - "npm:@noble/hashes@^1.8.0": "1.8.0", "npm:@playwright/test@^1.54.1": "1.54.1", - "npm:@popperjs/core@2.11": "2.11.8", - "npm:@tailwindcss/forms@0.5": "0.5.10_tailwindcss@3.4.17__postcss@8.5.6", - "npm:@tailwindcss/typography@0.5": "0.5.16_tailwindcss@3.4.17__postcss@8.5.6", "npm:@types/d3@^7.4.3": "7.4.3", "npm:@types/he@1.2": "1.2.3", "npm:@types/mathjax@^0.0.40": "0.0.40", "npm:@types/node@^24.0.15": "24.0.15", "npm:@types/qrcode@^1.5.5": "1.5.5", - "npm:asciidoctor@3.0": "3.0.4_@asciidoctor+core@3.0.4", - "npm:autoprefixer@^10.4.21": "10.4.21_postcss@8.5.6", "npm:bech32@2": "2.0.0", - "npm:eslint-plugin-svelte@^3.11.0": "3.11.0_eslint@9.31.0_svelte@5.36.8__acorn@8.15.0_postcss@8.5.6", - "npm:flowbite-svelte-icons@2.1": "2.1.1_svelte@5.36.8__acorn@8.15.0_tailwind-merge@3.3.1", - "npm:flowbite-svelte@0.48": "0.48.6_svelte@5.36.8__acorn@8.15.0", - "npm:flowbite@2": "2.5.2", - "npm:flowbite@^3.1.2": "3.1.2", "npm:he@1.2": "1.2.0", "npm:highlight.js@^11.11.1": "11.11.1", "npm:node-emoji@^2.2.0": "2.2.0", "npm:plantuml-encoder@^1.4.0": "1.4.0", "npm:playwright@^1.50.1": "1.54.1", "npm:playwright@^1.54.1": "1.54.1", - "npm:postcss-load-config@6": "6.0.1_postcss@8.5.6", - "npm:postcss@^8.5.6": "8.5.6", - "npm:prettier-plugin-svelte@^3.4.0": "3.4.0_prettier@3.6.2_svelte@5.36.8__acorn@8.15.0", - "npm:prettier@^3.6.2": "3.6.2", - "npm:qrcode@^1.5.4": "1.5.4", - "npm:svelte-check@4": "4.3.0_svelte@5.36.8__acorn@8.15.0_typescript@5.8.3", - "npm:svelte@^5.36.8": "5.36.8_acorn@8.15.0", - "npm:tailwind-merge@^3.3.1": "3.3.1", - "npm:tailwindcss@^3.4.17": "3.4.17_postcss@8.5.6", - "npm:tslib@2.8": "2.8.1", - "npm:typescript@^5.8.3": "5.8.3" + "npm:tslib@2.8": "2.8.1" }, "npm": { - "@alloc/quick-lru@5.2.0": { - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==" - }, - "@ampproject/remapping@2.3.0": { - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": [ - "@jridgewell/gen-mapping", - "@jridgewell/trace-mapping" - ] - }, - "@asciidoctor/cli@4.0.0_@asciidoctor+core@3.0.4": { - "integrity": "sha512-x2T9gW42921Zd90juEagtbViPZHNP2MWf0+6rJEkOzW7E9m3TGJtz+Guye9J0gwrpZsTMGCpfYMQy1We3X7osg==", - "dependencies": [ - "@asciidoctor/core", - "yargs@17.3.1" - ], - "bin": true - }, - "@asciidoctor/core@3.0.4": { - "integrity": "sha512-41SDMi7iRRBViPe0L6VWFTe55bv6HEOJeRqMj5+E5wB1YPdUPuTucL4UAESPZM6OWmn4t/5qM5LusXomFUVwVQ==", - "dependencies": [ - "@asciidoctor/opal-runtime", - "unxhr" - ] - }, - "@asciidoctor/opal-runtime@3.0.1": { - "integrity": "sha512-iW7ACahOG0zZft4A/4CqDcc7JX+fWRNjV5tFAVkNCzwZD+EnFolPaUOPYt8jzadc0+Bgd80cQTtRMQnaaV1kkg==", - "dependencies": [ - "glob@8.1.0", - "unxhr" - ] - }, - "@babel/helper-string-parser@7.27.1": { - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" - }, - "@babel/helper-validator-identifier@7.27.1": { - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" - }, - "@babel/parser@7.28.0": { - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "dependencies": [ - "@babel/types" - ], - "bin": true - }, - "@babel/types@7.28.1": { - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", - "dependencies": [ - "@babel/helper-string-parser", - "@babel/helper-validator-identifier" - ] - }, "@esbuild/aix-ppc64@0.25.7": { "integrity": "sha512-uD0kKFHh6ETr8TqEtaAcV+dn/2qnYbH/+8wGEdY70Qf7l1l/jmBUbrmQqwiPKAQE6cOQ7dTj6Xr0HzQDGHyceQ==", "os": ["aix"], @@ -221,155 +147,6 @@ "os": ["win32"], "cpu": ["x64"] }, - "@eslint-community/eslint-utils@4.7.0_eslint@9.31.0": { - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dependencies": [ - "eslint", - "eslint-visitor-keys@3.4.3" - ] - }, - "@eslint-community/regexpp@4.12.1": { - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==" - }, - "@eslint/config-array@0.21.0": { - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dependencies": [ - "@eslint/object-schema", - "debug", - "minimatch@3.1.2" - ] - }, - "@eslint/config-helpers@0.3.0": { - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==" - }, - "@eslint/core@0.15.1": { - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dependencies": [ - "@types/json-schema" - ] - }, - "@eslint/eslintrc@3.3.1": { - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dependencies": [ - "ajv", - "debug", - "espree", - "globals@14.0.0", - "ignore", - "import-fresh", - "js-yaml", - "minimatch@3.1.2", - "strip-json-comments" - ] - }, - "@eslint/js@9.31.0": { - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==" - }, - "@eslint/object-schema@2.1.6": { - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==" - }, - "@eslint/plugin-kit@0.3.3": { - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", - "dependencies": [ - "@eslint/core", - "levn" - ] - }, - "@floating-ui/core@1.7.2": { - "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", - "dependencies": [ - "@floating-ui/utils" - ] - }, - "@floating-ui/dom@1.7.2": { - "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", - "dependencies": [ - "@floating-ui/core", - "@floating-ui/utils" - ] - }, - "@floating-ui/utils@0.2.10": { - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" - }, - "@humanfs/core@0.19.1": { - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==" - }, - "@humanfs/node@0.16.6": { - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dependencies": [ - "@humanfs/core", - "@humanwhocodes/retry@0.3.1" - ] - }, - "@humanwhocodes/module-importer@1.0.1": { - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" - }, - "@humanwhocodes/retry@0.3.1": { - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==" - }, - "@humanwhocodes/retry@0.4.3": { - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" - }, - "@isaacs/cliui@8.0.2": { - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dependencies": [ - "string-width@5.1.2", - "string-width-cjs@npm:string-width@4.2.3", - "strip-ansi@7.1.0", - "strip-ansi-cjs@npm:strip-ansi@6.0.1", - "wrap-ansi@8.1.0", - "wrap-ansi-cjs@npm:wrap-ansi@7.0.0" - ] - }, - "@jridgewell/gen-mapping@0.3.12": { - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dependencies": [ - "@jridgewell/sourcemap-codec", - "@jridgewell/trace-mapping" - ] - }, - "@jridgewell/resolve-uri@3.1.2": { - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" - }, - "@jridgewell/sourcemap-codec@1.5.4": { - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" - }, - "@jridgewell/trace-mapping@0.3.29": { - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dependencies": [ - "@jridgewell/resolve-uri", - "@jridgewell/sourcemap-codec" - ] - }, - "@noble/curves@1.9.4": { - "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", - "dependencies": [ - "@noble/hashes" - ] - }, - "@noble/hashes@1.8.0": { - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" - }, - "@nodelib/fs.scandir@2.1.5": { - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": [ - "@nodelib/fs.stat", - "run-parallel" - ] - }, - "@nodelib/fs.stat@2.0.5": { - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" - }, - "@nodelib/fs.walk@1.2.8": { - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": [ - "@nodelib/fs.scandir", - "fastq" - ] - }, - "@pkgjs/parseargs@0.11.0": { - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==" - }, "@playwright/test@1.54.1": { "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", "dependencies": [ @@ -377,157 +154,9 @@ ], "bin": true }, - "@popperjs/core@2.11.8": { - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" - }, - "@rollup/plugin-node-resolve@15.3.1": { - "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", - "dependencies": [ - "@rollup/pluginutils", - "@types/resolve", - "deepmerge", - "is-module", - "resolve" - ] - }, - "@rollup/pluginutils@5.2.0_rollup@4.45.1": { - "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", - "dependencies": [ - "@types/estree", - "estree-walker", - "picomatch@4.0.3", - "rollup" - ], - "optionalPeers": [ - "rollup" - ] - }, - "@rollup/rollup-android-arm-eabi@4.45.1": { - "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", - "os": ["android"], - "cpu": ["arm"] - }, - "@rollup/rollup-android-arm64@4.45.1": { - "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", - "os": ["android"], - "cpu": ["arm64"] - }, - "@rollup/rollup-darwin-arm64@4.45.1": { - "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@rollup/rollup-darwin-x64@4.45.1": { - "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", - "os": ["darwin"], - "cpu": ["x64"] - }, - "@rollup/rollup-freebsd-arm64@4.45.1": { - "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", - "os": ["freebsd"], - "cpu": ["arm64"] - }, - "@rollup/rollup-freebsd-x64@4.45.1": { - "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", - "os": ["freebsd"], - "cpu": ["x64"] - }, - "@rollup/rollup-linux-arm-gnueabihf@4.45.1": { - "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", - "os": ["linux"], - "cpu": ["arm"] - }, - "@rollup/rollup-linux-arm-musleabihf@4.45.1": { - "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", - "os": ["linux"], - "cpu": ["arm"] - }, - "@rollup/rollup-linux-arm64-gnu@4.45.1": { - "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@rollup/rollup-linux-arm64-musl@4.45.1": { - "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@rollup/rollup-linux-loongarch64-gnu@4.45.1": { - "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", - "os": ["linux"], - "cpu": ["loong64"] - }, - "@rollup/rollup-linux-powerpc64le-gnu@4.45.1": { - "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", - "os": ["linux"], - "cpu": ["ppc64"] - }, - "@rollup/rollup-linux-riscv64-gnu@4.45.1": { - "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", - "os": ["linux"], - "cpu": ["riscv64"] - }, - "@rollup/rollup-linux-riscv64-musl@4.45.1": { - "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", - "os": ["linux"], - "cpu": ["riscv64"] - }, - "@rollup/rollup-linux-s390x-gnu@4.45.1": { - "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", - "os": ["linux"], - "cpu": ["s390x"] - }, - "@rollup/rollup-linux-x64-gnu@4.45.1": { - "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@rollup/rollup-linux-x64-musl@4.45.1": { - "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@rollup/rollup-win32-arm64-msvc@4.45.1": { - "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", - "os": ["win32"], - "cpu": ["arm64"] - }, - "@rollup/rollup-win32-ia32-msvc@4.45.1": { - "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", - "os": ["win32"], - "cpu": ["ia32"] - }, - "@rollup/rollup-win32-x64-msvc@4.45.1": { - "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", - "os": ["win32"], - "cpu": ["x64"] - }, "@sindresorhus/is@4.6.0": { "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" }, - "@sveltejs/acorn-typescript@1.0.5_acorn@8.15.0": { - "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", - "dependencies": [ - "acorn@8.15.0" - ] - }, - "@tailwindcss/forms@0.5.10_tailwindcss@3.4.17__postcss@8.5.6": { - "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", - "dependencies": [ - "mini-svg-data-uri", - "tailwindcss" - ] - }, - "@tailwindcss/typography@0.5.16_tailwindcss@3.4.17__postcss@8.5.6": { - "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", - "dependencies": [ - "lodash.castarray", - "lodash.isplainobject", - "lodash.merge", - "postcss-selector-parser@6.0.10", - "tailwindcss" - ] - }, "@types/d3-array@3.2.1": { "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" }, @@ -688,18 +317,12 @@ "@types/d3-zoom" ] }, - "@types/estree@1.0.8": { - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" - }, "@types/geojson@7946.0.16": { "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" }, "@types/he@1.2.3": { "integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==" }, - "@types/json-schema@7.0.15": { - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, "@types/mathjax@0.0.40": { "integrity": "sha512-rHusx08LCg92WJxrsM3SPjvLTSvK5C+gealtSuhKbEOcUZfWlwigaFoPLf6Dfxhg4oryN5qP9Sj7zOQ4HYXINw==" }, @@ -721,1802 +344,70 @@ "@types/node@22.15.15" ] }, - "@types/resolve@1.20.2": { - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" - }, - "@yr/monotone-cubic-spline@1.0.3": { - "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" + "bech32@2.0.0": { + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" }, - "a-sync-waterfall@1.0.1": { - "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==" + "char-regex@1.0.2": { + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" }, - "acorn-jsx@5.3.2_acorn@8.15.0": { - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dependencies": [ - "acorn@8.15.0" - ] + "emojilib@2.4.0": { + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" }, - "acorn@7.4.1": { - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": true + "fsevents@2.3.2": { + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "os": ["darwin"], + "scripts": true }, - "acorn@8.15.0": { - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "he@1.2.0": { + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "bin": true }, - "ajv@6.12.6": { - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": [ - "fast-deep-equal", - "fast-json-stable-stringify", - "json-schema-traverse", - "uri-js" - ] - }, - "ansi-regex@5.0.1": { - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-regex@6.1.0": { - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" - }, - "ansi-styles@4.3.0": { - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": [ - "color-convert" - ] - }, - "ansi-styles@6.2.1": { - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" - }, - "any-promise@1.3.0": { - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" - }, - "anymatch@3.1.3": { - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": [ - "normalize-path", - "picomatch@2.3.1" - ] + "highlight.js@11.11.1": { + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==" }, - "apexcharts@3.54.1": { - "integrity": "sha512-E4et0h/J1U3r3EwS/WlqJCQIbepKbp6wGUmaAwJOMjHUP4Ci0gxanLa7FR3okx6p9coi4st6J853/Cb1NP0vpA==", + "node-emoji@2.2.0": { + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", "dependencies": [ - "@yr/monotone-cubic-spline", - "svg.draggable.js", - "svg.easing.js", - "svg.filter.js", - "svg.pathmorphing.js", - "svg.resize.js", - "svg.select.js@3.0.1" + "@sindresorhus/is", + "char-regex", + "emojilib", + "skin-tone" ] }, - "arg@5.0.2": { - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, - "argparse@2.0.1": { - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "aria-query@5.3.2": { - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" - }, - "asap@2.0.6": { - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + "plantuml-encoder@1.4.0": { + "integrity": "sha512-sxMwpDw/ySY1WB2CE3+IdMuEcWibJ72DDOsXLkSmEaSzwEUaYBT6DWgOfBiHGCux4q433X6+OEFWjlVqp7gL6g==" }, - "asciidoctor@3.0.4_@asciidoctor+core@3.0.4": { - "integrity": "sha512-hIc0Bx73wePxtic+vWBHOIgMfKSNiCmRz7BBfkyykXATrw20YGd5a3CozCHvqEPH+Wxp5qKD4aBsgtokez8nEA==", - "dependencies": [ - "@asciidoctor/cli", - "@asciidoctor/core", - "ejs", - "handlebars", - "nunjucks", - "pug" - ], + "playwright-core@1.54.1": { + "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", "bin": true }, - "assert-never@1.4.0": { - "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==" - }, - "async@3.2.6": { - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" - }, - "autoprefixer@10.4.21_postcss@8.5.6": { - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "playwright@1.54.1": { + "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", "dependencies": [ - "browserslist", - "caniuse-lite", - "fraction.js", - "normalize-range", - "picocolors", - "postcss", - "postcss-value-parser" + "playwright-core" ], - "bin": true - }, - "axobject-query@4.1.0": { - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" - }, - "babel-walk@3.0.0-canary-5": { - "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", - "dependencies": [ - "@babel/types" - ] - }, - "balanced-match@1.0.2": { - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "bech32@2.0.0": { - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" - }, - "binary-extensions@2.3.0": { - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==" - }, - "brace-expansion@1.1.12": { - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dependencies": [ - "balanced-match", - "concat-map" - ] - }, - "brace-expansion@2.0.2": { - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dependencies": [ - "balanced-match" - ] - }, - "braces@3.0.3": { - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dependencies": [ - "fill-range" - ] - }, - "browserslist@4.25.1": { - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", - "dependencies": [ - "caniuse-lite", - "electron-to-chromium", - "node-releases", - "update-browserslist-db" + "optionalDependencies": [ + "fsevents" ], "bin": true }, - "call-bind-apply-helpers@1.0.2": { - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": [ - "es-errors", - "function-bind" - ] - }, - "call-bound@1.0.4": { - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "skin-tone@2.0.0": { + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", "dependencies": [ - "call-bind-apply-helpers", - "get-intrinsic" + "unicode-emoji-modifier-base" ] }, - "callsites@3.1.0": { - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - }, - "camelcase-css@2.0.1": { - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" - }, - "camelcase@5.3.1": { - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "caniuse-lite@1.0.30001727": { - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==" - }, - "chalk@4.1.2": { - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": [ - "ansi-styles@4.3.0", - "supports-color" - ] + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "char-regex@1.0.2": { - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" + "undici-types@6.21.0": { + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, - "character-parser@2.2.0": { - "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", - "dependencies": [ - "is-regex" - ] - }, - "chokidar@3.6.0": { - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": [ - "anymatch", - "braces", - "glob-parent@5.1.2", - "is-binary-path", - "is-glob", - "normalize-path", - "readdirp@3.6.0" - ], - "optionalDependencies": [ - "fsevents@2.3.3" - ] - }, - "chokidar@4.0.3": { - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dependencies": [ - "readdirp@4.1.2" - ] - }, - "cliui@6.0.0": { - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dependencies": [ - "string-width@4.2.3", - "strip-ansi@6.0.1", - "wrap-ansi@6.2.0" - ] - }, - "cliui@7.0.4": { - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": [ - "string-width@4.2.3", - "strip-ansi@6.0.1", - "wrap-ansi@7.0.0" - ] - }, - "clsx@2.1.1": { - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" - }, - "color-convert@2.0.1": { - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": [ - "color-name" - ] - }, - "color-name@1.1.4": { - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "commander@4.1.1": { - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" - }, - "commander@5.1.0": { - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" - }, - "concat-map@0.0.1": { - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "constantinople@4.0.1": { - "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", - "dependencies": [ - "@babel/parser", - "@babel/types" - ] - }, - "cross-spawn@7.0.6": { - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dependencies": [ - "path-key", - "shebang-command", - "which" - ] - }, - "cssesc@3.0.0": { - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": true - }, - "debug@4.4.1": { - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dependencies": [ - "ms" - ] - }, - "decamelize@1.2.0": { - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" - }, - "deep-is@0.1.4": { - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, - "deepmerge@4.3.1": { - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" - }, - "didyoumean@1.2.2": { - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" - }, - "dijkstrajs@1.0.3": { - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" - }, - "dlv@1.1.3": { - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" - }, - "doctypes@1.1.0": { - "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" - }, - "dunder-proto@1.0.1": { - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": [ - "call-bind-apply-helpers", - "es-errors", - "gopd" - ] - }, - "eastasianwidth@0.2.0": { - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "ejs@3.1.10": { - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dependencies": [ - "jake" - ], - "bin": true - }, - "electron-to-chromium@1.5.187": { - "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==" - }, - "emoji-regex@8.0.0": { - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "emoji-regex@9.2.2": { - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "emojilib@2.4.0": { - "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" - }, - "es-define-property@1.0.1": { - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" - }, - "es-errors@1.3.0": { - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "es-object-atoms@1.1.1": { - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": [ - "es-errors" - ] - }, - "escalade@3.2.0": { - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" - }, - "escape-string-regexp@4.0.0": { - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - }, - "eslint-plugin-svelte@3.11.0_eslint@9.31.0_svelte@5.36.8__acorn@8.15.0_postcss@8.5.6": { - "integrity": "sha512-KliWlkieHyEa65aQIkRwUFfHzT5Cn4u3BQQsu3KlkJOs7c1u7ryn84EWaOjEzilbKgttT4OfBURA8Uc4JBSQIw==", - "dependencies": [ - "@eslint-community/eslint-utils", - "@jridgewell/sourcemap-codec", - "eslint", - "esutils", - "globals@16.3.0", - "known-css-properties", - "postcss", - "postcss-load-config@3.1.4_postcss@8.5.6", - "postcss-safe-parser", - "semver", - "svelte", - "svelte-eslint-parser" - ], - "optionalPeers": [ - "svelte" - ] - }, - "eslint-scope@8.4.0": { - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dependencies": [ - "esrecurse", - "estraverse" - ] - }, - "eslint-visitor-keys@3.4.3": { - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" - }, - "eslint-visitor-keys@4.2.1": { - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==" - }, - "eslint@9.31.0": { - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", - "dependencies": [ - "@eslint-community/eslint-utils", - "@eslint-community/regexpp", - "@eslint/config-array", - "@eslint/config-helpers", - "@eslint/core", - "@eslint/eslintrc", - "@eslint/js", - "@eslint/plugin-kit", - "@humanfs/node", - "@humanwhocodes/module-importer", - "@humanwhocodes/retry@0.4.3", - "@types/estree", - "@types/json-schema", - "ajv", - "chalk", - "cross-spawn", - "debug", - "escape-string-regexp", - "eslint-scope", - "eslint-visitor-keys@4.2.1", - "espree", - "esquery", - "esutils", - "fast-deep-equal", - "file-entry-cache", - "find-up@5.0.0", - "glob-parent@6.0.2", - "ignore", - "imurmurhash", - "is-glob", - "json-stable-stringify-without-jsonify", - "lodash.merge", - "minimatch@3.1.2", - "natural-compare", - "optionator" - ], - "bin": true - }, - "esm-env@1.2.2": { - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" - }, - "espree@10.4.0_acorn@8.15.0": { - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dependencies": [ - "acorn@8.15.0", - "acorn-jsx", - "eslint-visitor-keys@4.2.1" - ] - }, - "esquery@1.6.0": { - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dependencies": [ - "estraverse" - ] - }, - "esrap@2.1.0": { - "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", - "dependencies": [ - "@jridgewell/sourcemap-codec" - ] - }, - "esrecurse@4.3.0": { - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dependencies": [ - "estraverse" - ] - }, - "estraverse@5.3.0": { - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - }, - "estree-walker@2.0.2": { - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "esutils@2.0.3": { - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "fast-deep-equal@3.1.3": { - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-glob@3.3.3": { - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dependencies": [ - "@nodelib/fs.stat", - "@nodelib/fs.walk", - "glob-parent@5.1.2", - "merge2", - "micromatch" - ] - }, - "fast-json-stable-stringify@2.1.0": { - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-levenshtein@2.0.6": { - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "fastq@1.19.1": { - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dependencies": [ - "reusify" - ] - }, - "fdir@6.4.6_picomatch@4.0.3": { - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dependencies": [ - "picomatch@4.0.3" - ], - "optionalPeers": [ - "picomatch@4.0.3" - ] - }, - "file-entry-cache@8.0.0": { - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dependencies": [ - "flat-cache" - ] - }, - "filelist@1.0.4": { - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dependencies": [ - "minimatch@5.1.6" - ] - }, - "fill-range@7.1.1": { - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dependencies": [ - "to-regex-range" - ] - }, - "find-up@4.1.0": { - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": [ - "locate-path@5.0.0", - "path-exists" - ] - }, - "find-up@5.0.0": { - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dependencies": [ - "locate-path@6.0.0", - "path-exists" - ] - }, - "flat-cache@4.0.1": { - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dependencies": [ - "flatted", - "keyv" - ] - }, - "flatted@3.3.3": { - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" - }, - "flowbite-datepicker@1.3.2": { - "integrity": "sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g==", - "dependencies": [ - "@rollup/plugin-node-resolve", - "flowbite@2.5.2" - ] - }, - "flowbite-svelte-icons@2.1.1_svelte@5.36.8__acorn@8.15.0_tailwind-merge@3.3.1": { - "integrity": "sha512-VNNMcekjbM1bQEGgbdGsdYR9mRdTj/L0A5ba0P1tiFv5QB9GvbvJMABJoiD80eqpZUkfR2QVOmiZfgCwHicT/Q==", - "dependencies": [ - "svelte", - "tailwind-merge" - ] - }, - "flowbite-svelte@0.48.6_svelte@5.36.8__acorn@8.15.0": { - "integrity": "sha512-/PmeR3ipHHvda8vVY9MZlymaRoJsk8VddEeoLzIygfYwJV68ey8gHuQPC1dq9J6NDCTE5+xOPtBiYUtVjCfvZw==", - "dependencies": [ - "@floating-ui/dom", - "apexcharts", - "flowbite@3.1.2", - "svelte", - "tailwind-merge" - ] - }, - "flowbite@2.5.2": { - "integrity": "sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA==", - "dependencies": [ - "@popperjs/core", - "flowbite-datepicker", - "mini-svg-data-uri" - ] - }, - "flowbite@3.1.2": { - "integrity": "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==", - "dependencies": [ - "@popperjs/core", - "flowbite-datepicker", - "mini-svg-data-uri", - "postcss" - ] - }, - "foreground-child@3.3.1": { - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dependencies": [ - "cross-spawn", - "signal-exit" - ] - }, - "fraction.js@4.3.7": { - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==" - }, - "fs.realpath@1.0.0": { - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "fsevents@2.3.2": { - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "os": ["darwin"], - "scripts": true - }, - "fsevents@2.3.3": { - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "os": ["darwin"], - "scripts": true - }, - "function-bind@1.1.2": { - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-caller-file@2.0.5": { - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-intrinsic@1.3.0": { - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": [ - "call-bind-apply-helpers", - "es-define-property", - "es-errors", - "es-object-atoms", - "function-bind", - "get-proto", - "gopd", - "has-symbols", - "hasown", - "math-intrinsics" - ] - }, - "get-proto@1.0.1": { - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": [ - "dunder-proto", - "es-object-atoms" - ] - }, - "glob-parent@5.1.2": { - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": [ - "is-glob" - ] - }, - "glob-parent@6.0.2": { - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dependencies": [ - "is-glob" - ] - }, - "glob@10.4.5": { - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dependencies": [ - "foreground-child", - "jackspeak", - "minimatch@9.0.5", - "minipass", - "package-json-from-dist", - "path-scurry" - ], - "bin": true - }, - "glob@8.1.0": { - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dependencies": [ - "fs.realpath", - "inflight", - "inherits", - "minimatch@5.1.6", - "once" - ], - "deprecated": true - }, - "globals@14.0.0": { - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" - }, - "globals@16.3.0": { - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==" - }, - "gopd@1.2.0": { - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" - }, - "handlebars@4.7.8": { - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dependencies": [ - "minimist", - "neo-async", - "source-map", - "wordwrap" - ], - "optionalDependencies": [ - "uglify-js" - ], - "bin": true - }, - "has-flag@4.0.0": { - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-symbols@1.1.0": { - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" - }, - "has-tostringtag@1.0.2": { - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": [ - "has-symbols" - ] - }, - "hasown@2.0.2": { - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": [ - "function-bind" - ] - }, - "he@1.2.0": { - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": true - }, - "highlight.js@11.11.1": { - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==" - }, - "ignore@5.3.2": { - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" - }, - "import-fresh@3.3.1": { - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dependencies": [ - "parent-module", - "resolve-from" - ] - }, - "imurmurhash@0.1.4": { - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" - }, - "inflight@1.0.6": { - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": [ - "once", - "wrappy" - ], - "deprecated": true - }, - "inherits@2.0.4": { - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "is-binary-path@2.1.0": { - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": [ - "binary-extensions" - ] - }, - "is-core-module@2.16.1": { - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dependencies": [ - "hasown" - ] - }, - "is-expression@4.0.0": { - "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", - "dependencies": [ - "acorn@7.4.1", - "object-assign" - ] - }, - "is-extglob@2.1.1": { - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" - }, - "is-fullwidth-code-point@3.0.0": { - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-glob@4.0.3": { - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": [ - "is-extglob" - ] - }, - "is-module@1.0.0": { - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" - }, - "is-number@7.0.0": { - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-promise@2.2.2": { - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" - }, - "is-reference@3.0.3": { - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dependencies": [ - "@types/estree" - ] - }, - "is-regex@1.2.1": { - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dependencies": [ - "call-bound", - "gopd", - "has-tostringtag", - "hasown" - ] - }, - "isexe@2.0.0": { - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "jackspeak@3.4.3": { - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dependencies": [ - "@isaacs/cliui" - ], - "optionalDependencies": [ - "@pkgjs/parseargs" - ] - }, - "jake@10.9.2": { - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dependencies": [ - "async", - "chalk", - "filelist", - "minimatch@3.1.2" - ], - "bin": true - }, - "jiti@1.21.7": { - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "bin": true - }, - "js-stringify@1.0.2": { - "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" - }, - "js-yaml@4.1.0": { - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": [ - "argparse" - ], - "bin": true - }, - "json-buffer@3.0.1": { - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "json-schema-traverse@0.4.1": { - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stable-stringify-without-jsonify@1.0.1": { - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" - }, - "jstransformer@1.0.0": { - "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", - "dependencies": [ - "is-promise", - "promise" - ] - }, - "keyv@4.5.4": { - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dependencies": [ - "json-buffer" - ] - }, - "known-css-properties@0.37.0": { - "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==" - }, - "levn@0.4.1": { - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dependencies": [ - "prelude-ls", - "type-check" - ] - }, - "lilconfig@2.1.0": { - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==" - }, - "lilconfig@3.1.3": { - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==" - }, - "lines-and-columns@1.2.4": { - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "locate-character@3.0.0": { - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" - }, - "locate-path@5.0.0": { - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": [ - "p-locate@4.1.0" - ] - }, - "locate-path@6.0.0": { - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": [ - "p-locate@5.0.0" - ] - }, - "lodash.castarray@4.4.0": { - "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==" - }, - "lodash.isplainobject@4.0.6": { - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "lodash.merge@4.6.2": { - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "lru-cache@10.4.3": { - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" - }, - "magic-string@0.30.17": { - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dependencies": [ - "@jridgewell/sourcemap-codec" - ] - }, - "math-intrinsics@1.1.0": { - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" - }, - "merge2@1.4.1": { - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" - }, - "micromatch@4.0.8": { - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dependencies": [ - "braces", - "picomatch@2.3.1" - ] - }, - "mini-svg-data-uri@1.4.4": { - "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", - "bin": true - }, - "minimatch@3.1.2": { - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": [ - "brace-expansion@1.1.12" - ] - }, - "minimatch@5.1.6": { - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": [ - "brace-expansion@2.0.2" - ] - }, - "minimatch@9.0.5": { - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": [ - "brace-expansion@2.0.2" - ] - }, - "minimist@1.2.8": { - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - }, - "minipass@7.1.2": { - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" - }, - "mri@1.2.0": { - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" - }, - "ms@2.1.3": { - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "mz@2.7.0": { - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dependencies": [ - "any-promise", - "object-assign", - "thenify-all" - ] - }, - "nanoid@3.3.11": { - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "bin": true - }, - "natural-compare@1.4.0": { - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" - }, - "neo-async@2.6.2": { - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node-emoji@2.2.0": { - "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", - "dependencies": [ - "@sindresorhus/is", - "char-regex", - "emojilib", - "skin-tone" - ] - }, - "node-releases@2.0.19": { - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" - }, - "normalize-path@3.0.0": { - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "normalize-range@0.1.2": { - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" - }, - "nunjucks@3.2.4": { - "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", - "dependencies": [ - "a-sync-waterfall", - "asap", - "commander@5.1.0" - ], - "bin": true - }, - "object-assign@4.1.1": { - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "object-hash@3.0.0": { - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" - }, - "once@1.4.0": { - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": [ - "wrappy" - ] - }, - "optionator@0.9.4": { - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dependencies": [ - "deep-is", - "fast-levenshtein", - "levn", - "prelude-ls", - "type-check", - "word-wrap" - ] - }, - "p-limit@2.3.0": { - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": [ - "p-try" - ] - }, - "p-limit@3.1.0": { - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": [ - "yocto-queue" - ] - }, - "p-locate@4.1.0": { - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": [ - "p-limit@2.3.0" - ] - }, - "p-locate@5.0.0": { - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": [ - "p-limit@3.1.0" - ] - }, - "p-try@2.2.0": { - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "package-json-from-dist@1.0.1": { - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" - }, - "parent-module@1.0.1": { - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": [ - "callsites" - ] - }, - "path-exists@4.0.0": { - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-key@3.1.1": { - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-parse@1.0.7": { - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "path-scurry@1.11.1": { - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dependencies": [ - "lru-cache", - "minipass" - ] - }, - "picocolors@1.1.1": { - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "picomatch@2.3.1": { - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" - }, - "picomatch@4.0.3": { - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" - }, - "pify@2.3.0": { - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" - }, - "pirates@4.0.7": { - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==" - }, - "plantuml-encoder@1.4.0": { - "integrity": "sha512-sxMwpDw/ySY1WB2CE3+IdMuEcWibJ72DDOsXLkSmEaSzwEUaYBT6DWgOfBiHGCux4q433X6+OEFWjlVqp7gL6g==" - }, - "playwright-core@1.54.1": { - "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", - "bin": true - }, - "playwright@1.54.1": { - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", - "dependencies": [ - "playwright-core" - ], - "optionalDependencies": [ - "fsevents@2.3.2" - ], - "bin": true - }, - "pngjs@5.0.0": { - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" - }, - "postcss-import@15.1.0_postcss@8.5.6": { - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dependencies": [ - "postcss", - "postcss-value-parser", - "read-cache", - "resolve" - ] - }, - "postcss-js@4.0.1_postcss@8.5.6": { - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dependencies": [ - "camelcase-css", - "postcss" - ] - }, - "postcss-load-config@3.1.4_postcss@8.5.6": { - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dependencies": [ - "lilconfig@2.1.0", - "postcss", - "yaml@1.10.2" - ], - "optionalPeers": [ - "postcss" - ] - }, - "postcss-load-config@4.0.2_postcss@8.5.6": { - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dependencies": [ - "lilconfig@3.1.3", - "postcss", - "yaml@2.8.0" - ], - "optionalPeers": [ - "postcss" - ] - }, - "postcss-load-config@6.0.1_postcss@8.5.6": { - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dependencies": [ - "lilconfig@3.1.3", - "postcss" - ], - "optionalPeers": [ - "postcss" - ] - }, - "postcss-nested@6.2.0_postcss@8.5.6": { - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dependencies": [ - "postcss", - "postcss-selector-parser@6.1.2" - ] - }, - "postcss-safe-parser@7.0.1_postcss@8.5.6": { - "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", - "dependencies": [ - "postcss" - ] - }, - "postcss-scss@4.0.9_postcss@8.5.6": { - "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", - "dependencies": [ - "postcss" - ] - }, - "postcss-selector-parser@6.0.10": { - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dependencies": [ - "cssesc", - "util-deprecate" - ] - }, - "postcss-selector-parser@6.1.2": { - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dependencies": [ - "cssesc", - "util-deprecate" - ] - }, - "postcss-selector-parser@7.1.0": { - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "dependencies": [ - "cssesc", - "util-deprecate" - ] - }, - "postcss-value-parser@4.2.0": { - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "postcss@8.5.6": { - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dependencies": [ - "nanoid", - "picocolors", - "source-map-js" - ] - }, - "prelude-ls@1.2.1": { - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" - }, - "prettier-plugin-svelte@3.4.0_prettier@3.6.2_svelte@5.36.8__acorn@8.15.0": { - "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", - "dependencies": [ - "prettier", - "svelte" - ] - }, - "prettier@3.6.2": { - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "bin": true - }, - "promise@7.3.1": { - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dependencies": [ - "asap" - ] - }, - "pug-attrs@3.0.0": { - "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", - "dependencies": [ - "constantinople", - "js-stringify", - "pug-runtime" - ] - }, - "pug-code-gen@3.0.3": { - "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", - "dependencies": [ - "constantinople", - "doctypes", - "js-stringify", - "pug-attrs", - "pug-error", - "pug-runtime", - "void-elements", - "with" - ] - }, - "pug-error@2.1.0": { - "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==" - }, - "pug-filters@4.0.0": { - "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", - "dependencies": [ - "constantinople", - "jstransformer", - "pug-error", - "pug-walk", - "resolve" - ] - }, - "pug-lexer@5.0.1": { - "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", - "dependencies": [ - "character-parser", - "is-expression", - "pug-error" - ] - }, - "pug-linker@4.0.0": { - "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", - "dependencies": [ - "pug-error", - "pug-walk" - ] - }, - "pug-load@3.0.0": { - "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", - "dependencies": [ - "object-assign", - "pug-walk" - ] - }, - "pug-parser@6.0.0": { - "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", - "dependencies": [ - "pug-error", - "token-stream" - ] - }, - "pug-runtime@3.0.1": { - "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==" - }, - "pug-strip-comments@2.0.0": { - "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", - "dependencies": [ - "pug-error" - ] - }, - "pug-walk@2.0.0": { - "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" - }, - "pug@3.0.3": { - "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", - "dependencies": [ - "pug-code-gen", - "pug-filters", - "pug-lexer", - "pug-linker", - "pug-load", - "pug-parser", - "pug-runtime", - "pug-strip-comments" - ] - }, - "punycode@2.3.1": { - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" - }, - "qrcode@1.5.4": { - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "dependencies": [ - "dijkstrajs", - "pngjs", - "yargs@15.4.1" - ], - "bin": true - }, - "queue-microtask@1.2.3": { - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" - }, - "read-cache@1.0.0": { - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dependencies": [ - "pify" - ] - }, - "readdirp@3.6.0": { - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": [ - "picomatch@2.3.1" - ] - }, - "readdirp@4.1.2": { - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" - }, - "require-directory@2.1.1": { - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" - }, - "require-main-filename@2.0.0": { - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "resolve-from@4.0.0": { - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - }, - "resolve@1.22.10": { - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dependencies": [ - "is-core-module", - "path-parse", - "supports-preserve-symlinks-flag" - ], - "bin": true - }, - "reusify@1.1.0": { - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==" - }, - "rollup@4.45.1": { - "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", - "dependencies": [ - "@types/estree" - ], - "optionalDependencies": [ - "@rollup/rollup-android-arm-eabi", - "@rollup/rollup-android-arm64", - "@rollup/rollup-darwin-arm64", - "@rollup/rollup-darwin-x64", - "@rollup/rollup-freebsd-arm64", - "@rollup/rollup-freebsd-x64", - "@rollup/rollup-linux-arm-gnueabihf", - "@rollup/rollup-linux-arm-musleabihf", - "@rollup/rollup-linux-arm64-gnu", - "@rollup/rollup-linux-arm64-musl", - "@rollup/rollup-linux-loongarch64-gnu", - "@rollup/rollup-linux-powerpc64le-gnu", - "@rollup/rollup-linux-riscv64-gnu", - "@rollup/rollup-linux-riscv64-musl", - "@rollup/rollup-linux-s390x-gnu", - "@rollup/rollup-linux-x64-gnu", - "@rollup/rollup-linux-x64-musl", - "@rollup/rollup-win32-arm64-msvc", - "@rollup/rollup-win32-ia32-msvc", - "@rollup/rollup-win32-x64-msvc", - "fsevents@2.3.3" - ], - "bin": true - }, - "run-parallel@1.2.0": { - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dependencies": [ - "queue-microtask" - ] - }, - "sade@1.8.1": { - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dependencies": [ - "mri" - ] - }, - "semver@7.7.2": { - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "bin": true - }, - "set-blocking@2.0.0": { - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "shebang-command@2.0.0": { - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": [ - "shebang-regex" - ] - }, - "shebang-regex@3.0.0": { - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "signal-exit@4.1.0": { - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" - }, - "skin-tone@2.0.0": { - "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", - "dependencies": [ - "unicode-emoji-modifier-base" - ] - }, - "source-map-js@1.2.1": { - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" - }, - "source-map@0.6.1": { - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "string-width@4.2.3": { - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": [ - "emoji-regex@8.0.0", - "is-fullwidth-code-point", - "strip-ansi@6.0.1" - ] - }, - "string-width@5.1.2": { - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": [ - "eastasianwidth", - "emoji-regex@9.2.2", - "strip-ansi@7.1.0" - ] - }, - "strip-ansi@6.0.1": { - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": [ - "ansi-regex@5.0.1" - ] - }, - "strip-ansi@7.1.0": { - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": [ - "ansi-regex@6.1.0" - ] - }, - "strip-json-comments@3.1.1": { - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" - }, - "sucrase@3.35.0": { - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dependencies": [ - "@jridgewell/gen-mapping", - "commander@4.1.1", - "glob@10.4.5", - "lines-and-columns", - "mz", - "pirates", - "ts-interface-checker" - ], - "bin": true - }, - "supports-color@7.2.0": { - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": [ - "has-flag" - ] - }, - "supports-preserve-symlinks-flag@1.0.0": { - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - }, - "svelte-check@4.3.0_svelte@5.36.8__acorn@8.15.0_typescript@5.8.3": { - "integrity": "sha512-Iz8dFXzBNAM7XlEIsUjUGQhbEE+Pvv9odb9+0+ITTgFWZBGeJRRYqHUUglwe2EkLD5LIsQaAc4IUJyvtKuOO5w==", - "dependencies": [ - "@jridgewell/trace-mapping", - "chokidar@4.0.3", - "fdir", - "picocolors", - "sade", - "svelte", - "typescript" - ], - "bin": true - }, - "svelte-eslint-parser@1.3.0_svelte@5.36.8__acorn@8.15.0_postcss@8.5.6": { - "integrity": "sha512-VCgMHKV7UtOGcGLGNFSbmdm6kEKjtzo5nnpGU/mnx4OsFY6bZ7QwRF5DUx+Hokw5Lvdyo8dpk8B1m8mliomrNg==", - "dependencies": [ - "eslint-scope", - "eslint-visitor-keys@4.2.1", - "espree", - "postcss", - "postcss-scss", - "postcss-selector-parser@7.1.0", - "svelte" - ], - "optionalPeers": [ - "svelte" - ] - }, - "svelte@5.36.8_acorn@8.15.0": { - "integrity": "sha512-8JbZWQu96hMjH/oYQPxXW6taeC6Awl6muGHeZzJTxQx7NGRQ/J9wN1hkzRKLOlSDlbS2igiFg7p5xyTp5uXG3A==", - "dependencies": [ - "@ampproject/remapping", - "@jridgewell/sourcemap-codec", - "@sveltejs/acorn-typescript", - "@types/estree", - "acorn@8.15.0", - "aria-query", - "axobject-query", - "clsx", - "esm-env", - "esrap", - "is-reference", - "locate-character", - "magic-string", - "zimmerframe" - ] - }, - "svg.draggable.js@2.2.2": { - "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", - "dependencies": [ - "svg.js" - ] - }, - "svg.easing.js@2.0.0": { - "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", - "dependencies": [ - "svg.js" - ] - }, - "svg.filter.js@2.0.2": { - "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", - "dependencies": [ - "svg.js" - ] - }, - "svg.js@2.7.1": { - "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" - }, - "svg.pathmorphing.js@0.1.3": { - "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", - "dependencies": [ - "svg.js" - ] - }, - "svg.resize.js@1.4.3": { - "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", - "dependencies": [ - "svg.js", - "svg.select.js@2.1.2" - ] - }, - "svg.select.js@2.1.2": { - "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", - "dependencies": [ - "svg.js" - ] - }, - "svg.select.js@3.0.1": { - "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", - "dependencies": [ - "svg.js" - ] - }, - "tailwind-merge@3.3.1": { - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==" - }, - "tailwindcss@3.4.17_postcss@8.5.6": { - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dependencies": [ - "@alloc/quick-lru", - "arg", - "chokidar@3.6.0", - "didyoumean", - "dlv", - "fast-glob", - "glob-parent@6.0.2", - "is-glob", - "jiti", - "lilconfig@3.1.3", - "micromatch", - "normalize-path", - "object-hash", - "picocolors", - "postcss", - "postcss-import", - "postcss-js", - "postcss-load-config@4.0.2_postcss@8.5.6", - "postcss-nested", - "postcss-selector-parser@6.1.2", - "resolve", - "sucrase" - ], - "bin": true - }, - "thenify-all@1.6.0": { - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dependencies": [ - "thenify" - ] - }, - "thenify@3.3.1": { - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dependencies": [ - "any-promise" - ] - }, - "to-regex-range@5.0.1": { - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": [ - "is-number" - ] - }, - "token-stream@1.0.0": { - "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" - }, - "ts-interface-checker@0.1.13": { - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" - }, - "tslib@2.8.1": { - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "type-check@0.4.0": { - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dependencies": [ - "prelude-ls" - ] - }, - "typescript@5.8.3": { - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "bin": true - }, - "uglify-js@3.19.3": { - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "bin": true - }, - "undici-types@6.21.0": { - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" - }, - "undici-types@7.8.0": { - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==" + "undici-types@7.8.0": { + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==" }, "unicode-emoji-modifier-base@1.0.0": { "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==" - }, - "unxhr@1.2.0": { - "integrity": "sha512-6cGpm8NFXPD9QbSNx0cD2giy7teZ6xOkCUH3U89WKVkL9N9rBrWjlCwhR94Re18ZlAop4MOc3WU1M3Hv/bgpIw==" - }, - "update-browserslist-db@1.1.3_browserslist@4.25.1": { - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dependencies": [ - "browserslist", - "escalade", - "picocolors" - ], - "bin": true - }, - "uri-js@4.4.1": { - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": [ - "punycode" - ] - }, - "util-deprecate@1.0.2": { - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "void-elements@3.1.0": { - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" - }, - "which-module@2.0.1": { - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, - "which@2.0.2": { - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": [ - "isexe" - ], - "bin": true - }, - "with@7.0.2": { - "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", - "dependencies": [ - "@babel/parser", - "@babel/types", - "assert-never", - "babel-walk" - ] - }, - "word-wrap@1.2.5": { - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" - }, - "wordwrap@1.0.0": { - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" - }, - "wrap-ansi@6.2.0": { - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": [ - "ansi-styles@4.3.0", - "string-width@4.2.3", - "strip-ansi@6.0.1" - ] - }, - "wrap-ansi@7.0.0": { - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": [ - "ansi-styles@4.3.0", - "string-width@4.2.3", - "strip-ansi@6.0.1" - ] - }, - "wrap-ansi@8.1.0": { - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": [ - "ansi-styles@6.2.1", - "string-width@5.1.2", - "strip-ansi@7.1.0" - ] - }, - "wrappy@1.0.2": { - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "y18n@4.0.3": { - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, - "y18n@5.0.8": { - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yaml@1.10.2": { - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" - }, - "yaml@2.8.0": { - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "bin": true - }, - "yargs-parser@18.1.3": { - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dependencies": [ - "camelcase", - "decamelize" - ] - }, - "yargs-parser@21.1.1": { - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" - }, - "yargs@15.4.1": { - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dependencies": [ - "cliui@6.0.0", - "decamelize", - "find-up@4.1.0", - "get-caller-file", - "require-directory", - "require-main-filename", - "set-blocking", - "string-width@4.2.3", - "which-module", - "y18n@4.0.3", - "yargs-parser@18.1.3" - ] - }, - "yargs@17.3.1": { - "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", - "dependencies": [ - "cliui@7.0.4", - "escalade", - "get-caller-file", - "require-directory", - "string-width@4.2.3", - "y18n@5.0.8", - "yargs-parser@21.1.1" - ] - }, - "yocto-queue@0.1.0": { - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" - }, - "zimmerframe@1.1.2": { - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" } }, "redirects": { diff --git a/playwright.config.ts b/playwright.config.ts index 5779001..bd4b2c4 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ /* 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://localhost:5173', + baseURL: "http://localhost:5173", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", @@ -49,7 +49,6 @@ export default defineConfig({ name: "webkit", use: { ...devices["Desktop Safari"] }, }, - /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', @@ -73,8 +72,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'npm run dev', - url: 'http://localhost:5173', + command: "npm run dev", + url: "http://localhost:5173", reuseExistingServer: !process.env.CI, }, diff --git a/src/app.css b/src/app.css index 2ca3c92..e369c72 100644 --- a/src/app.css +++ b/src/app.css @@ -2,7 +2,6 @@ @import "./styles/scrollbar.css"; @import "./styles/publications.css"; @import "./styles/visualize.css"; -@import "./styles/events.css"; @import "./styles/asciidoc.css"; /* Custom styles */ @@ -28,7 +27,9 @@ } 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; + @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 { @@ -36,8 +37,10 @@ } div.card-leather { - @apply shadow-none text-primary-1000 border-s-4 bg-highlight border-primary-200 has-[:hover]:border-primary-700; - @apply dark:bg-primary-1000 dark:border-primary-800 dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500; + @apply shadow-none text-primary-1000 border-s-4 bg-highlight + border-primary-200 has-[:hover]:border-primary-700; + @apply dark:bg-primary-1000 dark:border-primary-800 + dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500; } div.card-leather h1, @@ -46,11 +49,13 @@ div.card-leather h4, div.card-leather h5, div.card-leather h6 { - @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; + @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-700 dark:text-gray-100 dark:hover:text-primary-300; + @apply text-gray-900 hover:text-primary-700 dark:text-gray-100 + dark:hover:text-primary-300; } main { @@ -74,7 +79,8 @@ div.note-leather, p.note-leather, section.note-leather { - @apply bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 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)), @@ -117,7 +123,8 @@ } div.modal-leather > div { - @apply bg-primary-0 dark:bg-primary-950 border-b-[1px] border-primary-100 dark:border-primary-600; + @apply bg-primary-0 dark:bg-primary-950 border-b-[1px] border-primary-100 + dark:border-primary-600; } div.modal-leather > div > h1, @@ -126,11 +133,14 @@ 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; + @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-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; + @apply bg-primary-0 hover:bg-primary-0 dark:bg-primary-950 + dark:hover:bg-primary-950 text-gray-900 hover:text-primary-600 + dark:text-gray-100 dark:hover:text-primary-400; } /* Navbar */ @@ -143,7 +153,8 @@ } nav.navbar-leather svg { - @apply fill-gray-900 hover:fill-primary-600 dark:fill-gray-100 dark:hover:fill-primary-400; + @apply fill-gray-900 hover:fill-primary-600 dark:fill-gray-100 + dark:hover:fill-primary-400; } nav.navbar-leather h1, @@ -152,7 +163,8 @@ nav.navbar-leather h4, nav.navbar-leather h5, nav.navbar-leather h6 { - @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; + @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 + dark:hover:text-primary-400; } div.skeleton-leather div { @@ -201,16 +213,16 @@ .network-node-content { @apply fill-primary-100; } - + /* Person link colors */ .person-link-signed { @apply stroke-green-500; } - + .person-link-referenced { @apply stroke-blue-400; } - + /* Person anchor node */ .person-anchor-node { @apply fill-green-400 stroke-green-600; @@ -272,11 +284,13 @@ /* Lists */ .ol-leather li a, .ul-leather li a { - @apply text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; + @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-600 dark:hover:text-primary-400; + @apply underline cursor-pointer hover:text-primary-600 + dark:hover:text-primary-400; } /* Card with transition */ @@ -290,11 +304,14 @@ } .tags span { - @apply bg-primary-50 text-primary-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-primary-900 dark:text-primary-200; + @apply bg-primary-50 text-primary-800 text-sm font-medium me-2 px-2.5 py-0.5 + rounded-sm dark:bg-primary-900 dark:text-primary-200; } .npub-badge { - @apply inline-flex space-x-1 items-center text-primary-600 dark:text-primary-500 hover:underline me-2 px-2 py-0.5 rounded-sm border border-primary-600 dark:border-primary-500; + @apply inline-flex space-x-1 items-center text-primary-600 + dark:text-primary-500 hover:underline me-2 px-2 py-0.5 rounded-sm border + border-primary-600 dark:border-primary-500; svg { @apply fill-primary-600 dark:fill-primary-500; @@ -303,16 +320,25 @@ } @layer components { + canvas.qr-code { + @apply block mx-auto my-4; + } + /* Legend */ .leather-legend { - @apply relative m-4 sm:m-0 sm:absolute sm:top-1 sm:left-1 flex-shrink-0 p-2 rounded; - @apply shadow-none text-primary-1000 border border-s-4 bg-highlight border-primary-200 has-[:hover]:border-primary-700; - @apply dark:bg-primary-1000 dark:border-primary-800 dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500; + @apply relative m-4 sm:m-0 sm:absolute sm:top-1 sm:left-1 flex-shrink-0 p-2 + rounded; + @apply shadow-none text-primary-1000 border border-s-4 bg-highlight + border-primary-200 has-[:hover]:border-primary-700; + @apply dark:bg-primary-1000 dark:border-primary-800 + dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500; } /* Tooltip */ .tooltip-leather { - @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; + @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; } @@ -536,13 +562,15 @@ input[type="tel"], input[type="url"], textarea { - @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 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; } /* Table of Contents highlighting */ .toc-highlight { - @apply bg-primary-200 dark:bg-primary-700 border-l-4 border-primary-600 dark:border-primary-400 font-medium; + @apply bg-primary-200 dark:bg-primary-700 border-l-4 border-primary-600 + dark:border-primary-400 font-medium; transition: all 0.2s ease-in-out; } @@ -551,14 +579,8 @@ } /* Override prose first-line bold styling */ - .prose p:first-line { - font-weight: normal !important; - } - - .prose-sm p:first-line { - font-weight: normal !important; - } - + .prose p:first-line, + .prose-sm p:first-line, .prose-invert p:first-line { font-weight: normal !important; } diff --git a/src/app.d.ts b/src/app.d.ts index 25c13d3..1e997cc 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -23,7 +23,9 @@ declare global { var MathJax: any; var nostr: NDKNip07Signer & { - getRelays: () => Promise>>; + getRelays: () => Promise< + Record> + >; // deno-lint-ignore no-explicit-any signEvent: (event: any) => Promise; }; diff --git a/src/app.html b/src/app.html index 97127be..345607e 100644 --- a/src/app.html +++ b/src/app.html @@ -1,4 +1,4 @@ - + @@ -26,14 +26,18 @@ }, }; - + - + %sveltekit.head% diff --git a/src/lib/components/CommentViewer.svelte b/src/lib/components/CommentViewer.svelte index 6ed9b4c..67cda8a 100644 --- a/src/lib/components/CommentViewer.svelte +++ b/src/lib/components/CommentViewer.svelte @@ -2,16 +2,16 @@ import { Button, P, Heading } from "flowbite-svelte"; import { getUserMetadata, toNpub } from "$lib/utils/nostrUtils"; import { neventEncode } from "$lib/utils"; - import { activeInboxRelays, ndkInstance } from "$lib/ndk"; + import { activeInboxRelays, getNdkContext } from "$lib/ndk"; import { goto } from "$app/navigation"; import { onMount } from "svelte"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; - import { userBadge } from "$lib/snippets/UserSnippets.svelte"; - import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; - import { parseRepostContent, parseContent as parseNotificationContent } from "$lib/utils/notification_utils"; + import EmbeddedEvent from "./embedded_events/EmbeddedEvent.svelte"; const { event } = $props<{ event: NDKEvent }>(); + const ndk = getNdkContext(); + // AI-NOTE: 2025-01-08 - Clean, efficient comment viewer implementation // This component fetches and displays threaded comments with proper hierarchy // Uses simple, reliable profile fetching and efficient state management @@ -126,15 +126,15 @@ // Get all available relays for a more comprehensive search // Use the full NDK pool relays instead of just active relays - const ndkPoolRelays = Array.from($ndkInstance.pool.relays.values()).map(relay => relay.url); + const ndkPoolRelays = Array.from(ndk.pool.relays.values()).map(relay => relay.url); console.log(`[CommentViewer] Using ${ndkPoolRelays.length} NDK pool relays for search:`, ndkPoolRelays); // Try all filters to find comments with full relay set - activeSub = $ndkInstance.subscribe(filters); + activeSub = ndk.subscribe(filters); // Also try a direct search for the specific comment we're looking for console.log(`[CommentViewer] Also searching for specific comment: 64173a81c2a8e26342d4a75d3def804da8644377bde99cfdfeaf189dff87f942`); - const specificCommentSub = $ndkInstance.subscribe({ + const specificCommentSub = ndk.subscribe({ ids: ["64173a81c2a8e26342d4a75d3def804da8644377bde99cfdfeaf189dff87f942"] }); @@ -293,7 +293,7 @@ try { // Try a broader search to see if there are any events that might be comments - const testSub = $ndkInstance.subscribe({ + const testSub = ndk.subscribe({ kinds: [1, 1111, 9802], "#e": [event.id], limit: 10, @@ -464,7 +464,7 @@ console.log(`[CommentViewer] Fetching nested replies for event:`, eventId); // Search for replies to this specific event - const nestedSub = $ndkInstance.subscribe({ + const nestedSub = ndk.subscribe({ kinds: [1, 1111, 9802], "#e": [eventId], limit: 50, @@ -508,7 +508,7 @@ if (dTag) { const eventAddress = `${event.kind}:${event.pubkey}:${dTag}`; - const nip22Sub = $ndkInstance.subscribe({ + const nip22Sub = ndk.subscribe({ kinds: [1111, 9802], "#a": [eventAddress], limit: 50, @@ -654,19 +654,6 @@ return `${actualLevel * 16}px`; } - async function parseContent(content: string, eventKind?: number): Promise { - if (!content) return ""; - - // Use parseRepostContent for kind 6 and 16 events (reposts) - if (eventKind === 6 || eventKind === 16) { - return await parseRepostContent(content); - } else { - return await parseNotificationContent(content); - } - } - - - // AI-NOTE: 2025-01-24 - Get highlight source information function getHighlightSource(highlightEvent: NDKEvent): { type: string; value: string; url?: string } | null { // Check for e-tags (nostr events) @@ -785,11 +772,7 @@
Comment:
- {#await parseContent(node.event.getMatchingTags("comment")[0]?.[1] || "") then parsedContent} - {@html parsedContent} - {:catch} - {@html node.event.getMatchingTags("comment")[0]?.[1] || ""} - {/await} + {:else} @@ -829,11 +812,7 @@ {:else} - {#await parseContent(node.event.content || "", node.event.kind) then parsedContent} - {@html parsedContent} - {:catch} - {@html node.event.content || ""} - {/await} + {/if} diff --git a/src/lib/components/ContentWithEmbeddedEvents.svelte b/src/lib/components/ContentWithEmbeddedEvents.svelte deleted file mode 100644 index 75d9008..0000000 --- a/src/lib/components/ContentWithEmbeddedEvents.svelte +++ /dev/null @@ -1,100 +0,0 @@ - - -
- {@html parsedContent} - - - {#each embeddedEvents as eventInfo} -
- -
- {/each} -
- - diff --git a/src/lib/components/EmbeddedEventRenderer.svelte b/src/lib/components/EmbeddedEventRenderer.svelte deleted file mode 100644 index d1752e9..0000000 --- a/src/lib/components/EmbeddedEventRenderer.svelte +++ /dev/null @@ -1,83 +0,0 @@ - - -
- {@html renderContent()} - - - {#each embeddedEvents as eventInfo} -
- -
- {/each} -
- - diff --git a/src/lib/components/EventDetails.svelte b/src/lib/components/EventDetails.svelte index a27a024..687b9f8 100644 --- a/src/lib/components/EventDetails.svelte +++ b/src/lib/components/EventDetails.svelte @@ -1,11 +1,8 @@
diff --git a/src/lib/components/RelayDisplay.svelte b/src/lib/components/RelayDisplay.svelte index 02ff24b..941e697 100644 --- a/src/lib/components/RelayDisplay.svelte +++ b/src/lib/components/RelayDisplay.svelte @@ -1,7 +1,7 @@ + +{#snippet parsedContent(content: string)} + {#await parseEmbeddedMarkup(content, 0) then parsed} + {@html parsed} + {/await} +{/snippet} + +{#snippet repostContent(content: string)} + {@const originalEvent = (() => { + try { + return JSON.parse(content); + } catch { + return null; + } + })()} + + {#if originalEvent} + {@const originalContent = originalEvent.content || ""} + {@const originalAuthor = originalEvent.pubkey || ""} + {@const originalCreatedAt = originalEvent.created_at || 0} + {@const originalKind = originalEvent.kind || 1} + {@const formattedDate = originalCreatedAt ? new Date(originalCreatedAt * 1000).toLocaleDateString() : "Unknown date"} + {@const shortAuthor = originalAuthor ? `${originalAuthor.slice(0, 8)}...${originalAuthor.slice(-4)}` : "Unknown"} + +
+ +
+
+ + Kind {originalKind} + + + (repost) + + + Author: + + {shortAuthor} + + + + {formattedDate} + +
+ +
+ + +
+ {#await parseEmbeddedMarkup(originalContent, 0) then parsedOriginalContent} + {@html parsedOriginalContent} + {/await} +
+
+ {:else} + {#await parseEmbeddedMarkup(content, 0) then parsedContent} + {@html parsedContent} + {/await} + {/if} +{/snippet} + +{#snippet quotedContent(message: NDKEvent, publicMessages: NDKEvent[], ndk: NDK)} + {@const qTags = message.getMatchingTags("q")} + {#if qTags.length > 0} + {@const qTag = qTags[0]} + {@const eventId = qTag[1]} + + {#if eventId} + {#await findQuotedMessage(eventId, publicMessages, ndk) then quotedMessage} + {#if quotedMessage} + {@const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"} + {#await parseEmbeddedMarkup(quotedContent, 0) then parsedContent} + + {/await} + {:else} + {@const isValidEventId = /^[a-fA-F0-9]{64}$/.test(eventId)} + {#if isValidEventId} + {@const nevent = (() => { + try { + return nip19.neventEncode({ id: eventId }); + } catch (error) { + console.warn(`[quotedContent] Failed to encode nevent for ${eventId}:`, error); + return null; + } + })()} + {#if nevent} + + {:else} +
+ Quoted message not found. Event ID: {eventId.slice(0, 8)}... +
+ {/if} + {:else} +
+ Invalid quoted message reference +
+ {/if} + {/if} + {/await} + {/if} + {/if} +{/snippet} diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index d913de0..c353627 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -1,6 +1,6 @@
@@ -574,7 +447,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
{#each searchResults as result, index} - {@const profileData = (result as any).profileData || parseProfileContent(result)} + {@const profileData = parseProfileContent(result)}
{#if result.kind === 0 && profileData} @@ -714,11 +574,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
- {#await ((result.kind === 6 || result.kind === 16) ? parseRepostContent(result.content) : parseContent(result.content)) then parsedContent} - {@html parsedContent.slice(0, 200)}{parsedContent.length > 200 ? "..." : ""} - {:catch} - {result.content.slice(0, 200)}{result.content.length > 200 ? "..." : ""} - {/await} +
{/if} {/if} @@ -761,22 +617,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; Kind: {result.kind} - {#if profileData?.isInUserLists} -
- - - -
- {:else if result.pubkey && communityStatus[result.pubkey]} + {#if result.pubkey && communityStatus[result.pubkey]}
- {formatEventDate(result)} + {result.created_at + ? new Date( + result.created_at * 1000, + ).toLocaleDateString() + : "Unknown date"}
@@ -892,11 +737,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
- {#await ((result.kind === 6 || result.kind === 16) ? parseRepostContent(result.content) : parseContent(result.content)) then parsedContent} - {@html parsedContent.slice(0, 200)}{parsedContent.length > 200 ? "..." : ""} - {:catch} - {result.content.slice(0, 200)}{result.content.length > 200 ? "..." : ""} - {/await} +
{/if} {/if} @@ -933,22 +774,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; Kind: {result.kind} - {#if profileData?.isInUserLists} -
- - - -
- {:else if result.pubkey && communityStatus[result.pubkey]} + {#if result.pubkey && communityStatus[result.pubkey]}
- {formatEventDate(result)} + {result.created_at + ? new Date( + result.created_at * 1000, + ).toLocaleDateString() + : "Unknown date"}
{#if result.kind === 0 && profileData} @@ -1057,11 +887,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
- {#await ((result.kind === 6 || result.kind === 16) ? parseRepostContent(result.content) : parseContent(result.content)) then parsedContent} - {@html parsedContent.slice(0, 200)}{parsedContent.length > 200 ? "..." : ""} - {:catch} - {result.content.slice(0, 200)}{result.content.length > 200 ? "..." : ""} - {/await} +
{/if} {/if} @@ -1116,7 +942,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte"; {/if}
- +
@@ -1126,7 +952,7 @@ import CommentViewer from "$lib/components/CommentViewer.svelte";
- {#if user?.signedIn} + {#if isLoggedIn && userPubkey}
Add Comment diff --git a/src/routes/my-notes/+page.svelte b/src/routes/my-notes/+page.svelte index 58b3634..9e7dfa4 100644 --- a/src/routes/my-notes/+page.svelte +++ b/src/routes/my-notes/+page.svelte @@ -1,14 +1,15 @@ diff --git a/src/routes/proxy+layout.ts b/src/routes/proxy+layout.ts deleted file mode 100644 index 8a97a72..0000000 --- a/src/routes/proxy+layout.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { LayoutLoad } from "./$types"; - -export const load: LayoutLoad = async () => { - return {}; -}; \ No newline at end of file diff --git a/src/routes/publication/+page.server.ts b/src/routes/publication/+page.server.ts index fa30a0d..0be4172 100644 --- a/src/routes/publication/+page.server.ts +++ b/src/routes/publication/+page.server.ts @@ -5,7 +5,7 @@ import type { PageServerLoad } from "./$types"; const ROUTES = { PUBLICATION_BASE: "/publication", NADDR: "/publication/naddr", - NEVENT: "/publication/nevent", + NEVENT: "/publication/nevent", ID: "/publication/id", D_TAG: "/publication/d", START: "/start", @@ -17,7 +17,7 @@ const IDENTIFIER_PREFIXES = { NEVENT: "nevent", } as const; -export const load: PageServerLoad = ({ url }) => { +export const load: PageServerLoad = ({ url }: { url: URL }) => { const id = url.searchParams.get("id"); const dTag = url.searchParams.get("d"); @@ -38,4 +38,4 @@ export const load: PageServerLoad = ({ url }) => { // If no query parameters, redirect to the start page redirect(301, ROUTES.START); -}; \ No newline at end of file +}; diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts index 2a90624..4670248 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.server.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -1,34 +1,12 @@ -import { error } from "@sveltejs/kit"; import type { LayoutServerLoad } from "./$types"; -import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; -// AI-NOTE: Server-side event fetching for SEO metadata -async function fetchEventServerSide(type: string, identifier: string): Promise { - // For now, return null to indicate server-side fetch not implemented - // This will fall back to client-side fetching - return null; -} -export const load: LayoutServerLoad = async ({ params, url }) => { - const { type, identifier } = params; - - // Try to fetch event server-side for metadata - const indexEvent = await fetchEventServerSide(type, identifier); - - // Extract metadata for meta tags (use fallbacks if no event found) - const title = indexEvent?.tags.find((tag) => tag[0] === "title")?.[1] || "Alexandria Publication"; - const summary = indexEvent?.tags.find((tag) => tag[0] === "summary")?.[1] || - "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; - const image = indexEvent?.tags.find((tag) => tag[0] === "image")?.[1] || "/screenshots/old_books.jpg"; +export const load: LayoutServerLoad = ({ url }: { url: URL }) => { const currentUrl = `${url.origin}${url.pathname}`; return { - indexEvent, // Will be null, triggering client-side fetch metadata: { - title, - summary, - image, currentUrl, }, }; -}; \ No newline at end of file +}; diff --git a/src/routes/publication/[type]/[identifier]/+page.ts b/src/routes/publication/[type]/[identifier]/+page.ts index 8f3bbaf..69d8a59 100644 --- a/src/routes/publication/[type]/[identifier]/+page.ts +++ b/src/routes/publication/[type]/[identifier]/+page.ts @@ -1,30 +1,36 @@ import { error } from "@sveltejs/kit"; import type { PageLoad } from "./$types"; -import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts"; +import { + fetchEventByDTag, + fetchEventById, + fetchEventByNaddr, + fetchEventByNevent, +} from "../../../../lib/utils/websocket_utils.ts"; import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; -export const load: PageLoad = async ({ params, parent }: { params: { type: string; identifier: string }; parent: any }) => { +export const load: PageLoad = async ( + { params }: { + params: { type: string; identifier: string }; + }, +) => { const { type, identifier } = params; - - // Get layout data (no server-side data since SSR is disabled) - const layoutData = await parent(); // AI-NOTE: Always fetch client-side since server-side fetch returns null for now let indexEvent: NostrEvent | null = null; - + try { // Handle different identifier types switch (type) { - case 'id': + case "id": indexEvent = await fetchEventById(identifier); break; - case 'd': + case "d": indexEvent = await fetchEventByDTag(identifier); break; - case 'naddr': + case "naddr": indexEvent = await fetchEventByNaddr(identifier); break; - case 'nevent': + case "nevent": indexEvent = await fetchEventByNevent(identifier); break; default: @@ -33,48 +39,41 @@ export const load: PageLoad = async ({ params, parent }: { params: { type: strin } catch (err) { throw err; } - + if (!indexEvent) { // AI-NOTE: Handle case where no relays are available during preloading // This prevents 404 errors when relay stores haven't been populated yet - + // Create appropriate search link based on type - let searchParam = ''; + let searchParam = ""; switch (type) { - case 'id': + case "id": searchParam = `id=${identifier}`; break; - case 'd': + case "d": searchParam = `d=${identifier}`; break; - case 'naddr': - case 'nevent': + case "naddr": + case "nevent": searchParam = `id=${identifier}`; break; default: searchParam = `q=${identifier}`; } - - error(404, `Event not found for ${type}: ${identifier}. href="/events?${searchParam}"`); - } - const publicationType = indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; - - // AI-NOTE: Use proper NDK instance from layout or create one with relays - let ndk = layoutData?.ndk; - if (!ndk) { - // Import NDK dynamically to avoid SSR issues - const NDK = (await import("@nostr-dev-kit/ndk")).default; - // Import initNdk to get properly configured NDK with relays - const { initNdk } = await import("$lib/ndk"); - ndk = initNdk(); + error( + 404, + `Event not found for ${type}: ${identifier}. href="/events?${searchParam}"`, + ); } + const publicationType = + indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; + const result = { publicationType, indexEvent, - ndk, // Use minimal NDK instance }; - + return result; }; diff --git a/src/routes/visualize/+page.svelte b/src/routes/visualize/+page.svelte index 17553a4..7d8124a 100644 --- a/src/routes/visualize/+page.svelte +++ b/src/routes/visualize/+page.svelte @@ -8,7 +8,6 @@ import { onMount } from "svelte"; import { get } from "svelte/store"; import EventNetwork from "$lib/navigator/EventNetwork/index.svelte"; - import { ndkInstance } from "$lib/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { filterValidIndexEvents } from "$lib/utils"; import { networkFetchLimit } from "$lib/state"; @@ -17,7 +16,7 @@ import type { PageData } from './$types'; import { getEventKindColor, getEventKindName } from "$lib/utils/eventColors"; import { extractPubkeysFromEvents, batchFetchProfiles } from "$lib/utils/profileCache"; - import { userStore } from "$lib/stores/userStore"; + import { activePubkey, getNdkContext } from "$lib/ndk"; // Import utility functions for tag-based event fetching // These functions handle the complex logic of finding publications by tags // and extracting their associated content events @@ -28,6 +27,8 @@ } from "$lib/utils/tag_event_fetch"; import { deduplicateAndCombineEvents } from "$lib/utils/eventDeduplication"; import type { EventCounts } from "$lib/types"; + + const ndk = getNdkContext(); // Configuration const DEBUG = true; // Set to true to enable debug logging @@ -122,15 +123,15 @@ } // Get the current user's pubkey - const currentUserPubkey = get(userStore).pubkey; - if (!currentUserPubkey) { + const currentUserPubkey = get(activePubkey); + if (!currentUserPubkey) { console.warn("No logged-in user, cannot fetch user's follow list"); return []; } // If limit is 1, only fetch the current user's follow list if (config.limit === 1) { - const userFollowList = await $ndkInstance.fetchEvents({ + const userFollowList = await ndk.fetchEvents({ kinds: [3], authors: [currentUserPubkey], limit: 1 @@ -148,7 +149,7 @@ debug(`Fetched user's follow list`); } else { // If limit > 1, fetch the user's follow list plus additional ones from people they follow - const userFollowList = await $ndkInstance.fetchEvents({ + const userFollowList = await ndk.fetchEvents({ kinds: [3], authors: [currentUserPubkey], limit: 1 @@ -180,7 +181,7 @@ debug(`Fetching ${pubkeysToFetch.length} additional follow lists (total limit: ${config.limit})`); - const additionalFollowLists = await $ndkInstance.fetchEvents({ + const additionalFollowLists = await ndk.fetchEvents({ kinds: [3], authors: pubkeysToFetch }); @@ -215,7 +216,7 @@ debug(`Fetching level ${level} follow lists for ${currentLevelPubkeys.length} pubkeys`); // Fetch follow lists for this level - const levelFollowLists = await $ndkInstance.fetchEvents({ + const levelFollowLists = await ndk.fetchEvents({ kinds: [3], authors: currentLevelPubkeys }); @@ -362,7 +363,7 @@ const followEvents = await fetchFollowLists(config); allFetchedEvents.push(...followEvents); } else { - const fetchedEvents = await $ndkInstance.fetchEvents( + const fetchedEvents = await ndk.fetchEvents( { kinds: [config.kind], limit: config.limit @@ -394,7 +395,7 @@ if (data.eventId) { // Fetch specific publication debug(`Fetching specific publication: ${data.eventId}`); - const event = await $ndkInstance.fetchEvent(data.eventId); + const event = await ndk.fetchEvent(data.eventId); if (!event) { throw new Error(`Publication not found: ${data.eventId}`); @@ -414,7 +415,7 @@ const indexConfig = publicationConfigs.find(ec => ec.kind === INDEX_EVENT_KIND); const indexLimit = indexConfig?.limit || 20; - const indexEvents = await $ndkInstance.fetchEvents( + const indexEvents = await ndk.fetchEvents( { kinds: [INDEX_EVENT_KIND], limit: indexLimit @@ -455,7 +456,7 @@ const contentEventPromises = Array.from(referencesByAuthor.entries()).map( async ([author, refs]) => { const dTags = [...new Set(refs.map(r => r.dTag))]; // Dedupe d-tags - return $ndkInstance.fetchEvents({ + return ndk.fetchEvents({ kinds: enabledContentKinds, // Only fetch enabled kinds authors: [author], "#d": dTags, diff --git a/src/routes/visualize/+page.ts b/src/routes/visualize/+page.ts index 3a0c7d1..b63dcee 100644 --- a/src/routes/visualize/+page.ts +++ b/src/routes/visualize/+page.ts @@ -1,9 +1,9 @@ -import type { PageLoad } from './$types'; +import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ url }) => { - const eventId = url.searchParams.get('event'); - + const eventId = url.searchParams.get("event"); + return { - eventId + eventId, }; -}; \ No newline at end of file +}; diff --git a/src/styles/events.css b/src/styles/events.css deleted file mode 100644 index 3c61536..0000000 --- a/src/styles/events.css +++ /dev/null @@ -1,5 +0,0 @@ -@layer components { - canvas.qr-code { - @apply block mx-auto my-4; - } -} diff --git a/src/styles/notifications.css b/src/styles/notifications.css index 27b193d..c11a0ea 100644 --- a/src/styles/notifications.css +++ b/src/styles/notifications.css @@ -151,7 +151,13 @@ /* Transition utilities */ .transition-colors { - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, text-decoration-color 0.15s ease-in-out, fill 0.15s ease-in-out, stroke 0.15s ease-in-out; + transition: + color 0.15s ease-in-out, + background-color 0.15s ease-in-out, + border-color 0.15s ease-in-out, + text-decoration-color 0.15s ease-in-out, + fill 0.15s ease-in-out, + stroke 0.15s ease-in-out; } .transition-all { diff --git a/src/styles/publications.css b/src/styles/publications.css index 71b70b6..9ac48b0 100644 --- a/src/styles/publications.css +++ b/src/styles/publications.css @@ -100,7 +100,8 @@ /* 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; + @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 { @@ -154,7 +155,8 @@ } .publication-leather .admonitionblock.tip { - @apply rounded overflow-hidden border border-success-100 dark:border-success-800; + @apply rounded overflow-hidden border border-success-100 + dark:border-success-800; } .publication-leather .admonitionblock.tip .icon, @@ -172,7 +174,8 @@ } .publication-leather .admonitionblock.important { - @apply rounded overflow-hidden border border-primary-200 dark:border-primary-700; + @apply rounded overflow-hidden border border-primary-200 + dark:border-primary-700; } .publication-leather .admonitionblock.important .icon, @@ -181,7 +184,8 @@ } .publication-leather .admonitionblock.caution { - @apply rounded overflow-hidden border border-warning-200 dark:border-warning-700; + @apply rounded overflow-hidden border border-warning-200 + dark:border-warning-700; } .publication-leather .admonitionblock.caution .icon, @@ -190,7 +194,8 @@ } .publication-leather .admonitionblock.warning { - @apply rounded overflow-hidden border border-danger-200 dark:border-danger-800; + @apply rounded overflow-hidden border border-danger-200 + dark:border-danger-800; } .publication-leather .admonitionblock.warning .icon, @@ -201,7 +206,7 @@ /* listingblock, literalblock */ .publication-leather .listingblock, .publication-leather .literalblock { - @apply p-4 rounded bg-highlight dark:bg-primary-700; + @apply p-4 rounded bg-highlight dark:bg-primary-700; } .publication-leather .sidebarblock .title, @@ -254,7 +259,8 @@ @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; + @apply bg-primary-50 dark:bg-primary-1000 opacity-75 transition + duration-500 ease-in-out; } .blog .discreet .group { @apply bg-transparent; diff --git a/src/styles/scrollbar.css b/src/styles/scrollbar.css index 4691a9b..c337549 100644 --- a/src/styles/scrollbar.css +++ b/src/styles/scrollbar.css @@ -1,7 +1,8 @@ @layer components { /* Global scrollbar styles */ * { - scrollbar-color: rgba(87, 66, 41, 0.8) transparent; /* Transparent track, default scrollbar thumb */ + scrollbar-color: rgba(87, 66, 41, 0.8) + transparent; /* Transparent track, default scrollbar thumb */ } /* Webkit Browsers (Chrome, Safari, Edge) */ @@ -14,7 +15,8 @@ } *::-webkit-scrollbar-thumb { - @apply bg-primary-500 dark:bg-primary-600 hover:bg-primary-600 dark:hover:bg-primary-800; + @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 d0631b5..ea8f9bd 100644 --- a/src/styles/visualize.css +++ b/src/styles/visualize.css @@ -30,7 +30,8 @@ } .legend-letter { - @apply absolute inset-0 flex items-center justify-center text-black text-xs font-bold; + @apply absolute inset-0 flex items-center justify-center text-black text-xs + font-bold; } .legend-text { @@ -39,7 +40,8 @@ /* Network visualization styles - specific to visualization */ .network-container { - @apply flex flex-col w-full h-[calc(100vh-138px)] min-h-[400px] max-h-[900px]; + @apply flex flex-col w-full h-[calc(100vh-138px)] min-h-[400px] + max-h-[900px]; } .network-svg-container { @@ -48,11 +50,15 @@ .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; + @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; + @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 { @@ -78,8 +84,9 @@ /* 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; + @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 { @@ -91,7 +98,8 @@ } .tooltip-title-link { - @apply text-gray-800 hover:text-blue-600 dark:text-gray-200 dark:hover:text-blue-400; + @apply text-gray-800 hover:text-blue-600 dark:text-gray-200 + dark:hover:text-blue-400; } .tooltip-metadata { @@ -99,11 +107,13 @@ } .tooltip-summary { - @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto max-h-40; + @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; + @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto + max-h-40; } .tooltip-help-text { diff --git a/test_data/LaTeXtestfile.md b/test_data/LaTeXtestfile.md index eec857c..01e6264 100644 --- a/test_data/LaTeXtestfile.md +++ b/test_data/LaTeXtestfile.md @@ -1,12 +1,24 @@ # 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. +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 @@ -16,36 +28,25 @@ 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)$$` +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} +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: -` -$$ +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} +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. +LaTeX ypesetting won't be rendered. Use NostrMarkup delimeter tables for this +sort of thing. `\\begin{tabular}{|c|c|c|l|r|} \\hline @@ -69,13 +70,17 @@ We also recognize common LaTeX statements: 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}$`. +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}` +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. +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. @@ -109,19 +114,23 @@ Using the quadratic formula, the roots of `$x^2-6x+4=0$` are 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))$$`. +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)))$$` +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))$$` +- 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 @@ -134,9 +143,9 @@ Here are some examples mixing LaTeX and AsciiMath: - 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 -$$ +5. Edge cases are handled gracefully $$ diff --git a/tests/e2e/my_notes_layout.pw.spec.ts b/tests/e2e/my_notes_layout.pw.spec.ts index 23db168..b45e403 100644 --- a/tests/e2e/my_notes_layout.pw.spec.ts +++ b/tests/e2e/my_notes_layout.pw.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { expect, type Page, test } from "@playwright/test"; // Utility to check for horizontal scroll bar async function hasHorizontalScroll(page: Page, selector: string) { @@ -9,16 +9,16 @@ async function hasHorizontalScroll(page: Page, selector: string) { }, selector); } -test.describe('My Notes Layout', () => { +test.describe("My Notes Layout", () => { test.beforeEach(async ({ page }) => { - await page.goto('/my-notes'); + await page.goto("/my-notes"); await page.waitForSelector('h1:text("My Notes")'); }); - test('no horizontal scroll bar for all tag type and tag filter combinations', async ({ page }) => { + test("no horizontal scroll bar for all tag type and tag filter combinations", async ({ page }) => { // Helper to check scroll for current state async function assertNoScroll() { - const hasScroll = await hasHorizontalScroll(page, 'main, body, html'); + const hasScroll = await hasHorizontalScroll(page, "main, body, html"); expect(hasScroll).toBeFalsy(); } @@ -26,9 +26,11 @@ test.describe('My Notes Layout', () => { await assertNoScroll(); // Get all tag type buttons - const tagTypeButtons = await page.locator('aside button').all(); + const tagTypeButtons = await page.locator("aside button").all(); // Only consider tag type buttons (first N) - const tagTypeCount = await page.locator('aside > div.flex.flex-wrap.gap-2.mb-6 > button').count(); + const tagTypeCount = await page.locator( + "aside > div.flex.flex-wrap.gap-2.mb-6 > button", + ).count(); // For each single tag type for (let i = 0; i < tagTypeCount; i++) { // Click tag type button @@ -36,7 +38,9 @@ test.describe('My Notes Layout', () => { await page.waitForTimeout(100); // Wait for UI update await assertNoScroll(); // Get tag filter buttons (after tag type buttons) - const tagFilterButtons = await page.locator('aside > div.flex.flex-wrap.gap-2.mb-4 > button').all(); + const tagFilterButtons = await page.locator( + "aside > div.flex.flex-wrap.gap-2.mb-4 > button", + ).all(); // Try all single tag filter selections for (let j = 0; j < tagFilterButtons.length; j++) { await tagFilterButtons[j].click(); @@ -72,7 +76,9 @@ test.describe('My Notes Layout', () => { await page.waitForTimeout(100); await assertNoScroll(); // Get tag filter buttons for this combination - const tagFilterButtons = await page.locator('aside > div.flex.flex-wrap.gap-2.mb-4 > button').all(); + const tagFilterButtons = await page.locator( + "aside > div.flex.flex-wrap.gap-2.mb-4 > button", + ).all(); // Try all single tag filter selections for (let k = 0; k < tagFilterButtons.length; k++) { await tagFilterButtons[k].click(); @@ -100,4 +106,4 @@ test.describe('My Notes Layout', () => { } } }); -}); \ No newline at end of file +}); diff --git a/tests/unit/ZettelEditor.test.ts b/tests/unit/ZettelEditor.test.ts index 3490286..3bfe172 100644 --- a/tests/unit/ZettelEditor.test.ts +++ b/tests/unit/ZettelEditor.test.ts @@ -1,37 +1,45 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { AsciiDocMetadata } from "../../src/lib/utils/asciidoc_metadata"; // Mock all Svelte components and dependencies vi.mock("flowbite-svelte", () => ({ Textarea: vi.fn().mockImplementation((props) => { return { - $$render: () => ``, - $$bind: { value: props.bind, oninput: props.oninput } + $$render: () => + ``, + $$bind: { value: props.bind, oninput: props.oninput }, }; }), Button: vi.fn().mockImplementation((props) => { return { - $$render: () => ``, - $$bind: { onclick: props.onclick } + $$render: () => + ``, + $$bind: { onclick: props.onclick }, }; - }) + }), })); vi.mock("flowbite-svelte-icons", () => ({ EyeOutline: vi.fn().mockImplementation(() => ({ - $$render: () => `` - })) + $$render: () => ``, + })), })); vi.mock("asciidoctor", () => ({ default: vi.fn(() => ({ convert: vi.fn((content, options) => { // Mock AsciiDoctor conversion - return simple HTML - return content.replace(/^==\s+(.+)$/gm, '

$1

') - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/\*(.+?)\*/g, '$1'); - }) - })) + return content.replace(/^==\s+(.+)$/gm, "

$1

") + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/\*(.+?)\*/g, "$1"); + }), + })), })); // Mock sessionStorage @@ -41,21 +49,21 @@ const mockSessionStorage = { removeItem: vi.fn(), clear: vi.fn(), }; -Object.defineProperty(global, 'sessionStorage', { +Object.defineProperty(global, "sessionStorage", { value: mockSessionStorage, - writable: true + writable: true, }); // Mock window object for DOM manipulation -Object.defineProperty(global, 'window', { +Object.defineProperty(global, "window", { value: { sessionStorage: mockSessionStorage, document: { querySelector: vi.fn(), createElement: vi.fn(), - } + }, }, - writable: true + writable: true, }); // Mock DOM methods @@ -64,14 +72,14 @@ const mockCreateElement = vi.fn(); const mockAddEventListener = vi.fn(); const mockRemoveEventListener = vi.fn(); -Object.defineProperty(global, 'document', { +Object.defineProperty(global, "document", { value: { querySelector: mockQuerySelector, createElement: mockCreateElement, addEventListener: mockAddEventListener, removeEventListener: mockRemoveEventListener, }, - writable: true + writable: true, }); describe("ZettelEditor Component Logic", () => { @@ -90,8 +98,9 @@ describe("ZettelEditor Component Logic", () => { describe("Publication Format Detection Logic", () => { it("should detect document header format", () => { - const contentWithDocumentHeader = "= Document Title\n\n== Section 1\nContent"; - + const contentWithDocumentHeader = + "= Document Title\n\n== Section 1\nContent"; + // Test the regex pattern used in the component const hasDocumentHeader = contentWithDocumentHeader.match(/^=\s+/m); expect(hasDocumentHeader).toBeTruthy(); @@ -99,12 +108,12 @@ describe("ZettelEditor Component Logic", () => { it("should detect index card format", () => { const contentWithIndexCard = "index card\n\n== Section 1\nContent"; - + // Test the logic used in the component const lines = contentWithIndexCard.split(/\r?\n/); let hasIndexCard = false; for (const line of lines) { - if (line.trim().toLowerCase() === 'index card') { + if (line.trim().toLowerCase() === "index card") { hasIndexCard = true; break; } @@ -113,8 +122,9 @@ describe("ZettelEditor Component Logic", () => { }); it("should not detect publication format for normal section content", () => { - const normalContent = "== Section 1\nContent\n\n== Section 2\nMore content"; - + const normalContent = + "== Section 1\nContent\n\n== Section 2\nMore content"; + // Test the logic used in the component const lines = normalContent.split(/\r?\n/); let hasPublicationHeader = false; @@ -123,7 +133,7 @@ describe("ZettelEditor Component Logic", () => { hasPublicationHeader = true; break; } - if (line.trim().toLowerCase() === 'index card') { + if (line.trim().toLowerCase() === "index card") { hasPublicationHeader = true; break; } @@ -135,26 +145,30 @@ describe("ZettelEditor Component Logic", () => { describe("Content Parsing Logic", () => { it("should parse sections with document header", () => { const content = "== Section 1\n:author: Test Author\n\nContent 1"; - + // Test the parsing logic const hasDocumentHeader = content.match(/^=\s+/m); expect(hasDocumentHeader).toBeFalsy(); // This content doesn't have a document header - + // Test section splitting logic - const sectionStrings = content.split(/(?=^==\s+)/gm).filter((section: string) => section.trim()); + const sectionStrings = content.split(/(?=^==\s+)/gm).filter(( + section: string, + ) => section.trim()); expect(sectionStrings).toHaveLength(1); expect(sectionStrings[0]).toContain("== Section 1"); }); it("should parse sections without document header", () => { const content = "== Section 1\nContent 1"; - + // Test the parsing logic const hasDocumentHeader = content.match(/^=\s+/m); expect(hasDocumentHeader).toBeFalsy(); - + // Test section splitting logic - const sectionStrings = content.split(/(?=^==\s+)/gm).filter((section: string) => section.trim()); + const sectionStrings = content.split(/(?=^==\s+)/gm).filter(( + section: string, + ) => section.trim()); expect(sectionStrings).toHaveLength(1); expect(sectionStrings[0]).toContain("== Section 1"); }); @@ -168,49 +182,70 @@ describe("ZettelEditor Component Logic", () => { describe("Content Conversion Logic", () => { it("should convert document title to section title", () => { - const contentWithDocumentHeader = "= Document Title\n\n== Section 1\nContent"; - + const contentWithDocumentHeader = + "= Document Title\n\n== Section 1\nContent"; + // Test the conversion logic - let convertedContent = contentWithDocumentHeader.replace(/^=\s+(.+)$/gm, '== $1'); - convertedContent = convertedContent.replace(/^index card$/gim, ''); - const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, '\n\n'); - + let convertedContent = contentWithDocumentHeader.replace( + /^=\s+(.+)$/gm, + "== $1", + ); + convertedContent = convertedContent.replace(/^index card$/gim, ""); + const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, "\n\n"); + expect(finalContent).toBe("== Document Title\n\n== Section 1\nContent"); }); it("should remove index card line", () => { const contentWithIndexCard = "index card\n\n== Section 1\nContent"; - + // Test the conversion logic - let convertedContent = contentWithIndexCard.replace(/^=\s+(.+)$/gm, '== $1'); - convertedContent = convertedContent.replace(/^index card$/gim, ''); - const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, '\n\n'); - + let convertedContent = contentWithIndexCard.replace( + /^=\s+(.+)$/gm, + "== $1", + ); + convertedContent = convertedContent.replace(/^index card$/gim, ""); + const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, "\n\n"); + expect(finalContent).toBe("\n\n== Section 1\nContent"); }); it("should clean up double newlines", () => { - const contentWithExtraNewlines = "= Document Title\n\n\n== Section 1\nContent"; - + const contentWithExtraNewlines = + "= Document Title\n\n\n== Section 1\nContent"; + // Test the conversion logic - let convertedContent = contentWithExtraNewlines.replace(/^=\s+(.+)$/gm, '== $1'); - convertedContent = convertedContent.replace(/^index card$/gim, ''); - const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, '\n\n'); - + let convertedContent = contentWithExtraNewlines.replace( + /^=\s+(.+)$/gm, + "== $1", + ); + convertedContent = convertedContent.replace(/^index card$/gim, ""); + const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, "\n\n"); + expect(finalContent).toBe("== Document Title\n\n== Section 1\nContent"); }); }); describe("SessionStorage Integration", () => { it("should store content in sessionStorage when switching to publication editor", () => { - const contentWithDocumentHeader = "= Document Title\n\n== Section 1\nContent"; - + const contentWithDocumentHeader = + "= Document Title\n\n== Section 1\nContent"; + // Test the sessionStorage logic - mockSessionStorage.setItem('zettelEditorContent', contentWithDocumentHeader); - mockSessionStorage.setItem('zettelEditorSource', 'publication-format'); - - expect(mockSessionStorage.setItem).toHaveBeenCalledWith('zettelEditorContent', contentWithDocumentHeader); - expect(mockSessionStorage.setItem).toHaveBeenCalledWith('zettelEditorSource', 'publication-format'); + mockSessionStorage.setItem( + "zettelEditorContent", + contentWithDocumentHeader, + ); + mockSessionStorage.setItem("zettelEditorSource", "publication-format"); + + expect(mockSessionStorage.setItem).toHaveBeenCalledWith( + "zettelEditorContent", + contentWithDocumentHeader, + ); + expect(mockSessionStorage.setItem).toHaveBeenCalledWith( + "zettelEditorSource", + "publication-format", + ); }); }); @@ -219,7 +254,7 @@ describe("ZettelEditor Component Logic", () => { const sections = [{ title: "Section 1", content: "Content 1", tags: [] }]; const eventCount = sections.length; const eventText = `${eventCount} event${eventCount !== 1 ? "s" : ""}`; - + expect(eventCount).toBe(1); expect(eventText).toBe("1 event"); }); @@ -227,11 +262,11 @@ describe("ZettelEditor Component Logic", () => { it("should calculate correct event count for multiple sections", () => { const sections = [ { title: "Section 1", content: "Content 1", tags: [] }, - { title: "Section 2", content: "Content 2", tags: [] } + { title: "Section 2", content: "Content 2", tags: [] }, ]; const eventCount = sections.length; const eventText = `${eventCount} event${eventCount !== 1 ? "s" : ""}`; - + expect(eventCount).toBe(2); expect(eventText).toBe("2 events"); }); @@ -240,11 +275,17 @@ describe("ZettelEditor Component Logic", () => { describe("Tag Processing Logic", () => { it("should process tags correctly", () => { // Mock the metadataToTags function - const mockMetadataToTags = vi.fn().mockReturnValue([["author", "Test Author"]]); - - const mockMetadata = { title: "Section 1", author: "Test Author" } as AsciiDocMetadata; + const mockMetadataToTags = vi.fn().mockReturnValue([[ + "author", + "Test Author", + ]]); + + const mockMetadata = { + title: "Section 1", + author: "Test Author", + } as AsciiDocMetadata; const tags = mockMetadataToTags(mockMetadata); - + expect(tags).toEqual([["author", "Test Author"]]); expect(mockMetadataToTags).toHaveBeenCalledWith(mockMetadata); }); @@ -252,10 +293,10 @@ describe("ZettelEditor Component Logic", () => { it("should handle empty tags", () => { // Mock the metadataToTags function const mockMetadataToTags = vi.fn().mockReturnValue([]); - + const mockMetadata = { title: "Section 1" } as AsciiDocMetadata; const tags = mockMetadataToTags(mockMetadata); - + expect(tags).toEqual([]); }); }); @@ -264,11 +305,11 @@ describe("ZettelEditor Component Logic", () => { it("should process AsciiDoc content correctly", () => { // Mock the asciidoctor conversion const mockConvert = vi.fn((content, options) => { - return content.replace(/^==\s+(.+)$/gm, '

$1

') - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/\*(.+?)\*/g, '$1'); + return content.replace(/^==\s+(.+)$/gm, "

$1

") + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/\*(.+?)\*/g, "$1"); }); - + const content = "== Test Section\n\nThis is **bold** and *italic* text."; const processedContent = mockConvert(content, { standalone: false, @@ -278,10 +319,10 @@ describe("ZettelEditor Component Logic", () => { sectids: true, }, }); - - expect(processedContent).toContain('

Test Section

'); - expect(processedContent).toContain('bold'); - expect(processedContent).toContain('italic'); + + expect(processedContent).toContain("

Test Section

"); + expect(processedContent).toContain("bold"); + expect(processedContent).toContain("italic"); }); }); @@ -291,9 +332,9 @@ describe("ZettelEditor Component Logic", () => { const mockParseFunction = vi.fn().mockImplementation(() => { throw new Error("Parsing error"); }); - + const content = "== Section 1\nContent 1"; - + // Should not throw error when called expect(() => { try { @@ -321,12 +362,12 @@ describe("ZettelEditor Component Logic", () => { onContentChange: vi.fn(), onPreviewToggle: vi.fn(), }; - - expect(expectedProps).toHaveProperty('content'); - expect(expectedProps).toHaveProperty('placeholder'); - expect(expectedProps).toHaveProperty('showPreview'); - expect(expectedProps).toHaveProperty('onContentChange'); - expect(expectedProps).toHaveProperty('onPreviewToggle'); + + expect(expectedProps).toHaveProperty("content"); + expect(expectedProps).toHaveProperty("placeholder"); + expect(expectedProps).toHaveProperty("showPreview"); + expect(expectedProps).toHaveProperty("onContentChange"); + expect(expectedProps).toHaveProperty("onPreviewToggle"); }); }); @@ -334,12 +375,12 @@ describe("ZettelEditor Component Logic", () => { it("should integrate with ZettelParser utilities", () => { // Mock the parseAsciiDocSections function const mockParseAsciiDocSections = vi.fn().mockReturnValue([ - { title: "Section 1", content: "Content 1", tags: [] } + { title: "Section 1", content: "Content 1", tags: [] }, ]); - + const content = "== Section 1\nContent 1"; const sections = mockParseAsciiDocSections(content, 2); - + expect(sections).toHaveLength(1); expect(sections[0].title).toBe("Section 1"); }); @@ -348,21 +389,21 @@ describe("ZettelEditor Component Logic", () => { // Mock the utility functions const mockExtractDocumentMetadata = vi.fn().mockReturnValue({ metadata: { title: "Document Title" } as AsciiDocMetadata, - content: "Document content" + content: "Document content", }); - + const mockExtractSectionMetadata = vi.fn().mockReturnValue({ metadata: { title: "Section Title" } as AsciiDocMetadata, content: "Section content", - title: "Section Title" + title: "Section Title", }); - + const documentContent = "= Document Title\nDocument content"; const sectionContent = "== Section Title\nSection content"; - + const documentResult = mockExtractDocumentMetadata(documentContent); const sectionResult = mockExtractSectionMetadata(sectionContent); - + expect(documentResult.metadata.title).toBe("Document Title"); expect(sectionResult.title).toBe("Section Title"); }); @@ -370,27 +411,35 @@ describe("ZettelEditor Component Logic", () => { describe("Content Validation", () => { it("should validate content structure", () => { - const validContent = "== Section 1\nContent here\n\n== Section 2\nMore content"; + const validContent = + "== Section 1\nContent here\n\n== Section 2\nMore content"; const invalidContent = "Just some text without sections"; - + // Test section detection - const validSections = validContent.split(/(?=^==\s+)/gm).filter((section: string) => section.trim()); - const invalidSections = invalidContent.split(/(?=^==\s+)/gm).filter((section: string) => section.trim()); - + const validSections = validContent.split(/(?=^==\s+)/gm).filter(( + section: string, + ) => section.trim()); + const invalidSections = invalidContent.split(/(?=^==\s+)/gm).filter(( + section: string, + ) => section.trim()); + expect(validSections.length).toBeGreaterThan(0); // The invalid content will have one section (the entire content) since it doesn't start with == expect(invalidSections.length).toBe(1); }); it("should handle mixed content types", () => { - const mixedContent = "= Document Title\n\n== Section 1\nContent\n\n== Section 2\nMore content"; - + const mixedContent = + "= Document Title\n\n== Section 1\nContent\n\n== Section 2\nMore content"; + // Test document header detection const hasDocumentHeader = mixedContent.match(/^=\s+/m); expect(hasDocumentHeader).toBeTruthy(); - + // Test section extraction - const sections = mixedContent.split(/(?=^==\s+)/gm).filter((section: string) => section.trim()); + const sections = mixedContent.split(/(?=^==\s+)/gm).filter(( + section: string, + ) => section.trim()); expect(sections.length).toBeGreaterThan(0); }); }); @@ -398,13 +447,13 @@ describe("ZettelEditor Component Logic", () => { describe("String Manipulation", () => { it("should handle string replacements correctly", () => { const originalContent = "= Title\n\n== Section\nContent"; - + // Test various string manipulations const convertedContent = originalContent - .replace(/^=\s+(.+)$/gm, '== $1') - .replace(/^index card$/gim, '') - .replace(/\n\s*\n\s*\n/g, '\n\n'); - + .replace(/^=\s+(.+)$/gm, "== $1") + .replace(/^index card$/gim, "") + .replace(/\n\s*\n\s*\n/g, "\n\n"); + expect(convertedContent).toBe("== Title\n\n== Section\nContent"); }); @@ -414,16 +463,16 @@ describe("ZettelEditor Component Logic", () => { "index card\n\n== Section\nContent", // Index card "= Title\nindex card\n== Section\nContent", // Both ]; - - edgeCases.forEach(content => { + + edgeCases.forEach((content) => { const converted = content - .replace(/^=\s+(.+)$/gm, '== $1') - .replace(/^index card$/gim, '') - .replace(/\n\s*\n\s*\n/g, '\n\n'); - + .replace(/^=\s+(.+)$/gm, "== $1") + .replace(/^index card$/gim, "") + .replace(/\n\s*\n\s*\n/g, "\n\n"); + expect(converted).toBeDefined(); - expect(typeof converted).toBe('string'); + expect(typeof converted).toBe("string"); }); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/eventInput30040.test.ts b/tests/unit/eventInput30040.test.ts index c7dadc3..a7064c3 100644 --- a/tests/unit/eventInput30040.test.ts +++ b/tests/unit/eventInput30040.test.ts @@ -1,6 +1,8 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { build30040EventSet, validate30040EventSet } from "../../src/lib/utils/event_input_utils"; -import { extractDocumentMetadata, parseAsciiDocWithMetadata } from "../../src/lib/utils/asciidoc_metadata"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + build30040EventSet, + validate30040EventSet, +} from "../../src/lib/utils/event_input_utils"; // Mock NDK and other dependencies vi.mock("@nostr-dev-kit/ndk", () => ({ @@ -16,6 +18,7 @@ vi.mock("@nostr-dev-kit/ndk", () => ({ })), })); +// TODO: Replace with getNdkContext mock. vi.mock("../../src/lib/ndk", () => ({ ndkInstance: { subscribe: vi.fn(), @@ -60,16 +63,29 @@ This is the content of the second section.`; const tags: [string, string][] = [["type", "article"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); expect(indexEvent.content).toBe(""); - expect(indexEvent.tags).toContainEqual(["d", "test-document-with-preamble"]); - expect(indexEvent.tags).toContainEqual(["title", "Test Document with Preamble"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "test-document-with-preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Test Document with Preamble", + ]); expect(indexEvent.tags).toContainEqual(["author", "John Doe"]); expect(indexEvent.tags).toContainEqual(["version", "1.0"]); - expect(indexEvent.tags).toContainEqual(["summary", "This is a test document with preamble"]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is a test document with preamble", + ]); expect(indexEvent.tags).toContainEqual(["t", "test"]); expect(indexEvent.tags).toContainEqual(["t", "preamble"]); expect(indexEvent.tags).toContainEqual(["t", "asciidoc"]); @@ -80,22 +96,47 @@ This is the content of the second section.`; // First section expect(sectionEvents[0].kind).toBe(30041); - expect(sectionEvents[0].content).toBe("This is the content of the first section."); - expect(sectionEvents[0].tags).toContainEqual(["d", "test-document-with-preamble-first-section"]); + expect(sectionEvents[0].content).toBe( + "This is the content of the first section.", + ); + expect(sectionEvents[0].tags).toContainEqual([ + "d", + "test-document-with-preamble-first-section", + ]); expect(sectionEvents[0].tags).toContainEqual(["title", "First Section"]); - expect(sectionEvents[0].tags).toContainEqual(["author", "Section Author"]); - expect(sectionEvents[0].tags).toContainEqual(["summary", "This is the first section"]); + expect(sectionEvents[0].tags).toContainEqual([ + "author", + "Section Author", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "summary", + "This is the first section", + ]); // Second section expect(sectionEvents[1].kind).toBe(30041); - expect(sectionEvents[1].content).toBe("This is the content of the second section."); - expect(sectionEvents[1].tags).toContainEqual(["d", "test-document-with-preamble-second-section"]); + expect(sectionEvents[1].content).toBe( + "This is the content of the second section.", + ); + expect(sectionEvents[1].tags).toContainEqual([ + "d", + "test-document-with-preamble-second-section", + ]); expect(sectionEvents[1].tags).toContainEqual(["title", "Second Section"]); - expect(sectionEvents[1].tags).toContainEqual(["summary", "This is the second section"]); + expect(sectionEvents[1].tags).toContainEqual([ + "summary", + "This is the second section", + ]); // Test a-tags in index event - expect(indexEvent.tags).toContainEqual(["a", "30041:test-pubkey:test-document-with-preamble-first-section"]); - expect(indexEvent.tags).toContainEqual(["a", "30041:test-pubkey:test-document-with-preamble-second-section"]); + expect(indexEvent.tags).toContainEqual([ + "a", + "30041:test-pubkey:test-document-with-preamble-first-section", + ]); + expect(indexEvent.tags).toContainEqual([ + "a", + "30041:test-pubkey:test-document-with-preamble-second-section", + ]); }); }); @@ -118,32 +159,64 @@ This is the content of the second section.`; const tags: [string, string][] = [["type", "article"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); expect(indexEvent.content).toBe(""); - expect(indexEvent.tags).toContainEqual(["d", "test-document-without-preamble"]); - expect(indexEvent.tags).toContainEqual(["title", "Test Document without Preamble"]); - expect(indexEvent.tags).toContainEqual(["summary", "This is a test document without preamble"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "test-document-without-preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Test Document without Preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is a test document without preamble", + ]); // Test section events expect(sectionEvents).toHaveLength(2); // First section expect(sectionEvents[0].kind).toBe(30041); - expect(sectionEvents[0].content).toBe("This is the content of the first section."); - expect(sectionEvents[0].tags).toContainEqual(["d", "test-document-without-preamble-first-section"]); + expect(sectionEvents[0].content).toBe( + "This is the content of the first section.", + ); + expect(sectionEvents[0].tags).toContainEqual([ + "d", + "test-document-without-preamble-first-section", + ]); expect(sectionEvents[0].tags).toContainEqual(["title", "First Section"]); - expect(sectionEvents[0].tags).toContainEqual(["author", "Section Author"]); - expect(sectionEvents[0].tags).toContainEqual(["summary", "This is the first section"]); + expect(sectionEvents[0].tags).toContainEqual([ + "author", + "Section Author", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "summary", + "This is the first section", + ]); // Second section expect(sectionEvents[1].kind).toBe(30041); - expect(sectionEvents[1].content).toBe("This is the content of the second section."); - expect(sectionEvents[1].tags).toContainEqual(["d", "test-document-without-preamble-second-section"]); + expect(sectionEvents[1].content).toBe( + "This is the content of the second section.", + ); + expect(sectionEvents[1].tags).toContainEqual([ + "d", + "test-document-without-preamble-second-section", + ]); expect(sectionEvents[1].tags).toContainEqual(["title", "Second Section"]); - expect(sectionEvents[1].tags).toContainEqual(["summary", "This is the second section"]); + expect(sectionEvents[1].tags).toContainEqual([ + "summary", + "This is the second section", + ]); }); }); @@ -163,24 +236,43 @@ This is the preamble content. const tags: [string, string][] = [["type", "skeleton"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); expect(indexEvent.content).toBe(""); - expect(indexEvent.tags).toContainEqual(["d", "skeleton-document-with-preamble"]); - expect(indexEvent.tags).toContainEqual(["title", "Skeleton Document with Preamble"]); - expect(indexEvent.tags).toContainEqual(["summary", "This is a skeleton document with preamble"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "skeleton-document-with-preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Skeleton Document with Preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is a skeleton document with preamble", + ]); // Test section events expect(sectionEvents).toHaveLength(3); // All sections should have empty content - sectionEvents.forEach((section, index) => { + sectionEvents.forEach((section: any, index: number) => { expect(section.kind).toBe(30041); expect(section.content).toBe(""); - expect(section.tags).toContainEqual(["d", `skeleton-document-with-preamble-empty-section-${index + 1}`]); - expect(section.tags).toContainEqual(["title", `Empty Section ${index + 1}`]); + expect(section.tags).toContainEqual([ + "d", + `skeleton-document-with-preamble-empty-section-${index + 1}`, + ]); + expect(section.tags).toContainEqual([ + "title", + `Empty Section ${index + 1}`, + ]); }); }); }); @@ -199,24 +291,43 @@ This is the preamble content. const tags: [string, string][] = [["type", "skeleton"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); expect(indexEvent.content).toBe(""); - expect(indexEvent.tags).toContainEqual(["d", "skeleton-document-without-preamble"]); - expect(indexEvent.tags).toContainEqual(["title", "Skeleton Document without Preamble"]); - expect(indexEvent.tags).toContainEqual(["summary", "This is a skeleton document without preamble"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "skeleton-document-without-preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Skeleton Document without Preamble", + ]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is a skeleton document without preamble", + ]); // Test section events expect(sectionEvents).toHaveLength(3); // All sections should have empty content - sectionEvents.forEach((section, index) => { + sectionEvents.forEach((section: any, index: number) => { expect(section.kind).toBe(30041); expect(section.content).toBe(""); - expect(section.tags).toContainEqual(["d", `skeleton-document-without-preamble-empty-section-${index + 1}`]); - expect(section.tags).toContainEqual(["title", `Empty Section ${index + 1}`]); + expect(section.tags).toContainEqual([ + "d", + `skeleton-document-without-preamble-empty-section-${index + 1}`, + ]); + expect(section.tags).toContainEqual([ + "title", + `Empty Section ${index + 1}`, + ]); }); }); }); @@ -228,7 +339,11 @@ index card`; const tags: [string, string][] = [["type", "index-card"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); @@ -249,14 +364,27 @@ index card`; const tags: [string, string][] = [["type", "index-card"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event expect(indexEvent.kind).toBe(30040); expect(indexEvent.content).toBe(""); - expect(indexEvent.tags).toContainEqual(["d", "test-index-card-with-metadata"]); - expect(indexEvent.tags).toContainEqual(["title", "Test Index Card with Metadata"]); - expect(indexEvent.tags).toContainEqual(["summary", "This is an index card with metadata"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "test-index-card-with-metadata", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Test Index Card with Metadata", + ]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is an index card with metadata", + ]); expect(indexEvent.tags).toContainEqual(["t", "index"]); expect(indexEvent.tags).toContainEqual(["t", "card"]); expect(indexEvent.tags).toContainEqual(["t", "metadata"]); @@ -303,23 +431,45 @@ This is the section content.`; const tags: [string, string][] = [["type", "complex"]]; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); // Test index event metadata expect(indexEvent.kind).toBe(30040); - expect(indexEvent.tags).toContainEqual(["d", "complex-metadata-document"]); - expect(indexEvent.tags).toContainEqual(["title", "Complex Metadata Document"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "complex-metadata-document", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Complex Metadata Document", + ]); expect(indexEvent.tags).toContainEqual(["author", "Jane Smith"]); // Should use header line author expect(indexEvent.tags).toContainEqual(["author", "Override Author"]); // Additional author from attribute expect(indexEvent.tags).toContainEqual(["author", "Third Author"]); // Additional author from attribute expect(indexEvent.tags).toContainEqual(["version", "2.0"]); // Should use revision line version - expect(indexEvent.tags).toContainEqual(["summary", "This is a complex document with all metadata types Alternative description field"]); + expect(indexEvent.tags).toContainEqual([ + "summary", + "This is a complex document with all metadata types Alternative description field", + ]); expect(indexEvent.tags).toContainEqual(["published_on", "2024-03-01"]); - expect(indexEvent.tags).toContainEqual(["published_by", "Alexandria Complex"]); + expect(indexEvent.tags).toContainEqual([ + "published_by", + "Alexandria Complex", + ]); expect(indexEvent.tags).toContainEqual(["type", "book"]); - expect(indexEvent.tags).toContainEqual(["image", "https://example.com/cover.jpg"]); + expect(indexEvent.tags).toContainEqual([ + "image", + "https://example.com/cover.jpg", + ]); expect(indexEvent.tags).toContainEqual(["i", "978-0-123456-78-9"]); - expect(indexEvent.tags).toContainEqual(["source", "https://github.com/alexandria/complex"]); + expect(indexEvent.tags).toContainEqual([ + "source", + "https://github.com/alexandria/complex", + ]); expect(indexEvent.tags).toContainEqual(["auto-update", "yes"]); expect(indexEvent.tags).toContainEqual(["t", "complex"]); expect(indexEvent.tags).toContainEqual(["t", "metadata"]); @@ -332,13 +482,31 @@ This is the section content.`; expect(sectionEvents).toHaveLength(1); expect(sectionEvents[0].kind).toBe(30041); expect(sectionEvents[0].content).toBe("This is the section content."); - expect(sectionEvents[0].tags).toContainEqual(["d", "complex-metadata-document-section-with-complex-metadata"]); - expect(sectionEvents[0].tags).toContainEqual(["title", "Section with Complex Metadata"]); - expect(sectionEvents[0].tags).toContainEqual(["author", "Section Author"]); - expect(sectionEvents[0].tags).toContainEqual(["author", "Section Co-Author"]); - expect(sectionEvents[0].tags).toContainEqual(["summary", "This section has complex metadata Alternative description for section"]); + expect(sectionEvents[0].tags).toContainEqual([ + "d", + "complex-metadata-document-section-with-complex-metadata", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "title", + "Section with Complex Metadata", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "author", + "Section Author", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "author", + "Section Co-Author", + ]); + expect(sectionEvents[0].tags).toContainEqual([ + "summary", + "This section has complex metadata Alternative description for section", + ]); expect(sectionEvents[0].tags).toContainEqual(["type", "chapter"]); - expect(sectionEvents[0].tags).toContainEqual(["image", "https://example.com/section-image.jpg"]); + expect(sectionEvents[0].tags).toContainEqual([ + "image", + "https://example.com/section-image.jpg", + ]); expect(sectionEvents[0].tags).toContainEqual(["t", "section"]); expect(sectionEvents[0].tags).toContainEqual(["t", "complex"]); expect(sectionEvents[0].tags).toContainEqual(["t", "metadata"]); @@ -387,7 +555,9 @@ index card`; const validation = validate30040EventSet(content); expect(validation.valid).toBe(false); - expect(validation.reason).toContain("30040 events must have a document title"); + expect(validation.reason).toContain( + "30040 events must have a document title", + ); }); }); @@ -400,11 +570,21 @@ This is just preamble content.`; const tags: [string, string][] = []; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); expect(indexEvent.kind).toBe(30040); - expect(indexEvent.tags).toContainEqual(["d", "document-with-no-sections"]); - expect(indexEvent.tags).toContainEqual(["title", "Document with No Sections"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "document-with-no-sections", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Document with No Sections", + ]); expect(sectionEvents).toHaveLength(0); }); @@ -418,16 +598,27 @@ Content here.`; const tags: [string, string][] = []; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); expect(indexEvent.kind).toBe(30040); - expect(indexEvent.tags).toContainEqual(["d", "document-with-special-characters-test-more"]); - expect(indexEvent.tags).toContainEqual(["title", "Document with Special Characters: Test & More!"]); + expect(indexEvent.tags).toContainEqual([ + "d", + "document-with-special-characters-test-more", + ]); + expect(indexEvent.tags).toContainEqual([ + "title", + "Document with Special Characters: Test & More!", + ]); expect(sectionEvents).toHaveLength(1); }); it("should handle document with very long title", () => { - const content = `= This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality + const content = + `= This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality :summary: This document has a very long title == Section 1 @@ -436,11 +627,18 @@ Content here.`; const tags: [string, string][] = []; - const { indexEvent, sectionEvents } = build30040EventSet(content, tags, baseEvent); + const { indexEvent, sectionEvents } = build30040EventSet( + content, + tags, + baseEvent, + ); expect(indexEvent.kind).toBe(30040); - expect(indexEvent.tags).toContainEqual(["title", "This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality"]); + expect(indexEvent.tags).toContainEqual([ + "title", + "This is a very long document title that should be handled properly by the system and should not cause any issues with the d-tag generation or any other functionality", + ]); expect(sectionEvents).toHaveLength(1); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/latexRendering.test.ts b/tests/unit/latexRendering.test.ts index ed38f4d..eac80c5 100644 --- a/tests/unit/latexRendering.test.ts +++ b/tests/unit/latexRendering.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { parseAdvancedmarkup } from "../../src/lib/utils/markup/advancedMarkupParser"; import { readFileSync } from "fs"; import { join } from "path"; diff --git a/tests/unit/metadataExtraction.test.ts b/tests/unit/metadataExtraction.test.ts index 65a50b8..01c7e6e 100644 --- a/tests/unit/metadataExtraction.test.ts +++ b/tests/unit/metadataExtraction.test.ts @@ -1,10 +1,10 @@ -import { describe, it, expect } from "vitest"; -import { - extractDocumentMetadata, - extractSectionMetadata, - parseAsciiDocWithMetadata, +import { describe, expect, it } from "vitest"; +import { + extractDocumentMetadata, + extractSectionMetadata, + extractSmartMetadata, metadataToTags, - extractSmartMetadata + parseAsciiDocWithMetadata, } from "../../src/lib/utils/asciidoc_metadata.ts"; describe("AsciiDoc Metadata Extraction", () => { @@ -39,13 +39,15 @@ This is the content of the second section.`; it("extractDocumentMetadata should extract document metadata correctly", () => { const { metadata, content } = extractDocumentMetadata(testContent); - + expect(metadata.title).toBe("Test Document with Metadata"); expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); expect(metadata.version).toBe("1.0"); expect(metadata.publicationDate).toBe("2024-01-15"); expect(metadata.publishedBy).toBe("Alexandria Test"); - expect(metadata.summary).toBe("This is a test document for metadata extraction"); + expect(metadata.summary).toBe( + "This is a test document for metadata extraction", + ); expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); expect(metadata.type).toBe("article"); expect(metadata.tags).toEqual(["test", "metadata", "asciidoc"]); @@ -53,7 +55,7 @@ This is the content of the second section.`; expect(metadata.isbn).toBe("978-0-123456-78-9"); expect(metadata.source).toBe("https://github.com/alexandria/test"); expect(metadata.autoUpdate).toBe("yes"); - + // Content should not include the header metadata expect(content).toContain("This is the preamble content"); expect(content).toContain("== First Section"); @@ -70,7 +72,7 @@ This is the content of the second section.`; This is the content of the first section.`; const { metadata, content, title } = extractSectionMetadata(sectionContent); - + expect(title).toBe("First Section"); expect(metadata.authors).toEqual(["Section Author"]); expect(metadata.summary).toBe("This is the first section"); @@ -86,7 +88,7 @@ Stella Some context text`; const { metadata, content, title } = extractSectionMetadata(sectionContent); - + expect(title).toBe("Section Header1"); expect(metadata.authors).toEqual(["Stella"]); expect(metadata.summary).toBe("Some summary"); @@ -102,7 +104,7 @@ Stella Some context text`; const { metadata, content, title } = extractSectionMetadata(sectionContent); - + expect(title).toBe("Section Header1"); expect(metadata.authors).toEqual(["Stella", "John Doe"]); expect(metadata.summary).toBe("Some summary"); @@ -118,22 +120,26 @@ This is not an author line Some context text`; const { metadata, content, title } = extractSectionMetadata(sectionContent); - + expect(title).toBe("Section Header1"); expect(metadata.authors).toEqual(["Stella"]); expect(metadata.summary).toBe("Some summary"); - expect(content.trim()).toBe("This is not an author line\nSome context text"); + expect(content.trim()).toBe( + "This is not an author line\nSome context text", + ); }); it("parseAsciiDocWithMetadata should parse complete document", () => { const parsed = parseAsciiDocWithMetadata(testContent); - + expect(parsed.metadata.title).toBe("Test Document with Metadata"); expect(parsed.sections).toHaveLength(2); expect(parsed.sections[0].title).toBe("First Section"); expect(parsed.sections[1].title).toBe("Second Section"); expect(parsed.sections[0].metadata.authors).toEqual(["Section Author"]); - expect(parsed.sections[1].metadata.summary).toBe("This is the second section"); + expect(parsed.sections[1].metadata.summary).toBe( + "This is the second section", + ); }); it("metadataToTags should convert metadata to Nostr tags", () => { @@ -142,11 +148,11 @@ Some context text`; authors: ["Author 1", "Author 2"], version: "1.0", summary: "Test summary", - tags: ["tag1", "tag2"] + tags: ["tag1", "tag2"], }; - + const tags = metadataToTags(metadata); - + expect(tags).toContainEqual(["title", "Test Title"]); expect(tags).toContainEqual(["author", "Author 1"]); expect(tags).toContainEqual(["author", "Author 2"]); @@ -161,16 +167,16 @@ Some context text`; index card`; const { metadata, content } = extractDocumentMetadata(indexCardContent); - + expect(metadata.title).toBe("Test Index Card"); expect(content.trim()).toBe("index card"); }); it("should handle empty content gracefully", () => { const emptyContent = ""; - + const { metadata, content } = extractDocumentMetadata(emptyContent); - + expect(metadata.title).toBeUndefined(); expect(content).toBe(""); }); @@ -182,7 +188,7 @@ index card`; Some content here.`; const { metadata } = extractDocumentMetadata(contentWithKeywords); - + expect(metadata.tags).toEqual(["keyword1", "keyword2", "keyword3"]); }); @@ -194,7 +200,7 @@ Some content here.`; Some content here.`; const { metadata } = extractDocumentMetadata(contentWithBoth); - + // Both tags and keywords are valid, both should be accumulated expect(metadata.tags).toEqual(["tag1", "tag2", "keyword1", "keyword2"]); }); @@ -206,7 +212,7 @@ Some content here.`; Content here.`; const { metadata } = extractDocumentMetadata(contentWithTags); - + expect(metadata.tags).toEqual(["tag1", "tag2", "tag3"]); }); @@ -221,15 +227,19 @@ Content here.`; Content here.`; - const { metadata: summaryMetadata } = extractDocumentMetadata(contentWithSummary); - const { metadata: descriptionMetadata } = extractDocumentMetadata(contentWithDescription); - + const { metadata: summaryMetadata } = extractDocumentMetadata( + contentWithSummary, + ); + const { metadata: descriptionMetadata } = extractDocumentMetadata( + contentWithDescription, + ); + expect(summaryMetadata.summary).toBe("This is a summary"); expect(descriptionMetadata.summary).toBe("This is a description"); }); - describe('Smart metadata extraction', () => { - it('should handle section-only content correctly', () => { + describe("Smart metadata extraction", () => { + it("should handle section-only content correctly", () => { const sectionOnlyContent = `== First Section :author: Section Author :description: This is the first section @@ -244,20 +254,20 @@ This is the content of the first section. This is the content of the second section.`; const { metadata, content } = extractSmartMetadata(sectionOnlyContent); - + // Should extract title from first section - expect(metadata.title).toBe('First Section'); - + expect(metadata.title).toBe("First Section"); + // Should not have document-level metadata since there's no document header expect(metadata.authors).toBeUndefined(); expect(metadata.version).toBeUndefined(); expect(metadata.publicationDate).toBeUndefined(); - + // Content should be preserved expect(content).toBe(sectionOnlyContent); }); - it('should handle minimal document header (just title) correctly', () => { + it("should handle minimal document header (just title) correctly", () => { const minimalDocumentHeader = `= Test Document == First Section @@ -273,22 +283,22 @@ This is the content of the first section. This is the content of the second section.`; const { metadata, content } = extractSmartMetadata(minimalDocumentHeader); - + // Should extract title from document header - expect(metadata.title).toBe('Test Document'); - + expect(metadata.title).toBe("Test Document"); + // Should not have document-level metadata since there's no other metadata expect(metadata.authors).toBeUndefined(); // Note: version might be set from section attributes like :type: chapter expect(metadata.publicationDate).toBeUndefined(); - + // Content should preserve the title line for 30040 events - expect(content).toContain('= Test Document'); - expect(content).toContain('== First Section'); - expect(content).toContain('== Second Section'); + expect(content).toContain("= Test Document"); + expect(content).toContain("== First Section"); + expect(content).toContain("== Second Section"); }); - it('should handle document with full header correctly', () => { + it("should handle document with full header correctly", () => { const documentWithHeader = `= Test Document John Doe 1.0, 2024-01-15: Alexandria Test @@ -302,21 +312,21 @@ John Doe This is the content.`; const { metadata, content } = extractSmartMetadata(documentWithHeader); - + // Should extract document-level metadata - expect(metadata.title).toBe('Test Document'); - expect(metadata.authors).toEqual(['John Doe', 'Jane Smith']); - expect(metadata.version).toBe('1.0'); - expect(metadata.publishedBy).toBe('Alexandria Test'); - expect(metadata.publicationDate).toBe('2024-01-15'); - expect(metadata.summary).toBe('This is a test document'); - + expect(metadata.title).toBe("Test Document"); + expect(metadata.authors).toEqual(["John Doe", "Jane Smith"]); + expect(metadata.version).toBe("1.0"); + expect(metadata.publishedBy).toBe("Alexandria Test"); + expect(metadata.publicationDate).toBe("2024-01-15"); + expect(metadata.summary).toBe("This is a test document"); + // Content should be cleaned - expect(content).not.toContain('= Test Document'); - expect(content).not.toContain('John Doe '); - expect(content).not.toContain('1.0, 2024-01-15: Alexandria Test'); - expect(content).not.toContain(':summary: This is a test document'); - expect(content).not.toContain(':author: Jane Smith'); + expect(content).not.toContain("= Test Document"); + expect(content).not.toContain("John Doe "); + expect(content).not.toContain("1.0, 2024-01-15: Alexandria Test"); + expect(content).not.toContain(":summary: This is a test document"); + expect(content).not.toContain(":author: Jane Smith"); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/nostr_identifiers.test.ts b/tests/unit/nostr_identifiers.test.ts index d4c2d1f..a70c7bf 100644 --- a/tests/unit/nostr_identifiers.test.ts +++ b/tests/unit/nostr_identifiers.test.ts @@ -1,106 +1,112 @@ -import { describe, it, expect } from 'vitest'; -import { - isEventId, - isCoordinate, - parseCoordinate, +import { describe, expect, it } from "vitest"; +import { createCoordinate, - isNostrIdentifier -} from '../../src/lib/utils/nostr_identifiers'; + isCoordinate, + isEventId, + isNostrIdentifier, + parseCoordinate, +} from "../../src/lib/utils/nostr_identifiers"; -describe('Nostr Identifier Validation', () => { - describe('isEventId', () => { - it('should validate correct hex event IDs', () => { - const validId = 'a'.repeat(64); +describe("Nostr Identifier Validation", () => { + describe("isEventId", () => { + it("should validate correct hex event IDs", () => { + const validId = "a".repeat(64); expect(isEventId(validId)).toBe(true); - - const validIdWithMixedCase = 'A'.repeat(32) + 'f'.repeat(32); + + const validIdWithMixedCase = "A".repeat(32) + "f".repeat(32); expect(isEventId(validIdWithMixedCase)).toBe(true); }); - it('should reject invalid event IDs', () => { - expect(isEventId('')).toBe(false); - expect(isEventId('abc')).toBe(false); - expect(isEventId('a'.repeat(63))).toBe(false); // too short - expect(isEventId('a'.repeat(65))).toBe(false); // too long - expect(isEventId('g'.repeat(64))).toBe(false); // invalid hex char + it("should reject invalid event IDs", () => { + expect(isEventId("")).toBe(false); + expect(isEventId("abc")).toBe(false); + expect(isEventId("a".repeat(63))).toBe(false); // too short + expect(isEventId("a".repeat(65))).toBe(false); // too long + expect(isEventId("g".repeat(64))).toBe(false); // invalid hex char }); }); - describe('isCoordinate', () => { - it('should validate correct coordinates', () => { - const validCoordinate = `30040:${'a'.repeat(64)}:chapter-1`; + describe("isCoordinate", () => { + it("should validate correct coordinates", () => { + const validCoordinate = `30040:${"a".repeat(64)}:chapter-1`; expect(isCoordinate(validCoordinate)).toBe(true); - - const coordinateWithColonsInDTag = `30041:${'b'.repeat(64)}:chapter:with:colons`; + + const coordinateWithColonsInDTag = `30041:${ + "b".repeat(64) + }:chapter:with:colons`; expect(isCoordinate(coordinateWithColonsInDTag)).toBe(true); }); - it('should reject invalid coordinates', () => { - expect(isCoordinate('')).toBe(false); - expect(isCoordinate('abc')).toBe(false); - expect(isCoordinate('30040:abc:chapter-1')).toBe(false); // invalid pubkey - expect(isCoordinate('30040:abc')).toBe(false); // missing d-tag - expect(isCoordinate('abc:def:ghi')).toBe(false); // invalid kind - expect(isCoordinate('-1:abc:def')).toBe(false); // negative kind + it("should reject invalid coordinates", () => { + expect(isCoordinate("")).toBe(false); + expect(isCoordinate("abc")).toBe(false); + expect(isCoordinate("30040:abc:chapter-1")).toBe(false); // invalid pubkey + expect(isCoordinate("30040:abc")).toBe(false); // missing d-tag + expect(isCoordinate("abc:def:ghi")).toBe(false); // invalid kind + expect(isCoordinate("-1:abc:def")).toBe(false); // negative kind }); }); - describe('parseCoordinate', () => { - it('should parse valid coordinates correctly', () => { - const coordinate = `30040:${'a'.repeat(64)}:chapter-1`; + describe("parseCoordinate", () => { + it("should parse valid coordinates correctly", () => { + const coordinate = `30040:${"a".repeat(64)}:chapter-1`; const parsed = parseCoordinate(coordinate); - + expect(parsed).toEqual({ kind: 30040, - pubkey: 'a'.repeat(64), - dTag: 'chapter-1' + pubkey: "a".repeat(64), + dTag: "chapter-1", }); }); - it('should handle d-tags with colons', () => { - const coordinate = `30041:${'b'.repeat(64)}:chapter:with:colons`; + it("should handle d-tags with colons", () => { + const coordinate = `30041:${"b".repeat(64)}:chapter:with:colons`; const parsed = parseCoordinate(coordinate); - + expect(parsed).toEqual({ kind: 30041, - pubkey: 'b'.repeat(64), - dTag: 'chapter:with:colons' + pubkey: "b".repeat(64), + dTag: "chapter:with:colons", }); }); - it('should return null for invalid coordinates', () => { - expect(parseCoordinate('')).toBeNull(); - expect(parseCoordinate('abc')).toBeNull(); - expect(parseCoordinate('30040:abc:chapter-1')).toBeNull(); + it("should return null for invalid coordinates", () => { + expect(parseCoordinate("")).toBeNull(); + expect(parseCoordinate("abc")).toBeNull(); + expect(parseCoordinate("30040:abc:chapter-1")).toBeNull(); }); }); - describe('createCoordinate', () => { - it('should create valid coordinates', () => { - const coordinate = createCoordinate(30040, 'a'.repeat(64), 'chapter-1'); - expect(coordinate).toBe(`30040:${'a'.repeat(64)}:chapter-1`); + describe("createCoordinate", () => { + it("should create valid coordinates", () => { + const coordinate = createCoordinate(30040, "a".repeat(64), "chapter-1"); + expect(coordinate).toBe(`30040:${"a".repeat(64)}:chapter-1`); }); - it('should handle d-tags with colons', () => { - const coordinate = createCoordinate(30041, 'b'.repeat(64), 'chapter:with:colons'); - expect(coordinate).toBe(`30041:${'b'.repeat(64)}:chapter:with:colons`); + it("should handle d-tags with colons", () => { + const coordinate = createCoordinate( + 30041, + "b".repeat(64), + "chapter:with:colons", + ); + expect(coordinate).toBe(`30041:${"b".repeat(64)}:chapter:with:colons`); }); }); - describe('isNostrIdentifier', () => { - it('should accept valid event IDs', () => { - expect(isNostrIdentifier('a'.repeat(64))).toBe(true); + describe("isNostrIdentifier", () => { + it("should accept valid event IDs", () => { + expect(isNostrIdentifier("a".repeat(64))).toBe(true); }); - it('should accept valid coordinates', () => { - const coordinate = `30040:${'a'.repeat(64)}:chapter-1`; + it("should accept valid coordinates", () => { + const coordinate = `30040:${"a".repeat(64)}:chapter-1`; expect(isNostrIdentifier(coordinate)).toBe(true); }); - it('should reject invalid identifiers', () => { - expect(isNostrIdentifier('')).toBe(false); - expect(isNostrIdentifier('abc')).toBe(false); - expect(isNostrIdentifier('30040:abc:chapter-1')).toBe(false); + it("should reject invalid identifiers", () => { + expect(isNostrIdentifier("")).toBe(false); + expect(isNostrIdentifier("abc")).toBe(false); + expect(isNostrIdentifier("30040:abc:chapter-1")).toBe(false); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/relayDeduplication.test.ts b/tests/unit/relayDeduplication.test.ts index 9344cc2..4ea6b91 100644 --- a/tests/unit/relayDeduplication.test.ts +++ b/tests/unit/relayDeduplication.test.ts @@ -1,11 +1,11 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { NDKEvent } from '@nostr-dev-kit/ndk'; -import { - deduplicateContentEvents, +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import { deduplicateAndCombineEvents, + deduplicateContentEvents, + getEventCoordinate, isReplaceableEvent, - getEventCoordinate -} from '../../src/lib/utils/eventDeduplication'; +} from "../../src/lib/utils/eventDeduplication"; // Mock NDKEvent for testing class MockNDKEvent { @@ -16,162 +16,264 @@ class MockNDKEvent { content: string; tags: string[][]; - constructor(id: string, kind: number, pubkey: string, created_at: number, dTag: string, content: string = '') { + constructor( + id: string, + kind: number, + pubkey: string, + created_at: number, + dTag: string, + content: string = "", + ) { this.id = id; this.kind = kind; this.pubkey = pubkey; this.created_at = created_at; this.content = content; - this.tags = [['d', dTag]]; + this.tags = [["d", dTag]]; } tagValue(tagName: string): string | undefined { - const tag = this.tags.find(t => t[0] === tagName); + const tag = this.tags.find((t) => t[0] === tagName); return tag ? tag[1] : undefined; } } -describe('Relay Deduplication Behavior Tests', () => { +describe("Relay Deduplication Behavior Tests", () => { let mockEvents: MockNDKEvent[]; beforeEach(() => { // Create test events with different timestamps mockEvents = [ // Older version of a publication content event - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old content'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Old content", + ), // Newer version of the same publication content event - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'Updated content'), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-1", + "Updated content", + ), // Different publication content event - new MockNDKEvent('event3', 30041, 'pubkey1', 1500, 'chapter-2', 'Different content'), + new MockNDKEvent( + "event3", + 30041, + "pubkey1", + 1500, + "chapter-2", + "Different content", + ), // Publication index event (should not be deduplicated) - new MockNDKEvent('event4', 30040, 'pubkey1', 1200, 'book-1', 'Index content'), + new MockNDKEvent( + "event4", + 30040, + "pubkey1", + 1200, + "book-1", + "Index content", + ), // Regular text note (should not be deduplicated) - new MockNDKEvent('event5', 1, 'pubkey1', 1300, '', 'Regular note'), + new MockNDKEvent("event5", 1, "pubkey1", 1300, "", "Regular note"), ]; }); - describe('Addressable Event Deduplication', () => { - it('should keep only the most recent version of addressable events by coordinate', () => { + describe("Addressable Event Deduplication", () => { + it("should keep only the most recent version of addressable events by coordinate", () => { // Test the deduplication logic for content events - const eventSets = [new Set(mockEvents.filter(e => e.kind === 30041) as NDKEvent[])]; + const eventSets = [ + new Set(mockEvents.filter((e) => e.kind === 30041) as NDKEvent[]), + ]; const result = deduplicateContentEvents(eventSets); - + // Should have 2 unique coordinates: chapter-1 and chapter-2 expect(result.size).toBe(2); - + // Should keep the newer version of chapter-1 - const chapter1Event = result.get('30041:pubkey1:chapter-1'); - expect(chapter1Event?.id).toBe('event2'); - expect(chapter1Event?.content).toBe('Updated content'); - + const chapter1Event = result.get("30041:pubkey1:chapter-1"); + expect(chapter1Event?.id).toBe("event2"); + expect(chapter1Event?.content).toBe("Updated content"); + // Should keep chapter-2 - const chapter2Event = result.get('30041:pubkey1:chapter-2'); - expect(chapter2Event?.id).toBe('event3'); + const chapter2Event = result.get("30041:pubkey1:chapter-2"); + expect(chapter2Event?.id).toBe("event3"); }); - it('should handle events with missing d-tags gracefully', () => { - const eventWithoutDTag = new MockNDKEvent('event6', 30041, 'pubkey1', 1400, '', 'No d-tag'); + it("should handle events with missing d-tags gracefully", () => { + const eventWithoutDTag = new MockNDKEvent( + "event6", + 30041, + "pubkey1", + 1400, + "", + "No d-tag", + ); eventWithoutDTag.tags = []; // Remove d-tag - + const eventSets = [new Set([eventWithoutDTag] as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Should not include events without d-tags expect(result.size).toBe(0); }); - it('should handle events with missing timestamps', () => { - const eventWithoutTimestamp = new MockNDKEvent('event7', 30041, 'pubkey1', 0, 'chapter-3', 'No timestamp'); - const eventWithTimestamp = new MockNDKEvent('event8', 30041, 'pubkey1', 1500, 'chapter-3', 'With timestamp'); - - const eventSets = [new Set([eventWithoutTimestamp, eventWithTimestamp] as NDKEvent[])]; + it("should handle events with missing timestamps", () => { + const eventWithoutTimestamp = new MockNDKEvent( + "event7", + 30041, + "pubkey1", + 0, + "chapter-3", + "No timestamp", + ); + const eventWithTimestamp = new MockNDKEvent( + "event8", + 30041, + "pubkey1", + 1500, + "chapter-3", + "With timestamp", + ); + + const eventSets = [ + new Set([eventWithoutTimestamp, eventWithTimestamp] as NDKEvent[]), + ]; const result = deduplicateContentEvents(eventSets); - + // Should prefer the event with timestamp - const chapter3Event = result.get('30041:pubkey1:chapter-3'); - expect(chapter3Event?.id).toBe('event8'); + const chapter3Event = result.get("30041:pubkey1:chapter-3"); + expect(chapter3Event?.id).toBe("event8"); }); }); - describe('Mixed Event Type Deduplication', () => { - it('should only deduplicate addressable events (kinds 30000-39999)', () => { + describe("Mixed Event Type Deduplication", () => { + it("should only deduplicate addressable events (kinds 30000-39999)", () => { const result = deduplicateAndCombineEvents( [mockEvents[4]] as NDKEvent[], // Regular text note new Set([mockEvents[3]] as NDKEvent[]), // Publication index - new Set([mockEvents[0], mockEvents[1], mockEvents[2]] as NDKEvent[]) // Content events + new Set([mockEvents[0], mockEvents[1], mockEvents[2]] as NDKEvent[]), // Content events ); - + // Should have 4 events total: // - 1 regular text note (not deduplicated) // - 1 publication index (not deduplicated) // - 2 unique content events (deduplicated from 3) expect(result.length).toBe(4); - + // Verify the content events were deduplicated - const contentEvents = result.filter(e => e.kind === 30041); + const contentEvents = result.filter((e) => e.kind === 30041); expect(contentEvents.length).toBe(2); - + // Verify the newer version was kept - const newerEvent = contentEvents.find(e => e.id === 'event2'); + const newerEvent = contentEvents.find((e) => e.id === "event2"); expect(newerEvent).toBeDefined(); }); - it('should handle non-addressable events correctly', () => { + it("should handle non-addressable events correctly", () => { const regularEvents = [ - new MockNDKEvent('note1', 1, 'pubkey1', 1000, '', 'Note 1'), - new MockNDKEvent('note2', 1, 'pubkey1', 2000, '', 'Note 2'), - new MockNDKEvent('profile1', 0, 'pubkey1', 1500, '', 'Profile 1'), + new MockNDKEvent("note1", 1, "pubkey1", 1000, "", "Note 1"), + new MockNDKEvent("note2", 1, "pubkey1", 2000, "", "Note 2"), + new MockNDKEvent("profile1", 0, "pubkey1", 1500, "", "Profile 1"), ]; - + const result = deduplicateAndCombineEvents( regularEvents as NDKEvent[], new Set(), - new Set() + new Set(), ); - + // All regular events should be included (no deduplication) expect(result.length).toBe(3); }); }); - describe('Coordinate System Validation', () => { - it('should correctly identify event coordinates', () => { - const event = new MockNDKEvent('test', 30041, 'pubkey123', 1000, 'test-chapter'); + describe("Coordinate System Validation", () => { + it("should correctly identify event coordinates", () => { + const event = new MockNDKEvent( + "test", + 30041, + "pubkey123", + 1000, + "test-chapter", + ); const coordinate = getEventCoordinate(event as NDKEvent); - - expect(coordinate).toBe('30041:pubkey123:test-chapter'); + + expect(coordinate).toBe("30041:pubkey123:test-chapter"); }); - it('should handle d-tags with colons correctly', () => { - const event = new MockNDKEvent('test', 30041, 'pubkey123', 1000, 'chapter:with:colons'); + it("should handle d-tags with colons correctly", () => { + const event = new MockNDKEvent( + "test", + 30041, + "pubkey123", + 1000, + "chapter:with:colons", + ); const coordinate = getEventCoordinate(event as NDKEvent); - - expect(coordinate).toBe('30041:pubkey123:chapter:with:colons'); + + expect(coordinate).toBe("30041:pubkey123:chapter:with:colons"); }); - it('should return null for non-replaceable events', () => { - const event = new MockNDKEvent('test', 1, 'pubkey123', 1000, ''); + it("should return null for non-replaceable events", () => { + const event = new MockNDKEvent("test", 1, "pubkey123", 1000, ""); const coordinate = getEventCoordinate(event as NDKEvent); - + expect(coordinate).toBeNull(); }); }); - describe('Replaceable Event Detection', () => { - it('should correctly identify replaceable events', () => { - const addressableEvent = new MockNDKEvent('test', 30041, 'pubkey123', 1000, 'test'); - const regularEvent = new MockNDKEvent('test', 1, 'pubkey123', 1000, ''); - + describe("Replaceable Event Detection", () => { + it("should correctly identify replaceable events", () => { + const addressableEvent = new MockNDKEvent( + "test", + 30041, + "pubkey123", + 1000, + "test", + ); + const regularEvent = new MockNDKEvent("test", 1, "pubkey123", 1000, ""); + expect(isReplaceableEvent(addressableEvent as NDKEvent)).toBe(true); expect(isReplaceableEvent(regularEvent as NDKEvent)).toBe(false); }); - it('should handle edge cases of replaceable event ranges', () => { - const event29999 = new MockNDKEvent('test', 29999, 'pubkey123', 1000, 'test'); - const event30000 = new MockNDKEvent('test', 30000, 'pubkey123', 1000, 'test'); - const event39999 = new MockNDKEvent('test', 39999, 'pubkey123', 1000, 'test'); - const event40000 = new MockNDKEvent('test', 40000, 'pubkey123', 1000, 'test'); - + it("should handle edge cases of replaceable event ranges", () => { + const event29999 = new MockNDKEvent( + "test", + 29999, + "pubkey123", + 1000, + "test", + ); + const event30000 = new MockNDKEvent( + "test", + 30000, + "pubkey123", + 1000, + "test", + ); + const event39999 = new MockNDKEvent( + "test", + 39999, + "pubkey123", + 1000, + "test", + ); + const event40000 = new MockNDKEvent( + "test", + 40000, + "pubkey123", + 1000, + "test", + ); + expect(isReplaceableEvent(event29999 as NDKEvent)).toBe(false); expect(isReplaceableEvent(event30000 as NDKEvent)).toBe(true); expect(isReplaceableEvent(event39999 as NDKEvent)).toBe(true); @@ -179,279 +281,429 @@ describe('Relay Deduplication Behavior Tests', () => { }); }); - describe('Edge Cases', () => { - it('should handle empty event sets', () => { + describe("Edge Cases", () => { + it("should handle empty event sets", () => { const result = deduplicateContentEvents([]); expect(result.size).toBe(0); }); - it('should handle events with null/undefined values', () => { + it("should handle events with null/undefined values", () => { const invalidEvent = { id: undefined, kind: 30041, - pubkey: 'pubkey1', + pubkey: "pubkey1", created_at: 1000, tagValue: () => undefined, // Return undefined for d-tag } as unknown as NDKEvent; - + const eventSets = [new Set([invalidEvent])]; const result = deduplicateContentEvents(eventSets); - + // Should handle gracefully without crashing expect(result.size).toBe(0); }); - it('should handle events from different authors with same d-tag', () => { - const event1 = new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'same-chapter', 'Author 1'); - const event2 = new MockNDKEvent('event2', 30041, 'pubkey2', 1000, 'same-chapter', 'Author 2'); - + it("should handle events from different authors with same d-tag", () => { + const event1 = new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "same-chapter", + "Author 1", + ); + const event2 = new MockNDKEvent( + "event2", + 30041, + "pubkey2", + 1000, + "same-chapter", + "Author 2", + ); + const eventSets = [new Set([event1, event2] as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Should have 2 events (different coordinates due to different authors) expect(result.size).toBe(2); - expect(result.has('30041:pubkey1:same-chapter')).toBe(true); - expect(result.has('30041:pubkey2:same-chapter')).toBe(true); + expect(result.has("30041:pubkey1:same-chapter")).toBe(true); + expect(result.has("30041:pubkey2:same-chapter")).toBe(true); }); }); }); -describe('Relay Behavior Simulation', () => { - it('should simulate what happens when relays return duplicate events', () => { +describe("Relay Behavior Simulation", () => { + it("should simulate what happens when relays return duplicate events", () => { // Simulate a relay that returns multiple versions of the same event const relayEvents = [ - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old version'), - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'New version'), - new MockNDKEvent('event3', 30041, 'pubkey1', 1500, 'chapter-1', 'Middle version'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Old version", + ), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-1", + "New version", + ), + new MockNDKEvent( + "event3", + 30041, + "pubkey1", + 1500, + "chapter-1", + "Middle version", + ), ]; - + // This simulates what a "bad" relay might return const eventSets = [new Set(relayEvents as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Should only keep the newest version expect(result.size).toBe(1); - const keptEvent = result.get('30041:pubkey1:chapter-1'); - expect(keptEvent?.id).toBe('event2'); - expect(keptEvent?.content).toBe('New version'); + const keptEvent = result.get("30041:pubkey1:chapter-1"); + expect(keptEvent?.id).toBe("event2"); + expect(keptEvent?.content).toBe("New version"); }); - it('should simulate multiple relays returning different versions', () => { + it("should simulate multiple relays returning different versions", () => { // Simulate multiple relays returning different versions const relay1Events = [ - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Relay 1 version'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Relay 1 version", + ), ]; - + const relay2Events = [ - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'Relay 2 version'), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-1", + "Relay 2 version", + ), + ]; + + const eventSets = [ + new Set(relay1Events as NDKEvent[]), + new Set(relay2Events as NDKEvent[]), ]; - - const eventSets = [new Set(relay1Events as NDKEvent[]), new Set(relay2Events as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Should keep the newest version from any relay expect(result.size).toBe(1); - const keptEvent = result.get('30041:pubkey1:chapter-1'); - expect(keptEvent?.id).toBe('event2'); - expect(keptEvent?.content).toBe('Relay 2 version'); + const keptEvent = result.get("30041:pubkey1:chapter-1"); + expect(keptEvent?.id).toBe("event2"); + expect(keptEvent?.content).toBe("Relay 2 version"); }); }); -describe('Real Relay Deduplication Tests', () => { +describe("Real Relay Deduplication Tests", () => { // These tests actually query real relays to see if they deduplicate // Note: These are integration tests and may be flaky due to network conditions - - it('should detect if relays are returning duplicate replaceable events', async () => { - // This test queries real relays to see if they return duplicates - // We'll use a known author who has published multiple versions of content - - // Known author with multiple publication content events - const testAuthor = 'npub1z4m7gkva6yxgvdyclc7zp0qt69x9zgn8lu8sllg06wx6432h77qs0k97ks'; - - // Query for publication content events (kind 30041) from this author - // We expect relays to return only the most recent version of each d-tag - - // This is a placeholder - in a real test, we would: - // 1. Query multiple relays for the same author's 30041 events - // 2. Check if any relay returns multiple events with the same d-tag - // 3. Verify that if duplicates exist, our deduplication logic handles them - - console.log('Note: This test would require actual relay queries to verify deduplication behavior'); - console.log('To run this test properly, we would need to:'); - console.log('1. Query real relays for replaceable events'); - console.log('2. Check if relays return duplicates'); - console.log('3. Verify our deduplication logic works on real data'); - - // For now, we'll just assert that our logic is ready to handle real data - expect(true).toBe(true); - }, 30000); // 30 second timeout for network requests - - it('should verify that our deduplication logic works on real relay data', async () => { - // This test would: - // 1. Fetch real events from relays - // 2. Apply our deduplication logic - // 3. Verify that the results are correct - - console.log('Note: This test would require actual relay queries'); - console.log('To implement this test, we would need to:'); - console.log('1. Set up NDK with real relays'); - console.log('2. Fetch events for a known author with multiple versions'); - console.log('3. Apply deduplication and verify results'); - - expect(true).toBe(true); - }, 30000); + + it( + "should detect if relays are returning duplicate replaceable events", + async () => { + // This test queries real relays to see if they return duplicates + // We'll use a known author who has published multiple versions of content + + // Known author with multiple publication content events + const testAuthor = + "npub1z4m7gkva6yxgvdyclc7zp0qt69x9zgn8lu8sllg06wx6432h77qs0k97ks"; + + // Query for publication content events (kind 30041) from this author + // We expect relays to return only the most recent version of each d-tag + + // This is a placeholder - in a real test, we would: + // 1. Query multiple relays for the same author's 30041 events + // 2. Check if any relay returns multiple events with the same d-tag + // 3. Verify that if duplicates exist, our deduplication logic handles them + + console.log( + "Note: This test would require actual relay queries to verify deduplication behavior", + ); + console.log("To run this test properly, we would need to:"); + console.log("1. Query real relays for replaceable events"); + console.log("2. Check if relays return duplicates"); + console.log("3. Verify our deduplication logic works on real data"); + + // For now, we'll just assert that our logic is ready to handle real data + expect(true).toBe(true); + }, + 30000, + ); // 30 second timeout for network requests + + it( + "should verify that our deduplication logic works on real relay data", + async () => { + // This test would: + // 1. Fetch real events from relays + // 2. Apply our deduplication logic + // 3. Verify that the results are correct + + console.log("Note: This test would require actual relay queries"); + console.log("To implement this test, we would need to:"); + console.log("1. Set up NDK with real relays"); + console.log("2. Fetch events for a known author with multiple versions"); + console.log("3. Apply deduplication and verify results"); + + expect(true).toBe(true); + }, + 30000, + ); }); -describe('Practical Relay Behavior Analysis', () => { - it('should document what we know about relay deduplication behavior', () => { +describe("Practical Relay Behavior Analysis", () => { + it("should document what we know about relay deduplication behavior", () => { // This test documents our current understanding of relay behavior // based on the code analysis and the comment from onedev - - console.log('\n=== RELAY DEDUPLICATION BEHAVIOR ANALYSIS ==='); - console.log('\nBased on the code analysis and the comment from onedev:'); - console.log('\n1. THEORETICAL BEHAVIOR:'); - console.log(' - Relays SHOULD handle deduplication for replaceable events'); - console.log(' - Only the most recent version of each coordinate should be stored'); - console.log(' - Client-side deduplication should only be needed for cached/local events'); - - console.log('\n2. REALITY CHECK:'); - console.log(' - Not all relays implement deduplication correctly'); - console.log(' - Some relays may return multiple versions of the same event'); - console.log(' - Network conditions and relay availability can cause inconsistencies'); - - console.log('\n3. ALEXANDRIA\'S APPROACH:'); - console.log(' - Implements client-side deduplication as a safety net'); - console.log(' - Uses coordinate system (kind:pubkey:d-tag) for addressable events'); - console.log(' - Keeps the most recent version based on created_at timestamp'); - console.log(' - Only applies to replaceable events (kinds 30000-39999)'); - - console.log('\n4. WHY KEEP THE DEDUPLICATION:'); - console.log(' - Defensive programming against imperfect relay implementations'); - console.log(' - Handles multiple relay sources with different data'); - console.log(' - Works with cached events that might be outdated'); - console.log(' - Ensures consistent user experience regardless of relay behavior'); - - console.log('\n5. TESTING STRATEGY:'); - console.log(' - Unit tests verify our deduplication logic works correctly'); - console.log(' - Integration tests would verify relay behavior (when network allows)'); - console.log(' - Monitoring can help determine if relays improve over time'); - + + console.log("\n=== RELAY DEDUPLICATION BEHAVIOR ANALYSIS ==="); + console.log("\nBased on the code analysis and the comment from onedev:"); + console.log("\n1. THEORETICAL BEHAVIOR:"); + console.log( + " - Relays SHOULD handle deduplication for replaceable events", + ); + console.log( + " - Only the most recent version of each coordinate should be stored", + ); + console.log( + " - Client-side deduplication should only be needed for cached/local events", + ); + + console.log("\n2. REALITY CHECK:"); + console.log(" - Not all relays implement deduplication correctly"); + console.log( + " - Some relays may return multiple versions of the same event", + ); + console.log( + " - Network conditions and relay availability can cause inconsistencies", + ); + + console.log("\n3. ALEXANDRIA'S APPROACH:"); + console.log(" - Implements client-side deduplication as a safety net"); + console.log( + " - Uses coordinate system (kind:pubkey:d-tag) for addressable events", + ); + console.log( + " - Keeps the most recent version based on created_at timestamp", + ); + console.log(" - Only applies to replaceable events (kinds 30000-39999)"); + + console.log("\n4. WHY KEEP THE DEDUPLICATION:"); + console.log( + " - Defensive programming against imperfect relay implementations", + ); + console.log(" - Handles multiple relay sources with different data"); + console.log(" - Works with cached events that might be outdated"); + console.log( + " - Ensures consistent user experience regardless of relay behavior", + ); + + console.log("\n5. TESTING STRATEGY:"); + console.log( + " - Unit tests verify our deduplication logic works correctly", + ); + console.log( + " - Integration tests would verify relay behavior (when network allows)", + ); + console.log( + " - Monitoring can help determine if relays improve over time", + ); + // This test documents our understanding rather than asserting specific behavior expect(true).toBe(true); }); - it('should provide recommendations for when to remove deduplication', () => { - console.log('\n=== RECOMMENDATIONS FOR REMOVING DEDUPLICATION ==='); - console.log('\nThe deduplication logic should be kept until:'); - console.log('\n1. RELAY STANDARDS:'); - console.log(' - NIP-33 (replaceable events) is widely implemented by relays'); - console.log(' - Relays consistently return only the most recent version'); - console.log(' - No major relay implementations return duplicates'); - - console.log('\n2. TESTING EVIDENCE:'); - console.log(' - Real-world testing shows relays don\'t return duplicates'); - console.log(' - Multiple relay operators confirm deduplication behavior'); - console.log(' - No user reports of duplicate content issues'); - - console.log('\n3. MONITORING:'); - console.log(' - Add logging to track when deduplication is actually used'); - console.log(' - Monitor relay behavior over time'); - console.log(' - Collect metrics on duplicate events found'); - - console.log('\n4. GRADUAL REMOVAL:'); - console.log(' - Make deduplication configurable (on/off)'); - console.log(' - Test with deduplication disabled in controlled environments'); - console.log(' - Monitor for issues before removing completely'); - - console.log('\n5. FALLBACK STRATEGY:'); - console.log(' - Keep deduplication as a fallback option'); - console.log(' - Allow users to enable it if they experience issues'); - console.log(' - Maintain the code for potential future use'); - + it("should provide recommendations for when to remove deduplication", () => { + console.log("\n=== RECOMMENDATIONS FOR REMOVING DEDUPLICATION ==="); + console.log("\nThe deduplication logic should be kept until:"); + console.log("\n1. RELAY STANDARDS:"); + console.log( + " - NIP-33 (replaceable events) is widely implemented by relays", + ); + console.log(" - Relays consistently return only the most recent version"); + console.log(" - No major relay implementations return duplicates"); + + console.log("\n2. TESTING EVIDENCE:"); + console.log(" - Real-world testing shows relays don't return duplicates"); + console.log(" - Multiple relay operators confirm deduplication behavior"); + console.log(" - No user reports of duplicate content issues"); + + console.log("\n3. MONITORING:"); + console.log( + " - Add logging to track when deduplication is actually used", + ); + console.log(" - Monitor relay behavior over time"); + console.log(" - Collect metrics on duplicate events found"); + + console.log("\n4. GRADUAL REMOVAL:"); + console.log(" - Make deduplication configurable (on/off)"); + console.log( + " - Test with deduplication disabled in controlled environments", + ); + console.log(" - Monitor for issues before removing completely"); + + console.log("\n5. FALLBACK STRATEGY:"); + console.log(" - Keep deduplication as a fallback option"); + console.log(" - Allow users to enable it if they experience issues"); + console.log(" - Maintain the code for potential future use"); + expect(true).toBe(true); }); }); -describe('Logging and Monitoring Tests', () => { - it('should verify that logging works when duplicates are found', () => { +describe("Logging and Monitoring Tests", () => { + it("should verify that logging works when duplicates are found", () => { // Mock console.log to capture output - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + // Create events with duplicates const duplicateEvents = [ - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old version'), - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'New version'), - new MockNDKEvent('event3', 30041, 'pubkey1', 1500, 'chapter-1', 'Middle version'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Old version", + ), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-1", + "New version", + ), + new MockNDKEvent( + "event3", + 30041, + "pubkey1", + 1500, + "chapter-1", + "Middle version", + ), ]; - + const eventSets = [new Set(duplicateEvents as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Verify the deduplication worked expect(result.size).toBe(1); - + // Verify that logging was called expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('[eventDeduplication] Found 2 duplicate events out of 3 total events') + expect.stringContaining( + "[eventDeduplication] Found 2 duplicate events out of 3 total events", + ), ); expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('[eventDeduplication] Reduced to 1 unique coordinates') + expect.stringContaining( + "[eventDeduplication] Reduced to 1 unique coordinates", + ), ); - + // Restore console.log consoleSpy.mockRestore(); }); - it('should verify that logging works when no duplicates are found', () => { + it("should verify that logging works when no duplicates are found", () => { // Mock console.log to capture output - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + // Create events without duplicates const uniqueEvents = [ - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Content 1'), - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-2', 'Content 2'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Content 1", + ), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-2", + "Content 2", + ), ]; - + const eventSets = [new Set(uniqueEvents as NDKEvent[])]; const result = deduplicateContentEvents(eventSets); - + // Verify no deduplication was needed expect(result.size).toBe(2); - + // Verify that logging was called with "no duplicates" message expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('[eventDeduplication] No duplicates found in 2 events') + expect.stringContaining( + "[eventDeduplication] No duplicates found in 2 events", + ), ); - + // Restore console.log consoleSpy.mockRestore(); }); - it('should verify that deduplicateAndCombineEvents logging works', () => { + it("should verify that deduplicateAndCombineEvents logging works", () => { // Mock console.log to capture output - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + // Create events with duplicates const duplicateEvents = [ - new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old version'), - new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'New version'), + new MockNDKEvent( + "event1", + 30041, + "pubkey1", + 1000, + "chapter-1", + "Old version", + ), + new MockNDKEvent( + "event2", + 30041, + "pubkey1", + 2000, + "chapter-1", + "New version", + ), ]; - + const result = deduplicateAndCombineEvents( [] as NDKEvent[], new Set(), - new Set(duplicateEvents as NDKEvent[]) + new Set(duplicateEvents as NDKEvent[]), ); - + // Verify the deduplication worked expect(result.length).toBe(1); - + // Verify that logging was called expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('[eventDeduplication] deduplicateAndCombineEvents: Found 1 duplicate coordinates') + expect.stringContaining( + "[eventDeduplication] deduplicateAndCombineEvents: Found 1 duplicate coordinates", + ), ); - + // Restore console.log consoleSpy.mockRestore(); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/tagExpansion.test.ts b/tests/unit/tagExpansion.test.ts index 65e71fa..307ebd9 100644 --- a/tests/unit/tagExpansion.test.ts +++ b/tests/unit/tagExpansion.test.ts @@ -1,11 +1,10 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { NDKEvent } from '@nostr-dev-kit/ndk'; -import { +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import { + fetchProfilesForNewEvents, fetchTaggedEventsFromRelays, findTaggedEventsInFetched, - fetchProfilesForNewEvents, - type TagExpansionResult -} from '../../src/lib/utils/tag_event_fetch'; +} from "../../src/lib/utils/tag_event_fetch"; // Mock NDKEvent for testing class MockNDKEvent { @@ -16,7 +15,14 @@ class MockNDKEvent { content: string; tags: string[][]; - constructor(id: string, kind: number, pubkey: string, created_at: number, content: string = '', tags: string[][] = []) { + constructor( + id: string, + kind: number, + pubkey: string, + created_at: number, + content: string = "", + tags: string[][] = [], + ) { this.id = id; this.kind = kind; this.pubkey = pubkey; @@ -26,151 +32,193 @@ class MockNDKEvent { } tagValue(tagName: string): string | undefined { - const tag = this.tags.find(t => t[0] === tagName); + const tag = this.tags.find((t) => t[0] === tagName); return tag ? tag[1] : undefined; } getMatchingTags(tagName: string): string[][] { - return this.tags.filter(tag => tag[0] === tagName); + return this.tags.filter((tag) => tag[0] === tagName); } } // Mock NDK instance const mockNDK = { - fetchEvents: vi.fn() + fetchEvents: vi.fn(), }; // Mock the ndkInstance store -vi.mock('../../src/lib/ndk', () => ({ +// TODO: Replace with getNdkContext mock. +vi.mock("../../src/lib/ndk", () => ({ ndkInstance: { subscribe: vi.fn((fn) => { fn(mockNDK); return { unsubscribe: vi.fn() }; - }) - } + }), + }, })); // Mock the profile cache utilities -vi.mock('../../src/lib/utils/profileCache', () => ({ +vi.mock("../../src/lib/utils/profileCache", () => ({ extractPubkeysFromEvents: vi.fn((events: NDKEvent[]) => { const pubkeys = new Set(); - events.forEach(event => { + events.forEach((event) => { if (event.pubkey) pubkeys.add(event.pubkey); }); return pubkeys; }), - batchFetchProfiles: vi.fn(async (pubkeys: string[], onProgress: (fetched: number, total: number) => void) => { - // Simulate progress updates - onProgress(0, pubkeys.length); - onProgress(pubkeys.length, pubkeys.length); - return []; - }) + batchFetchProfiles: vi.fn( + async ( + pubkeys: string[], + onProgress: (fetched: number, total: number) => void, + ) => { + // Simulate progress updates + onProgress(0, pubkeys.length); + onProgress(pubkeys.length, pubkeys.length); + return []; + }, + ), })); -describe('Tag Expansion Tests', () => { +describe("Tag Expansion Tests", () => { let mockPublications: MockNDKEvent[]; let mockContentEvents: MockNDKEvent[]; let mockAllEvents: MockNDKEvent[]; beforeEach(() => { vi.clearAllMocks(); - + // Create test publication index events (kind 30040) mockPublications = [ - new MockNDKEvent('pub1', 30040, 'author1', 1000, 'Book 1', [ - ['t', 'bitcoin'], - ['t', 'cryptocurrency'], - ['a', '30041:author1:chapter-1'], - ['a', '30041:author1:chapter-2'] + new MockNDKEvent("pub1", 30040, "author1", 1000, "Book 1", [ + ["t", "bitcoin"], + ["t", "cryptocurrency"], + ["a", "30041:author1:chapter-1"], + ["a", "30041:author1:chapter-2"], + ]), + new MockNDKEvent("pub2", 30040, "author2", 1100, "Book 2", [ + ["t", "bitcoin"], + ["t", "blockchain"], + ["a", "30041:author2:chapter-1"], ]), - new MockNDKEvent('pub2', 30040, 'author2', 1100, 'Book 2', [ - ['t', 'bitcoin'], - ['t', 'blockchain'], - ['a', '30041:author2:chapter-1'] + new MockNDKEvent("pub3", 30040, "author3", 1200, "Book 3", [ + ["t", "ethereum"], + ["a", "30041:author3:chapter-1"], ]), - new MockNDKEvent('pub3', 30040, 'author3', 1200, 'Book 3', [ - ['t', 'ethereum'], - ['a', '30041:author3:chapter-1'] - ]) ]; // Create test content events (kind 30041) mockContentEvents = [ - new MockNDKEvent('content1', 30041, 'author1', 1000, 'Chapter 1 content', [['d', 'chapter-1']]), - new MockNDKEvent('content2', 30041, 'author1', 1100, 'Chapter 2 content', [['d', 'chapter-2']]), - new MockNDKEvent('content3', 30041, 'author2', 1200, 'Author 2 Chapter 1', [['d', 'chapter-1']]), - new MockNDKEvent('content4', 30041, 'author3', 1300, 'Author 3 Chapter 1', [['d', 'chapter-1']]) + new MockNDKEvent( + "content1", + 30041, + "author1", + 1000, + "Chapter 1 content", + [["d", "chapter-1"]], + ), + new MockNDKEvent( + "content2", + 30041, + "author1", + 1100, + "Chapter 2 content", + [["d", "chapter-2"]], + ), + new MockNDKEvent( + "content3", + 30041, + "author2", + 1200, + "Author 2 Chapter 1", + [["d", "chapter-1"]], + ), + new MockNDKEvent( + "content4", + 30041, + "author3", + 1300, + "Author 3 Chapter 1", + [["d", "chapter-1"]], + ), ]; // Combine all events for testing mockAllEvents = [...mockPublications, ...mockContentEvents]; }); - describe('fetchTaggedEventsFromRelays', () => { - it('should fetch publications with matching tags from relays', async () => { + describe("fetchTaggedEventsFromRelays", () => { + it("should fetch publications with matching tags from relays", async () => { // Mock the NDK fetch to return publications with 'bitcoin' tag - const bitcoinPublications = mockPublications.filter(pub => - pub.tags.some(tag => tag[0] === 't' && tag[1] === 'bitcoin') + const bitcoinPublications = mockPublications.filter((pub) => + pub.tags.some((tag) => tag[0] === "t" && tag[1] === "bitcoin") + ); + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(bitcoinPublications as NDKEvent[]), + ); + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(mockContentEvents as NDKEvent[]), ); - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(bitcoinPublications as NDKEvent[])); - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockContentEvents as NDKEvent[])); - const existingEventIds = new Set(['existing-event']); + const existingEventIds = new Set(["existing-event"]); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = await fetchTaggedEventsFromRelays( - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should fetch publications with bitcoin tag expect(mockNDK.fetchEvents).toHaveBeenCalledWith({ kinds: [30040], - "#t": ['bitcoin'], - limit: 30 + "#t": ["bitcoin"], + limit: 30, }); // Should return the matching publications expect(result.publications).toHaveLength(2); - expect(result.publications.map(p => p.id)).toContain('pub1'); - expect(result.publications.map(p => p.id)).toContain('pub2'); + expect(result.publications.map((p: any) => p.id)).toContain("pub1"); + expect(result.publications.map((p: any) => p.id)).toContain("pub2"); // Should fetch content events for the publications expect(mockNDK.fetchEvents).toHaveBeenCalledWith({ kinds: [30041, 30818], - "#d": ['chapter-1', 'chapter-2'] + "#d": ["chapter-1", "chapter-2"], }); }); - it('should filter out existing events to avoid duplicates', async () => { - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockPublications as NDKEvent[])); - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockContentEvents as NDKEvent[])); + it("should filter out existing events to avoid duplicates", async () => { + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(mockPublications as NDKEvent[]), + ); + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(mockContentEvents as NDKEvent[]), + ); - const existingEventIds = new Set(['pub1']); // pub1 already exists + const existingEventIds = new Set(["pub1"]); // pub1 already exists const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = await fetchTaggedEventsFromRelays( - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should exclude pub1 since it already exists expect(result.publications).toHaveLength(2); - expect(result.publications.map(p => p.id)).not.toContain('pub1'); - expect(result.publications.map(p => p.id)).toContain('pub2'); - expect(result.publications.map(p => p.id)).toContain('pub3'); + expect(result.publications.map((p: any) => p.id)).not.toContain("pub1"); + expect(result.publications.map((p: any) => p.id)).toContain("pub2"); + expect(result.publications.map((p: any) => p.id)).toContain("pub3"); }); - it('should handle empty tag array gracefully', async () => { + it("should handle empty tag array gracefully", async () => { // Mock empty result for empty tags mockNDK.fetchEvents.mockResolvedValueOnce(new Set()); - + const existingEventIds = new Set(); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); @@ -179,7 +227,7 @@ describe('Tag Expansion Tests', () => { [], existingEventIds, baseEvents, - debug + debug, ); expect(result.publications).toHaveLength(0); @@ -187,95 +235,101 @@ describe('Tag Expansion Tests', () => { }); }); - describe('findTaggedEventsInFetched', () => { - it('should find publications with matching tags in already fetched events', () => { - const existingEventIds = new Set(['existing-event']); + describe("findTaggedEventsInFetched", () => { + it("should find publications with matching tags in already fetched events", () => { + const existingEventIds = new Set(["existing-event"]); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = findTaggedEventsInFetched( mockAllEvents as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should find publications with bitcoin tag expect(result.publications).toHaveLength(2); - expect(result.publications.map(p => p.id)).toContain('pub1'); - expect(result.publications.map(p => p.id)).toContain('pub2'); + expect(result.publications.map((p: any) => p.id)).toContain("pub1"); + expect(result.publications.map((p: any) => p.id)).toContain("pub2"); // Should find content events for those publications expect(result.contentEvents).toHaveLength(4); - expect(result.contentEvents.map(c => c.id)).toContain('content1'); - expect(result.contentEvents.map(c => c.id)).toContain('content2'); - expect(result.contentEvents.map(c => c.id)).toContain('content3'); - expect(result.contentEvents.map(c => c.id)).toContain('content4'); + expect(result.contentEvents.map((c: any) => c.id)).toContain("content1"); + expect(result.contentEvents.map((c: any) => c.id)).toContain("content2"); + expect(result.contentEvents.map((c: any) => c.id)).toContain("content3"); + expect(result.contentEvents.map((c: any) => c.id)).toContain("content4"); }); - it('should exclude base events from search results', () => { - const existingEventIds = new Set(['pub1']); // pub1 is a base event + it("should exclude base events from search results", () => { + const existingEventIds = new Set(["pub1"]); // pub1 is a base event const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = findTaggedEventsInFetched( mockAllEvents as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should exclude pub1 since it's a base event expect(result.publications).toHaveLength(1); - expect(result.publications.map(p => p.id)).not.toContain('pub1'); - expect(result.publications.map(p => p.id)).toContain('pub2'); + expect(result.publications.map((p: any) => p.id)).not.toContain("pub1"); + expect(result.publications.map((p: any) => p.id)).toContain("pub2"); }); - it('should handle multiple tags (OR logic)', () => { + it("should handle multiple tags (OR logic)", () => { const existingEventIds = new Set(); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = findTaggedEventsInFetched( mockAllEvents as NDKEvent[], - ['bitcoin', 'ethereum'], + ["bitcoin", "ethereum"], existingEventIds, baseEvents, - debug + debug, ); // Should find publications with either bitcoin OR ethereum tags expect(result.publications).toHaveLength(3); - expect(result.publications.map(p => p.id)).toContain('pub1'); // bitcoin - expect(result.publications.map(p => p.id)).toContain('pub2'); // bitcoin - expect(result.publications.map(p => p.id)).toContain('pub3'); // ethereum + expect(result.publications.map((p: any) => p.id)).toContain("pub1"); // bitcoin + expect(result.publications.map((p: any) => p.id)).toContain("pub2"); // bitcoin + expect(result.publications.map((p: any) => p.id)).toContain("pub3"); // ethereum }); - it('should handle events without tags gracefully', () => { - const eventWithoutTags = new MockNDKEvent('no-tags', 30040, 'author4', 1000, 'No tags'); + it("should handle events without tags gracefully", () => { + const eventWithoutTags = new MockNDKEvent( + "no-tags", + 30040, + "author4", + 1000, + "No tags", + ); const allEventsWithNoTags = [...mockAllEvents, eventWithoutTags]; - + const existingEventIds = new Set(); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = findTaggedEventsInFetched( allEventsWithNoTags as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should not include events without tags - expect(result.publications.map(p => p.id)).not.toContain('no-tags'); + expect(result.publications.map((p: any) => p.id)).not.toContain("no-tags"); }); }); - describe('fetchProfilesForNewEvents', () => { - it('should extract pubkeys and fetch profiles for new events', async () => { + describe("fetchProfilesForNewEvents", () => { + it("should extract pubkeys and fetch profiles for new events", async () => { const onProgressUpdate = vi.fn(); const debug = vi.fn(); @@ -283,7 +337,7 @@ describe('Tag Expansion Tests', () => { mockPublications as NDKEvent[], mockContentEvents as NDKEvent[], onProgressUpdate, - debug + debug, ); // Should call progress update with initial state @@ -296,7 +350,7 @@ describe('Tag Expansion Tests', () => { expect(onProgressUpdate).toHaveBeenCalledWith(null); }); - it('should handle empty event arrays gracefully', async () => { + it("should handle empty event arrays gracefully", async () => { const onProgressUpdate = vi.fn(); const debug = vi.fn(); @@ -304,7 +358,7 @@ describe('Tag Expansion Tests', () => { [], [], onProgressUpdate, - debug + debug, ); // Should not call progress update for empty arrays @@ -312,27 +366,31 @@ describe('Tag Expansion Tests', () => { }); }); - describe('Tag Expansion Integration', () => { - it('should demonstrate the complete tag expansion flow', async () => { + describe("Tag Expansion Integration", () => { + it("should demonstrate the complete tag expansion flow", async () => { // This test simulates the complete flow from the visualize page - + // Step 1: Mock relay fetch for 'bitcoin' tag - const bitcoinPublications = mockPublications.filter(pub => - pub.tags.some(tag => tag[0] === 't' && tag[1] === 'bitcoin') + const bitcoinPublications = mockPublications.filter((pub) => + pub.tags.some((tag) => tag[0] === "t" && tag[1] === "bitcoin") + ); + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(bitcoinPublications as NDKEvent[]), + ); + mockNDK.fetchEvents.mockResolvedValueOnce( + new Set(mockContentEvents as NDKEvent[]), ); - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(bitcoinPublications as NDKEvent[])); - mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockContentEvents as NDKEvent[])); - const existingEventIds = new Set(['base-event']); + const existingEventIds = new Set(["base-event"]); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); // Step 2: Fetch from relays const relayResult = await fetchTaggedEventsFromRelays( - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); expect(relayResult.publications).toHaveLength(2); @@ -341,10 +399,10 @@ describe('Tag Expansion Tests', () => { // Step 3: Search in fetched events const searchResult = findTaggedEventsInFetched( mockAllEvents as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); expect(searchResult.publications).toHaveLength(2); @@ -356,20 +414,27 @@ describe('Tag Expansion Tests', () => { relayResult.publications, relayResult.contentEvents, onProgressUpdate, - debug + debug, ); expect(onProgressUpdate).toHaveBeenCalledWith(null); }); }); - describe('Edge Cases and Error Handling', () => { - it('should handle malformed a-tags gracefully', () => { - const malformedPublication = new MockNDKEvent('malformed', 30040, 'author1', 1000, 'Malformed', [ - ['t', 'bitcoin'], - ['a', 'invalid-tag-format'], // Missing parts - ['a', '30041:author1:chapter-1'] // Valid format - ]); + describe("Edge Cases and Error Handling", () => { + it("should handle malformed a-tags gracefully", () => { + const malformedPublication = new MockNDKEvent( + "malformed", + 30040, + "author1", + 1000, + "Malformed", + [ + ["t", "bitcoin"], + ["a", "invalid-tag-format"], // Missing parts + ["a", "30041:author1:chapter-1"], // Valid format + ], + ); const allEventsWithMalformed = [...mockAllEvents, malformedPublication]; const existingEventIds = new Set(); @@ -378,10 +443,10 @@ describe('Tag Expansion Tests', () => { const result = findTaggedEventsInFetched( allEventsWithMalformed as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should still work and include the publication with valid a-tags @@ -389,32 +454,50 @@ describe('Tag Expansion Tests', () => { expect(result.contentEvents.length).toBeGreaterThan(0); }); - it('should handle events with d-tags containing colons', () => { - const publicationWithColonDTag = new MockNDKEvent('colon-pub', 30040, 'author1', 1000, 'Colon d-tag', [ - ['t', 'bitcoin'], - ['a', '30041:author1:chapter:with:colons'] - ]); + it("should handle events with d-tags containing colons", () => { + const publicationWithColonDTag = new MockNDKEvent( + "colon-pub", + 30040, + "author1", + 1000, + "Colon d-tag", + [ + ["t", "bitcoin"], + ["a", "30041:author1:chapter:with:colons"], + ], + ); - const contentWithColonDTag = new MockNDKEvent('colon-content', 30041, 'author1', 1100, 'Content with colon d-tag', [ - ['d', 'chapter:with:colons'] - ]); + const contentWithColonDTag = new MockNDKEvent( + "colon-content", + 30041, + "author1", + 1100, + "Content with colon d-tag", + [ + ["d", "chapter:with:colons"], + ], + ); - const allEventsWithColons = [...mockAllEvents, publicationWithColonDTag, contentWithColonDTag]; + const allEventsWithColons = [ + ...mockAllEvents, + publicationWithColonDTag, + contentWithColonDTag, + ]; const existingEventIds = new Set(); const baseEvents: NDKEvent[] = []; const debug = vi.fn(); const result = findTaggedEventsInFetched( allEventsWithColons as NDKEvent[], - ['bitcoin'], + ["bitcoin"], existingEventIds, baseEvents, - debug + debug, ); // Should handle d-tags with colons correctly expect(result.publications).toHaveLength(3); - expect(result.contentEvents.map(c => c.id)).toContain('colon-content'); + expect(result.contentEvents.map((c: any) => c.id)).toContain("colon-content"); }); }); -}); \ No newline at end of file +}); diff --git a/vite.config.ts b/vite.config.ts index 47552d4..a81279c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -43,18 +43,20 @@ export default defineConfig({ // Expose the app version as a global variable "import.meta.env.APP_VERSION": JSON.stringify(getAppVersionString()), // Enable debug logging for relays when needed - "process.env.DEBUG_RELAYS": JSON.stringify(process.env.DEBUG_RELAYS || "false"), + "process.env.DEBUG_RELAYS": JSON.stringify( + process.env.DEBUG_RELAYS || "false", + ), }, optimizeDeps: { esbuildOptions: { define: { - global: 'globalThis', + global: "globalThis", }, }, }, server: { fs: { - allow: ['..'], + allow: [".."], }, hmr: { overlay: false, // Disable HMR overlay to prevent ESM URL scheme errors