From 401bd7b27cec71ffbdb466299e3addfa1fde943a Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 15 Dec 2024 12:57:57 -0600 Subject: [PATCH 01/11] Only handle 30041 events as zettels --- src/lib/consts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/consts.ts b/src/lib/consts.ts index fbe3477..2b114fd 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -1,6 +1,6 @@ export const wikiKind = 30818; export const indexKind = 30040; -export const zettelKinds = [ 1, 30024, 30041, 30818]; +export const zettelKinds = [ 30041 ]; export const standardRelays = [ "wss://thecitadel.nostr1.com", "wss://relay.noswhere.com" ]; export enum FeedType { From fdefbcb1f967ddda98d9c075a353806501f5d365 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 15 Dec 2024 12:58:45 -0600 Subject: [PATCH 02/11] Enable parser to fetch publications from relays --- src/lib/parser.ts | 79 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/lib/parser.ts b/src/lib/parser.ts index 0c0b0a8..874bb34 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -12,6 +12,7 @@ import asciidoctor, { } from 'asciidoctor'; import he from 'he'; import { writable, type Writable } from 'svelte/store'; +import { indexKind, zettelKinds } from './consts'; interface IndexMetadata { authors?: string[]; @@ -154,6 +155,28 @@ export default class Pharos { } } + /** + * Fetches and parses the event tree for a publication given the event or event ID of the + * publication's root index. + * @param event The event or event ID of the publication's root index. + */ + async fetch(event: NDKEvent | string): Promise { + let content: string; + + if (typeof event === 'string') { + const index = await this.ndk.fetchEvent({ ids: [event] }); + if (!index) { + throw new Error('Failed to fetch publication.'); + } + + content = await this.getPublicationContent(index); + } else { + content = await this.getPublicationContent(event); + } + + this.parse(content); + } + /** * Generates and stores Nostr events from the parsed AsciiDoc document. The events can be * modified via the parser's API and retrieved via the `getEvents()` method. @@ -558,6 +581,62 @@ export default class Pharos { } } + /** + * Uses the NDK to crawl the event tree of a publication and return its content as a string. + * @param event The root index event of the publication. + * @returns The content of the publication as a string. + * @remarks This function does a depth-first crawl of the event tree using the relays specified + * on the NDK instance. + */ + private async getPublicationContent(event: NDKEvent, depth: number = 0): Promise { + let content: string = ''; + + // Format title into AsciiDoc header. + const title = event.getMatchingTags('title')[0][1]; + let titleLevel = ''; + for (let i = 0; i <= depth; i++) { + titleLevel += '='; + } + content += `${titleLevel} ${title}\n\n`; + + // TODO: Deprecate `e` tags in favor of `a` tags required by NIP-62. + let tags = event.getMatchingTags('a'); + if (tags.length === 0) { + tags = event.getMatchingTags('e'); + } + + // Base case: The event is a zettel. + if (zettelKinds.includes(event.kind ?? -1)) { + content += event.content; + return content; + } + + // Recursive case: The event is an index. + const childEvents = await Promise.all( + tags.map(tag => this.ndk.fetchEventFromTag(tag, event)) + ); + + // Michael J - 15 December 2024 - This could be further parallelized by recursively fetching + // children of index events before processing them for content. We won't make that change now, + // as it would increase complexity, but if performance suffers, we can revisit this option. + const childContentPromises: Promise[] = []; + for (let i = 0; i < childEvents.length; i++) { + const childEvent = childEvents[i]; + + if (!childEvent) { + console.warn(`NDK could not find event ${tags[i][1]}.`); + continue; + } + + childContentPromises.push(this.getPublicationContent(childEvent, depth + 1)); + } + + const childContents = await Promise.all(childContentPromises); + content += childContents.join('\n\n'); + + return content; + } + // #endregion // #region NDKEvent Generation From af5ae819160ac6f10c9b0f52e2297ddcdf90a2b3 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 15 Dec 2024 12:59:35 -0600 Subject: [PATCH 03/11] Use Preview component for reader view --- src/lib/components/Article.svelte | 116 +++++++++++++----------------- 1 file changed, 48 insertions(+), 68 deletions(-) diff --git a/src/lib/components/Article.svelte b/src/lib/components/Article.svelte index 032865c..2935342 100644 --- a/src/lib/components/Article.svelte +++ b/src/lib/components/Article.svelte @@ -3,36 +3,24 @@ import type { NDKEvent } from '@nostr-dev-kit/ndk'; import { page } from '$app/stores'; import { Button, Heading, Sidebar, SidebarGroup, SidebarItem, SidebarWrapper, Skeleton, TextPlaceholder, Tooltip } from 'flowbite-svelte'; - import showdown from 'showdown'; import { onMount } from 'svelte'; import { BookOutline } from 'flowbite-svelte-icons'; - import { zettelKinds } from '../consts'; + import Pharos, { parser } from '$lib/parser'; + import Preview from './Preview.svelte'; export let index: NDKEvent | null | undefined; + $parser ??= new Pharos($ndk); + $: activeHash = $page.url.hash; - const getEvents = async (index?: NDKEvent | null | undefined): Promise> => { - if (index == null) { - // TODO: Add error handling. + const getContentRoot = async (index?: NDKEvent | null | undefined): Promise => { + if (!index) { + return null; } - const eventIds = index!.getMatchingTags('e').map((value) => value[1]); - const events = await $ndk.fetchEvents( - { - // @ts-ignore - kinds: zettelKinds, - ids: eventIds, - }, - { - groupable: false, - skipVerification: false, - skipValidation: false - } - ); - - console.debug(`Fetched ${events.size} events from ${eventIds.length} references.`); - return events; + await $parser.fetch(index); + return $parser.getRootIndexId(); }; function normalizeHashPath(str: string): string { @@ -104,62 +92,54 @@ window.removeEventListener('click', hideTocOnClick); }; }); - - const converter = new showdown.Converter(); -{#await getEvents(index)} +{#await getContentRoot(index)} -{:then events} - {#if showTocButton && !showToc} - - - Show Table of Contents - - {/if} - {#if showToc} - - - - {#each events as event} - - {/each} - - - +{:then rootId} + {#if rootId} + {#if showTocButton && !showToc} + + + Show Table of Contents + + {/if} + + +
+ +
+ {:else} + {/if} -
- {#each events as event} -
- - {event.getMatchingTags('title')[0][1]} - - {@html converter.makeHtml(event.content)} -
- {/each} -
{/await}