|
|
|
|
@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
|
|
|
|
|
import { SvelteMap } from 'svelte/reactivity'; |
|
|
|
|
import type { SveltePublicationTree } from './svelte_publication_tree.svelte.ts'; |
|
|
|
|
import { SveltePublicationTree } from './svelte_publication_tree.svelte.ts'; |
|
|
|
|
import type { NDKEvent } from '../../utils/nostrUtils.ts'; |
|
|
|
|
|
|
|
|
|
export interface TocEntry { |
|
|
|
|
@ -9,6 +9,9 @@ export interface TocEntry {
@@ -9,6 +9,9 @@ export interface TocEntry {
|
|
|
|
|
children: TocEntry[]; |
|
|
|
|
parent?: TocEntry; |
|
|
|
|
depth: number; |
|
|
|
|
expanded: boolean; |
|
|
|
|
childrenResolved: boolean; |
|
|
|
|
resolveChildren: () => Promise<void>; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
@ -35,10 +38,7 @@ export class TableOfContents {
@@ -35,10 +38,7 @@ export class TableOfContents {
|
|
|
|
|
constructor(rootAddress: string, publicationTree: SveltePublicationTree, pagePathname: string) { |
|
|
|
|
this.#publicationTree = publicationTree; |
|
|
|
|
this.#pagePathname = pagePathname; |
|
|
|
|
void this.#initRoot(rootAddress); |
|
|
|
|
this.#publicationTree.onNodeResolved((address: string) => { |
|
|
|
|
void this.#handleNodeResolved(address); |
|
|
|
|
}); |
|
|
|
|
this.#init(rootAddress); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// #region Public Methods
|
|
|
|
|
@ -84,12 +84,16 @@ export class TableOfContents {
@@ -84,12 +84,16 @@ export class TableOfContents {
|
|
|
|
|
if (id && title) { |
|
|
|
|
const href = `${this.#pagePathname}#${id}`; |
|
|
|
|
|
|
|
|
|
// TODO: Check this logic.
|
|
|
|
|
const tocEntry: TocEntry = { |
|
|
|
|
address: parentEntry.address, |
|
|
|
|
title, |
|
|
|
|
href, |
|
|
|
|
depth, |
|
|
|
|
children: [], |
|
|
|
|
expanded: false, |
|
|
|
|
childrenResolved: true, |
|
|
|
|
resolveChildren: () => Promise.resolve(), |
|
|
|
|
}; |
|
|
|
|
parentEntry.children.push(tocEntry); |
|
|
|
|
|
|
|
|
|
@ -127,84 +131,96 @@ export class TableOfContents {
@@ -127,84 +131,96 @@ export class TableOfContents {
|
|
|
|
|
|
|
|
|
|
// #region Private Methods
|
|
|
|
|
|
|
|
|
|
async #initRoot(rootAddress: string) { |
|
|
|
|
async #init(rootAddress: string) { |
|
|
|
|
const rootEvent = await this.#publicationTree.getEvent(rootAddress); |
|
|
|
|
if (!rootEvent) { |
|
|
|
|
throw new Error(`[ToC] Root event ${rootAddress} not found.`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this.#root = { |
|
|
|
|
address: rootAddress, |
|
|
|
|
title: this.#getTitle(rootEvent), |
|
|
|
|
children: [], |
|
|
|
|
depth: 0, |
|
|
|
|
}; |
|
|
|
|
this.#root = await this.#buildTocEntry(rootAddress); |
|
|
|
|
|
|
|
|
|
this.addressMap.set(rootAddress, this.#root); |
|
|
|
|
|
|
|
|
|
// TODO: Parallelize this.
|
|
|
|
|
// Handle any other nodes that have already been resolved.
|
|
|
|
|
await this.#handleNodeResolved(rootAddress); |
|
|
|
|
} |
|
|
|
|
this.#publicationTree.resolvedAddresses.forEach(async (address) => { |
|
|
|
|
await this.#buildTocEntryFromResolvedNode(address); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
async #handleNodeResolved(address: string) { |
|
|
|
|
if (this.addressMap.has(address)) { |
|
|
|
|
return; |
|
|
|
|
// Set up an observer to handle progressive resolution of the publication tree.
|
|
|
|
|
this.#publicationTree.onNodeResolved(async (address: string) => { |
|
|
|
|
await this.#buildTocEntryFromResolvedNode(address); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
const event = await this.#publicationTree.getEvent(address); |
|
|
|
|
|
|
|
|
|
#getTitle(event: NDKEvent | null): string { |
|
|
|
|
if (!event) { |
|
|
|
|
return; |
|
|
|
|
// TODO: What do we want to return in this case?
|
|
|
|
|
return '[untitled]'; |
|
|
|
|
} |
|
|
|
|
const titleTag = event.getMatchingTags?.('title')?.[0]?.[1]; |
|
|
|
|
return titleTag || event.tagAddress() || '[untitled]'; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const parentEvent = await this.#publicationTree.getParent(address); |
|
|
|
|
const parentAddress = parentEvent?.tagAddress(); |
|
|
|
|
if (!parentAddress) { |
|
|
|
|
// All non-root nodes must have a parent.
|
|
|
|
|
if (!this.#root || address !== this.#root.address) { |
|
|
|
|
throw new Error(`[ToC] Parent not found for address ${address}`); |
|
|
|
|
#normalizeHashPath(title: string): string { |
|
|
|
|
// TODO: Confirm this uses good normalization logic to produce unique hrefs within the page.
|
|
|
|
|
return title.toLowerCase().replace(/ /g, '-'); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async #buildTocEntry(address: string): Promise<TocEntry> { |
|
|
|
|
const resolver = async () => { |
|
|
|
|
if (entry.childrenResolved) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const parentEntry = this.addressMap.get(parentAddress); |
|
|
|
|
if (!parentEntry) { |
|
|
|
|
throw new Error(`[ToC] Parent ToC entry not found for address ${address}`); |
|
|
|
|
const childAddresses = await this.#publicationTree.getChildAddresses(address); |
|
|
|
|
for (const childAddress of childAddresses) { |
|
|
|
|
if (!childAddress) { |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Michael J - 05 June 2025 - The `getChildAddresses` method forces node resolution on the
|
|
|
|
|
// publication tree. This is acceptable here, because the tree is always resolved
|
|
|
|
|
// top-down. Therefore, by the time we handle a node's resolution, its parent and
|
|
|
|
|
// siblings have already been resolved.
|
|
|
|
|
const childEntry = await this.#buildTocEntry(childAddress); |
|
|
|
|
childEntry.parent = entry; |
|
|
|
|
childEntry.depth = entry.depth + 1; |
|
|
|
|
entry.children.push(childEntry); |
|
|
|
|
this.addressMap.set(childAddress, childEntry); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
entry.childrenResolved = true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const event = await this.#publicationTree.getEvent(address); |
|
|
|
|
if (!event) { |
|
|
|
|
throw new Error(`[ToC] Event ${address} not found.`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const depth = (await this.#publicationTree.getHierarchy(address)).length; |
|
|
|
|
|
|
|
|
|
const entry: TocEntry = { |
|
|
|
|
address, |
|
|
|
|
title: this.#getTitle(event), |
|
|
|
|
href: `${this.#pagePathname}#${address}`, |
|
|
|
|
children: [], |
|
|
|
|
parent: parentEntry, |
|
|
|
|
depth: parentEntry.depth + 1, |
|
|
|
|
depth, |
|
|
|
|
expanded: false, |
|
|
|
|
childrenResolved: false, |
|
|
|
|
resolveChildren: resolver, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// Michael J - 05 June 2025 - The `getChildAddresses` method forces node resolution on the
|
|
|
|
|
// publication tree. This is acceptable here, because the tree is always resolved top-down.
|
|
|
|
|
// Therefore, by the time we handle a node's resolution, its parent and siblings have already
|
|
|
|
|
// been resolved.
|
|
|
|
|
const childAddresses = await this.#publicationTree.getChildAddresses(parentAddress); |
|
|
|
|
const filteredChildAddresses = childAddresses.filter((a): a is string => !!a); |
|
|
|
|
const insertIndex = filteredChildAddresses.findIndex(a => a === address); |
|
|
|
|
if (insertIndex === -1 || insertIndex > parentEntry.children.length) { |
|
|
|
|
parentEntry.children.push(entry); |
|
|
|
|
} else { |
|
|
|
|
parentEntry.children.splice(insertIndex, 0, entry); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this.addressMap.set(address, entry); |
|
|
|
|
return entry; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#getTitle(event: NDKEvent | null): string { |
|
|
|
|
if (!event) { |
|
|
|
|
// TODO: What do we want to return in this case?
|
|
|
|
|
return '[untitled]'; |
|
|
|
|
} |
|
|
|
|
const titleTag = event.getMatchingTags?.('title')?.[0]?.[1]; |
|
|
|
|
return titleTag || event.tagAddress() || '[untitled]'; |
|
|
|
|
async #buildTocEntryFromResolvedNode(address: string) { |
|
|
|
|
if (this.addressMap.has(address)) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#normalizeHashPath(title: string): string { |
|
|
|
|
// TODO: Confirm this uses good normalization logic to produce unique hrefs within the page.
|
|
|
|
|
return title.toLowerCase().replace(/ /g, '-'); |
|
|
|
|
const entry = await this.#buildTocEntry(address); |
|
|
|
|
this.addressMap.set(address, entry); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// #endregion
|
|
|
|
|
|