diff --git a/deno.lock b/deno.lock index 35676a3..ef86772 100644 --- a/deno.lock +++ b/deno.lock @@ -8,11 +8,6 @@ "npm:@nostr-dev-kit/ndk@^2.14.32": "2.14.32_nostr-tools@2.15.1__typescript@5.8.3_typescript@5.8.3", "npm:@playwright/test@^1.54.1": "1.54.1", "npm:@popperjs/core@2.11": "2.11.8", - "npm:@sveltejs/adapter-auto@^6.0.1": "6.0.1_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@7.0.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15", - "npm:@sveltejs/adapter-node@^5.2.13": "5.2.13_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@7.0.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_rollup@4.45.1_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15", - "npm:@sveltejs/adapter-static@3": "3.0.8_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@7.0.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15", - "npm:@sveltejs/kit@^2.25.0": "2.25.1_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_acorn@8.15.0_@types+node@24.0.15", - "npm:@sveltejs/vite-plugin-svelte@^6.1.0": "6.1.0_svelte@5.36.8__acorn@8.15.0_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15", "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", @@ -50,9 +45,7 @@ "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:vite@^7.0.5": "7.0.5_@types+node@24.0.15_picomatch@4.0.3", - "npm:vitest@^3.1.3": "3.2.4_@types+node@24.0.15_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3" + "npm:typescript@^5.8.3": "5.8.3" }, "npm": { "@alloc/quick-lru@5.2.0": { @@ -441,38 +434,9 @@ ], "bin": true }, - "@polka/url@1.0.0-next.29": { - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==" - }, "@popperjs/core@2.11.8": { "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, - "@rollup/plugin-commonjs@28.0.6_rollup@4.45.1_picomatch@4.0.3": { - "integrity": "sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==", - "dependencies": [ - "@rollup/pluginutils", - "commondir", - "estree-walker@2.0.2", - "fdir", - "is-reference@1.2.1", - "magic-string", - "picomatch@4.0.3", - "rollup" - ], - "optionalPeers": [ - "rollup" - ] - }, - "@rollup/plugin-json@6.1.0_rollup@4.45.1": { - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", - "dependencies": [ - "@rollup/pluginutils", - "rollup" - ], - "optionalPeers": [ - "rollup" - ] - }, "@rollup/plugin-node-resolve@15.3.1": { "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", "dependencies": [ @@ -483,25 +447,11 @@ "resolve" ] }, - "@rollup/plugin-node-resolve@16.0.1_rollup@4.45.1": { - "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", - "dependencies": [ - "@rollup/pluginutils", - "@types/resolve", - "deepmerge", - "is-module", - "resolve", - "rollup" - ], - "optionalPeers": [ - "rollup" - ] - }, "@rollup/pluginutils@5.2.0_rollup@4.45.1": { "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", "dependencies": [ "@types/estree", - "estree-walker@2.0.2", + "estree-walker", "picomatch@4.0.3", "rollup" ], @@ -639,71 +589,6 @@ "acorn@8.15.0" ] }, - "@sveltejs/adapter-auto@6.0.1_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@7.0.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { - "integrity": "sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ==", - "dependencies": [ - "@sveltejs/kit" - ] - }, - "@sveltejs/adapter-node@5.2.13_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@7.0.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_rollup@4.45.1_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { - "integrity": "sha512-yS2TVFmIrxjGhYaV5/iIUrJ3mJl6zjaYn0lBD70vTLnYvJeqf3cjvLXeXCUCuYinhSBoyF4DpfGla49BnIy7sQ==", - "dependencies": [ - "@rollup/plugin-commonjs", - "@rollup/plugin-json", - "@rollup/plugin-node-resolve@16.0.1_rollup@4.45.1", - "@sveltejs/kit", - "rollup" - ] - }, - "@sveltejs/adapter-static@3.0.8_@sveltejs+kit@2.25.1__@sveltejs+vite-plugin-svelte@6.1.0___svelte@5.36.8____acorn@8.15.0___vite@7.0.5____@types+node@24.0.15____picomatch@4.0.3___@types+node@24.0.15__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__acorn@8.15.0__@types+node@24.0.15_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { - "integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==", - "dependencies": [ - "@sveltejs/kit" - ] - }, - "@sveltejs/kit@2.25.1_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_acorn@8.15.0_@types+node@24.0.15": { - "integrity": "sha512-8H+fxDEp7Xq6tLFdrGdS5fLu6ONDQQ9DgyjboXpChubuFdfH9QoFX09ypssBpyNkJNZFt9eW3yLmXIc9CesPCA==", - "dependencies": [ - "@sveltejs/acorn-typescript", - "@sveltejs/vite-plugin-svelte", - "@types/cookie", - "acorn@8.15.0", - "cookie", - "devalue", - "esm-env", - "kleur", - "magic-string", - "mrmime", - "sade", - "set-cookie-parser", - "sirv", - "svelte", - "vite" - ], - "bin": true - }, - "@sveltejs/vite-plugin-svelte-inspector@5.0.0_@sveltejs+vite-plugin-svelte@6.1.0__svelte@5.36.8___acorn@8.15.0__vite@7.0.5___@types+node@24.0.15___picomatch@4.0.3__@types+node@24.0.15_svelte@5.36.8__acorn@8.15.0_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { - "integrity": "sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==", - "dependencies": [ - "@sveltejs/vite-plugin-svelte", - "debug", - "svelte", - "vite" - ] - }, - "@sveltejs/vite-plugin-svelte@6.1.0_svelte@5.36.8__acorn@8.15.0_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { - "integrity": "sha512-+U6lz1wvGEG/BvQyL4z/flyNdQ9xDNv5vrh+vWBWTHaebqT0c9RNggpZTo/XSPoHsSCWBlYaTlRX8pZ9GATXCw==", - "dependencies": [ - "@sveltejs/vite-plugin-svelte-inspector", - "debug", - "deepmerge", - "kleur", - "magic-string", - "svelte", - "vite", - "vitefu" - ] - }, "@svgdotjs/svg.draggable.js@3.0.6_@svgdotjs+svg.js@3.2.4": { "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==", "dependencies": [ @@ -749,15 +634,6 @@ "tailwindcss" ] }, - "@types/chai@5.2.2": { - "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", - "dependencies": [ - "@types/deep-eql" - ] - }, - "@types/cookie@0.6.0": { - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" - }, "@types/d3-array@3.2.1": { "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" }, @@ -918,9 +794,6 @@ "@types/d3-zoom" ] }, - "@types/deep-eql@4.0.2": { - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==" - }, "@types/estree@1.0.8": { "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, @@ -957,64 +830,6 @@ "@types/resolve@1.20.2": { "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" }, - "@vitest/expect@3.2.4": { - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dependencies": [ - "@types/chai", - "@vitest/spy", - "@vitest/utils", - "chai", - "tinyrainbow" - ] - }, - "@vitest/mocker@3.2.4_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dependencies": [ - "@vitest/spy", - "estree-walker@3.0.3", - "magic-string", - "vite" - ], - "optionalPeers": [ - "vite" - ] - }, - "@vitest/pretty-format@3.2.4": { - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dependencies": [ - "tinyrainbow" - ] - }, - "@vitest/runner@3.2.4": { - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dependencies": [ - "@vitest/utils", - "pathe", - "strip-literal" - ] - }, - "@vitest/snapshot@3.2.4": { - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dependencies": [ - "@vitest/pretty-format", - "magic-string", - "pathe" - ] - }, - "@vitest/spy@3.2.4": { - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dependencies": [ - "tinyspy" - ] - }, - "@vitest/utils@3.2.4": { - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dependencies": [ - "@vitest/pretty-format", - "loupe", - "tinyrainbow" - ] - }, "@yr/monotone-cubic-spline@1.0.3": { "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" }, @@ -1119,9 +934,6 @@ "assert-never@1.4.0": { "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==" }, - "assertion-error@2.0.1": { - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==" - }, "async@3.2.6": { "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, @@ -1185,9 +997,6 @@ ], "bin": true }, - "cac@6.7.14": { - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==" - }, "call-bind-apply-helpers@1.0.2": { "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": [ @@ -1214,16 +1023,6 @@ "caniuse-lite@1.0.30001727": { "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==" }, - "chai@5.2.1": { - "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", - "dependencies": [ - "assertion-error", - "check-error", - "deep-eql", - "loupe", - "pathval" - ] - }, "chalk@4.1.2": { "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": [ @@ -1240,9 +1039,6 @@ "is-regex" ] }, - "check-error@2.1.1": { - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==" - }, "chokidar@3.6.0": { "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": [ @@ -1301,9 +1097,6 @@ "commander@7.2.0": { "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" }, - "commondir@1.0.1": { - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, "concat-map@0.0.1": { "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, @@ -1314,9 +1107,6 @@ "@babel/types" ] }, - "cookie@0.6.0": { - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" - }, "cross-spawn@7.0.6": { "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": [ @@ -1544,9 +1334,6 @@ "decamelize@1.2.0": { "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" }, - "deep-eql@5.0.2": { - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==" - }, "deep-is@0.1.4": { "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, @@ -1559,9 +1346,6 @@ "robust-predicates" ] }, - "devalue@5.1.1": { - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==" - }, "dexie@4.0.11": { "integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==" }, @@ -1613,48 +1397,12 @@ "es-errors@1.3.0": { "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, - "es-module-lexer@1.7.0": { - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==" - }, "es-object-atoms@1.1.1": { "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dependencies": [ "es-errors" ] }, - "esbuild@0.25.7": { - "integrity": "sha512-daJB0q2dmTzo90L9NjRaohhRWrCzYxWNFTjEi72/h+p5DcY3yn4MacWfDakHmaBaDzDiuLJsCh0+6LK/iX+c+Q==", - "optionalDependencies": [ - "@esbuild/aix-ppc64", - "@esbuild/android-arm", - "@esbuild/android-arm64", - "@esbuild/android-x64", - "@esbuild/darwin-arm64", - "@esbuild/darwin-x64", - "@esbuild/freebsd-arm64", - "@esbuild/freebsd-x64", - "@esbuild/linux-arm", - "@esbuild/linux-arm64", - "@esbuild/linux-ia32", - "@esbuild/linux-loong64", - "@esbuild/linux-mips64el", - "@esbuild/linux-ppc64", - "@esbuild/linux-riscv64", - "@esbuild/linux-s390x", - "@esbuild/linux-x64", - "@esbuild/netbsd-arm64", - "@esbuild/netbsd-x64", - "@esbuild/openbsd-arm64", - "@esbuild/openbsd-x64", - "@esbuild/openharmony-arm64", - "@esbuild/sunos-x64", - "@esbuild/win32-arm64", - "@esbuild/win32-ia32", - "@esbuild/win32-x64" - ], - "scripts": true, - "bin": true - }, "escalade@3.2.0": { "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" }, @@ -1770,18 +1518,9 @@ "estree-walker@2.0.2": { "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, - "estree-walker@3.0.3": { - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": [ - "@types/estree" - ] - }, "esutils@2.0.3": { "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, - "expect-type@1.2.2": { - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==" - }, "fast-deep-equal@3.1.3": { "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, @@ -1861,7 +1600,7 @@ "flowbite-datepicker@1.3.2": { "integrity": "sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g==", "dependencies": [ - "@rollup/plugin-node-resolve@15.3.1", + "@rollup/plugin-node-resolve", "flowbite@2.5.2" ] }, @@ -2128,12 +1867,6 @@ "is-promise@2.2.2": { "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, - "is-reference@1.2.1": { - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dependencies": [ - "@types/estree" - ] - }, "is-reference@3.0.3": { "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "dependencies": [ @@ -2178,9 +1911,6 @@ "js-stringify@1.0.2": { "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" }, - "js-tokens@9.0.1": { - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==" - }, "js-yaml@4.1.0": { "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": [ @@ -2210,9 +1940,6 @@ "json-buffer" ] }, - "kleur@4.1.5": { - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" - }, "known-css-properties@0.37.0": { "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==" }, @@ -2262,9 +1989,6 @@ "lodash.merge@4.6.2": { "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "loupe@3.1.4": { - "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==" - }, "lru-cache@10.4.3": { "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, @@ -2318,9 +2042,6 @@ "mri@1.2.0": { "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" }, - "mrmime@2.0.1": { - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==" - }, "ms@2.1.3": { "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, @@ -2463,12 +2184,6 @@ "minipass" ] }, - "pathe@2.0.3": { - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" - }, - "pathval@2.0.1": { - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==" - }, "picocolors@1.1.1": { "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, @@ -2820,9 +2535,6 @@ "set-blocking@2.0.0": { "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, - "set-cookie-parser@2.7.1": { - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" - }, "shebang-command@2.0.0": { "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dependencies": [ @@ -2832,20 +2544,9 @@ "shebang-regex@3.0.0": { "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, - "siginfo@2.0.0": { - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==" - }, "signal-exit@4.1.0": { "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" }, - "sirv@3.0.1": { - "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", - "dependencies": [ - "@polka/url", - "mrmime", - "totalist" - ] - }, "skin-tone@2.0.0": { "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", "dependencies": [ @@ -2858,12 +2559,6 @@ "source-map@0.6.1": { "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, - "stackback@0.0.2": { - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==" - }, - "std-env@3.9.0": { - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==" - }, "string-width@4.2.3": { "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": [ @@ -2895,12 +2590,6 @@ "strip-json-comments@3.1.1": { "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, - "strip-literal@3.0.0": { - "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", - "dependencies": [ - "js-tokens" - ] - }, "sucrase@3.35.0": { "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "dependencies": [ @@ -2964,7 +2653,7 @@ "clsx", "esm-env", "esrap", - "is-reference@3.0.3", + "is-reference", "locate-character", "magic-string", "zimmerframe" @@ -3069,28 +2758,6 @@ "any-promise" ] }, - "tinybench@2.9.0": { - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==" - }, - "tinyexec@0.3.2": { - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==" - }, - "tinyglobby@0.2.14_picomatch@4.0.3": { - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dependencies": [ - "fdir", - "picomatch@4.0.3" - ] - }, - "tinypool@1.1.1": { - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==" - }, - "tinyrainbow@2.0.0": { - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==" - }, - "tinyspy@4.0.3": { - "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==" - }, "to-regex-range@5.0.1": { "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dependencies": [ @@ -3100,9 +2767,6 @@ "token-stream@1.0.0": { "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" }, - "totalist@3.0.1": { - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==" - }, "ts-interface-checker@0.1.13": { "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, @@ -3159,78 +2823,6 @@ "util-deprecate@1.0.2": { "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "vite-node@3.2.4_@types+node@24.0.15": { - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dependencies": [ - "cac", - "debug", - "es-module-lexer", - "pathe", - "vite" - ], - "bin": true - }, - "vite@7.0.5_@types+node@24.0.15_picomatch@4.0.3": { - "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", - "dependencies": [ - "@types/node@24.0.15", - "esbuild", - "fdir", - "picomatch@4.0.3", - "postcss", - "rollup", - "tinyglobby" - ], - "optionalDependencies": [ - "fsevents@2.3.3" - ], - "optionalPeers": [ - "@types/node@24.0.15" - ], - "bin": true - }, - "vitefu@1.1.1_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3_@types+node@24.0.15": { - "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", - "dependencies": [ - "vite" - ], - "optionalPeers": [ - "vite" - ] - }, - "vitest@3.2.4_@types+node@24.0.15_vite@7.0.5__@types+node@24.0.15__picomatch@4.0.3": { - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dependencies": [ - "@types/chai", - "@types/node@24.0.15", - "@vitest/expect", - "@vitest/mocker", - "@vitest/pretty-format", - "@vitest/runner", - "@vitest/snapshot", - "@vitest/spy", - "@vitest/utils", - "chai", - "debug", - "expect-type", - "magic-string", - "pathe", - "picomatch@4.0.3", - "std-env", - "tinybench", - "tinyexec", - "tinyglobby", - "tinypool", - "tinyrainbow", - "vite", - "vite-node", - "why-is-node-running" - ], - "optionalPeers": [ - "@types/node@24.0.15" - ], - "bin": true - }, "void-elements@3.1.0": { "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" }, @@ -3244,14 +2836,6 @@ ], "bin": true }, - "why-is-node-running@2.3.0": { - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dependencies": [ - "siginfo", - "stackback" - ], - "bin": true - }, "with@7.0.2": { "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", "dependencies": [ @@ -3421,7 +3005,7 @@ "npm:tailwindcss@^3.4.17", "npm:tslib@2.8", "npm:typescript@^5.8.3", - "npm:vite@^7.0.5", + "npm:vite@^6.3.5", "npm:vitest@^3.1.3" ] } diff --git a/package-lock.json b/package-lock.json index 37b6de5..95e0f71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ "tailwindcss": "^3.4.17", "tslib": "2.8.x", "typescript": "^5.8.3", - "vite": "^7.0.5", + "vite": "^6.3.5", "vitest": "^3.1.3" } }, @@ -2783,37 +2783,19 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" } }, "node_modules/cliui": { @@ -3729,15 +3711,6 @@ } } }, - "node_modules/eslint-plugin-svelte/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -5980,25 +5953,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/require-directory": { @@ -6506,38 +6471,10 @@ "typescript": ">=5.0.0" } }, - "node_modules/svelte-check/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/svelte-check/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/svelte-eslint-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.3.0.tgz", - "integrity": "sha512-VCgMHKV7UtOGcGLGNFSbmdm6kEKjtzo5nnpGU/mnx4OsFY6bZ7QwRF5DUx+Hokw5Lvdyo8dpk8B1m8mliomrNg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.3.1.tgz", + "integrity": "sha512-0Iztj5vcOVOVkhy1pbo5uA9r+d3yaVoE5XPc9eABIWDOSJZ2mOsZ4D+t45rphWCOr0uMw3jtSG2fh2e7GvKnPg==", "dev": true, "license": "MIT", "dependencies": { @@ -6734,6 +6671,54 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tailwindcss/node_modules/postcss-load-config": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", @@ -6782,6 +6767,30 @@ "node": ">=4" } }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tailwindcss/node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -7032,24 +7041,24 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", - "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.6", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.40.0", - "tinyglobby": "^0.2.14" + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -7058,14 +7067,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", - "less": "^4.0.0", + "less": "*", "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -7367,14 +7376,13 @@ } }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "bin": { - "yaml": "bin.mjs" - }, + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", "engines": { - "node": ">= 14.6" + "node": ">= 6" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index 647d2c6..2225a7a 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", + "dev:node": "node --version && vite dev", "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", @@ -14,8 +15,8 @@ "test": "vitest" }, "dependencies": { - "@noble/hashes": "^1.8.0", "@noble/curves": "^1.9.4", + "@noble/hashes": "^1.8.0", "@nostr-dev-kit/ndk": "^2.14.32", "@nostr-dev-kit/ndk-cache-dexie": "2.6.x", "@popperjs/core": "2.11.x", @@ -59,7 +60,7 @@ "tailwindcss": "^3.4.17", "tslib": "2.8.x", "typescript": "^5.8.3", - "vite": "^7.0.5", + "vite": "^6.3.5", "vitest": "^3.1.3" } } diff --git a/src/lib/components/EventInput.svelte b/src/lib/components/EventInput.svelte index 7834a11..0519692 100644 --- a/src/lib/components/EventInput.svelte +++ b/src/lib/components/EventInput.svelte @@ -12,6 +12,12 @@ analyze30040Event, get30040FixGuidance, } from "$lib/utils/event_input_utils"; + import { + extractDocumentMetadata, + extractSmartMetadata, + metadataToTags, + removeMetadataFromContent + } from "$lib/utils/asciidoc_metadata"; import { get } from "svelte/store"; import { ndkInstance } from "$lib/ndk"; import { userPubkey } from "$lib/stores/authStore.Svelte"; @@ -24,7 +30,7 @@ import { goto } from "$app/navigation"; import { WebSocketPool } from "$lib/data_structures/websocket_pool"; - let kind = $state(30023); + let kind = $state(30040); let tags = $state<[string, string][]>([]); let content = $state(""); let createdAt = $state(Math.floor(Date.now() / 1000)); @@ -39,22 +45,68 @@ let dTagManuallyEdited = $state(false); let dTagError = $state(""); let lastPublishedEventId = $state(null); + let showWarning = $state(false); + let warningMessage = $state(""); + let pendingPublish = $state(false); + let extractedMetadata = $state<[string, string][]>([]); + let hasLoadedFromStorage = $state(false); + + // Load content from sessionStorage if available (from ZettelEditor) + $effect(() => { + if (hasLoadedFromStorage) return; // Prevent multiple loads + + const storedContent = sessionStorage.getItem('zettelEditorContent'); + const storedSource = sessionStorage.getItem('zettelEditorSource'); + + if (storedContent && storedSource === 'publication-format') { + content = storedContent; + hasLoadedFromStorage = true; + + // Clear the stored content after loading + sessionStorage.removeItem('zettelEditorContent'); + sessionStorage.removeItem('zettelEditorSource'); + + // Extract title and metadata using the standardized parser + const { metadata } = extractSmartMetadata(content); + if (metadata.title) { + title = metadata.title; + titleManuallyEdited = false; + dTagManuallyEdited = false; + } + + // Extract metadata for 30040 and 30041 events + if (kind === 30040 || kind === 30041) { + extractedMetadata = metadataToTags(metadata); + } + } + }); /** - * Extracts the first Markdown/AsciiDoc header as the title. + * Extracts the first Markdown/AsciiDoc header as the title using the standardized parser. */ function extractTitleFromContent(content: string): string { - // Match Markdown (# Title) or AsciiDoc (= Title) headers - const match = content.match(/^(#|=)\s*(.+)$/m); - return match ? match[2].trim() : ""; + const { metadata } = extractSmartMetadata(content); + return metadata.title || ""; } function handleContentInput(e: Event) { content = (e.target as HTMLTextAreaElement).value; + + // Extract title and metadata using the standardized parser + const { metadata } = extractSmartMetadata(content); + if (!titleManuallyEdited) { - const extracted = extractTitleFromContent(content); - console.log("Content input - extracted title:", extracted); - title = extracted; + console.log("Content input - extracted title:", metadata.title); + title = metadata.title || ""; + // Reset dTagManuallyEdited when title changes so d-tag can be auto-generated + dTagManuallyEdited = false; + } + + // Extract metadata from AsciiDoc content for 30040 and 30041 events + if (kind === 30040 || kind === 30041) { + extractedMetadata = metadataToTags(metadata); + } else { + extractedMetadata = []; } } @@ -92,12 +144,24 @@ tags = tags.filter((_, i) => i !== index); } + function addExtractedTag(key: string, value: string): void { + // Check if tag already exists + const existingIndex = tags.findIndex(([k]) => k === key); + if (existingIndex >= 0) { + // Update existing tag + tags = tags.map((t, i) => (i === existingIndex ? [key, value] : t)); + } else { + // Add new tag + tags = [...tags, [key, value]]; + } + } + function isValidKind(kind: number | string): boolean { const n = Number(kind); return Number.isInteger(n) && n >= 0 && n <= 65535; } - function validate(): { valid: boolean; reason?: string } { + function validate(): { valid: boolean; reason?: string; warning?: string } { const currentUserPubkey = get(userPubkey as any); const userState = get(userStore); @@ -113,6 +177,7 @@ if (kind === 30040) { const v = validate30040EventSet(content); if (!v.valid) return v; + if (v.warning) return { valid: true, warning: v.warning }; } if (kind === 30041 || kind === 30818) { const v = validateAsciiDoc(content); @@ -124,10 +189,26 @@ function handleSubmit(e: Event) { e.preventDefault(); dTagError = ""; + error = null; // Clear any previous errors + if (requiresDTag(kind) && (!dTag || dTag.trim() === "")) { dTagError = "A d-tag is required."; return; } + + const validation = validate(); + if (!validation.valid) { + error = validation.reason || "Validation failed."; + return; + } + + if (validation.warning) { + warningMessage = validation.warning; + showWarning = true; + pendingPublish = true; + return; + } + handlePublish(); } @@ -235,8 +316,14 @@ eventTags = [...eventTags, ["title", titleValue]]; } + // For AsciiDoc events, remove metadata from content + let finalContent = content; + if (kind === 30040 || kind === 30041) { + finalContent = removeMetadataFromContent(content); + } + // Prefix Nostr addresses before publishing - const prefixedContent = prefixNostrAddresses(content); + const prefixedContent = prefixNostrAddresses(finalContent); // Create event with proper serialization const eventData = { @@ -330,6 +417,9 @@ } } }; + + // Send the event to the relay + ws.send(JSON.stringify(["EVENT", signedEvent])); }); if (published) break; } catch (e) { @@ -391,6 +481,18 @@ goto(`/events?id=${encodeURIComponent(lastPublishedEventId)}`); } } + + function confirmWarning() { + showWarning = false; + pendingPublish = false; + handlePublish(); + } + + function cancelWarning() { + showWarning = false; + pendingPublish = false; + warningMessage = ""; + }
{/if} - {#if kind === 30040} + {#if Number(kind) === 30040}
30040 - Publication Index: {get30040EventDescription()} @@ -423,6 +525,36 @@
+ + + {#if extractedMetadata.length > 0} +
+

+ Extracted Metadata (from AsciiDoc header) +

+
+ {#each extractedMetadata as [key, value], i} +
+ {key}: + + +
+ {/each} +
+
+ {/if} +
{#each tags as [key, value], i}
@@ -525,6 +657,31 @@
{/if} - {/if} - -
+ {/if} + +
+ + {#if showWarning} +
+
+

Warning

+

{warningMessage}

+
+ + +
+
+
+ {/if} diff --git a/src/lib/components/ZettelEditor.svelte b/src/lib/components/ZettelEditor.svelte index 1934293..da96f74 100644 --- a/src/lib/components/ZettelEditor.svelte +++ b/src/lib/components/ZettelEditor.svelte @@ -2,22 +2,31 @@ import { Textarea, Button } from "flowbite-svelte"; import { EyeOutline } from "flowbite-svelte-icons"; import { - parseAsciiDocSections, - type ZettelSection, - } from "$lib/utils/ZettelParser"; - import asciidoctor from "asciidoctor"; + extractSmartMetadata, + parseAsciiDocWithMetadata, + type AsciiDocMetadata, + metadataToTags, +} from "$lib/utils/asciidoc_metadata"; +import asciidoctor from "asciidoctor"; // Component props let { content = "", placeholder = `== Note Title -:author: {author} // author is optional -:tags: tag1, tag2, tag3 // tags are optional +:author: Your Name +:version: 1.0 +:published_on: 2024-01-01 +:published_by: Alexandria +:summary: A brief description of this note +:tags: note, example, metadata +:image: https://example.com/image.jpg note content here... == Note Title 2 -:tags: tag1, tag2, tag3 +Some Other Author (this weeks even if there is no :author: attribute) +:keywords: second, note, example (keywords are converted to tags) +:description: This is a description of the note (description is converted to a summary tag) Note content here... `, showPreview = false, @@ -31,11 +40,52 @@ Note content here... onPreviewToggle?: (show: boolean) => void; }>(); - // Initialize AsciiDoctor processor - const asciidoctorProcessor = asciidoctor(); + // Parse sections for preview using the smart metadata service + let parsedSections = $derived.by(() => { + if (!content.trim()) return []; + + // Use smart metadata extraction that handles both document headers and section-only content + const { metadata: docMetadata } = extractSmartMetadata(content); + + // Parse the content using the standardized parser + const parsed = parseAsciiDocWithMetadata(content); + + // Debug logging + console.log("Parsed sections:", parsed.sections); + + return parsed.sections.map((section: { metadata: AsciiDocMetadata; content: string; title: string }) => { + // Use only section metadata for each section + // Don't combine with document metadata to avoid overriding section-specific metadata + const tags = metadataToTags(section.metadata); + + // Debug logging + console.log(`Section "${section.title}":`, { metadata: section.metadata, tags }); + + return { + title: section.title || "Untitled", + content: section.content.trim(), + tags, + }; + }); + }); - // Parse sections for preview - let parsedSections = $derived(parseAsciiDocSections(content, 2)); + // Check for 30040-style document headers (publication format) + let hasPublicationHeader = $derived.by(() => { + if (!content.trim()) return false; + + const lines = content.split(/\r?\n/); + for (const line of lines) { + // Check for document title (level 0 header) + if (line.match(/^=\s+(.+)$/)) { + return true; + } + // Check for "index card" format (case insensitive) + if (line.trim().toLowerCase() === 'index card') { + return true; + } + } + return false; + }); // Toggle preview panel function togglePreview() { @@ -51,12 +101,95 @@ Note content here...
+ + {#if hasPublicationHeader} +
+
+
+ + + +
+
+

+ Publication Format Detected +

+

+ You're using a publication format (document title with = or "index card"). + This editor is for individual notes only. Use the + Events form + to create structured publications. +

+
+ { + // Store the content in sessionStorage so it can be loaded in the Events form + sessionStorage.setItem('zettelEditorContent', content); + sessionStorage.setItem('zettelEditorSource', 'publication-format'); + }} + class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-800 border border-red-200 dark:border-red-700 rounded-md hover:bg-red-200 dark:hover:bg-red-700 transition-colors" + > + Switch to Publication Editor + + +
+
+
+
+ {:else} + +
+
+
+ + + +
+
+

+ Note-Taking Tool +

+

+ This editor is for creating individual notes (30041 events) only. Each section becomes a separate note event. + You can add metadata like author, version, publication date, summary, and tags using AsciiDoc attributes. + To create structured publications with a 30040 index event that ties multiple notes together, + use the Events form. +

+ +
+
+
+ {/if} +
- {#if showPreview} + {#if showPreview && !hasPublicationHeader}

- {@html asciidoctorProcessor.convert( + {@html asciidoctor().convert( `== ${section.title}\n\n${section.content}`, { standalone: false, @@ -119,33 +253,33 @@ Note content here... )}

- {#if index < parsedSections.length - 1} - -
- -
-
- {#if section.tags && section.tags.length > 0} - {#each section.tags as tag} -
- {tag[0]}: - {tag[1]} -
- {/each} - {:else} - No tags +
+ +
+
+ {#if section.tags && section.tags.length > 0} + {#each section.tags as tag} +
- {/if} -
+ {tag[0]}: + {tag[1]} +
+ {/each} + {:else} + No tags + {/if}
+
- + {#if index < parsedSections.length - 1} +
@@ -155,8 +289,8 @@ Note content here... Event Boundary
-
- {/if} + {/if} +
{/each}
@@ -169,7 +303,6 @@ Note content here... ? "s" : ""}
- Note: Currently only the first event will be published. {/if} diff --git a/src/lib/components/publications/PublicationFeed.svelte b/src/lib/components/publications/PublicationFeed.svelte index 8156cfe..44db458 100644 --- a/src/lib/components/publications/PublicationFeed.svelte +++ b/src/lib/components/publications/PublicationFeed.svelte @@ -1,7 +1,7 @@ @@ -81,16 +159,76 @@ - {#if publishResult} - {#if publishResult.success} + {#if publishResults} + {#if publishResults.successCount === publishResults.total} Success! - Event published successfully. Event ID: {publishResult.eventId} + {publishResults.successCount} events published. + {#if publishResults.successfulEvents.length > 0} +
+ Published events: +
+ {#each publishResults.successfulEvents as event} + {@const nevent = nip19.neventEncode({ id: event.eventId })} + + {/each} +
+
+ {/if}
{:else} - Error! - {publishResult.error} + Some events failed to publish. + {publishResults.successCount} of {publishResults.total} events published. + + {#if publishResults.successfulEvents.length > 0} +
+ Successfully published: +
+ {#each publishResults.successfulEvents as event} + {@const nevent = nip19.neventEncode({ id: event.eventId })} + + {/each} +
+
+ {/if} + + {#if publishResults.failedEvents.length > 0} +
+ Failed to publish: +
+ {#each publishResults.failedEvents as failedEvent, index} +
+
{failedEvent.title}
+
{failedEvent.error}
+ +
+ {/each} +
+
+ {/if}
{/if} {/if} diff --git a/src/routes/publication/[type]/[identifier]/+page.ts b/src/routes/publication/[type]/[identifier]/+page.ts index daa640a..1c00099 100644 --- a/src/routes/publication/[type]/[identifier]/+page.ts +++ b/src/routes/publication/[type]/[identifier]/+page.ts @@ -3,7 +3,7 @@ import type { PageLoad } from "./$types"; 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 }) => { +export const load: PageLoad = async ({ params }: { params: { type: string; identifier: string } }) => { const { type, identifier } = params; let indexEvent: NostrEvent | null; diff --git a/tests/unit/ZettelEditor.test.ts b/tests/unit/ZettelEditor.test.ts new file mode 100644 index 0000000..3490286 --- /dev/null +++ b/tests/unit/ZettelEditor.test.ts @@ -0,0 +1,429 @@ +import { describe, it, expect, vi, beforeEach, afterEach } 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 } + }; + }), + Button: vi.fn().mockImplementation((props) => { + return { + $$render: () => ``, + $$bind: { onclick: props.onclick } + }; + }) +})); + +vi.mock("flowbite-svelte-icons", () => ({ + EyeOutline: vi.fn().mockImplementation(() => ({ + $$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'); + }) + })) +})); + +// Mock sessionStorage +const mockSessionStorage = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), +}; +Object.defineProperty(global, 'sessionStorage', { + value: mockSessionStorage, + writable: true +}); + +// Mock window object for DOM manipulation +Object.defineProperty(global, 'window', { + value: { + sessionStorage: mockSessionStorage, + document: { + querySelector: vi.fn(), + createElement: vi.fn(), + } + }, + writable: true +}); + +// Mock DOM methods +const mockQuerySelector = vi.fn(); +const mockCreateElement = vi.fn(); +const mockAddEventListener = vi.fn(); +const mockRemoveEventListener = vi.fn(); + +Object.defineProperty(global, 'document', { + value: { + querySelector: mockQuerySelector, + createElement: mockCreateElement, + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + }, + writable: true +}); + +describe("ZettelEditor Component Logic", () => { + let mockOnContentChange: ReturnType; + let mockOnPreviewToggle: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + mockOnContentChange = vi.fn(); + mockOnPreviewToggle = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("Publication Format Detection Logic", () => { + it("should detect document header format", () => { + 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(); + }); + + 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') { + hasIndexCard = true; + break; + } + } + expect(hasIndexCard).toBe(true); + }); + + it("should not detect publication format for normal section 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; + for (const line of lines) { + if (line.match(/^=\s+(.+)$/)) { + hasPublicationHeader = true; + break; + } + if (line.trim().toLowerCase() === 'index card') { + hasPublicationHeader = true; + break; + } + } + expect(hasPublicationHeader).toBe(false); + }); + }); + + 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()); + 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()); + expect(sectionStrings).toHaveLength(1); + expect(sectionStrings[0]).toContain("== Section 1"); + }); + + it("should handle empty content", () => { + const content = ""; + const hasDocumentHeader = content.match(/^=\s+/m); + expect(hasDocumentHeader).toBeFalsy(); + }); + }); + + describe("Content Conversion Logic", () => { + it("should convert document title to section title", () => { + 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'); + + 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'); + + expect(finalContent).toBe("\n\n== Section 1\nContent"); + }); + + it("should clean up double newlines", () => { + 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'); + + 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"; + + // 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'); + }); + }); + + describe("Event Count Logic", () => { + it("should calculate correct event count for single section", () => { + 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"); + }); + + 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: [] } + ]; + const eventCount = sections.length; + const eventText = `${eventCount} event${eventCount !== 1 ? "s" : ""}`; + + expect(eventCount).toBe(2); + expect(eventText).toBe("2 events"); + }); + }); + + 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 tags = mockMetadataToTags(mockMetadata); + + expect(tags).toEqual([["author", "Test Author"]]); + expect(mockMetadataToTags).toHaveBeenCalledWith(mockMetadata); + }); + + 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([]); + }); + }); + + describe("AsciiDoctor Processing", () => { + 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'); + }); + + const content = "== Test Section\n\nThis is **bold** and *italic* text."; + const processedContent = mockConvert(content, { + standalone: false, + doctype: "article", + attributes: { + showtitle: true, + sectids: true, + }, + }); + + expect(processedContent).toContain('

Test Section

'); + expect(processedContent).toContain('bold'); + expect(processedContent).toContain('italic'); + }); + }); + + describe("Error Handling", () => { + it("should handle parsing errors gracefully", () => { + // Mock a function that might throw an error + const mockParseFunction = vi.fn().mockImplementation(() => { + throw new Error("Parsing error"); + }); + + const content = "== Section 1\nContent 1"; + + // Should not throw error when called + expect(() => { + try { + mockParseFunction(content); + } catch (error) { + // Expected error, but should be handled gracefully + } + }).not.toThrow(); + }); + + it("should handle empty content without errors", () => { + const content = ""; + const hasDocumentHeader = content.match(/^=\s+/m); + expect(hasDocumentHeader).toBeFalsy(); + }); + }); + + describe("Component Props Interface", () => { + it("should have correct prop types", () => { + // Test that the component props interface is correctly defined + const expectedProps = { + content: "", + placeholder: "Default placeholder", + showPreview: false, + 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'); + }); + }); + + describe("Utility Function Integration", () => { + it("should integrate with ZettelParser utilities", () => { + // Mock the parseAsciiDocSections function + const mockParseAsciiDocSections = vi.fn().mockReturnValue([ + { 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"); + }); + + it("should integrate with asciidoc_metadata utilities", () => { + // Mock the utility functions + const mockExtractDocumentMetadata = vi.fn().mockReturnValue({ + metadata: { title: "Document Title" } as AsciiDocMetadata, + content: "Document content" + }); + + const mockExtractSectionMetadata = vi.fn().mockReturnValue({ + metadata: { title: "Section Title" } as AsciiDocMetadata, + content: "Section content", + 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"); + }); + }); + + describe("Content Validation", () => { + it("should validate content structure", () => { + 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()); + + 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"; + + // 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()); + expect(sections.length).toBeGreaterThan(0); + }); + }); + + 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'); + + expect(convertedContent).toBe("== Title\n\n== Section\nContent"); + }); + + it("should handle edge cases in string manipulation", () => { + const edgeCases = [ + "= Title\n\n\n== Section\nContent", // Multiple newlines + "index card\n\n== Section\nContent", // Index card + "= Title\nindex card\n== Section\nContent", // Both + ]; + + edgeCases.forEach(content => { + const converted = content + .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'); + }); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/eventInput30040.test.ts b/tests/unit/eventInput30040.test.ts new file mode 100644 index 0000000..c7dadc3 --- /dev/null +++ b/tests/unit/eventInput30040.test.ts @@ -0,0 +1,446 @@ +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"; + +// Mock NDK and other dependencies +vi.mock("@nostr-dev-kit/ndk", () => ({ + NDKEvent: vi.fn().mockImplementation((ndk, eventData) => ({ + ...eventData, + id: "mock-event-id", + sig: "mock-signature", + kind: eventData.kind, + content: eventData.content, + tags: eventData.tags, + pubkey: eventData.pubkey, + created_at: eventData.created_at, + })), +})); + +vi.mock("../../src/lib/ndk", () => ({ + ndkInstance: { + subscribe: vi.fn(), + }, + getNdk: vi.fn(() => ({})), +})); + +vi.mock("svelte/store", () => ({ + get: vi.fn(() => ({})), +})); + +describe("EventInput 30040 Publishing", () => { + const baseEvent = { + pubkey: "test-pubkey", + created_at: 1234567890, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("Normal Structure with Preamble", () => { + it("should build 30040 event set with preamble content", () => { + const content = `= Test Document with Preamble +John Doe +1.0, 2024-01-15, Alexandria Test +:summary: This is a test document with preamble +:keywords: test, preamble, asciidoc + +This is the preamble content that should be included. + +== First Section +:author: Section Author +:summary: This is the first section + +This is the content of the first section. + +== Second Section +:summary: This is the second section + +This is the content of the second section.`; + + const tags: [string, string][] = [["type", "article"]]; + + 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(["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(["t", "test"]); + expect(indexEvent.tags).toContainEqual(["t", "preamble"]); + expect(indexEvent.tags).toContainEqual(["t", "asciidoc"]); + expect(indexEvent.tags).toContainEqual(["type", "article"]); + + // 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-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"]); + + // 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].tags).toContainEqual(["title", "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"]); + }); + }); + + describe("Normal Structure without Preamble", () => { + it("should build 30040 event set without preamble content", () => { + const content = `= Test Document without Preamble +:summary: This is a test document without preamble +:keywords: test, no-preamble, asciidoc + +== First Section +:author: Section Author +:summary: This is the first section + +This is the content of the first section. + +== Second Section +:summary: This is the second section + +This is the content of the second section.`; + + const tags: [string, string][] = [["type", "article"]]; + + 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"]); + + // 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].tags).toContainEqual(["title", "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].tags).toContainEqual(["title", "Second Section"]); + expect(sectionEvents[1].tags).toContainEqual(["summary", "This is the second section"]); + }); + }); + + describe("Skeleton Structure with Preamble", () => { + it("should build 30040 event set with skeleton structure and preamble", () => { + const content = `= Skeleton Document with Preamble +:summary: This is a skeleton document with preamble +:keywords: skeleton, preamble, empty + +This is the preamble content. + +== Empty Section 1 + +== Empty Section 2 + +== Empty Section 3`; + + const tags: [string, string][] = [["type", "skeleton"]]; + + 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"]); + + // Test section events + expect(sectionEvents).toHaveLength(3); + + // All sections should have empty content + sectionEvents.forEach((section, index) => { + 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}`]); + }); + }); + }); + + describe("Skeleton Structure without Preamble", () => { + it("should build 30040 event set with skeleton structure without preamble", () => { + const content = `= Skeleton Document without Preamble +:summary: This is a skeleton document without preamble +:keywords: skeleton, no-preamble, empty + +== Empty Section 1 + +== Empty Section 2 + +== Empty Section 3`; + + const tags: [string, string][] = [["type", "skeleton"]]; + + 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"]); + + // Test section events + expect(sectionEvents).toHaveLength(3); + + // All sections should have empty content + sectionEvents.forEach((section, index) => { + 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}`]); + }); + }); + }); + + describe("Index Card Format", () => { + it("should build 30040 event set for index card format", () => { + const content = `= Test Index Card +index card`; + + const tags: [string, string][] = [["type", "index-card"]]; + + 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"]); + expect(indexEvent.tags).toContainEqual(["title", "Test Index Card"]); + expect(indexEvent.tags).toContainEqual(["type", "index-card"]); + + // Should have no section events for index card + expect(sectionEvents).toHaveLength(0); + }); + + it("should build 30040 event set for index card with metadata", () => { + const content = `= Test Index Card with Metadata +:summary: This is an index card with metadata +:keywords: index, card, metadata +index card`; + + const tags: [string, string][] = [["type", "index-card"]]; + + 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(["t", "index"]); + expect(indexEvent.tags).toContainEqual(["t", "card"]); + expect(indexEvent.tags).toContainEqual(["t", "metadata"]); + expect(indexEvent.tags).toContainEqual(["type", "index-card"]); + + // Should have no section events for index card + expect(sectionEvents).toHaveLength(0); + }); + }); + + describe("Complex Metadata Structures", () => { + it("should handle complex metadata with all attribute types", () => { + const content = `= Complex Metadata Document +Jane Smith +2.0, 2024-02-20, Alexandria Complex +:summary: This is a complex document with all metadata types +:description: Alternative description field +:keywords: complex, metadata, all-types +:tags: additional, tags, here +:author: Override Author +:author: Third Author +:version: 3.0 +:published_on: 2024-03-01 +:published_by: Alexandria Complex +:type: book +:image: https://example.com/cover.jpg +:isbn: 978-0-123456-78-9 +:source: https://github.com/alexandria/complex +:auto-update: yes + +This is the preamble content. + +== Section with Complex Metadata +:author: Section Author +:author: Section Co-Author +:summary: This section has complex metadata +:description: Alternative description for section +:keywords: section, complex, metadata +:tags: section, tags +:type: chapter +:image: https://example.com/section-image.jpg + +This is the section content.`; + + const tags: [string, string][] = [["type", "complex"]]; + + 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(["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(["published_on", "2024-03-01"]); + 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(["i", "978-0-123456-78-9"]); + 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"]); + expect(indexEvent.tags).toContainEqual(["t", "all-types"]); + expect(indexEvent.tags).toContainEqual(["t", "additional"]); + expect(indexEvent.tags).toContainEqual(["t", "tags"]); + expect(indexEvent.tags).toContainEqual(["t", "here"]); + + // Test section metadata + 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(["type", "chapter"]); + 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"]); + expect(sectionEvents[0].tags).toContainEqual(["t", "tags"]); + }); + }); + + describe("Validation Tests", () => { + it("should validate normal structure correctly", () => { + const content = `= Valid Document +:summary: This is a valid document + +== Section 1 + +Content here. + +== Section 2 + +More content.`; + + const validation = validate30040EventSet(content); + expect(validation.valid).toBe(true); + }); + + it("should validate index card format correctly", () => { + const content = `= Valid Index Card +index card`; + + const validation = validate30040EventSet(content); + expect(validation.valid).toBe(true); + }); + + it("should validate skeleton structure correctly", () => { + const content = `= Skeleton Document + +== Empty Section 1 + +== Empty Section 2`; + + const validation = validate30040EventSet(content); + expect(validation.valid).toBe(true); + }); + + it("should reject invalid structure", () => { + const content = `This is not a valid AsciiDoc document.`; + + const validation = validate30040EventSet(content); + expect(validation.valid).toBe(false); + expect(validation.reason).toContain("30040 events must have a document title"); + }); + }); + + describe("Edge Cases", () => { + it("should handle document with only title and no sections", () => { + const content = `= Document with No Sections +:summary: This document has no sections + +This is just preamble content.`; + + const tags: [string, string][] = []; + + 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(sectionEvents).toHaveLength(0); + }); + + it("should handle document with special characters in title", () => { + const content = `= Document with Special Characters: Test & More! +:summary: This document has special characters in the title + +== Section 1 + +Content here.`; + + const tags: [string, string][] = []; + + 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(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 +:summary: This document has a very long title + +== Section 1 + +Content here.`; + + const tags: [string, string][] = []; + + 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(sectionEvents).toHaveLength(1); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/metadataExtraction.test.ts b/tests/unit/metadataExtraction.test.ts new file mode 100644 index 0000000..65a50b8 --- /dev/null +++ b/tests/unit/metadataExtraction.test.ts @@ -0,0 +1,322 @@ +import { describe, it, expect } from "vitest"; +import { + extractDocumentMetadata, + extractSectionMetadata, + parseAsciiDocWithMetadata, + metadataToTags, + extractSmartMetadata +} from "../../src/lib/utils/asciidoc_metadata.ts"; + +describe("AsciiDoc Metadata Extraction", () => { + const testContent = `= Test Document with Metadata +John Doe +1.0, 2024-01-15: Alexandria Test +:summary: This is a test document for metadata extraction +:author: Jane Smith +:published_on: 2024-01-15 +:published_by: Alexandria Project +:type: article +:keywords: test, metadata, asciidoc +:image: https://example.com/cover.jpg +:isbn: 978-0-123456-78-9 +:source: https://github.com/alexandria/test +:auto-update: yes + +This is the preamble content that should be included in the document body. + +== First Section +:author: Section Author +:summary: This is the first section +:keywords: section1, content + +This is the content of the first section. + +== Second Section +:summary: This is the second section +:type: chapter + +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.authors).toEqual(["John Doe", "Jane Smith"]); + expect(metadata.type).toBe("article"); + expect(metadata.tags).toEqual(["test", "metadata", "asciidoc"]); + expect(metadata.coverImage).toBe("https://example.com/cover.jpg"); + 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"); + expect(content).not.toContain("= Test Document with Metadata"); + expect(content).not.toContain(":summary:"); + }); + + it("extractSectionMetadata should extract section metadata correctly", () => { + const sectionContent = `== First Section +:author: Section Author +:description: This is the first section +:tags: section1, content + +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"); + expect(metadata.tags).toEqual(["section1", "content"]); + expect(content).toBe("This is the content of the first section."); + }); + + it("extractSectionMetadata should extract standalone author names and remove them from content", () => { + const sectionContent = `== Section Header1 +Stella +:description: Some summary + +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("Some context text"); + }); + + it("extractSectionMetadata should handle multiple standalone author names", () => { + const sectionContent = `== Section Header1 +Stella +:author: John Doe +:description: Some summary + +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"); + expect(content.trim()).toBe("Some context text"); + }); + + it("extractSectionMetadata should not extract non-author lines as authors", () => { + const sectionContent = `== Section Header1 +Stella +This is not an author line +:description: Some summary + +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"); + }); + + 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"); + }); + + it("metadataToTags should convert metadata to Nostr tags", () => { + const metadata = { + title: "Test Title", + authors: ["Author 1", "Author 2"], + version: "1.0", + summary: "Test summary", + 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"]); + expect(tags).toContainEqual(["version", "1.0"]); + expect(tags).toContainEqual(["summary", "Test summary"]); + expect(tags).toContainEqual(["t", "tag1"]); + expect(tags).toContainEqual(["t", "tag2"]); + }); + + it("should handle index card format correctly", () => { + const indexCardContent = `= Test Index Card +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(""); + }); + + it("should handle keywords as tags", () => { + const contentWithKeywords = `= Test Document +:keywords: keyword1, keyword2, keyword3 + +Some content here.`; + + const { metadata } = extractDocumentMetadata(contentWithKeywords); + + expect(metadata.tags).toEqual(["keyword1", "keyword2", "keyword3"]); + }); + + it("should handle both tags and keywords", () => { + const contentWithBoth = `= Test Document +:tags: tag1, tag2 +:keywords: keyword1, keyword2 + +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"]); + }); + + it("should handle tags only", () => { + const contentWithTags = `= Test Document +:tags: tag1, tag2, tag3 + +Content here.`; + + const { metadata } = extractDocumentMetadata(contentWithTags); + + expect(metadata.tags).toEqual(["tag1", "tag2", "tag3"]); + }); + + it("should handle both summary and description", () => { + const contentWithSummary = `= Test Document +:summary: This is a summary + +Content here.`; + + const contentWithDescription = `= Test Document +:description: This is a description + +Content here.`; + + 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', () => { + const sectionOnlyContent = `== First Section +:author: Section Author +:description: This is the first section +:tags: section1, content + +This is the content of the first section. + +== Second Section +:summary: This is the second section +:type: chapter + +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'); + + // 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', () => { + const minimalDocumentHeader = `= Test Document + +== First Section +:author: Section Author +:description: This is the first section + +This is the content of the first section. + +== Second Section +:summary: This is the second section +:type: chapter + +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'); + + // 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'); + }); + + it('should handle document with full header correctly', () => { + const documentWithHeader = `= Test Document +John Doe +1.0, 2024-01-15: Alexandria Test +:summary: This is a test document +:author: Jane Smith + +== First Section +:author: Section Author +:description: This is the first section + +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'); + + // 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'); + }); + }); +}); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index fc4ccc3..82206c3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -43,4 +43,16 @@ export default defineConfig({ // Expose the app version as a global variable "import.meta.env.APP_VERSION": JSON.stringify(getAppVersionString()), }, + optimizeDeps: { + esbuildOptions: { + define: { + global: 'globalThis', + }, + }, + }, + server: { + fs: { + allow: ['..'], + }, + }, });