19 changed files with 831 additions and 139 deletions
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
export class Lazy<T> { |
||||
#value?: T; |
||||
#resolver: () => Promise<T>; |
||||
|
||||
constructor(resolver: () => Promise<T>) { |
||||
this.#resolver = resolver; |
||||
} |
||||
|
||||
async value(): Promise<T> { |
||||
if (!this.#value) { |
||||
this.#value = await this.#resolver(); |
||||
} |
||||
|
||||
return this.#value; |
||||
} |
||||
} |
||||
@ -0,0 +1,430 @@
@@ -0,0 +1,430 @@
|
||||
import type NDK from "@nostr-dev-kit/ndk"; |
||||
import type { NDKEvent } from "@nostr-dev-kit/ndk"; |
||||
import { Lazy } from "./lazy.ts"; |
||||
import { findIndexAsync as _findIndexAsync } from '../utils.ts'; |
||||
|
||||
enum PublicationTreeNodeType { |
||||
Root, |
||||
Branch, |
||||
Leaf, |
||||
} |
||||
|
||||
interface PublicationTreeNode { |
||||
type: PublicationTreeNodeType; |
||||
address: string; |
||||
parent?: PublicationTreeNode; |
||||
children?: Array<Lazy<PublicationTreeNode>>; |
||||
} |
||||
|
||||
export class PublicationTree implements AsyncIterable<NDKEvent> { |
||||
/** |
||||
* The root node of the tree. |
||||
*/ |
||||
#root: PublicationTreeNode; |
||||
|
||||
/** |
||||
* A map of addresses in the tree to their corresponding nodes. |
||||
*/ |
||||
#nodes: Map<string, Lazy<PublicationTreeNode>>; |
||||
|
||||
/** |
||||
* A map of addresses in the tree to their corresponding events. |
||||
*/ |
||||
#events: Map<string, NDKEvent>; |
||||
|
||||
/** |
||||
* An ordered list of the addresses of the leaves of the tree. |
||||
*/ |
||||
#leaves: string[] = []; |
||||
|
||||
/** |
||||
* The address of the last-visited node. Used for iteration and progressive retrieval. |
||||
*/ |
||||
#bookmark?: string; |
||||
|
||||
/** |
||||
* The NDK instance used to fetch events. |
||||
*/ |
||||
#ndk: NDK; |
||||
|
||||
constructor(rootEvent: NDKEvent, ndk: NDK) { |
||||
const rootAddress = rootEvent.tagAddress(); |
||||
this.#root = { |
||||
type: PublicationTreeNodeType.Root, |
||||
address: rootAddress, |
||||
children: [], |
||||
}; |
||||
|
||||
this.#nodes = new Map<string, Lazy<PublicationTreeNode>>(); |
||||
this.#nodes.set(rootAddress, new Lazy<PublicationTreeNode>(() => Promise.resolve(this.#root))); |
||||
|
||||
this.#events = new Map<string, NDKEvent>(); |
||||
this.#events.set(rootAddress, rootEvent); |
||||
|
||||
this.#ndk = ndk; |
||||
} |
||||
|
||||
/** |
||||
* Adds an event to the publication tree. |
||||
* @param event The event to be added. |
||||
* @param parentEvent The parent event of the event to be added. |
||||
* @throws An error if the parent event is not in the tree. |
||||
* @description The parent event must already be in the tree. Use |
||||
* {@link PublicationTree.getEvent} to retrieve an event already in the tree. |
||||
*/ |
||||
async addEvent(event: NDKEvent, parentEvent: NDKEvent) { |
||||
const address = event.tagAddress(); |
||||
const parentAddress = parentEvent.tagAddress(); |
||||
const parentNode = await this.#nodes.get(parentAddress)?.value(); |
||||
|
||||
if (!parentNode) { |
||||
throw new Error( |
||||
`PublicationTree: Parent node with address ${parentAddress} not found.` |
||||
); |
||||
} |
||||
|
||||
const node: PublicationTreeNode = { |
||||
type: await this.#getNodeType(event), |
||||
address, |
||||
parent: parentNode, |
||||
children: [], |
||||
}; |
||||
const lazyNode = new Lazy<PublicationTreeNode>(() => Promise.resolve(node)); |
||||
parentNode.children!.push(lazyNode); |
||||
this.#nodes.set(address, lazyNode); |
||||
this.#events.set(address, event); |
||||
} |
||||
|
||||
/** |
||||
* Lazily adds an event to the publication tree by address if the full event is not already |
||||
* loaded into memory. |
||||
* @param address The address of the event to add. |
||||
* @param parentEvent The parent event of the event to add. |
||||
* @description The parent event must already be in the tree. Use |
||||
* {@link PublicationTree.getEvent} to retrieve an event already in the tree. |
||||
*/ |
||||
async addEventByAddress(address: string, parentEvent: NDKEvent) { |
||||
const parentAddress = parentEvent.tagAddress(); |
||||
const parentNode = await this.#nodes.get(parentAddress)?.value(); |
||||
|
||||
if (!parentNode) { |
||||
throw new Error( |
||||
`PublicationTree: Parent node with address ${parentAddress} not found.` |
||||
); |
||||
} |
||||
|
||||
await this.#addNode(address, parentNode); |
||||
} |
||||
|
||||
/** |
||||
* Retrieves an event from the publication tree. |
||||
* @param address The address of the event to retrieve. |
||||
* @returns The event, or null if the event is not found. |
||||
*/ |
||||
async getEvent(address: string): Promise<NDKEvent | null> { |
||||
let event = this.#events.get(address) ?? null; |
||||
if (!event) { |
||||
event = await this.#depthFirstRetrieve(address); |
||||
} |
||||
|
||||
return event; |
||||
} |
||||
|
||||
/** |
||||
* Retrieves the addresses of the loaded children, if any, of the node with the given address. |
||||
* @param address The address of the parent node. |
||||
* @returns An array of addresses of any loaded child nodes. |
||||
*/ |
||||
async getChildAddresses(address: string): Promise<string[]> { |
||||
const node = await this.#nodes.get(address)?.value(); |
||||
if (!node) { |
||||
throw new Error(`PublicationTree: Node with address ${address} not found.`); |
||||
} |
||||
|
||||
return Promise.all( |
||||
node.children?.map(async child => |
||||
(await child.value()).address |
||||
) ?? [] |
||||
); |
||||
} |
||||
/** |
||||
* Retrieves the events in the hierarchy of the event with the given address. |
||||
* @param address The address of the event for which to retrieve the hierarchy. |
||||
* @returns Returns an array of events in the addressed event's hierarchy, beginning with the |
||||
* root and ending with the addressed event. |
||||
*/ |
||||
async getHierarchy(address: string): Promise<NDKEvent[]> { |
||||
let node = await this.#nodes.get(address)?.value(); |
||||
if (!node) { |
||||
throw new Error(`PublicationTree: Node with address ${address} not found.`); |
||||
} |
||||
|
||||
const hierarchy: NDKEvent[] = [this.#events.get(address)!]; |
||||
|
||||
while (node.parent) { |
||||
hierarchy.push(this.#events.get(node.parent.address)!); |
||||
node = node.parent; |
||||
} |
||||
|
||||
return hierarchy.reverse(); |
||||
} |
||||
|
||||
/** |
||||
* Sets a start point for iteration over the leaves of the tree. |
||||
* @param address The address of the event to bookmark. |
||||
*/ |
||||
setBookmark(address: string) { |
||||
this.#bookmark = address; |
||||
this.#cursor.tryMoveTo(address); |
||||
} |
||||
|
||||
// #region Iteration Cursor
|
||||
|
||||
#cursor = new class { |
||||
target: PublicationTreeNode | null | undefined; |
||||
|
||||
#tree: PublicationTree; |
||||
|
||||
constructor(tree: PublicationTree) { |
||||
this.#tree = tree; |
||||
} |
||||
|
||||
async tryMoveTo(address?: string) { |
||||
if (!address) { |
||||
const startEvent = await this.#tree.#depthFirstRetrieve(); |
||||
this.target = await this.#tree.#nodes.get(startEvent!.tagAddress())?.value(); |
||||
} else { |
||||
this.target = await this.#tree.#nodes.get(address)?.value(); |
||||
} |
||||
|
||||
if (!this.target) { |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
async tryMoveToFirstChild(): Promise<boolean> { |
||||
if (!this.target) { |
||||
throw new Error("Cursor: Target node is null or undefined."); |
||||
} |
||||
|
||||
if (this.target.type === PublicationTreeNodeType.Leaf) { |
||||
return false; |
||||
} |
||||
|
||||
this.target = (await this.target.children?.at(0)?.value())!; |
||||
return true; |
||||
} |
||||
|
||||
async tryMoveToNextSibling(): Promise<boolean> { |
||||
if (!this.target) { |
||||
throw new Error("Cursor: Target node is null or undefined."); |
||||
} |
||||
|
||||
const parent = this.target.parent; |
||||
const siblings = parent?.children; |
||||
if (!siblings) { |
||||
return false; |
||||
} |
||||
|
||||
const currentIndex = await siblings.findIndexAsync( |
||||
async (sibling: Lazy<PublicationTreeNode>) => (await sibling.value()).address === this.target!.address |
||||
); |
||||
|
||||
if (currentIndex === -1) { |
||||
return false; |
||||
} |
||||
|
||||
const nextSibling = (await siblings.at(currentIndex + 1)?.value()) ?? null; |
||||
if (!nextSibling) { |
||||
return false; |
||||
} |
||||
|
||||
this.target = nextSibling; |
||||
return true; |
||||
} |
||||
|
||||
tryMoveToParent(): boolean { |
||||
if (!this.target) { |
||||
throw new Error("Cursor: Target node is null or undefined."); |
||||
} |
||||
|
||||
const parent = this.target.parent; |
||||
if (!parent) { |
||||
return false; |
||||
} |
||||
|
||||
this.target = parent; |
||||
return true; |
||||
} |
||||
}(this); |
||||
|
||||
// #endregion
|
||||
|
||||
// #region Async Iterator Implementation
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterator<NDKEvent> { |
||||
return this; |
||||
} |
||||
|
||||
async next(): Promise<IteratorResult<NDKEvent>> { |
||||
if (!this.#cursor.target) { |
||||
await this.#cursor.tryMoveTo(this.#bookmark); |
||||
} |
||||
|
||||
do { |
||||
if (await this.#cursor.tryMoveToFirstChild()) { |
||||
continue; |
||||
} |
||||
|
||||
if (await this.#cursor.tryMoveToNextSibling()) { |
||||
continue; |
||||
} |
||||
|
||||
if (this.#cursor.tryMoveToParent()) { |
||||
continue; |
||||
} |
||||
|
||||
if (this.#cursor.target?.type === PublicationTreeNodeType.Root) { |
||||
return { done: true, value: null }; |
||||
} |
||||
} while (this.#cursor.target?.type !== PublicationTreeNodeType.Leaf); |
||||
|
||||
const event = await this.getEvent(this.#cursor.target!.address); |
||||
return { done: false, value: event! }; |
||||
} |
||||
|
||||
// #endregion
|
||||
|
||||
// #region Private Methods
|
||||
|
||||
/** |
||||
* Traverses the publication tree in a depth-first manner to retrieve an event, filling in |
||||
* missing nodes during the traversal. |
||||
* @param address The address of the event to retrieve. If no address is provided, the function |
||||
* will return the first leaf in the tree. |
||||
* @returns The event, or null if the event is not found. |
||||
*/ |
||||
async #depthFirstRetrieve(address?: string): Promise<NDKEvent | null> { |
||||
if (address && this.#nodes.has(address)) { |
||||
return this.#events.get(address)!; |
||||
} |
||||
|
||||
const stack: string[] = [this.#root.address]; |
||||
let currentNode: PublicationTreeNode | null | undefined = this.#root; |
||||
let currentEvent: NDKEvent | null | undefined = this.#events.get(this.#root.address)!; |
||||
while (stack.length > 0) { |
||||
const currentAddress = stack.pop(); |
||||
currentNode = await this.#nodes.get(currentAddress!)?.value(); |
||||
if (!currentNode) { |
||||
throw new Error(`PublicationTree: Node with address ${currentAddress} not found.`); |
||||
} |
||||
|
||||
currentEvent = this.#events.get(currentAddress!); |
||||
if (!currentEvent) { |
||||
throw new Error(`PublicationTree: Event with address ${currentAddress} not found.`); |
||||
} |
||||
|
||||
// Stop immediately if the target of the search is found.
|
||||
if (address != null && currentAddress === address) { |
||||
return currentEvent; |
||||
} |
||||
|
||||
const currentChildAddresses = currentEvent.tags |
||||
.filter(tag => tag[0] === 'a') |
||||
.map(tag => tag[1]); |
||||
|
||||
// If the current event has no children, it is a leaf.
|
||||
if (currentChildAddresses.length === 0) { |
||||
// Return the first leaf if no address was provided.
|
||||
if (address == null) { |
||||
return currentEvent!; |
||||
} |
||||
|
||||
continue; |
||||
} |
||||
|
||||
// Augment the tree with the children of the current event.
|
||||
for (const childAddress of currentChildAddresses) { |
||||
if (this.#nodes.has(childAddress)) { |
||||
continue; |
||||
} |
||||
|
||||
await this.#addNode(childAddress, currentNode!); |
||||
} |
||||
|
||||
// Push the popped address's children onto the stack for the next iteration.
|
||||
while (currentChildAddresses.length > 0) { |
||||
const nextAddress = currentChildAddresses.pop()!; |
||||
stack.push(nextAddress); |
||||
} |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
#addNode(address: string, parentNode: PublicationTreeNode) { |
||||
const lazyNode = new Lazy<PublicationTreeNode>(() => this.#resolveNode(address, parentNode)); |
||||
parentNode.children!.push(lazyNode); |
||||
this.#nodes.set(address, lazyNode); |
||||
} |
||||
|
||||
/** |
||||
* Resolves a node address into an event, and creates new nodes for its children. |
||||
*
|
||||
* This method is intended for use as a {@link Lazy} resolver. |
||||
*
|
||||
* @param address The address of the node to resolve. |
||||
* @param parentNode The parent node of the node to resolve. |
||||
* @returns The resolved node. |
||||
*/ |
||||
async #resolveNode( |
||||
address: string, |
||||
parentNode: PublicationTreeNode |
||||
): Promise<PublicationTreeNode> { |
||||
const [kind, pubkey, dTag] = address.split(':'); |
||||
const event = await this.#ndk.fetchEvent({ |
||||
kinds: [parseInt(kind)], |
||||
authors: [pubkey], |
||||
'#d': [dTag], |
||||
}); |
||||
|
||||
if (!event) { |
||||
throw new Error( |
||||
`PublicationTree: Event with address ${address} not found.` |
||||
); |
||||
} |
||||
|
||||
this.#events.set(address, event); |
||||
|
||||
const childAddresses = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1]); |
||||
|
||||
const node: PublicationTreeNode = { |
||||
type: await this.#getNodeType(event), |
||||
address, |
||||
parent: parentNode, |
||||
children: [], |
||||
}; |
||||
|
||||
for (const address of childAddresses) { |
||||
this.addEventByAddress(address, event); |
||||
} |
||||
|
||||
return node; |
||||
} |
||||
|
||||
async #getNodeType(event: NDKEvent): Promise<PublicationTreeNodeType> { |
||||
if (event.tagAddress() === this.#root.address) { |
||||
return PublicationTreeNodeType.Root; |
||||
} |
||||
|
||||
if (event.kind === 30040 && event.tags.some(tag => tag[0] === 'a')) { |
||||
return PublicationTreeNodeType.Branch; |
||||
} |
||||
|
||||
return PublicationTreeNodeType.Leaf; |
||||
} |
||||
|
||||
// #endregion
|
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
<script module lang='ts'> |
||||
import { P } from 'flowbite-svelte'; |
||||
|
||||
export { contentParagraph, sectionHeading }; |
||||
</script> |
||||
|
||||
{#snippet sectionHeading(title: string, depth: number)} |
||||
{#if depth === 0} |
||||
<h1 class='h-leather'> |
||||
{title} |
||||
</h1> |
||||
{:else if depth === 1} |
||||
<h2 class='h-leather'> |
||||
{title} |
||||
</h2> |
||||
{:else if depth === 2} |
||||
<h3 class='h-leather'> |
||||
{title} |
||||
</h3> |
||||
{:else if depth === 3} |
||||
<h4 class='h-leather'> |
||||
{title} |
||||
</h4> |
||||
{:else if depth === 4} |
||||
<h5 class='h-leather'> |
||||
{title} |
||||
</h5> |
||||
{:else} |
||||
<h6 class='h-leather'> |
||||
{title} |
||||
</h6> |
||||
{/if} |
||||
{/snippet} |
||||
|
||||
{#snippet contentParagraph(content: string, publicationType: string, isSectionStart: boolean)} |
||||
{#if publicationType === 'novel'} |
||||
<P class='whitespace-normal' firstupper={isSectionStart}> |
||||
{@html content} |
||||
</P> |
||||
{:else} |
||||
<P class='whitespace-normal' firstupper={false}> |
||||
{@html content} |
||||
</P> |
||||
{/if} |
||||
{/snippet} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
<script lang="ts"> |
||||
import { page } from '$app/stores'; |
||||
import { goto } from '$app/navigation'; |
||||
import { Button, P } from 'flowbite-svelte'; |
||||
</script> |
||||
|
||||
<div class="leather flex flex-col items-center justify-center min-h-screen text-center px-4"> |
||||
<h1 class="h-leather mb-4">404 - Page Not Found</h1> |
||||
<P class="note-leather mb-6">The page you are looking for does not exist or has been moved.</P> |
||||
<div class="flex space-x-4"> |
||||
<Button class="btn-leather !w-fit" on:click={() => goto('/')}>Return to Home</Button> |
||||
<Button class="btn-leather !w-fit" outline on:click={() => window.history.back()}>Go Back</Button> |
||||
</div> |
||||
</div> |
||||
@ -1,62 +1,105 @@
@@ -1,62 +1,105 @@
|
||||
<script lang='ts'> |
||||
import { Heading } from "flowbite-svelte"; |
||||
import { Heading, Img, P, A } from "flowbite-svelte"; |
||||
|
||||
// Get the git tag version from environment variables |
||||
const gitTagVersion = import.meta.env.GIT_TAG || 'development'; |
||||
</script> |
||||
|
||||
<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-6 max-w-2xl w-full my-6 px-4'> |
||||
<div class="flex justify-between items-center"> |
||||
<Heading tag='h1' class='h-leather mb-2'>About</Heading> |
||||
<Heading tag='h1' class='h-leather text-left mb-4'>About</Heading> |
||||
<span class="text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">Version: {gitTagVersion}</span> |
||||
</div> |
||||
<p>Alexandria is a reader and writer for <a href="https://github.com/nostr-protocol/nips/pull/1600" class='underline' target="_blank">curated publications</a> (in Asciidoc), and will eventually also support long-form articles (Markdown) and wiki pages (Asciidoc). It is produced by the <a href="https://wikistr.com/gitcitadel-project" class='underline' target="_blank">GitCitadel project team</a>.</p> |
||||
|
||||
<p>Please submit support issues on the <a href="https://gitcitadel.com/r/naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqy88wumn8ghj7mn0wvhxcmmv9uqq5emfw33kjarpv3jkcs83wav" class='underline' target="_blank">project repo page</a> and follow us on <a href="https://github.com/ShadowySupercode/gitcitadel" class='underline' target="_blank">GitHub</a> and <a href="https://geyser.fund/project/gitcitadel" class='underline' target="_blank">Geyserfund</a>.</p> |
||||
<P class="mb-3"> |
||||
Alexandria is a reader and writer for <A href="/publication?d=gitcitadel-project-documentation-curated-publications-specification-7-by-stella-v-1">curated publications</A> (in Asciidoc), and will eventually also support long-form articles (Markdown) and wiki pages (Asciidoc). It is produced by the <A href="/publication?d=gitcitadel-project-documentation-gitcitadel-project-1-by-stella-v-1">GitCitadel project team</A>. |
||||
</P> |
||||
|
||||
<p>We are easiest to contact over our Nostr address <a href="https://njump.me/nprofile1qqsggm4l0xs23qfjwnkfwf6fqcs66s3lz637gaxhl4nwd2vtle8rnfqprfmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdaks5zhueg" class='underline' title="npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz" target="_blank">npub1s3h…75wz</a>.</p> |
||||
<P class="mb-3"> |
||||
Please submit support issues on the <A href="https://gitcitadel.com/r/naddr1qvzqqqrhnypzquqjyy5zww7uq7hehemjt7juf0q0c9rgv6lv8r2yxcxuf0rvcx9eqy88wumn8ghj7mn0wvhxcmmv9uq3wamnwvaz7tmjv4kxz7fwdehhxarj9e3xzmny9uqsuamnwvaz7tmwdaejumr0dshsqzjpd3jhsctwv3exjcgtpg0n0/issues" target="_blank">Alexandria repo page</A> and follow us on <A href="https://github.com/ShadowySupercode/gitcitadel" target="_blank">GitHub</A> and <A href="https://geyser.fund/project/gitcitadel" target="_blank">Geyserfund</A>. |
||||
</P> |
||||
|
||||
<Heading tag='h2' class='h-leather mb-2'>Overview</Heading> |
||||
<P> |
||||
We are easiest to contact over our Nostr address <A href="https://njump.me/nprofile1qqsggm4l0xs23qfjwnkfwf6fqcs66s3lz637gaxhl4nwd2vtle8rnfqprfmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdaks5zhueg" title="npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz" target="_blank">npub1s3h…75wz</A>. |
||||
</P> |
||||
|
||||
<p>Alexandria opens up to the <a href="https://next-alexandria.gitcitadel.eu/" class='underline'>landing page</a>, where the user can: login (top-right), select whether to only view the publications hosted on the <a href="https://thecitadel.nostr1.com/" class='underline' target="_blank">thecitadel document relay</a> or add in their own relays, and scroll/search the publications.</p> |
||||
<Heading tag='h2' class='h-leather mt-4 mb-2'>Overview</Heading> |
||||
|
||||
<p><img src="/screenshots/LandingPage.png" alt="Landing page" class='image-border'></p> |
||||
<p><img src="/screenshots/YourRelays.png" alt="Relay selection" class='image-border'></p> |
||||
<P class="mb-4"> |
||||
Alexandria opens up to the <A href="./">landing page</A>, where the user can: login (top-right), select whether to only view the publications hosted on the <A href="https://thecitadel.nostr1.com/" target="_blank">thecitadel document relay</A> or add in their own relays, and scroll/search the publications. |
||||
</P> |
||||
|
||||
<p>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).</p> |
||||
<div class="flex flex-col items-center space-y-4 my-4"> |
||||
<Img src="/screenshots/LandingPage.png" alt="Landing page" class='image-border rounded-lg' width="400" /> |
||||
<Img src="/screenshots/YourRelays.png" alt="Relay selection" class='image-border rounded-lg' width="400" /> |
||||
</div> |
||||
|
||||
<P class="mb-3"> |
||||
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). |
||||
</P> |
||||
|
||||
<P class="mb-3"> |
||||
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. |
||||
</P> |
||||
|
||||
<P class="mb-3"> |
||||
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.) |
||||
</P> |
||||
|
||||
<div class="flex flex-col items-center space-y-4 my-4"> |
||||
<Img src="/screenshots/ToC_icon.png" alt="ToC icon" class='image-border rounded-lg' width="400" /> |
||||
<Img src="/screenshots/TableOfContents.png" alt="Table of contents example" class='image-border rounded-lg' width="400" /> |
||||
</div> |
||||
|
||||
<p>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.</p> |
||||
<Heading tag='h2' class='h-leather mt-4 mb-2'>Typical use cases</Heading> |
||||
|
||||
<p>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.)</p> |
||||
<Heading tag='h3' class='h-leather mb-3'>For e-books</Heading> |
||||
|
||||
<p><img src="/screenshots/ToC_icon.png" alt="ToC icon" class='image-border'></p> |
||||
<p><img src="/screenshots/TableOfContents.png" alt="Table of contents example" class='image-border'></p> |
||||
<P class="mb-3"> |
||||
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. |
||||
</P> |
||||
|
||||
<Heading tag='h2' class='h-leather mb-2'>Typical use cases</Heading> |
||||
<P class="mb-3"> |
||||
An example of a book is <A href="/publication?d=jane-eyre-an-autobiography-by-charlotte-bront%C3%AB-v-3rd-edition">Jane Eyre</A> |
||||
</P> |
||||
|
||||
<Heading tag='h3' class='h-leather mb-2'>For e-books</Heading> |
||||
<p>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.</p> |
||||
<div class="flex justify-center my-4"> |
||||
<Img src="/screenshots/JaneEyre.png" alt="Jane Eyre, by Charlotte Brontë" class='image-border rounded-lg' width="400" /> |
||||
</div> |
||||
|
||||
<p>An example of a book is <a href="https://next-alexandria.gitcitadel.eu/publication?d=jane-eyre-an-autobiography-by-charlotte-bront%C3%83-v-third-edition" class='underline'>Jane Eyre</a></p> |
||||
<Heading tag='h3' class='h-leather mb-3'>For scientific papers</Heading> |
||||
|
||||
<p><img src="/screenshots/JaneEyre.png" alt="Jane Eyre, by Charlotte Brontë" class='image-border'></p> |
||||
<P class="mb-3"> |
||||
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. |
||||
</P> |
||||
|
||||
<Heading tag='h3' class='h-leather mb-2'>For scientific papers</Heading> |
||||
<p>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.</p> |
||||
<P class="mb-3"> |
||||
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. |
||||
</P> |
||||
|
||||
<p>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.</p> |
||||
<P class="mb-3"> |
||||
Euler will also pioneer the HTTP-based (rather than websocket-based) e-paper compatible version of the web app. |
||||
</P> |
||||
|
||||
<p>Euler will also pioneer the HTTP-based (rather than websocket-based) e-paper compatible version of the web app.</p> |
||||
<P class="mb-3"> |
||||
An example of a research paper is <A href="/publication?d=less-partnering-less-children-or-both-by-j.i.s.-hellstrand-v-1">Less Partnering, Less Children, or Both?</A> |
||||
</P> |
||||
|
||||
<p>An example of a research paper is <a href="https://next-alexandria.gitcitadel.eu/publication?d=less-partnering-less-children-or-both-by-j.i.s.-hellstrand-v-1" class='underline'>Less Partnering, Less Children, or Both?</a></p> |
||||
<div class="flex justify-center my-4"> |
||||
<Img src="/screenshots/ResearchPaper.png" alt="Research paper" class='image-border rounded-lg' width="400" /> |
||||
</div> |
||||
|
||||
<p><img src="/screenshots/ResearchPaper.png" alt="Research paper" class='image-border'></p> |
||||
<Heading tag='h3' class='h-leather mb-3'>For documentation</Heading> |
||||
|
||||
<Heading tag='h3' class='h-leather mb-2'>For documentation</Heading> |
||||
<p>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.</p> |
||||
<P class="mb-3"> |
||||
Our own team uses Alexandria to document the app, to display our <A href="/publication?d=the-gitcitadel-blog-by-stella-v-1">blog entries</A>, as well as to store copies of our most interesting <A href="/publication?d=gitcitadel-project-documentation-by-stella-v-1">technical specifications</A>. |
||||
</P> |
||||
|
||||
<p><img src="/screenshots/Documentation.png" alt="Documentation" class='image-border'></p> |
||||
<div class="flex justify-center my-4"> |
||||
<Img src="/screenshots/Documentation.png" alt="Documentation" class='image-border rounded-lg' width="400" /> |
||||
</div> |
||||
|
||||
</main> |
||||
</div> |
||||
|
||||
|
||||
Loading…
Reference in new issue