diff --git a/src/lib/components/WikiCard.svelte b/src/lib/components/WikiCard.svelte new file mode 100644 index 0000000..e608591 --- /dev/null +++ b/src/lib/components/WikiCard.svelte @@ -0,0 +1,38 @@ + + + +
+ +
+
\ No newline at end of file diff --git a/src/lib/utils/markup/basicMarkupParser.ts b/src/lib/utils/markup/basicMarkupParser.ts index cbd843b..d0d6ff1 100644 --- a/src/lib/utils/markup/basicMarkupParser.ts +++ b/src/lib/utils/markup/basicMarkupParser.ts @@ -142,7 +142,7 @@ function replaceWikilinks(text: string): string { return text.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_match, target, label) => { const normalized = normalizeDTag(target.trim()); const display = (label || target).trim(); - const url = `./publication?d=${normalized}`; + const url = `./wiki?d=${normalized}`; // Output as a clickable with the [[display]] format and matching link colors return `${display}`; }); diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index f702d24..c59e84b 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -24,7 +24,7 @@ function escapeHtml(text: string): string { /** * Get user metadata for a nostr identifier (npub or nprofile) */ -export async function getUserMetadata(identifier: string): Promise<{name?: string, displayName?: string}> { +export async function getUserMetadata(identifier: string): Promise<{name?: string, displayName?: string, nip05?: string}> { // Remove nostr: prefix if present const cleanId = identifier.replace(/^nostr:/, ''); @@ -73,7 +73,8 @@ export async function getUserMetadata(identifier: string): Promise<{name?: strin const metadata = { name: profile.name || fallback.name, - displayName: profile.displayName + displayName: profile.displayName, + nip05: profile.nip05 }; npubCache.set(cleanId, metadata); diff --git a/src/lib/wiki.ts b/src/lib/wiki.ts new file mode 100644 index 0000000..d545ffa --- /dev/null +++ b/src/lib/wiki.ts @@ -0,0 +1,100 @@ +import Asciidoctor from 'asciidoctor'; +import { parseBasicmarkup } from './utils/markup/basicMarkupParser'; +import { getUserMetadata } from './utils/nostrUtils'; +import { get } from 'svelte/store'; +import { ndkInstance } from '$lib/ndk'; +import { nip19 } from 'nostr-tools'; +import type { NDKEvent } from '@nostr-dev-kit/ndk'; + +async function fetchWikiEventById(id: string): Promise { + const ndk = get(ndkInstance); + if (!ndk) return null; + + let eventId = id; + // If bech32, decode to hex + if (id.startsWith('nevent') || id.startsWith('note') || id.startsWith('naddr')) { + try { + const decoded = nip19.decode(id); + if (decoded.type === 'nevent') { + eventId = decoded.data.id; + } else if (decoded.type === 'note') { + eventId = decoded.data; + } + } catch { + return null; + } + } + + // Fetch the event by id (hex) + const event = await ndk.fetchEvent({ ids: [eventId] }); + // Only return if it's a wiki event (kind 30818) + if (event && event.kind === 30818) { + return event; + } + return null; +} + +async function fetchWikiEventsByDTag(dtag: string): Promise { + const ndk = get(ndkInstance); + if (!ndk) return []; + + // Query for kind 30818 events with the given d-tag + const events = await ndk.fetchEvents({ + kinds: [30818], + '#d': [dtag] + }); + + // Convert Set to Array and return + return Array.from(events); +} + +// Placeholder: Fetch profile name for a pubkey (kind 0 event) +async function getProfileName(pubkey: string): Promise { + if (!pubkey) return 'unknown'; + const metadata = await getUserMetadata(pubkey); + return metadata.displayName || metadata.name || pubkey.slice(0, 10); +} + +export async function getWikiPageById(id: string) { + const event = await fetchWikiEventById(id); + if (!event) return null; + const pubhex = event.pubkey || ''; + const author = await getProfileName(pubhex); + const titleTag = event.tags?.find((tag: string[]) => tag[0] === 'title'); + const title = titleTag ? titleTag[1] : 'Untitled'; + const asciidoctor = Asciidoctor(); + const asciidocHtml = asciidoctor.convert(event.content).toString(); + // Optionally log for debugging: + // console.log('AsciiDoc HTML:', asciidocHtml); + const html = await parseBasicmarkup(asciidocHtml); + return { title, author, pubhex, html }; +} + +export async function searchWikiPagesByDTag(dtag: string) { + const events = await fetchWikiEventsByDTag(dtag); + // Return array of { title, pubhex, eventId, summary, nip05 } + return Promise.all(events.map(async (event: any) => { + const pubhex = event.pubkey || ''; + // Get title from 't' tag + const titleTag = event.tags?.find((tag: string[]) => tag[0] === 't'); + const title = titleTag ? titleTag[1] : 'Untitled'; + // Get summary from 'summary' tag + const summaryTag = event.tags?.find((tag: string[]) => tag[0] === 'summary'); + const summary = summaryTag ? summaryTag[1] : ''; + + // Get user metadata including NIP-05 + const metadata = await getUserMetadata(pubhex); + const nip05 = metadata.nip05 || ''; + + // Construct human-readable URL + const urlPath = nip05 ? `${dtag}/${nip05}` : `${dtag}*${pubhex}`; + + return { + title, + pubhex, + eventId: event.id, + summary, + urlPath + }; + })); +} \ No newline at end of file diff --git a/src/routes/wiki/+page.svelte b/src/routes/wiki/+page.svelte new file mode 100644 index 0000000..3cc7587 --- /dev/null +++ b/src/routes/wiki/+page.svelte @@ -0,0 +1,274 @@ + + +
+
+ { + if (wikiPage) { + wikiPage = null; + wikiContent = null; + } + fetchResults(search); + }} + autocomplete="off" + class="w-full px-6 py-4 rounded-2xl border border-primary-200 shadow bg-primary-50 focus:outline-none focus:ring-2 focus:ring-primary-400 text-lg transition" + /> +
+ + {#if loading} +
+
+

Loading wiki content...

+
+ {:else if error} +
+

{error}

+
+ {:else if wikiPage && wikiContent} +
+

{wikiContent.title}

+
+ by +
+ {#if wikiPage.hashtags.length} +
+ {#each wikiPage.hashtags as tag} + #{tag} + {/each} +
+ {/if} + {#if wikiPage.summary} +
{wikiPage.summary}
+ {/if} +
+ {@html wikiContent.html} +
+
+ {:else if !search} +
+

+ Welcome to the Alexandria Wiki! +

+

+ Use the search bar above to find wiki pages on any topic. + Alexandria wiki pages are stored on Nostr relays and can be collaboratively added to by anyone with a Nostr key. + Search by topic, and you'll see all relevant wiki pages, each signed by its author. +

+
+ {:else if results.length === 0} +

No entries found for this topic.

+ {:else} + + {/if} +
+ +
{@html '

Hello

This is a test.

'}
\ No newline at end of file diff --git a/src/routes/wiki/+page.ts b/src/routes/wiki/+page.ts new file mode 100644 index 0000000..c009f61 --- /dev/null +++ b/src/routes/wiki/+page.ts @@ -0,0 +1,25 @@ +import type { Load } from '@sveltejs/kit'; +import { getWikiPageById, searchWikiPagesByDTag } from '../../lib/wiki'; + +export const load: Load = async ({ url }) => { + const id = url.searchParams.get('id'); + const d = url.searchParams.get('d'); + + if (d) { + // Disambiguation/search page for d-tag + const results = await searchWikiPagesByDTag(d); + return { disambiguation: true, results, dtag: d }; + } + + if (id) { + // Single wiki page by event id (bech32 or hex) + const page = await getWikiPageById(id); + if (!page) { + return { status: 404, error: 'Wiki page not found.' }; + } + return { disambiguation: false, page }; + } + + // No query: show only the search bar + return { disambiguation: true, results: [], dtag: '' }; +}; \ No newline at end of file