Browse Source

Refactor and support tree node resolution via ToC entry closure

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

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

@ -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);
});
// 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<TocEntry> {
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

Loading…
Cancel
Save