Browse Source

Remove wiki stuff and update readme for issue #173

master
Silberengel 10 months ago
parent
commit
39419d38f4
  1. 12
      README.md
  2. 2
      src/lib/components/Navigation.svelte
  3. 49
      src/lib/components/WikiCard.svelte
  4. 184
      src/lib/wiki.ts
  5. 323
      src/routes/wiki/+page.svelte
  6. 25
      src/routes/wiki/+page.ts

12
README.md

@ -3,13 +3,15 @@ @@ -3,13 +3,15 @@
# Alexandria
Alexandria is a reader and writer for curated publications, including e-books.
For a thorough introduction, please refer to our [project documention](./publication?d=gitcitadel-project-documentation-by-stella-v-1), viewable on Alexandria, or to the Alexandria [About page](./about).
For a thorough introduction, please refer to our [project documention](https://next-alexandria.gitcitadel.eu/publication?d=gitcitadel-project-documentation-by-stella-v-1), viewable on Alexandria, or to the Alexandria [About page](https://next-alexandria.gitcitadel.eu/about).
It also contains a [universal event viewer](https://next-alexandria.gitcitadel.eu/events), with which you can search our relays, some aggregator relays, and your own relay list, to find and view event data.
## Issues and Patches
If you would like to suggest a feature or report a bug, please use the [Alexandria Contact page](./contact).
If you would like to suggest a feature or report a bug, please use the [Alexandria Contact page](https://next-alexandria.gitcitadel.eu/contact).
You can also contact us [on Nostr](./events?id=nprofile1qqsggm4l0xs23qfjwnkfwf6fqcs66s3lz637gaxhl4nwd2vtle8rnfqprfmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdaks5zhueg), directly.
You can also contact us [on Nostr](https://next-alexandria.gitcitadel.eu/events?id=nprofile1qqsggm4l0xs23qfjwnkfwf6fqcs66s3lz637gaxhl4nwd2vtle8rnfqprfmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdaks5zhueg), directly.
## Developing
@ -73,7 +75,7 @@ To run the container, in detached mode (-d): @@ -73,7 +75,7 @@ To run the container, in detached mode (-d):
docker run -d --rm --name=gc-alexandria -p 4174:80 gc-alexandria
```
The container is then viewable on your [local machine](./).
The container is then viewable on your [local machine](http://localhost:4174).
If you want to see the container process (assuming it's the last process to start), enter:
@ -118,4 +120,4 @@ npx playwright test @@ -118,4 +120,4 @@ npx playwright test
## Markup Support
Alexandria supports both Markdown and AsciiDoc markup for different content types. For a detailed list of supported tags and features in the basic and advanced markdown parsers, as well as information about AsciiDoc usage for publications and wikis, see [MarkupInfo.md](./src/lib/utils/markup/MarkupInfo.md).
Alexandria supports both Markdown and AsciiDoc markup for different content types. For a detailed list of supported tags and features in the basic and advanced markdown parsers, as well as information about AsciiDoc usage for publications and wikis, see [MarkupInfo.md](https://next-alexandria.gitcitadel.eu/src/lib/utils/markup/MarkupInfo.md).

2
src/lib/components/Navigation.svelte

@ -10,7 +10,6 @@ @@ -10,7 +10,6 @@
import Login from "./Login.svelte";
let { class: className = "" } = $props();
</script>
<Navbar class={`Navbar navbar-leather ${className}`}>
@ -25,7 +24,6 @@ @@ -25,7 +24,6 @@
</div>
<NavUl class="ul-leather">
<NavLi href="/">Publications</NavLi>
<NavLi href="/wiki">Wiki</NavLi>
<NavLi href="/events">Events</NavLi>
<NavLi href="/visualize">Visualize</NavLi>
<NavLi href="/start">Getting Started</NavLi>

49
src/lib/components/WikiCard.svelte

@ -1,49 +0,0 @@ @@ -1,49 +0,0 @@
<script lang="ts">
import { Card } from "flowbite-svelte";
import InlineProfile from "$components/util/InlineProfile.svelte";
export let title: string;
export let pubhex: string;
export let eventId: string;
export let summary: string;
export let urlPath: string;
export let hashtags: string[] = [];
export let html: string = '';
let expanded = false;
$: preview = html.slice(0, 250);
// Logging for debug
console.log('WikiCard props:', { title, pubhex, eventId, summary, urlPath, hashtags });
</script>
<Card class='ArticleBox card-leather w-lg flex flex-row space-x-2'>
<div class='col flex flex-row flex-grow space-x-4'>
<div class="flex flex-col flex-grow">
<a href="/wiki?id={urlPath}" class='flex flex-col space-y-2'>
<h2 class='text-lg font-bold line-clamp-2' title={title}>{title}</h2>
<div class="flex flex-col space-y-1">
<h3 class='text-base font-normal'>
by <InlineProfile pubkey={pubhex} />
</h3>
{#if summary}
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">{summary}</p>
{/if}
{#if hashtags.length}
<div class="flex flex-wrap gap-2 mt-2">
{#each hashtags as tag}
<span class="px-2 py-1 rounded bg-primary-100 text-primary-700 text-xs font-semibold">#{tag}</span>
{/each}
</div>
{/if}
</div>
<div class="prose dark:prose-invert max-w-none mt-2">
{@html expanded ? html : preview}
{#if !expanded && html.length > 250}
<button class="mt-2 text-primary-600 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300" onclick={() => expanded = true}>Read more...</button>
{/if}
</div>
</a>
</div>
</div>
</Card>

184
src/lib/wiki.ts

@ -1,184 +0,0 @@ @@ -1,184 +0,0 @@
import { parseBasicmarkup } from './utils/markup/basicMarkupParser';
import { getUserMetadata, fetchEventWithFallback } 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';
import type NDK from '@nostr-dev-kit/ndk';
import Pharos from '$lib/parser.ts';
import { wikiKind } from './consts';
/**
* Fetch a single wiki event by id (hex or bech32).
*/
export async function fetchWikiEventById(id: string): Promise<NDKEvent | null> {
const ndk = get(ndkInstance);
if (!ndk) {
console.warn('NDK instance not found in fetchWikiEventById');
return null;
}
let eventId = id;
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 (e) {
console.error('Failed to decode id in fetchWikiEventById:', e);
return null;
}
}
const event = await fetchEventWithFallback(ndk, eventId);
if (event && event.kind === wikiKind) {
console.log('Fetched wiki event:', event);
return event;
}
console.warn('No wiki event found for id:', eventId);
return null;
}
/**
* Fetch all wiki events by d-tag.
*/
export async function fetchWikiEventsByDTag(dtag: string): Promise<NDKEvent[]> {
const ndk = get(ndkInstance);
if (!ndk) {
console.warn('NDK instance not found in fetchWikiEventsByDTag');
return [];
}
const event = await fetchEventWithFallback(ndk, {
kinds: [wikiKind],
'#d': [dtag]
});
if (!event) {
console.warn(`No wiki events found for dtag: ${dtag}`);
return [];
}
// For d-tag queries, we want to get all matching events, not just the first one
const events = await ndk.fetchEvents({
kinds: [wikiKind],
'#d': [dtag]
});
const arr = Array.from(events);
console.log(`Fetched ${arr.length} wiki events for dtag:`, dtag);
return arr;
}
/**
* Get a display name for a pubkey.
*/
export async function getProfileName(pubkey: string): Promise<string> {
if (!pubkey) return 'unknown';
const metadata = await getUserMetadata(pubkey);
return metadata.displayName || metadata.name || pubkey.slice(0, 10);
}
/**
* Fetch and parse a wiki page by event id or nevent.
*/
export async function getWikiPageById(id: string, ndk: NDK) {
console.log('getWikiPageById: fetching wiki page for id', id);
if (!id) {
console.error('getWikiPageById: id is undefined');
return null;
}
let event;
try {
event = await fetchEventWithFallback(ndk, id);
if (!event) {
console.error('getWikiPageById: No event found for id:', id);
return null;
}
if (event.kind !== wikiKind) {
console.error('getWikiPageById: Event found but kind !== wikiKind:', event);
return null;
}
if (!event.content) {
console.error('getWikiPageById: Event has no content:', event);
return null;
}
if (!event.tags) {
console.error('getWikiPageById: Event has no tags:', event);
return null;
}
} catch (err) {
console.error('getWikiPageById: Exception fetching event:', err, 'id:', id);
return null;
}
const pubhex = event.pubkey || '';
const titleTag = event.tags.find((tag: string[]) => tag[0] === 'title');
const title = titleTag ? titleTag[1] : 'Untitled';
const summaryTag = event.tags.find((tag: string[]) => tag[0] === 'summary');
const summary = summaryTag ? summaryTag[1] : '';
const hashtags = event.tags.filter((tag: string[]) => tag[0] === 't').map((tag: string[]) => tag[1]) || [];
let asciidoc = event.content;
if (!/^=\s/m.test(asciidoc)) {
console.warn('getWikiPageById: No document header found, prepending fake header for title:', title);
asciidoc = `= ${title}\n\n` + asciidoc;
}
let html = '';
try {
const pharos = new Pharos(ndk);
console.log('Pharos instance:', pharos);
pharos.parse(asciidoc);
const pharosHtml = pharos.getHtml();
console.log('AsciiDoc:', asciidoc);
console.log('Pharos HTML:', pharosHtml);
html = await parseBasicmarkup(pharosHtml ?? '');
if (!html || html.trim() === '') {
console.error('getWikiPageById: Parsed HTML is empty for id:', id, 'event:', event, 'asciidoc:', asciidoc, 'pharosHtml:', pharosHtml);
}
} catch (err) {
console.error('getWikiPageById: Error parsing content:', err, 'event:', event);
return null;
}
return { title, pubhex, eventId: event.id, summary, hashtags, html, content: event.content };
}
/**
* Search wiki pages by d-tag.
*/
export async function searchWikiPagesByDTag(dtag: string) {
const events = await fetchWikiEventsByDTag(dtag);
return Promise.all(events.map(async (event: NDKEvent) => {
const pubhex = event.pubkey || '';
const titleTag = event.tags?.find((tag: string[]) => tag[0] === 't');
const title = titleTag ? titleTag[1] : 'Untitled';
const summaryTag = event.tags?.find((tag: string[]) => tag[0] === 'summary');
const summary = summaryTag ? summaryTag[1] : '';
const metadata = await getUserMetadata(pubhex);
const nip05 = metadata.nip05 || '';
const urlPath = nip05 ? `${dtag}/${nip05}` : `${dtag}*${pubhex}`;
return {
title,
pubhex,
eventId: event.id,
summary,
urlPath
};
}));
}
/**
* Parse wiki content using Pharos and basic markup parser.
*/
export async function parseWikiContent(content: string, ndk: NDK): Promise<string> {
const pharos = new Pharos(ndk);
pharos.parse(content);
const pharosHtml = pharos.getHtml();
return await parseBasicmarkup(pharosHtml);
}

323
src/routes/wiki/+page.svelte

@ -1,323 +0,0 @@ @@ -1,323 +0,0 @@
<script lang="ts">
import WikiCard from "$lib/components/WikiCard.svelte";
import InlineProfile from "$lib/components/util/InlineProfile.svelte";
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { ndkInstance } from '$lib/ndk';
import { page } from '$app/stores';
import { getWikiPageById, getProfileName } from '$lib/wiki';
import { type NDKEvent } from '@nostr-dev-kit/ndk';
import { neventEncode } from '$lib/utils';
import { processNostrIdentifiers } from '$lib/utils/nostrUtils';
import { standardRelays, wikiKind } from '$lib/consts';
import Pharos from '$lib/parser';
import { parseBasicmarkup } from '$lib/utils/markup/basicMarkupParser';
// @ts-ignore Svelte linter false positive: hashtags is used in the template
let { } = $props<{
title: string;
pubhex: string;
eventId: string;
summary: string;
urlPath: string;
hashtags?: string[];
}>();
type WikiPage = {
title: string;
pubhex: string;
eventId: string;
summary: string;
hashtags: string[];
html: string;
content: string;
};
let searchInput = $state('');
let results: WikiPage[] = $state([]);
let loading = $state(false);
let wikiPage: WikiPage | null = $state(null);
let wikiContent: { title: string; author: string; pubhex: string; html: string } | null = $state(null);
let error = $state<string | null>(null);
let expandedContent = $state(false);
let contentPreview = $derived(() => {
if (!wikiPage) return '';
return wikiPage.html.slice(0, 250);
});
function normalize(str: string) {
return str.toLowerCase().replace(/[-_]/g, ' ').replace(/\s+/g, ' ').trim();
}
async function fetchResults(query: string) {
if (!query.trim()) {
results = [];
loading = false;
return;
}
loading = true;
error = null;
try {
const ndk = $ndkInstance;
if (!ndk) {
results = [];
error = 'NDK instance not available';
loading = false;
return;
}
const events = await ndk.fetchEvents({ kinds: [wikiKind] });
const normQuery = normalize(query);
// Filter by title or hashtags
let filtered = Array.from(events).filter((event: NDKEvent) => {
const titleTag = event.tags?.find((tag: string[]) => tag[0] === 'title');
const title = titleTag && titleTag[1]?.trim() ? titleTag[1] : 'Untitled';
const hashtags = event.tags?.filter((tag: string[]) => tag[0] === 't').map((tag: string[]) => tag[1]) || [];
return normalize(title).includes(normQuery) ||
hashtags.some((hashtag: string) => normalize(hashtag).includes(normQuery));
});
const pages = await Promise.all(filtered.map(async (event: NDKEvent) => {
const pageData = await getWikiPageById(event.id, ndk);
if (pageData) {
// Process Nostr identifiers in the HTML content
pageData.html = await processNostrIdentifiers(pageData.html);
}
if (event && typeof event.getMatchingTags !== 'function') {
console.error('Fetched event is not an NDKEvent:', event);
}
return pageData as WikiPage | null;
}));
results = pages.filter((page): page is WikiPage => page !== null);
} catch (e) {
error = 'Error searching wiki pages';
results = [];
console.error('fetchResults: Exception:', e);
} finally {
loading = false;
}
}
async function fetchWikiPageById(id: string) {
loading = true;
error = null;
try {
const ndk = $ndkInstance;
if (!ndk) {
wikiPage = null;
wikiContent = null;
console.error('fetchWikiPageById: NDK instance not available');
return;
}
if (!id) {
console.error('fetchWikiPageById: id is undefined');
return;
}
console.log('fetchWikiPageById: fetching wiki page for id', id);
const pageData = await getWikiPageById(id, ndk);
if (pageData) {
// Process Nostr identifiers in the HTML content
const processedHtml = await processNostrIdentifiers(pageData.html);
wikiPage = {
title: pageData.title,
pubhex: pageData.pubhex,
eventId: pageData.eventId,
summary: pageData.summary,
hashtags: pageData.hashtags,
html: processedHtml,
content: pageData.content,
};
wikiContent = {
title: pageData.title,
author: await getProfileName(pageData.pubhex),
pubhex: pageData.pubhex,
html: processedHtml
};
if (!wikiPage.html) {
console.error('fetchWikiPageById: wikiPage.html is empty for id', id, wikiPage);
}
console.log('wikiPage.html:', wikiPage?.html);
} else {
wikiPage = null;
wikiContent = null;
error = 'Wiki page not found';
console.error('fetchWikiPageById: Wiki page not found for id', id);
}
} catch (e) {
error = 'Error loading wiki page';
wikiPage = null;
wikiContent = null;
console.error('fetchWikiPageById: Exception:', e);
} finally {
loading = false;
}
}
// Clear wikiPage if searching
$effect(() => {
if (searchInput && wikiPage) {
wikiPage = null;
}
});
// Watch for ?id= in the URL and load the wiki page if present
$effect(() => {
const id = $page.url.searchParams.get('id');
if (id) {
fetchWikiPageById(id);
searchInput = '';
results = [];
}
});
function handleCardClick(urlPath: string) {
goto(`/wiki?id=${encodeURIComponent(urlPath)}`);
}
function getNevent(eventId: string): string {
try {
const event = { id: eventId, kind: wikiKind } as NDKEvent;
return neventEncode(event, standardRelays);
} catch (e) {
console.error('Error encoding nevent:', e);
return eventId;
}
}
function handleProfileClick(pubkey: string) {
goto(`/profile?pubkey=${pubkey}`);
}
onMount(() => {
const params = new URLSearchParams(window.location.search);
const d = params.get('d');
const id = params.get('id');
if (id) {
wikiPage = null;
fetchWikiPageById(id);
searchInput = '';
results = [];
} else if (d) {
searchInput = d;
wikiPage = null;
fetchResults(searchInput);
} else {
searchInput = '';
results = [];
wikiPage = null;
}
});
(async () => {
let html = '';
try {
const pharos = new Pharos($ndkInstance);
pharos.parse('= Test\n\nHello world');
const pharosHtml = pharos.getHtml();
if (!pharosHtml || pharosHtml.trim() === '') {
console.error('Pharos failed to parse AsciiDoc:', '= Test\n\nHello world');
}
html = await parseBasicmarkup(pharosHtml ?? '');
console.log('Test parse result:', html);
} catch (err) {
console.error('Pharos parse error:', err);
}
})();
</script>
<div class="flex flex-col items-center min-h-[60vh] pt-8">
<div class="w-full max-w-xl flex flex-col items-center">
<input
type="text"
placeholder="Search for a wiki topic..."
bind:value={searchInput}
oninput={() => {
if (wikiPage) {
wikiPage = null;
wikiContent = null;
}
fetchResults(searchInput);
}}
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"
/>
</div>
{#if loading}
<div class="flex flex-col items-center mt-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
<p class="mt-4 text-gray-600">Loading wiki content...</p>
</div>
{:else if error}
<div class="flex flex-col items-center mt-8 text-red-600">
<p>{error}</p>
</div>
{:else if wikiPage && wikiContent}
<div class="flex flex-col items-center mt-8 max-w-4xl w-full px-4">
<div class="text-sm font-mono text-gray-600 dark:text-gray-400 mb-2 break-all whitespace-pre-wrap">{getNevent(wikiPage.eventId)}</div>
<h1 class="text-3xl font-bold mb-2">{wikiPage.title}</h1>
<div class="mb-2">
by <button
class="text-primary-600 hover:underline"
onclick={() => wikiPage && handleProfileClick(wikiPage.pubhex)}
>
<InlineProfile pubkey={wikiPage.pubhex} />
</button>
</div>
{#if wikiPage.hashtags.length}
<div class="flex flex-wrap gap-2 mb-6">
{#each wikiPage.hashtags as tag}
<span class="px-3 py-1 rounded-full bg-primary-100 text-primary-700 text-sm font-medium">#{tag}</span>
{/each}
</div>
{/if}
{#if wikiPage.summary}
<div class="mb-6 text-lg text-gray-700 max-w-2xl text-center">{wikiPage.summary}</div>
{/if}
<div class="w-full prose prose-lg dark:prose-invert max-w-none">
{#if wikiPage.html && wikiPage.html.trim().length > 0}
{@html wikiPage.html}
{:else}
<div class="text-red-600">
No content found for this wiki page.
{#if wikiPage.content}
<pre class="text-xs mt-2 bg-gray-100 dark:bg-gray-800 p-2 rounded">
{wikiPage.content}
</pre>
{/if}
<pre class="text-xs mt-2 bg-gray-100 dark:bg-gray-800 p-2 rounded">
{JSON.stringify(wikiPage, null, 2)}
</pre>
</div>
{/if}
</div>
</div>
{:else if !searchInput}
<div class="max-w-xl mx-auto mt-12 text-center text-lg space-y-4">
<p>
<strong>Welcome to the Alexandria Wiki!</strong>
</p>
<p>
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.
</p>
</div>
{:else if results.length === 0}
<p class="text-center mt-8">No entries found for this topic.</p>
{:else}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-8 max-w-6xl w-full px-4">
{#each results as result}
<WikiCard
title={result.title}
pubhex={result.pubhex}
eventId={result.eventId}
summary={result.summary}
hashtags={result.hashtags}
urlPath={result.eventId}
/>
{/each}
</div>
{/if}
</div>

25
src/routes/wiki/+page.ts

@ -1,25 +0,0 @@ @@ -1,25 +0,0 @@
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: '' };
};
Loading…
Cancel
Save