diff --git a/deno.lock b/deno.lock index e3b827f..f113237 100644 --- a/deno.lock +++ b/deno.lock @@ -2862,6 +2862,22 @@ } }, "workspace": { + "dependencies": [ + "npm:@nostr-dev-kit/ndk-cache-dexie@2.5", + "npm:@nostr-dev-kit/ndk@2.11", + "npm:@popperjs/core@2.11", + "npm:@tailwindcss/forms@0.5", + "npm:@tailwindcss/typography@0.5", + "npm:asciidoctor@3.0", + "npm:d3@7.9", + "npm:flowbite-svelte-icons@2.0", + "npm:flowbite-svelte@0.44", + "npm:flowbite@2.2", + "npm:he@1.2", + "npm:nostr-tools@2.10", + "npm:svelte@5.0", + "npm:tailwind-merge@2.5" + ], "packageJson": { "dependencies": [ "npm:@nostr-dev-kit/ndk-cache-dexie@2.5", diff --git a/src/app.css b/src/app.css index 358d034..011ebd9 100644 --- a/src/app.css +++ b/src/app.css @@ -74,7 +74,7 @@ @apply hover:bg-primary-100 dark:hover:bg-primary-800; } - /* Heading */ + /* Section headers */ h1.h-leather, h2.h-leather, h3.h-leather, diff --git a/src/lib/Modal.svelte b/src/lib/components/Modal.svelte similarity index 100% rename from src/lib/Modal.svelte rename to src/lib/components/Modal.svelte diff --git a/src/lib/components/Preview.svelte b/src/lib/components/Preview.svelte index f7dfe03..0c85484 100644 --- a/src/lib/components/Preview.svelte +++ b/src/lib/components/Preview.svelte @@ -3,6 +3,7 @@ import { Button, ButtonGroup, CloseButton, Input, P, Textarea, Tooltip } from 'flowbite-svelte'; import { CaretDownSolid, CaretUpSolid, EditOutline } from 'flowbite-svelte-icons'; import Self from './Preview.svelte'; + import { contentParagraph, sectionHeading } from '$lib/snippets/PublicationSnippets.svelte'; // TODO: Fix move between parents. @@ -150,31 +151,12 @@ {#snippet sectionHeading(title: string, depth: number)} - {#if depth === 0} -

- {title} -

- {:else if depth === 1} -

- {title} -

- {:else if depth === 2} -

- {title} -

- {:else if depth === 3} -

- {title} -

- {:else if depth === 4} -
- {title} -
- {:else} -
- {title} -
- {/if} + {@const headingLevel = Math.min(depth + 1, 6)} + {@const className = $pharosInstance.isFloatingTitle(rootId) ? 'discrete' : 'h-leather'} + + + {title} + {/snippet} {#snippet contentParagraph(content: string, publicationType: string)} diff --git a/src/lib/components/PublicationHeader.svelte b/src/lib/components/PublicationHeader.svelte index f9ded78..c7f9e15 100644 --- a/src/lib/components/PublicationHeader.svelte +++ b/src/lib/components/PublicationHeader.svelte @@ -1,6 +1,6 @@ - + + + diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index 00a14c8..fdcb73b 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -1,62 +1,105 @@
-
+
- About + About Version: {gitTagVersion}
-

Alexandria is a reader and writer for curated publications (in Asciidoc), and will eventually also support long-form articles (Markdown) and wiki pages (Asciidoc). It is produced by the GitCitadel project team.

- -

Please submit support issues on the project repo page and follow us on GitHub and Geyserfund.

- -

We are easiest to contact over our Nostr address npub1s3h…75wz.

- - Overview - -

Alexandria opens up to the landing page, where the user can: login (top-right), select whether to only view the publications hosted on the thecitadel document relay or add in their own relays, and scroll/search the publications.

- -

Landing page

-

Relay selection

- -

There is also the ability to view the publications as a diagram, if you click on "Visualize", and to publish an e-book or other document (coming soon).

- -

If you click on a card, which represents a 30040 index event, the associated reading view opens to the publication. The app then pulls all of the content events (30041s), in the order in which they are indexed, and displays them as a single document.

-

Each 30041 section is also a level in the table of contents, which can be accessed from the floating icon top-left in the reading view. This allows for navigation within the publication. (This functionality has been temporarily disabled.)

- -

ToC icon

-

Table of contents example

+

+ Alexandria is a reader and writer for curated publications (in Asciidoc), and will eventually also support long-form articles (Markdown) and wiki pages (Asciidoc). It is produced by the GitCitadel project team. +

- Typical use cases +

+ Please submit support issues on the Alexandria repo page and follow us on GitHub and Geyserfund. +

- For e-books -

The most common use for Alexandria is for e-books: both those users have written themselves and those uploaded to Nostr from other sources. The first minor version of the app, Gutenberg, is focused on displaying and producing these publications.

+

+ We are easiest to contact over our Nostr address npub1s3h…75wz. +

+ + Overview + +

+ Alexandria opens up to the landing page, where the user can: login (top-right), select whether to only view the publications hosted on the thecitadel document relay or add in their own relays, and scroll/search the publications. +

+ +
+ Landing page + Relay selection +
+ +

+ There is also the ability to view the publications as a diagram, if you click on "Visualize", and to publish an e-book or other document (coming soon). +

+ +

+ If you click on a card, which represents a 30040 index event, the associated reading view opens to the publication. The app then pulls all of the content events (30041s), in the order in which they are indexed, and displays them as a single document. +

+ +

+ Each 30041 section is also a level in the table of contents, which can be accessed from the floating icon top-left in the reading view. This allows for navigation within the publication. (This functionality has been temporarily disabled.) +

+ +
+ ToC icon + Table of contents example +
+ + Typical use cases -

An example of a book is Jane Eyre

+ For e-books + +

+ The most common use for Alexandria is for e-books: both those users have written themselves and those uploaded to Nostr from other sources. The first minor version of the app, Gutenberg, is focused on displaying and producing these publications. +

-

Jane Eyre, by Charlotte Brontë

+

+ An example of a book is Jane Eyre +

- For scientific papers -

Alexandria will also display research papers with Asciimath and LaTeX embedding, and the normal advanced formatting options available for Asciidoc. In addition, we will be implementing special citation events, which will serve as an alternative or addition to the normal footnotes.

+
+ Jane Eyre, by Charlotte Brontë +
+ + For scientific papers -

Correctly displaying such papers, integrating citations, and allowing them to be reviewed (with kind 1111 comments), and annotated (with highlights) by users, is the focus of the second minor version, Euler.

+

+ Alexandria will also display research papers with Asciimath and LaTeX embedding, and the normal advanced formatting options available for Asciidoc. In addition, we will be implementing special citation events, which will serve as an alternative or addition to the normal footnotes. +

-

Euler will also pioneer the HTTP-based (rather than websocket-based) e-paper compatible version of the web app.

+

+ Correctly displaying such papers, integrating citations, and allowing them to be reviewed (with kind 1111 comments), and annotated (with highlights) by users, is the focus of the second minor version, Euler. +

-

An example of a research paper is Less Partnering, Less Children, or Both?

+

+ Euler will also pioneer the HTTP-based (rather than websocket-based) e-paper compatible version of the web app. +

-

Research paper

+

+ An example of a research paper is Less Partnering, Less Children, or Both? +

- For documentation -

Our own team uses Alexandria to document the app, to display our blog entries, as well as to store copies of our most interesting technical specifications.

+
+ Research paper +
+ + For documentation -

Documentation

+

+ Our own team uses Alexandria to document the app, to display our blog entries, as well as to store copies of our most interesting technical specifications. +

-
+
+ Documentation +
+ +
+ diff --git a/src/routes/publication/+page.ts b/src/routes/publication/+page.ts index 3d58b39..286063a 100644 --- a/src/routes/publication/+page.ts +++ b/src/routes/publication/+page.ts @@ -1,43 +1,101 @@ import { error } from '@sveltejs/kit'; -import { NDKRelay, NDKRelaySet, type NDKEvent } from '@nostr-dev-kit/ndk'; +import type { NDKEvent } from '@nostr-dev-kit/ndk'; import type { PageLoad } from './$types'; -import { get } from 'svelte/store'; -import { getActiveRelays, inboxRelays, ndkInstance } from '$lib/ndk'; -import { standardRelays } from '$lib/consts'; +import { nip19 } from 'nostr-tools'; +import { getActiveRelays } from '$lib/ndk.ts'; -export const load: PageLoad = async ({ url, parent }) => { - const id = url.searchParams.get('id'); - const dTag = url.searchParams.get('d'); - - const { ndk, parser } = await parent(); +/** + * Decodes an naddr identifier and returns a filter object + */ +function decodeNaddr(id: string) { + try { + if (!id.startsWith('naddr')) return {}; + + const decoded = nip19.decode(id); + if (decoded.type !== 'naddr') return {}; + + const data = decoded.data; + return { + kinds: [data.kind], + authors: [data.pubkey], + '#d': [data.identifier] + }; + } catch (e) { + console.error('Failed to decode naddr:', e); + return null; + } +} - let eventPromise: Promise; - let indexEvent: NDKEvent | null; +/** + * Fetches an event by ID or filter + */ +async function fetchEventById(ndk: any, id: string): Promise { + const filter = decodeNaddr(id); + + // Handle the case where filter is null (decoding error) + if (filter === null) { + // If we can't decode the naddr, try using the raw ID + try { + const event = await ndk.fetchEvent(id); + if (!event) { + throw new Error(`Event not found for ID: ${id}`); + } + return event; + } catch (err) { + throw error(404, `Failed to fetch publication root event.\n${err}`); + } + } + + const hasFilter = Object.keys(filter).length > 0; + + try { + const event = await (hasFilter ? + ndk.fetchEvent(filter) : + ndk.fetchEvent(id)); + + if (!event) { + throw new Error(`Event not found for ID: ${id}`); + } + return event; + } catch (err) { + throw error(404, `Failed to fetch publication root event.\n${err}`); + } +} - 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 = new Promise(resolve => { - ndk - .fetchEvent({ '#d': [dTag] }, { closeOnEose: false }, getActiveRelays(ndk)) - .then((event: NDKEvent | null) => { - resolve(event); - }) - .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.'); +/** + * Fetches an event by d tag + */ +async function fetchEventByDTag(ndk: any, dTag: string): Promise { + try { + const event = await ndk.fetchEvent( + { '#d': [dTag] }, + { closeOnEose: false }, + getActiveRelays(ndk) + ); + + if (!event) { + throw new Error(`Event not found for d tag: ${dTag}`); + } + return event; + } catch (err) { + throw error(404, `Failed to fetch publication root event.\n${err}`); } +} - indexEvent = await eventPromise as NDKEvent; +export const load: PageLoad = async ({ url, parent }: { url: URL; parent: () => Promise }) => { + const id = url.searchParams.get('id'); + const dTag = url.searchParams.get('d'); + const { ndk, parser } = await parent(); + + if (!id && !dTag) { + throw error(400, 'No publication root event ID or d tag provided.'); + } + + // Fetch the event based on available parameters + const indexEvent = id + ? await fetchEventById(ndk, id) + : await fetchEventByDTag(ndk, dTag!); + const publicationType = indexEvent?.getMatchingTags('type')[0]?.[1]; const fetchPromise = parser.fetch(indexEvent); diff --git a/src/styles/publications.css b/src/styles/publications.css index b2b2847..fe30740 100644 --- a/src/styles/publications.css +++ b/src/styles/publications.css @@ -229,4 +229,28 @@ .audioblock .content audio { @apply w-full; } + + /* Discrete headers */ + h3.discrete, + h4.discrete, + h5.discrete, + h6.discrete { + @apply text-gray-800 dark:text-gray-300; + } + + h3.discrete { + @apply text-2xl font-bold; + } + + h4.discrete { + @apply text-xl font-bold; + } + + h5.discrete { + @apply text-lg font-semibold; + } + + h6.discrete { + @apply text-base font-semibold; + } } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 794b95b..ec41776 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, - "strict": true + "strict": true, + "allowImportingTsExtensions": true } // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // diff --git a/vite.config.ts b/vite.config.ts index 7a6e207..dfbb51a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,6 +16,12 @@ function getLatestGitTag() { export default defineConfig({ plugins: [sveltekit()], + resolve: { + alias: { + $lib: './src/lib', + $components: './src/components' + } + }, test: { include: ['./tests/unit/**/*.unit-test.js'] },