From d55085d02ea51e60a4e8c27206c6ba0fcd7958eb Mon Sep 17 00:00:00 2001 From: Silberengel Date: Fri, 6 Feb 2026 18:23:07 +0100 Subject: [PATCH] advanced markup editing (asciidoc and markdown) publishing utilities refined search --- package-lock.json | 395 +++++++++++++++ package.json | 11 + public/healthz.json | 4 +- .../find/SearchAddressableEvents.svelte | 297 +++++++++++- .../components/layout/UnifiedSearch.svelte | 255 +++++++++- .../components/write/AdvancedEditor.svelte | 378 +++++++++++++++ .../components/write/CreateEventForm.svelte | 457 +++++------------- src/lib/components/write/EditEventForm.svelte | 95 +++- src/lib/modules/comments/CommentForm.svelte | 12 +- src/lib/types/kind-metadata.ts | 420 ++++++++++++++++ src/lib/utils/nostr-link-processor.ts | 124 +++++ src/routes/find/+page.svelte | 233 +++++++-- 12 files changed, 2270 insertions(+), 411 deletions(-) create mode 100644 src/lib/components/write/AdvancedEditor.svelte create mode 100644 src/lib/types/kind-metadata.ts create mode 100644 src/lib/utils/nostr-link-processor.ts diff --git a/package-lock.json b/package-lock.json index b1a5071..b15ba7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,21 @@ "version": "0.2.0", "license": "MIT", "dependencies": { + "@codemirror/autocomplete": "^6.20.0", + "@codemirror/basic-setup": "^0.20.0", + "@codemirror/commands": "^6.10.1", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/language": "^6.12.1", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.5.4", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.39.12", + "@lezer/markdown": "^1.6.3", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", "@tanstack/svelte-virtual": "^3.0.0", "asciidoctor": "3.0.x", + "codemirror-asciidoc": "^2.0.1", "dompurify": "^3.0.6", "emoji-picker-element": "^1.28.1", "highlight.js": "^11.11.1", @@ -178,6 +189,293 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", + "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/basic-setup": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/basic-setup/-/basic-setup-0.20.0.tgz", + "integrity": "sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg==", + "deprecated": "In version 6.0, this package has been renamed to just 'codemirror'", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^0.20.0", + "@codemirror/commands": "^0.20.0", + "@codemirror/language": "^0.20.0", + "@codemirror/lint": "^0.20.0", + "@codemirror/search": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/autocomplete": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-0.20.3.tgz", + "integrity": "sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/commands": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.20.0.tgz", + "integrity": "sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/language": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz", + "integrity": "sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0", + "@lezer/highlight": "^0.16.0", + "@lezer/lr": "^0.16.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/search": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.20.1.tgz", + "integrity": "sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/state": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz", + "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==", + "license": "MIT" + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/view": { + "version": "0.20.7", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz", + "integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^0.20.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/common": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz", + "integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==", + "license": "MIT" + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/highlight": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz", + "integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/lr": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz", + "integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.1.tgz", + "integrity": "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", + "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript/node_modules/@codemirror/lint": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.3.tgz", + "integrity": "sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", + "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-0.20.3.tgz", + "integrity": "sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.2", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/lint/node_modules/@codemirror/state": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz", + "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==", + "license": "MIT" + }, + "node_modules/@codemirror/lint/node_modules/@codemirror/view": { + "version": "0.20.7", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz", + "integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^0.20.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.39.12", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.12.tgz", + "integrity": "sha512-f+/VsHVn/kOA9lltk/GFzuYwVVAKmOnNjxbrhkk3tPHntFqjWeI2TbIXx006YkBkqC10wZ4NsnWXCQiFPeAISQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", @@ -772,6 +1070,79 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz", + "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@noble/ciphers": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", @@ -2077,6 +2448,12 @@ "node": ">=6" } }, + "node_modules/codemirror-asciidoc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/codemirror-asciidoc/-/codemirror-asciidoc-2.0.1.tgz", + "integrity": "sha512-h6Xhj+ZsWh/DTNE3xMfRv9edufchsVVwPED7wSGMeEdoYk/UtCZmwRGH0ZZQkr43aNVF3tWGLZJGT+cAeYgUIg==", + "license": "BSD" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2131,6 +2508,12 @@ "node": ">= 0.6" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4531,6 +4914,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -5082,6 +5471,12 @@ "node": ">=0.10.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 16ec1f4..1f35947 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,21 @@ "format": "prettier --write ." }, "dependencies": { + "@codemirror/autocomplete": "^6.20.0", + "@codemirror/basic-setup": "^0.20.0", + "@codemirror/commands": "^6.10.1", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/language": "^6.12.1", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.5.4", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.39.12", + "@lezer/markdown": "^1.6.3", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", "@tanstack/svelte-virtual": "^3.0.0", "asciidoctor": "3.0.x", + "codemirror-asciidoc": "^2.0.1", "dompurify": "^3.0.6", "emoji-picker-element": "^1.28.1", "highlight.js": "^11.11.1", diff --git a/public/healthz.json b/public/healthz.json index 2be9069..59dd86e 100644 --- a/public/healthz.json +++ b/public/healthz.json @@ -2,7 +2,7 @@ "status": "ok", "service": "aitherboard", "version": "0.2.0", - "buildTime": "2026-02-06T16:38:51.661Z", + "buildTime": "2026-02-06T17:22:41.111Z", "gitCommit": "unknown", - "timestamp": 1770395931661 + "timestamp": 1770398561111 } \ No newline at end of file diff --git a/src/lib/components/find/SearchAddressableEvents.svelte b/src/lib/components/find/SearchAddressableEvents.svelte index ac22a38..9fc646f 100644 --- a/src/lib/components/find/SearchAddressableEvents.svelte +++ b/src/lib/components/find/SearchAddressableEvents.svelte @@ -1,6 +1,7 @@ - {#if results.length > 0} + {#if cacheResults.length > 0 || results.length > 0}
-

Results ({results.length})

-
+ {#if cacheResults.length > 0} +

Found in Cache: ({cacheResults.length})

+
+ {#each cacheResults as { event, matchedTag, matchedValue, relevance, relay }} +
handleResultClick(event)} + onkeydown={(e) => handleResultKeydown(e, event)} + > + {#if getImageTag(event)} +
+ { (e.target as HTMLImageElement).style.display = 'none'; }} /> +
+ {/if} + +
+
+
+ + +
+ {getKindInfo(event.kind).description} +
+ + + + +
+
+ {/each} +
+ {/if} + + {#if results.length > 0} +

Results from Relays ({results.length})

+
{#each results as { event, matchedTag, matchedValue, relevance, relay }}
{/each} -
+
+ {/if}
- {:else if !searching && searchQuery.trim()} + {:else if !searching && searchQuery.trim() && cacheResults.length === 0}
No addressable events found matching "{searchQuery}"
diff --git a/src/lib/components/layout/UnifiedSearch.svelte b/src/lib/components/layout/UnifiedSearch.svelte index 65ddb0b..8946aff 100644 --- a/src/lib/components/layout/UnifiedSearch.svelte +++ b/src/lib/components/layout/UnifiedSearch.svelte @@ -1,7 +1,7 @@ + + + + diff --git a/src/lib/components/write/CreateEventForm.svelte b/src/lib/components/write/CreateEventForm.svelte index c80d5f0..b44d641 100644 --- a/src/lib/components/write/CreateEventForm.svelte +++ b/src/lib/components/write/CreateEventForm.svelte @@ -9,31 +9,16 @@ import MarkdownRenderer from '../content/MarkdownRenderer.svelte'; import MediaAttachments from '../content/MediaAttachments.svelte'; import RichTextEditor from '../content/RichTextEditor.svelte'; + import AdvancedEditor from './AdvancedEditor.svelte'; import { shouldIncludeClientTag } from '../../services/client-tag-preference.js'; import { goto } from '$app/navigation'; import { KIND } from '../../types/kind-lookup.js'; + import { getKindMetadata, getWritableKinds } from '../../types/kind-metadata.js'; import type { NostrEvent } from '../../types/nostr.js'; import { autoExtractTags, ensureDTagForParameterizedReplaceable } from '../../services/auto-tagging.js'; import { isParameterizedReplaceableKind } from '../../types/kind-lookup.js'; - const SUPPORTED_KINDS = [ - { value: 1, label: '1 - Short Text Note' }, - { value: 11, label: '11 - Discussion Thread' }, - { value: 20, label: '20 - Picture Note' }, - { value: 21, label: '21 - Video Note' }, - { value: 22, label: '22 - Short Video Note' }, - { value: 24, label: '24 - Public Message' }, - { value: 1068, label: '1068 - Poll' }, - { value: 1222, label: '1222 - Voice Note' }, - { value: 9802, label: '9802 - Highlighted Article' }, - { value: 10895, label: '10895 - RSS Feed' }, - { value: 30023, label: '30023 - Long-form Note' }, - { value: 30040, label: '30040 - Event Index (metadata-only)' }, - { value: 30041, label: '30041 - AsciiDoc' }, - { value: 30817, label: '30817 - AsciiDoc' }, - { value: 30818, label: '30818 - AsciiDoc' }, - { value: -1, label: 'Unknown Kind' } - ]; + const SUPPORTED_KINDS = getWritableKinds(); interface Props { initialKind?: number | null; @@ -109,9 +94,27 @@ let showJsonModal = $state(false); let showPreviewModal = $state(false); let showExampleModal = $state(false); + let showAdvancedEditor = $state(false); let richTextEditorRef: { clearUploadedFiles: () => void; getUploadedFiles: () => Array<{ url: string; imetaTag: string[] }> } | null = $state(null); let uploadedFiles: Array<{ url: string; imetaTag: string[] }> = $state([]); let eventJson = $state('{}'); + + const isUnknownKind = $derived(selectedKind === -1); + const effectiveKind = $derived(isUnknownKind ? (parseInt(customKindId) || 1) : selectedKind); + + // Determine editor mode based on selected kind + const editorMode = $derived( + effectiveKind === 30818 || effectiveKind === 30041 ? 'asciidoc' : 'markdown' + ); + + // Show advanced editor button when editing (has initial content) or for AsciiDoc kinds + const showAdvancedEditorButton = $derived( + (propInitialContent !== null && propInitialContent !== undefined) || + effectiveKind === 30818 || + effectiveKind === 30041 || + effectiveKind === 30023 || // Long-form note (markdown) + effectiveKind === 1 // Short text note (markdown) + ); // Sync selectedKind when initialKind prop changes $effect(() => { @@ -140,331 +143,27 @@ // Clear content for metadata-only kinds $effect(() => { - if (selectedKind === 30040 || selectedKind === 10895) { + const metadata = getKindMetadata(selectedKind); + if (metadata.requiresContent === false) { content = ''; } }); - const isKind30040 = $derived(selectedKind === 30040); - const isKind10895 = $derived(selectedKind === 10895); - const isUnknownKind = $derived(selectedKind === -1); - const effectiveKind = $derived(isUnknownKind ? (parseInt(customKindId) || 1) : selectedKind); - - function getKindHelpText(kind: number): { description: string; suggestedTags: string[] } { - switch (kind) { - case 1: - return { - description: 'A simple plaintext note (NIP-10). The content property contains some human-readable text.', - suggestedTags: ['e (event references)', 'p (pubkey mentions)', 'q (quoted events)', 't (hashtags)'] - }; - case 11: - return { - description: 'A thread (NIP-7D). A thread is a kind 11 event. Threads SHOULD include a title tag. Replies use kind 1111 comments (NIP-22).', - suggestedTags: ['title (required)', 't (topics/hashtags)'] - }; - case 20: - return { - description: 'Picture-first feeds (NIP-68). Event kind 20 for picture-first clients. Images must be self-contained. They are hosted externally and referenced using imeta tags.', - suggestedTags: ['title', 'imeta (url, m, blurhash, dim, alt, x, fallback)', 'p (tagged users)', 'm (media type)', 'x (image hash)', 't (hashtags)', 'location', 'g (geohash)', 'L/l (language)', 'content-warning'] - }; - case 21: - return { - description: 'Video Events (NIP-71). Normal videos representing a dedicated post of externally hosted content. The content is a summary or description on the video content.', - suggestedTags: ['title (required)', 'imeta (url, dim, m, image, fallback, service, bitrate, duration)', 'published_at', 'text-track', 'content-warning', 'alt', 'segment', 't (hashtags)', 'p (participants)', 'r (web references)'] - }; - case 22: - return { - description: 'Video Events (NIP-71). Short videos (stories/reels style) representing a dedicated post of externally hosted content. The content is a summary or description on the video content.', - suggestedTags: ['title (required)', 'imeta (url, dim, m, image, fallback, service, bitrate, duration)', 'published_at', 'text-track', 'content-warning', 'alt', 'segment', 't (hashtags)', 'p (participants)', 'r (web references)'] - }; - case 24: - return { - description: 'Public Messages (NIP-A4). A simple plaintext message to one or more Nostr users. The content contains the message. p tags identify one or more receivers. Designed to be shown and replied to from notification screens.', - suggestedTags: ['p (receiver pubkeys, required)', 'expiration (recommended)', 'q (quoted events)', 'imeta (for image/video links)', 't (hashtags)'] - }; - case 1068: - return { - description: 'Polls (NIP-88). The poll event is defined as a kind 1068 event. The content key holds the label for the poll.', - suggestedTags: ['option (optionId, label)', 'relay (one or more)', 'polltype (singlechoice|multiplechoice)', 'endsAt (unix timestamp)'] - }; - case 1222: - return { - description: 'Voice Messages (NIP-A0). Root messages for short voice messages, typically up to 60 seconds in length. Content MUST be a URL pointing directly to an audio file (audio/mp4 recommended).', - suggestedTags: ['imeta (with url, waveform, duration)', 't (hashtags)', 'g (geohash)'] - }; - case 9802: - return { - description: 'Highlights (NIP-84). A highlight event to signal content a user finds valuable. The content of these events is the highlighted portion of the text.', - suggestedTags: ['a (addressable event)', 'e (event reference)', 'r (URL reference)', 'p (author pubkeys)', 'context (surrounding text)', 'comment (for quote highlights)'] - }; - case 10895: - return { - description: 'RSS Feed subscription event. Lists external RSS feeds to subscribe to. Content should be empty.', - suggestedTags: ['u (RSS feed URL, repeat for multiple feeds)'] - }; - case 30023: - return { - description: 'Long-form Content (NIP-23). Long-form text content, generally referred to as "articles" or "blog posts". The content should be a string text in Markdown syntax. Include a d tag for editability.', - suggestedTags: ['d (required for editability)', 'title', 'image', 'summary', 'published_at', 't (hashtags)'] - }; - case 30040: - return { - description: 'Publication Index (NKBIP-01). A publication index defines the structure and metadata of a publication. The content field MUST be empty.', - suggestedTags: ['d (required)', 'title (required)', 'a (referenced events)', 'auto-update (yes|ask|no)', 'p (original author)', 'E (original event)', 'source', 'version', 'type', 'author', 'i (ISBN)', 't (hashtags)', 'published_on', 'published_by', 'image', 'summary'] - }; - case 30041: - return { - description: 'Publication Content (NKBIP-01). Also known as sections, zettels, episodes, or chapters contain the actual content that makes up a publication. The content field MUST contain text meant for display to the end user and MAY contain AsciiDoc markup.', - suggestedTags: ['d (required)', 'title (required)', 'wikilink'] - }; - case 30817: - return { - description: 'An AsciiDoc article. Similar to 30818 but may have different conventions.', - suggestedTags: ['d (identifier)', 'title', 'summary', 'a (addressable event)', 'e (event reference)'] - }; - case 30818: - return { - description: 'Wiki (NIP-54). Descriptions (or encyclopedia entries) of particular subjects. Articles are identified by lowercase, normalized d tags. The content should be Asciidoc with wikilinks and nostr:... links.', - suggestedTags: ['d (required, normalized)', 'title', 'summary', 'a (addressable event)', 'e (event reference)'] - }; - default: - return { - description: `Custom kind ${kind}. Refer to the relevant NIP specification for tag requirements.`, - suggestedTags: [] - }; - } - } - - const helpText = $derived(getKindHelpText(effectiveKind)); - - function getExampleJSON(kind: number): string { + const kindMetadata = $derived(getKindMetadata(effectiveKind)); + const helpText = $derived(kindMetadata.helpText); + + function getExampleJSON(): string { const examplePubkey = '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d'; const exampleEventId = '67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446'; const exampleRelay = 'wss://relay.example.com'; const timestamp = Math.floor(Date.now() / 1000); - - switch (kind) { - case 1: - return JSON.stringify({ - kind: 1, - pubkey: examplePubkey, - created_at: timestamp, - content: 'Hello nostr! This is a simple text note.', - tags: [ - ['e', exampleEventId, exampleRelay], - ['p', examplePubkey], - ['t', 'nostr'] - ], - id: '...', - sig: '...' - }, null, 2); - case 11: - return JSON.stringify({ - kind: 11, - pubkey: examplePubkey, - created_at: timestamp, - content: 'This is a discussion thread about a topic.', - tags: [ - ['title', 'Discussion Thread Title'], - ['t', 'topic1'], - ['t', 'topic2'] - ], - id: '...', - sig: '...' - }, null, 2); - case 9802: - return JSON.stringify({ - kind: 9802, - pubkey: examplePubkey, - created_at: timestamp, - content: 'This is the highlighted text portion.', - tags: [ - ['e', exampleEventId, exampleRelay], - ['p', examplePubkey, '', 'author'], - ['context', 'This is the highlighted text portion within the surrounding context text...'] - ], - id: '...', - sig: '...' - }, null, 2); - case 1222: - return JSON.stringify({ - kind: 1222, - pubkey: examplePubkey, - created_at: timestamp, - content: 'https://example.com/audio/voice-message.m4a', - tags: [ - ['imeta', 'url https://example.com/audio/voice-message.m4a', 'waveform 0 7 35 8 100 100 49', 'duration 8'], - ['t', 'voice'] - ], - id: '...', - sig: '...' - }, null, 2); - case 20: - return JSON.stringify({ - kind: 20, - pubkey: examplePubkey, - created_at: timestamp, - content: 'A beautiful sunset photo', - tags: [ - ['title', 'Sunset Photo'], - ['imeta', 'url https://nostr.build/i/image.jpg', 'm image/jpeg', 'dim 3024x4032', 'alt A scenic sunset'], - ['t', 'photography'], - ['location', 'San Francisco, CA'] - ], - id: '...', - sig: '...' - }, null, 2); - case 21: - return JSON.stringify({ - kind: 21, - pubkey: examplePubkey, - created_at: timestamp, - content: 'A detailed video about Nostr protocol', - tags: [ - ['title', 'Introduction to Nostr'], - ['imeta', 'url https://example.com/video.mp4', 'dim 1920x1080', 'm video/mp4', 'duration 300', 'bitrate 3000000'], - ['published_at', timestamp.toString()], - ['t', 'tutorial'] - ], - id: '...', - sig: '...' - }, null, 2); - case 22: - return JSON.stringify({ - kind: 22, - pubkey: examplePubkey, - created_at: timestamp, - content: 'Quick video update', - tags: [ - ['title', 'Quick Update'], - ['imeta', 'url https://example.com/short.mp4', 'dim 1080x1920', 'm video/mp4', 'duration 15'], - ['published_at', timestamp.toString()] - ], - id: '...', - sig: '...' - }, null, 2); - case 30023: - return JSON.stringify({ - kind: 30023, - pubkey: examplePubkey, - created_at: timestamp, - content: '# Long-form Article\n\nThis is a long-form article written in Markdown...', - tags: [ - ['d', 'article-slug'], - ['title', 'My Long-form Article'], - ['summary', 'A brief summary of the article'], - ['published_at', timestamp.toString()], - ['t', 'article'], - ['t', 'longform'] - ], - id: '...', - sig: '...' - }, null, 2); - case 30818: - return JSON.stringify({ - kind: 30818, - pubkey: examplePubkey, - created_at: timestamp, - content: '= Wiki Article\n\nThis is a wiki article written in AsciiDoc.', - tags: [ - ['d', 'wiki-article'], - ['title', 'Wiki Article'], - ['summary', 'A brief summary'] - ], - id: '...', - sig: '...' - }, null, 2); - case 30817: - return JSON.stringify({ - kind: 30817, - pubkey: examplePubkey, - created_at: timestamp, - content: '= AsciiDoc Document\n\nContent in AsciiDoc format...', - tags: [ - ['d', 'asciidoc-doc'], - ['title', 'AsciiDoc Document'] - ], - id: '...', - sig: '...' - }, null, 2); - case 30041: - return JSON.stringify({ - kind: 30041, - pubkey: examplePubkey, - created_at: timestamp, - content: '= Chapter Title\n\nChapter content with [[wikilinks]]...', - tags: [ - ['d', 'publication-chapter-1'], - ['title', 'Chapter 1: Introduction'] - ], - id: '...', - sig: '...' - }, null, 2); - case 30040: - return JSON.stringify({ - kind: 30040, - pubkey: examplePubkey, - created_at: timestamp, - content: '', - tags: [ - ['d', 'publication-slug'], - ['title', 'My Publication'], - ['author', 'Author Name'], - ['summary', 'Publication summary'], - ['type', 'book'], - ['a', '30041:' + examplePubkey + ':chapter-1', exampleRelay], - ['a', '30041:' + examplePubkey + ':chapter-2', exampleRelay], - ['auto-update', 'ask'] - ], - id: '...', - sig: '...' - }, null, 2); - case 1068: - return JSON.stringify({ - kind: 1068, - pubkey: examplePubkey, - created_at: timestamp, - content: 'What is your favorite color?', - tags: [ - ['option', 'opt1', 'Red'], - ['option', 'opt2', 'Blue'], - ['option', 'opt3', 'Green'], - ['relay', exampleRelay], - ['polltype', 'singlechoice'], - ['endsAt', (timestamp + 86400).toString()] - ], - id: '...', - sig: '...' - }, null, 2); - case 10895: - return JSON.stringify({ - kind: 10895, - pubkey: examplePubkey, - created_at: timestamp, - content: '', - tags: [ - ['u', 'https://example.com/feed.rss'], - ['u', 'https://another-site.com/rss.xml'] - ], - id: '...', - sig: '...' - }, null, 2); - default: - return JSON.stringify({ - kind: kind, - pubkey: examplePubkey, - created_at: timestamp, - content: 'Custom event content', - tags: [ - ['example', 'tag', 'value'] - ], - id: '...', - sig: '...' - }, null, 2); - } + return JSON.stringify(kindMetadata.exampleJSON(examplePubkey, exampleEventId, exampleRelay, timestamp), null, 2); } - - const exampleJSON = $derived(getExampleJSON(effectiveKind)); + + const exampleJSON = $derived(getExampleJSON()); + + const isKind30040 = $derived(selectedKind === KIND.PUBLICATION_INDEX); + const isKind10895 = $derived(selectedKind === KIND.RSS_FEED); function addTag() { tags = [...tags, ['', '']]; @@ -613,13 +312,17 @@ allTags.push(['client', 'aitherboard']); } + // Process content to add "nostr:" prefix to valid Nostr addresses + const { processNostrLinks } = await import('../../utils/nostr-link-processor.js'); + const processedContent = processNostrLinks(contentWithUrls.trim()); + // Create a plain object (not a Proxy) to avoid cloning issues const eventTemplate: Omit = { kind: effectiveKind, pubkey: session.pubkey, created_at: Math.floor(Date.now() / 1000), tags: allTags, - content: contentWithUrls.trim() + content: processedContent }; const signedEvent = await session.signer(eventTemplate); @@ -708,12 +411,16 @@ .filter(t => t[0] && t[1]) .map(tag => [...tag]); // Create new array for each tag to avoid Proxy + // Process content to add "nostr:" prefix to valid Nostr addresses + const { processNostrLinks } = await import('../../utils/nostr-link-processor.js'); + const processedContent = processNostrLinks(content); + const eventTemplate: Omit = { kind: effectiveKind, pubkey: session.pubkey, created_at: Math.floor(Date.now() / 1000), tags: plainTags, - content + content: processedContent }; const results = await signAndPublish(eventTemplate, relays); @@ -780,7 +487,20 @@ {#if !isKind30040 && !isKind10895}
- +
+ + {#if showAdvancedEditorButton} + + {/if} +
+{#if showAdvancedEditor} + { + content = newContent; + }} + onClose={() => showAdvancedEditor = false} + /> +{/if} + {#if showJsonModal}
} | null>(null); + let showAdvancedEditor = $state(false); + + // Determine editor mode based on event kind + const editorMode = $derived( + event.kind === 30818 || event.kind === 30041 ? 'asciidoc' : 'markdown' + ); // Sync state when event prop changes $effect(() => { @@ -57,13 +64,17 @@ publishing = true; try { + // Process content to add "nostr:" prefix to valid Nostr addresses + const { processNostrLinks } = await import('../../utils/nostr-link-processor.js'); + const processedContent = processNostrLinks(content); + // Create new event (id, sig, created_at will be generated) const eventTemplate = { kind: event.kind, pubkey: session.pubkey, created_at: Math.floor(Date.now() / 1000), tags: tags.filter(t => t[0] && t[1]), // Filter out empty tags - content + content: processedContent }; // Sign event @@ -110,13 +121,17 @@ true ); + // Process content to add "nostr:" prefix to valid Nostr addresses + const { processNostrLinks } = await import('../../utils/nostr-link-processor.js'); + const processedContent = processNostrLinks(content); + const results = await signAndPublish( { kind: event.kind, pubkey: event.pubkey, created_at: event.created_at, tags: tags.filter(t => t[0] && t[1]), - content + content: processedContent }, relays ); @@ -135,7 +150,18 @@

Edit the event content and tags. ID, kind, pubkey, sig, and created_at are generated on publish.

- +
+ + +