Browse Source

Merge upstream/master' into feature/network-visual

master
limina1 1 year ago
parent
commit
46a274ee24
  1. 4157
      package-lock.json
  2. 67
      package.json
  3. 4651
      pnpm-lock.yaml
  4. 6
      src/app.d.ts
  5. 137
      src/lib/components/Article.svelte
  6. 38
      src/lib/components/Preview.svelte
  7. 67
      src/lib/parser.ts
  8. 1
      src/routes/+layout.server.ts
  9. 2
      src/routes/+layout.svelte
  10. 24
      src/routes/+layout.ts
  11. 56
      src/routes/+page.svelte
  12. 6
      src/routes/about/+page.svelte
  13. 4
      src/routes/new/compose/+page.svelte
  14. 18
      src/routes/new/edit/+page.svelte
  15. 31
      src/routes/publication/+error.svelte
  16. 44
      src/routes/publication/+page.svelte
  17. 42
      src/routes/publication/+page.ts
  18. 20
      svelte.config.js

4157
package-lock.json generated

File diff suppressed because it is too large Load Diff

67
package.json

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

4651
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 {
namespace App { namespace App {
// interface Error {} // interface Error {}
// interface Locals {} // interface Locals {}
// interface PageData {} interface PageData {
ndk?: NDK;
parser?: Pharos;
waitable?: Promise<any>;
}
// interface Platform {} // interface Platform {}
} }
} }

137
src/lib/components/Article.svelte

@ -1,33 +1,33 @@
<script lang="ts"> <script lang="ts">
import { ndk } from '$lib/ndk'; import {
import type { NDKEvent } from '@nostr-dev-kit/ndk'; Button,
import { page } from '$app/stores'; Sidebar,
import { Button, Heading, Sidebar, SidebarGroup, SidebarItem, SidebarWrapper, Skeleton, TextPlaceholder, Tooltip } from 'flowbite-svelte'; SidebarGroup,
import { onMount } from 'svelte'; SidebarItem,
import { BookOutline } from 'flowbite-svelte-icons'; SidebarWrapper,
import Pharos, { parser } from '$lib/parser'; Skeleton,
import Preview from './Preview.svelte'; TextPlaceholder,
Tooltip,
export let index: NDKEvent | null | undefined; } from "flowbite-svelte";
import { onMount } from "svelte";
$parser ??= new Pharos($ndk); import { BookOutline } from "flowbite-svelte-icons";
import Preview from "./Preview.svelte";
$: activeHash = $page.url.hash; import { pharosInstance } from "$lib/parser";
import { page } from "$app/state";
const getContentRoot = async (index?: NDKEvent | null | undefined): Promise<string | null> => {
if (!index) { let { rootId }: { rootId: string } = $props();
return null;
} if (rootId !== $pharosInstance.getRootIndexId()) {
console.error("Root ID does not match parser root index ID");
}
await $parser.fetch(index); let activeHash = $state(page.url.hash);
return $parser.getRootIndexId();
};
function normalizeHashPath(str: string): string { function normalizeHashPath(str: string): string {
return str return str
.toLowerCase() .toLowerCase()
.replace(/\s+/g, '-') .replace(/\s+/g, "-")
.replace(/[^\w-]/g, ''); .replace(/[^\w-]/g, "");
} }
function scrollToElementWithOffset() { function scrollToElementWithOffset() {
@ -41,7 +41,7 @@
window.scrollTo({ window.scrollTo({
top: offsetPosition, top: offsetPosition,
behavior: 'auto', behavior: "auto",
}); });
} }
} }
@ -66,7 +66,7 @@
const hideTocOnClick = (ev: MouseEvent) => { const hideTocOnClick = (ev: MouseEvent) => {
const target = ev.target as HTMLElement; const target = ev.target as HTMLElement;
if (target.closest('.sidebar-leather') || target.closest('.btn-leather')) { if (target.closest(".sidebar-leather") || target.closest(".btn-leather")) {
return; return;
} }
@ -79,68 +79,53 @@
// Always check whether the TOC sidebar should be visible. // Always check whether the TOC sidebar should be visible.
setTocVisibilityOnResize(); setTocVisibilityOnResize();
window.addEventListener('hashchange', scrollToElementWithOffset); window.addEventListener("hashchange", scrollToElementWithOffset);
// Also handle the case where the user lands on the page with a hash in the URL // Also handle the case where the user lands on the page with a hash in the URL
scrollToElementWithOffset(); scrollToElementWithOffset();
window.addEventListener('resize', setTocVisibilityOnResize); window.addEventListener("resize", setTocVisibilityOnResize);
window.addEventListener('click', hideTocOnClick); window.addEventListener("click", hideTocOnClick);
return () => { return () => {
window.removeEventListener('hashchange', scrollToElementWithOffset); window.removeEventListener("hashchange", scrollToElementWithOffset);
window.removeEventListener('resize', setTocVisibilityOnResize); window.removeEventListener("resize", setTocVisibilityOnResize);
window.removeEventListener('click', hideTocOnClick); window.removeEventListener("click", hideTocOnClick);
}; };
}); });
</script> </script>
{#await getContentRoot(index)} {#if showTocButton && !showToc}
<Sidebar class='sidebar-leather fixed top-20 left-0 px-4 w-60'> <Button
class="btn-leather fixed top-20 left-4 h-6 w-6"
outline={true}
on:click={(ev) => {
showToc = true;
ev.stopPropagation();
}}
>
<BookOutline />
</Button>
<Tooltip>Show Table of Contents</Tooltip>
{/if}
<!-- TODO: Get TOC from parser. -->
<!-- {#if showToc}
<Sidebar class='sidebar-leather fixed top-20 left-0 px-4 w-60' {activeHash}>
<SidebarWrapper> <SidebarWrapper>
<Skeleton/> <SidebarGroup class='sidebar-group-leather overflow-y-scroll'>
{#each events as event}
<SidebarItem
class='sidebar-item-leather'
label={event.getMatchingTags('title')[0][1]}
href={`${$page.url.pathname}#${normalizeHashPath(event.getMatchingTags('title')[0][1])}`}
/>
{/each}
</SidebarGroup>
</SidebarWrapper> </SidebarWrapper>
</Sidebar> </Sidebar>
<TextPlaceholder class='max-w-2xl'/> {/if} -->
{:then rootId} <div class="flex flex-col space-y-4 max-w-2xl">
{#if rootId} <Preview {rootId} />
{#if showTocButton && !showToc} </div>
<Button
class='btn-leather fixed top-20 left-4 h-6 w-6'
outline={true}
on:click={ev => {
showToc = true;
ev.stopPropagation();
}}
>
<BookOutline />
</Button>
<Tooltip>
Show Table of Contents
</Tooltip>
{/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'>
{#each events as event}
<SidebarItem
class='sidebar-item-leather'
label={event.getMatchingTags('title')[0][1]}
href={`${$page.url.pathname}#${normalizeHashPath(event.getMatchingTags('title')[0][1])}`}
/>
{/each}
</SidebarGroup>
</SidebarWrapper>
</Sidebar>
{/if} -->
<div class='flex flex-col space-y-4 max-w-2xl'>
<Preview rootId={rootId} />
</div>
{:else}
<!-- TODO: Display empty state. -->
{/if}
{/await}
<style> <style>
:global(.sidebar-group-leather) { :global(.sidebar-group-leather) {

38
src/lib/components/Preview.svelte

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

67
src/lib/parser.ts

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

2
src/routes/+layout.svelte

@ -1,7 +1,7 @@
<script> <script>
import "../app.css"; import "../app.css";
import Navigation from "$lib/components/Navigation.svelte"; import Navigation from "$lib/components/Navigation.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
// Compute viewport height. // Compute viewport height.
$: displayHeight = window.innerHeight; $: displayHeight = window.innerHeight;

24
src/routes/+layout.ts

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

6
src/routes/about/+page.svelte

@ -7,9 +7,9 @@
<div class='w-full flex justify-center'> <div class='w-full flex justify-center'>
<main class='main-leather flex flex-col space-y-4 max-w-2xl w-full mt-4 mb-4'> <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'>About</Heading> <Heading tag='h1' class='h-leather mb-2'>About</Heading>
<p>Alexandria is a <a href="https://wikistr.com/nkbip-01" class="text-indigo-600 underline">Nostr Knowledge Base (NKB)</a> and a reader for long-form articles. <p>Alexandria is an editor and generator for <a href="https://github.com/nostr-protocol/nips/pull/1600" class="text-indigo-600 underline">curated publications</a> and will soon also be a reader for long-form articles and wiki pages.
It is produced by <a href="https://wikistr.com/gitcitadel-project" class="text-indigo-600 underline">GitCitadel</a>.</p> It is produced by the <a href="https://wikistr.com/gitcitadel-project" class="text-indigo-600 underline">GitCitadel project team</a>.</p>
<p>Please submit support issues on the <a href="https://gitworkshop.dev/r/naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqy88wumn8ghj7mn0wvhxcmmv9uqq5emfw33kjarpv3jkcs83wav" class="text-indigo-600 underline">GitWorkshop page</a> and follow us on <a href="https://github.com/ShadowySupercode/gitcitadel" class="text-indigo-600 underline">GitHub</a>.</p> <p>Please submit support issues on the <a href="https://gitcitadel.com/r/naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqy88wumn8ghj7mn0wvhxcmmv9uqq5emfw33kjarpv3jkcs83wav" class="text-indigo-600 underline">project repo page</a> and follow us on <a href="https://github.com/ShadowySupercode/gitcitadel" class="text-indigo-600 underline">GitHub</a> and <a href="https://geyser.fund/project/gitcitadel" class="text-indigo-600 underline">Geyserfund</a>.</p>
</main> </main>
</div> </div>

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

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

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

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

31
src/routes/publication/+error.svelte

@ -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>

44
src/routes/publication/+page.svelte

@ -1,42 +1,18 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import Article from "$lib/components/Article.svelte";
import Article from '$lib/components/Article.svelte'; import { TextPlaceholder } from "flowbite-svelte";
import { ndk } from '$lib/ndk'; import type { PageData } from "./$types";
import type { NDKEvent } from '@nostr-dev-kit/ndk'; import { onDestroy } from "svelte";
import { TextPlaceholder } from 'flowbite-svelte';
const id = $page.url.searchParams.get('id'); let { data }: { data: PageData } = $props();
const dTag = $page.url.searchParams.get('d');
let event: NDKEvent | null | undefined; onDestroy(() => data.parser.reset());
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.
}
</script> </script>
<main> <main>
{#await event} {#await data.waitable}
<TextPlaceholder size='xxl' /> <TextPlaceholder size="xxl" />
{:then ev} {:then}
<Article index={ev} /> <Article rootId={data.parser.getRootIndexId()} />
{/await} {/await}
</main> </main>

42
src/routes/publication/+page.ts

@ -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,
};
};

20
svelte.config.js

@ -1,5 +1,5 @@
import adapter from '@sveltejs/adapter-static'; import adapter from "@sveltejs/adapter-static";
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
@ -10,18 +10,18 @@ const config = {
kit: { kit: {
// Static adapter // Static adapter
adapter: adapter({ adapter: adapter({
pages: 'build', pages: "build",
assets: 'build', assets: "build",
fallback: null, // TODO: Create a 404.html page. fallback: "index.html",
precompress: false, precompress: false,
strict: true, strict: true,
}), }),
alias: { alias: {
$lib: 'src/lib', $lib: "src/lib",
$components: 'src/lib/components', $components: "src/lib/components",
$cards: 'src/lib/cards' $cards: "src/lib/cards",
} },
} },
}; };
export default config; export default config;

Loading…
Cancel
Save