diff --git a/src/lib/components/publications/table_of_contents.svelte.ts b/src/lib/components/publications/table_of_contents.svelte.ts index c6a26e6..8665697 100644 --- a/src/lib/components/publications/table_of_contents.svelte.ts +++ b/src/lib/components/publications/table_of_contents.svelte.ts @@ -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 { children: TocEntry[]; parent?: TocEntry; depth: number; + expanded: boolean; + childrenResolved: boolean; + resolveChildren: () => Promise; } /** @@ -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 { 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 { // #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); + }); + + // Set up an observer to handle progressive resolution of the publication tree. + this.#publicationTree.onNodeResolved(async (address: string) => { + await this.#buildTocEntryFromResolvedNode(address); + }); } - async #handleNodeResolved(address: string) { - if (this.addressMap.has(address)) { - return; - } - 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]'; + } + + #normalizeHashPath(title: string): string { + // TODO: Confirm this uses good normalization logic to produce unique hrefs within the page. + return title.toLowerCase().replace(/ /g, '-'); + } - 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}`); + async #buildTocEntry(address: string): Promise { + const resolver = async () => { + if (entry.childrenResolved) { + return; } - return; + + 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 parentEntry = this.addressMap.get(parentAddress); - if (!parentEntry) { - throw new Error(`[ToC] Parent ToC entry not found for address ${address}`); + 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]'; + async #buildTocEntryFromResolvedNode(address: string) { + if (this.addressMap.has(address)) { + return; } - const titleTag = event.getMatchingTags?.('title')?.[0]?.[1]; - return titleTag || event.tagAddress() || '[untitled]'; - } - #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