Browse Source

Merge pull request #3 from buttercat1791/asciidoc-reader

Read Publications from Relays
master
Silberengel 1 year ago committed by GitHub
parent
commit
9e7396433c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4147
      package-lock.json
  2. 67
      package.json
  3. 4315
      pnpm-lock.yaml
  4. 6
      src/app.d.ts
  5. 76
      src/lib/components/Article.svelte
  6. 38
      src/lib/components/Preview.svelte
  7. 2
      src/lib/consts.ts
  8. 146
      src/lib/parser.ts
  9. 1
      src/routes/+layout.server.ts
  10. 24
      src/routes/+layout.ts
  11. 56
      src/routes/+page.svelte
  12. 4
      src/routes/new/compose/+page.svelte
  13. 18
      src/routes/new/edit/+page.svelte
  14. 31
      src/routes/publication/+error.svelte
  15. 40
      src/routes/publication/+page.svelte
  16. 42
      src/routes/publication/+page.ts
  17. 2
      svelte.config.js

4147
package-lock.json generated

File diff suppressed because it is too large Load Diff

67
package.json

@ -13,44 +13,37 @@ @@ -13,44 +13,37 @@
"format": "prettier --plugin-search-dir . --write ."
},
"dependencies": {
"@nostr-dev-kit/ndk": "^2.10.0",
"@nostr-dev-kit/ndk-cache-dexie": "^2.5.1",
"@popperjs/core": "^2.11.8",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"asciidoctor": "^3.0.4",
"he": "^1.2.0",
"markdown-it": "^14.0.0",
"markdown-it-plain-text": "^0.3.0",
"marked": "^11.1.1",
"nostr-tools": "^2.1.4",
"showdown": "^2.1.0",
"svelte-add": "2023.12.16-0.0",
"tailwind-merge": "^2.2.1"
"@nostr-dev-kit/ndk": "2.10.x",
"@nostr-dev-kit/ndk-cache-dexie": "2.5.x",
"@popperjs/core": "2.11.x",
"@tailwindcss/forms": "0.5.x",
"@tailwindcss/typography": "0.5.x",
"asciidoctor": "3.0.x",
"he": "1.2.x",
"nostr-tools": "2.10.x"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.4.3",
"@types/he": "^1.2.3",
"@types/markdown-it": "^13.0.7",
"@types/node": "^22.5.4",
"@types/showdown": "^2.0.6",
"autoprefixer": "^10.4.17",
"eslint-plugin-svelte": "^2.35.1",
"flowbite": "^2.2.1",
"flowbite-svelte": "^0.44.22",
"flowbite-svelte-icons": "^1.6.1",
"postcss": "^8.4.33",
"postcss-load-config": "^5.0.2",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.9",
"svelte-check": "^3.6.3",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.0.12"
"@sveltejs/adapter-auto": "3.x",
"@sveltejs/adapter-static": "3.x",
"@sveltejs/kit": "2.x",
"@sveltejs/vite-plugin-svelte": "4.x",
"@types/he": "1.2.x",
"@types/node": "22.x",
"autoprefixer": "10.x",
"eslint-plugin-svelte": "2.x",
"flowbite": "2.x",
"flowbite-svelte": "0.x",
"flowbite-svelte-icons": "2.x",
"postcss": "8.x",
"postcss-load-config": "6.x",
"prettier": "3.x",
"prettier-plugin-svelte": "3.x",
"svelte": "5.x",
"svelte-check": "4.x",
"tailwind-merge": "^2.5.5",
"tailwindcss": "3.x",
"tslib": "2.8.x",
"typescript": "5.7.x",
"vite": "5.x"
}
}

4315
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

6
src/app.d.ts vendored

@ -4,7 +4,11 @@ declare global { @@ -4,7 +4,11 @@ declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
interface PageData {
ndk?: NDK;
parser?: Pharos;
waitable?: Promise<any>;
}
// interface Platform {}
}
}

76
src/lib/components/Article.svelte

@ -1,39 +1,18 @@ @@ -1,39 +1,18 @@
<script lang="ts">
import { ndk } from '$lib/ndk';
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';
<script lang='ts'>
import { Button, Sidebar, SidebarGroup, SidebarItem, SidebarWrapper, Skeleton, TextPlaceholder, Tooltip } from 'flowbite-svelte';
import { onMount } from 'svelte';
import { BookOutline } from 'flowbite-svelte-icons';
import { zettelKinds } from '../consts';
import Preview from './Preview.svelte';
import { pharosInstance } from '$lib/parser';
import { page } from '$app/state';
export let index: NDKEvent | null | undefined;
let { rootId }: { rootId: string } = $props();
$: activeHash = $page.url.hash;
const getEvents = async (index?: NDKEvent | null | undefined): Promise<Set<NDKEvent>> => {
if (index == null) {
// TODO: Add error handling.
}
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
if (rootId !== $pharosInstance.getRootIndexId()) {
console.error('Root ID does not match parser root index ID');
}
);
console.debug(`Fetched ${events.size} events from ${eventIds.length} references.`);
return events;
};
let activeHash = $state(page.url.hash);
function normalizeHashPath(str: string): string {
return str
@ -104,19 +83,9 @@ @@ -104,19 +83,9 @@
window.removeEventListener('click', hideTocOnClick);
};
});
const converter = new showdown.Converter();
</script>
{#await getEvents(index)}
<Sidebar class='sidebar-leather fixed top-20 left-0 px-4 w-60'>
<SidebarWrapper>
<Skeleton/>
</SidebarWrapper>
</Sidebar>
<TextPlaceholder class='max-w-2xl'/>
{:then events}
{#if showTocButton && !showToc}
{#if showTocButton && !showToc}
<Button
class='btn-leather fixed top-20 left-4 h-6 w-6'
outline={true}
@ -130,8 +99,9 @@ @@ -130,8 +99,9 @@
<Tooltip>
Show Table of Contents
</Tooltip>
{/if}
{#if showToc}
{/if}
<!-- TODO: Get TOC from parser. -->
<!-- {#if showToc}
<Sidebar class='sidebar-leather fixed top-20 left-0 px-4 w-60' {activeHash}>
<SidebarWrapper>
<SidebarGroup class='sidebar-group-leather overflow-y-scroll'>
@ -145,22 +115,10 @@ @@ -145,22 +115,10 @@
</SidebarGroup>
</SidebarWrapper>
</Sidebar>
{/if}
<div class='flex flex-col space-y-4 max-w-2xl'>
{#each events as event}
<div class='note-leather flex flex-col space-y-2'>
<Heading
tag='h3'
class='h-leather'
id={normalizeHashPath(event.getMatchingTags('title')[0][1])}
>
{event.getMatchingTags('title')[0][1]}
</Heading>
{@html converter.makeHtml(event.content)}
</div>
{/each}
</div>
{/await}
{/if} -->
<div class='flex flex-col space-y-4 max-w-2xl'>
<Preview {rootId} />
</div>
<style>
:global(.sidebar-group-leather) {

38
src/lib/components/Preview.svelte

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
<script lang="ts">
import { parser, SiblingSearchDirection } from "$lib/parser";
import { page } from "$app/state";
import { pharosInstance, SiblingSearchDirection } from "$lib/parser";
import { Button, ButtonGroup, CloseButton, Heading, Input, P, Textarea, Tooltip } from "flowbite-svelte";
import { CaretDownSolid, CaretUpSolid, EditOutline } from "flowbite-svelte-icons";
import { createEventDispatcher } from "svelte";
@ -16,9 +17,9 @@ @@ -16,9 +17,9 @@
const dispatch = createEventDispatcher();
let currentContent: string = $parser.getContent(rootId);
let title: string | undefined = $parser.getIndexTitle(rootId);
let orderedChildren: string[] = $parser.getOrderedChildIds(rootId);
let currentContent: string = $pharosInstance.getContent(rootId);
let title: string | undefined = $pharosInstance.getIndexTitle(rootId);
let orderedChildren: string[] = $pharosInstance.getOrderedChildIds(rootId);
let isEditing: boolean = false;
let hasCursor: boolean = false;
@ -37,8 +38,8 @@ @@ -37,8 +38,8 @@
if (needsUpdate) {
updateCount++;
needsUpdate = false;
title = $parser.getIndexTitle(rootId);
currentContent = $parser.getContent(rootId);
title = $pharosInstance.getIndexTitle(rootId);
currentContent = $pharosInstance.getContent(rootId);
}
if (subtreeNeedsUpdate) {
@ -46,7 +47,7 @@ @@ -46,7 +47,7 @@
subtreeNeedsUpdate = false;
const prevChildCount = orderedChildren.length;
orderedChildren = $parser.getOrderedChildIds(rootId);
orderedChildren = $pharosInstance.getOrderedChildIds(rootId);
const newChildCount = orderedChildren.length;
// If the number of children has changed, a child has been added or removed, and a child may
@ -62,8 +63,8 @@ @@ -62,8 +63,8 @@
$: {
if (parentId && allowEditing) {
// Check for previous/next siblings on load
const previousSibling = $parser.getNearestSibling(rootId, depth - 1, SiblingSearchDirection.Previous);
const nextSibling = $parser.getNearestSibling(rootId, depth - 1, SiblingSearchDirection.Next);
const previousSibling = $pharosInstance.getNearestSibling(rootId, depth - 1, SiblingSearchDirection.Previous);
const nextSibling = $pharosInstance.getNearestSibling(rootId, depth - 1, SiblingSearchDirection.Next);
// Hide arrows if no siblings exist
hasPreviousSibling = !!previousSibling[0];
@ -113,7 +114,7 @@ @@ -113,7 +114,7 @@
}
$parser.updateEventContent(id, currentContent);
$pharosInstance.updateEventContent(id, currentContent);
}
isEditing = !editing;
@ -121,25 +122,25 @@ @@ -121,25 +122,25 @@
const moveUp = (rootId: string, parentId: string) => {
// Get the previous sibling and its index
const [prevSiblingId, prevIndex] = $parser.getNearestSibling(rootId, depth - 1, SiblingSearchDirection.Previous);
const [prevSiblingId, prevIndex] = $pharosInstance.getNearestSibling(rootId, depth - 1, SiblingSearchDirection.Previous);
if (!prevSiblingId || prevIndex == null) {
return;
}
// Move the current event before the previous sibling.
$parser.moveEvent(rootId, prevSiblingId, false);
$pharosInstance.moveEvent(rootId, prevSiblingId, false);
needsUpdate = true;
};
const moveDown = (rootId: string, parentId: string) => {
// Get the next sibling and its index
const [nextSiblingId, nextIndex] = $parser.getNearestSibling(rootId, depth - 1, SiblingSearchDirection.Next);
const [nextSiblingId, nextIndex] = $pharosInstance.getNearestSibling(rootId, depth - 1, SiblingSearchDirection.Next);
if (!nextSiblingId || nextIndex == null) {
return;
}
// Move the current event after the next sibling
$parser.moveEvent(rootId, nextSiblingId, true);
$pharosInstance.moveEvent(rootId, nextSiblingId, true);
needsUpdate = true;
};
</script>
@ -148,7 +149,7 @@ @@ -148,7 +149,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<section
id={rootId}
class={`note-leather flex space-x-2 justify-between ${sectionClass}`}
class={`note-leather flex space-x-2 justify-between text-wrap break-words ${sectionClass}`}
on:mouseenter={handleMouseEnter}
on:mouseleave={handleMouseLeave}
>
@ -157,7 +158,7 @@ @@ -157,7 +158,7 @@
{#key updateCount}
{#if isEditing}
<form class='w-full'>
<Textarea class='textarea-leather w-full' bind:value={currentContent}>
<Textarea class='textarea-leather w-full whitespace-normal' bind:value={currentContent}>
<div slot='footer' class='flex space-x-2 justify-end'>
<Button
type='reset'
@ -172,7 +173,6 @@ @@ -172,7 +173,6 @@
type='submit'
class='btn-leather min-w-fit'
size='sm'
solid
on:click={() => toggleEditing(rootId, true)}
>
Save
@ -181,13 +181,13 @@ @@ -181,13 +181,13 @@
</Textarea>
</form>
{:else}
<P firstupper={isSectionStart}>
<P class='whitespace-normal' firstupper={isSectionStart}>
{@html currentContent}
</P>
{/if}
{/key}
{:else}
<div class='flex flex-col space-y-2'>
<div class='flex flex-col space-y-2 w-full'>
{#if isEditing}
<ButtonGroup class='w-full'>
<Input type='text' class='input-leather' size='lg' bind:value={title}>

2
src/lib/consts.ts

@ -1,6 +1,6 @@ @@ -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 {

146
src/lib/parser.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk';
import { getNdkInstance } from './ndk';
import asciidoctor, {
import asciidoctor from 'asciidoctor';
import type {
AbstractBlock,
AbstractNode,
Asciidoctor,
@ -8,10 +9,11 @@ import asciidoctor, { @@ -8,10 +9,11 @@ import asciidoctor, {
Document,
Extensions,
Section,
type ProcessorOptions
ProcessorOptions,
} from 'asciidoctor';
import he from 'he';
import { writable, type Writable } from 'svelte/store';
import { indexKind, zettelKinds } from './consts';
interface IndexMetadata {
authors?: string[];
@ -154,6 +156,28 @@ export default class Pharos { @@ -154,6 +156,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<void> {
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.
@ -236,7 +260,8 @@ export default class Pharos { @@ -236,7 +260,8 @@ export default class Pharos {
* - Paragraph: The content is returned as a plain string.
*/
getContent(id: string): string {
const block = this.nodes.get(id) as AbstractBlock;
const normalizedId = this.normalizeId(id);
const block = this.nodes.get(normalizedId!) as AbstractBlock;
switch (block.getContext()) {
case 'paragraph':
@ -558,6 +583,62 @@ export default class Pharos { @@ -558,6 +583,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<string> {
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<string>[] = [];
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
@ -626,6 +707,7 @@ export default class Pharos { @@ -626,6 +707,7 @@ export default class Pharos {
*/
private generateIndexEvent(nodeId: string, pubkey: string): NDKEvent {
const title = (this.nodes.get(nodeId)! as AbstractBlock).getTitle();
// TODO: Use a tags as per NIP-62.
const childTags = Array.from(this.indexToChildEventsMap.get(nodeId)!)
.map(id => ['#e', this.eventIds.get(id)!]);
@ -746,169 +828,169 @@ export default class Pharos { @@ -746,169 +828,169 @@ export default class Pharos {
switch (context) {
case 'admonition':
blockNumber = this.contextCounters.get('admonition') ?? 0;
blockId = `${documentId}_admonition_${blockNumber++}`;
blockId = `${documentId}-admonition-${blockNumber++}`;
this.contextCounters.set('admonition', blockNumber);
break;
case 'audio':
blockNumber = this.contextCounters.get('audio') ?? 0;
blockId = `${documentId}_audio_${blockNumber++}`;
blockId = `${documentId}-audio-${blockNumber++}`;
this.contextCounters.set('audio', blockNumber);
break;
case 'colist':
blockNumber = this.contextCounters.get('colist') ?? 0;
blockId = `${documentId}_colist_${blockNumber++}`;
blockId = `${documentId}-colist-${blockNumber++}`;
this.contextCounters.set('colist', blockNumber);
break;
case 'dlist':
blockNumber = this.contextCounters.get('dlist') ?? 0;
blockId = `${documentId}_dlist_${blockNumber++}`;
blockId = `${documentId}-dlist-${blockNumber++}`;
this.contextCounters.set('dlist', blockNumber);
break;
case 'document':
blockNumber = this.contextCounters.get('document') ?? 0;
blockId = `${documentId}_document_${blockNumber++}`;
blockId = `${documentId}-document-${blockNumber++}`;
this.contextCounters.set('document', blockNumber);
break;
case 'example':
blockNumber = this.contextCounters.get('example') ?? 0;
blockId = `${documentId}_example_${blockNumber++}`;
blockId = `${documentId}-example-${blockNumber++}`;
this.contextCounters.set('example', blockNumber);
break;
case 'floating_title':
blockNumber = this.contextCounters.get('floating_title') ?? 0;
blockId = `${documentId}_floating_title_${blockNumber++}`;
blockId = `${documentId}-floating-title-${blockNumber++}`;
this.contextCounters.set('floating_title', blockNumber);
break;
case 'image':
blockNumber = this.contextCounters.get('image') ?? 0;
blockId = `${documentId}_image_${blockNumber++}`;
blockId = `${documentId}-image-${blockNumber++}`;
this.contextCounters.set('image', blockNumber);
break;
case 'list_item':
blockNumber = this.contextCounters.get('list_item') ?? 0;
blockId = `${documentId}_list_item_${blockNumber++}`;
blockId = `${documentId}-list-item-${blockNumber++}`;
this.contextCounters.set('list_item', blockNumber);
break;
case 'listing':
blockNumber = this.contextCounters.get('listing') ?? 0;
blockId = `${documentId}_listing_${blockNumber++}`;
blockId = `${documentId}-listing-${blockNumber++}`;
this.contextCounters.set('listing', blockNumber);
break;
case 'literal':
blockNumber = this.contextCounters.get('literal') ?? 0;
blockId = `${documentId}_literal_${blockNumber++}`;
blockId = `${documentId}-literal-${blockNumber++}`;
this.contextCounters.set('literal', blockNumber);
break;
case 'olist':
blockNumber = this.contextCounters.get('olist') ?? 0;
blockId = `${documentId}_olist_${blockNumber++}`;
blockId = `${documentId}-olist-${blockNumber++}`;
this.contextCounters.set('olist', blockNumber);
break;
case 'open':
blockNumber = this.contextCounters.get('open') ?? 0;
blockId = `${documentId}_open_${blockNumber++}`;
blockId = `${documentId}-open-${blockNumber++}`;
this.contextCounters.set('open', blockNumber);
break;
case 'page_break':
blockNumber = this.contextCounters.get('page_break') ?? 0;
blockId = `${documentId}_page_break_${blockNumber++}`;
blockId = `${documentId}-page-break-${blockNumber++}`;
this.contextCounters.set('page_break', blockNumber);
break;
case 'paragraph':
blockNumber = this.contextCounters.get('paragraph') ?? 0;
blockId = `${documentId}_paragraph_${blockNumber++}`;
blockId = `${documentId}-paragraph-${blockNumber++}`;
this.contextCounters.set('paragraph', blockNumber);
break;
case 'pass':
blockNumber = this.contextCounters.get('pass') ?? 0;
blockId = `${documentId}_pass_${blockNumber++}`;
blockId = `${documentId}-pass-${blockNumber++}`;
this.contextCounters.set('pass', blockNumber);
break;
case 'preamble':
blockNumber = this.contextCounters.get('preamble') ?? 0;
blockId = `${documentId}_preamble_${blockNumber++}`;
blockId = `${documentId}-preamble-${blockNumber++}`;
this.contextCounters.set('preamble', blockNumber);
break;
case 'quote':
blockNumber = this.contextCounters.get('quote') ?? 0;
blockId = `${documentId}_quote_${blockNumber++}`;
blockId = `${documentId}-quote-${blockNumber++}`;
this.contextCounters.set('quote', blockNumber);
break;
case 'section':
blockNumber = this.contextCounters.get('section') ?? 0;
blockId = `${documentId}_section_${blockNumber++}`;
blockId = `${documentId}-section-${blockNumber++}`;
this.contextCounters.set('section', blockNumber);
break;
case 'sidebar':
blockNumber = this.contextCounters.get('sidebar') ?? 0;
blockId = `${documentId}_sidebar_${blockNumber++}`;
blockId = `${documentId}-sidebar-${blockNumber++}`;
this.contextCounters.set('sidebar', blockNumber);
break;
case 'table':
blockNumber = this.contextCounters.get('table') ?? 0;
blockId = `${documentId}_table_${blockNumber++}`;
blockId = `${documentId}-table-${blockNumber++}`;
this.contextCounters.set('table', blockNumber);
break;
case 'table_cell':
blockNumber = this.contextCounters.get('table_cell') ?? 0;
blockId = `${documentId}_table_cell_${blockNumber++}`;
blockId = `${documentId}-table-cell-${blockNumber++}`;
this.contextCounters.set('table_cell', blockNumber);
break;
case 'thematic_break':
blockNumber = this.contextCounters.get('thematic_break') ?? 0;
blockId = `${documentId}_thematic_break_${blockNumber++}`;
blockId = `${documentId}-thematic-break-${blockNumber++}`;
this.contextCounters.set('thematic_break', blockNumber);
break;
case 'toc':
blockNumber = this.contextCounters.get('toc') ?? 0;
blockId = `${documentId}_toc_${blockNumber++}`;
blockId = `${documentId}-toc-${blockNumber++}`;
this.contextCounters.set('toc', blockNumber);
break;
case 'ulist':
blockNumber = this.contextCounters.get('ulist') ?? 0;
blockId = `${documentId}_ulist_${blockNumber++}`;
blockId = `${documentId}-ulist-${blockNumber++}`;
this.contextCounters.set('ulist', blockNumber);
break;
case 'verse':
blockNumber = this.contextCounters.get('verse') ?? 0;
blockId = `${documentId}_verse_${blockNumber++}`;
blockId = `${documentId}-verse-${blockNumber++}`;
this.contextCounters.set('verse', blockNumber);
break;
case 'video':
blockNumber = this.contextCounters.get('video') ?? 0;
blockId = `${documentId}_video_${blockNumber++}`;
blockId = `${documentId}-video-${blockNumber++}`;
this.contextCounters.set('video', blockNumber);
break;
default:
blockNumber = this.contextCounters.get('block') ?? 0;
blockId = `${documentId}_block_${blockNumber++}`;
blockId = `${documentId}-block-${blockNumber++}`;
this.contextCounters.set('block', blockNumber);
break;
}
@ -984,4 +1066,4 @@ export default class Pharos { @@ -984,4 +1066,4 @@ export default class Pharos {
// #endregion
}
export const parser: Writable<Pharos> = writable(new Pharos(getNdkInstance()));
export const pharosInstance: Writable<Pharos> = writable();

1
src/routes/+layout.server.ts

@ -1 +0,0 @@ @@ -1 +0,0 @@
export const ssr = false;

24
src/routes/+layout.ts

@ -1 +1,23 @@ @@ -1 +1,23 @@
export const prerender = true;
import NDK from '@nostr-dev-kit/ndk';
import type { LayoutLoad } from './$types';
import { standardRelays } from '$lib/consts';
import Pharos, { pharosInstance } from '$lib/parser';
export const ssr = false;
export const load: LayoutLoad = () => {
const ndk = new NDK({
autoConnectUserRelays: true,
enableOutboxModel: true,
explicitRelayUrls: standardRelays,
});
ndk.connect().then(() => console.debug('ndk connected'));
const parser = new Pharos(ndk);
pharosInstance.set(parser);
return {
ndk,
parser,
};
};

56
src/routes/+page.svelte

@ -1,26 +1,41 @@ @@ -1,26 +1,41 @@
<script lang="ts">
import ArticleHeader from "$lib/components/ArticleHeader.svelte";
import { FeedType, indexKind, standardRelays } from "$lib/consts";
import { ndk } from "$lib/ndk";
import { filterValidIndexEvents } from "$lib/utils";
import { NDKEvent, NDKRelaySet, type NDKUser } from "@nostr-dev-kit/ndk";
import { Button, Dropdown, Radio, Skeleton } from "flowbite-svelte";
import { ChevronDownOutline } from "flowbite-svelte-icons";
<script lang='ts'>
import ArticleHeader from '$lib/components/ArticleHeader.svelte';
import { FeedType, indexKind, standardRelays } from '$lib/consts';
import { filterValidIndexEvents } from '$lib/utils';
import NDK, { NDKEvent, NDKRelaySet, type NDKUser } from '@nostr-dev-kit/ndk';
import { Button, Dropdown, Radio, Skeleton } from 'flowbite-svelte';
import { ChevronDownOutline } from 'flowbite-svelte-icons';
import type { PageData } from './$types';
import { setContext } from 'svelte';
let { data }: { data: PageData } = $props();
let ndk: NDK = data.ndk;
let user: NDKUser | null | undefined = $state(ndk.activeUser);
let readRelays: string[] | null | undefined = $state(user?.relayUrls);
let userFollows: Set<NDKUser> | null | undefined = $state(null);
let feedType: FeedType = $state(FeedType.Relays);
$effect(() => {
if (user) {
user.follows().then(follows => userFollows = follows);
}
});
const getEvents = (): Promise<Set<NDKEvent>> =>
// @ts-ignore
$ndk.fetchEvents(
ndk.fetchEvents(
{ kinds: [indexKind] },
{
groupable: true,
skipVerification: false,
skipValidation: false
},
NDKRelaySet.fromRelayUrls(standardRelays, $ndk)
NDKRelaySet.fromRelayUrls(standardRelays, ndk)
).then(filterValidIndexEvents);
const getEventsFromUserRelays = (userRelays: string[]): Promise<Set<NDKEvent>> => {
return $ndk
return ndk
.fetchEvents(
// @ts-ignore
{ kinds: [indexKind] },
@ -35,7 +50,7 @@ @@ -35,7 +50,7 @@
}
const getEventsFromUserFollows = (follows: Set<NDKUser>, userRelays?: string[]): Promise<Set<NDKEvent>> => {
return $ndk
return ndk
.fetchEvents(
{
authors: Array.from(follows ?? []).map(user => user.pubkey),
@ -74,17 +89,6 @@ @@ -74,17 +89,6 @@
}
return skeletonIds;
}
let user: NDKUser | null | undefined;
let readRelays: string[] | null | undefined;
let userFollows: Set<NDKUser> | null | undefined;
let feedType: FeedType = FeedType.Relays;
$: {
user = $ndk.activeUser;
readRelays = user?.relayUrls;
user?.follows().then(follows => userFollows = follows);
}
</script>
<div class='leather flex flex-col flex-grow-0 space-y-4 overflow-y-auto w-max p-2'>
@ -92,7 +96,7 @@ @@ -92,7 +96,7 @@
{#if user == null || readRelays == null}
{#await getEvents()}
{#each getSkeletonIds() as id}
<Skeleton size='lg' id={id} />
<Skeleton size='lg' />
{/each}
{:then events}
{#if events.size > 0}
@ -120,7 +124,7 @@ @@ -120,7 +124,7 @@
{#if feedType === FeedType.Relays && readRelays != null}
{#await getEventsFromUserRelays(readRelays)}
{#each getSkeletonIds() as id}
<Skeleton size='lg' id={id} />
<Skeleton size='lg' />
{/each}
{:then events}
{#if events.size > 0}
@ -134,7 +138,7 @@ @@ -134,7 +138,7 @@
{:else if feedType === FeedType.Follows && userFollows != null}
{#await getEventsFromUserFollows(userFollows, readRelays)}
{#each getSkeletonIds() as id}
<Skeleton size='lg' id={id} />
<Skeleton size='lg' />
{/each}
{:then events}
{#if events.size > 0}

4
src/routes/new/compose/+page.svelte

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
<script lang='ts'>
import Preview from "$lib/components/Preview.svelte";
import { parser } from "$lib/parser";
import { pharosInstance } from "$lib/parser";
import { Heading } from "flowbite-svelte";
let treeNeedsUpdate: boolean = false;
@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
<main class='main-leather flex flex-col space-y-4 max-w-2xl w-full mt-4 mb-4'>
<Heading tag='h1' class='h-leather mb-2'>Compose</Heading>
{#key treeUpdateCount}
<Preview rootId={$parser.getRootIndexId()} allowEditing={true} bind:needsUpdate={treeNeedsUpdate} />
<Preview rootId={$pharosInstance.getRootIndexId()} allowEditing={true} bind:needsUpdate={treeNeedsUpdate} />
{/key}
</main>
</div>

18
src/routes/new/edit/+page.svelte

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
import { Heading, Textarea, Toolbar, ToolbarButton, Tooltip } from "flowbite-svelte";
import { CodeOutline, EyeSolid, PaperPlaneOutline } from "flowbite-svelte-icons";
import Preview from "$lib/components/Preview.svelte";
import Pharos, { parser } from "$lib/parser";
import Pharos, { pharosInstance } from "$lib/parser";
import { ndk } from "$lib/ndk";
import { goto } from "$app/navigation";
@ -15,16 +15,16 @@ @@ -15,16 +15,16 @@
const showPreview = () => {
try {
$parser ??= new Pharos($ndk);
$parser.reset();
$parser.parse(editorText);
$pharosInstance ??= new Pharos($ndk);
$pharosInstance.reset();
$pharosInstance.parse(editorText);
} catch (e) {
console.error(e);
// TODO: Indicate error in UI.
return;
}
rootIndexId = $parser.getRootIndexId();
rootIndexId = $pharosInstance.getRootIndexId();
isEditing = false;
};
@ -34,15 +34,15 @@ @@ -34,15 +34,15 @@
const prepareReview = () => {
try {
$parser.reset();
$parser.parse(editorText);
$pharosInstance.reset();
$pharosInstance.parse(editorText);
} catch (e) {
console.error(e);
// TODO: Indicate error in UI.
return;
}
$parser.generate($ndk.activeUser?.pubkey!);
$pharosInstance.generate($ndk.activeUser?.pubkey!);
goto('/new/compose');
}
</script>
@ -55,7 +55,7 @@ @@ -55,7 +55,7 @@
<Textarea
id='article-content'
class='textarea-leather'
rows=8
rows={8}
placeholder='Write AsciiDoc content'
bind:value={editorText}
>

31
src/routes/publication/+error.svelte

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<script lang='ts'>
import { invalidateAll, goto } from '$app/navigation';
import { Alert, P, Button } from 'flowbite-svelte';
import { ExclamationCircleOutline } from 'flowbite-svelte-icons';
import { page } from '$app/state';
</script>
<main>
<Alert>
<div class='flex items-center space-x-2'>
<ExclamationCircleOutline class='w-6 h-6' />
<span class='text-lg font-medium'>
Failed to load publication.
</span>
</div>
<P size='sm'>
Alexandria failed to find one or more of the events comprising this publication.
</P>
<P size='xs'>
{page.error?.message}
</P>
<div class='flex space-x-2'>
<Button class='btn-leather w-fit' size='sm' onclick={() => invalidateAll()}>
Try Again
</Button>
<Button class='btn-leather w-fit' size='sm' outline onclick={() => goto('/')}>
Return to Home
</Button>
</div>
</Alert>
</main>

40
src/routes/publication/+page.svelte

@ -1,42 +1,18 @@ @@ -1,42 +1,18 @@
<script lang="ts">
import { page } from '$app/stores';
<script lang='ts'>
import Article from '$lib/components/Article.svelte';
import { ndk } from '$lib/ndk';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import { TextPlaceholder } from 'flowbite-svelte';
import type { PageData } from './$types';
import { onDestroy } from 'svelte';
const id = $page.url.searchParams.get('id');
const dTag = $page.url.searchParams.get('d');
let { data }: { data: PageData } = $props();
let event: NDKEvent | null | undefined;
if (id) {
$ndk.fetchEvent(id)
.then(ev => {
event = ev;
})
.catch(err => {
console.error(err);
// TODO: Redirect to 404 page.
});
} else if (dTag) {
$ndk.fetchEvent({ '#d': [dTag] })
.then(ev => {
event = ev;
})
.catch(err => {
console.error(err);
// TODO: Redirect to 404 page.
});
} else {
// TODO: Redirect to 400 page.
}
onDestroy(() => data.parser.reset());
</script>
<main>
{#await event}
{#await data.waitable}
<TextPlaceholder size='xxl' />
{:then ev}
<Article index={ev} />
{:then}
<Article rootId={data.parser.getRootIndexId()} />
{/await}
</main>

42
src/routes/publication/+page.ts

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
import { error } from '@sveltejs/kit';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import type { PageLoad } from './$types';
import { pharosInstance } from '$lib/parser';
export const load: PageLoad = async ({ url, parent }) => {
const id = url.searchParams.get('id');
const dTag = url.searchParams.get('d');
const { ndk, parser } = await parent();
let eventPromise: Promise<NDKEvent | null>;
let indexEvent: NDKEvent | null;
if (id) {
eventPromise = ndk.fetchEvent(id)
.then((ev: NDKEvent | null) => {
return ev;
})
.catch((err: any) => {
error(404, `Failed to fetch publication root event for ID: ${id}\n${err}`);
});
} else if (dTag) {
eventPromise = ndk.fetchEvent({ '#d': [dTag] })
.then((ev: NDKEvent | null) => {
return ev;
})
.catch((err: any) => {
error(404, `Failed to fetch publication root event for d tag: ${dTag}\n${err}`);
});
} else {
error(400, 'No publication root event ID or d tag provided.');
}
indexEvent = await eventPromise as NDKEvent;
const fetchPromise = parser.fetch(indexEvent);
return {
waitable: fetchPromise,
};
};

2
svelte.config.js

@ -12,7 +12,7 @@ const config = { @@ -12,7 +12,7 @@ const config = {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: null, // TODO: Create a 404.html page.
fallback: 'index.html',
precompress: false,
strict: true,
}),

Loading…
Cancel
Save