Browse Source

Refactor and support tree node resolution via ToC entry closure

master
buttercat1791 9 months ago
parent
commit
fa6265b346
  1. 124
      src/lib/components/publications/table_of_contents.svelte.ts

124
src/lib/components/publications/table_of_contents.svelte.ts

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

Loading…
Cancel
Save