|
|
|
@ -13,7 +13,7 @@ import type NDK from "@nostr-dev-kit/ndk"; |
|
|
|
import { getMimeTags } from "$lib/utils/mime"; |
|
|
|
import { getMimeTags } from "$lib/utils/mime"; |
|
|
|
|
|
|
|
|
|
|
|
// For debugging tree structure
|
|
|
|
// For debugging tree structure
|
|
|
|
const DEBUG = process.env.DEBUG_TREE_PROCESSOR === false; |
|
|
|
const DEBUG = process.env.DEBUG_TREE_PROCESSOR === "true"; |
|
|
|
export interface ProcessorResult { |
|
|
|
export interface ProcessorResult { |
|
|
|
tree: PublicationTree; |
|
|
|
tree: PublicationTree; |
|
|
|
indexEvent: NDKEvent | null; |
|
|
|
indexEvent: NDKEvent | null; |
|
|
|
@ -435,6 +435,7 @@ function buildScatteredNotesStructure( |
|
|
|
const eventStructure: EventStructureNode[] = []; |
|
|
|
const eventStructure: EventStructureNode[] = []; |
|
|
|
|
|
|
|
|
|
|
|
const firstSegment = segments[0]; |
|
|
|
const firstSegment = segments[0]; |
|
|
|
|
|
|
|
// No publication title for scattered notes
|
|
|
|
const rootEvent = createContentEvent(firstSegment, ndk); |
|
|
|
const rootEvent = createContentEvent(firstSegment, ndk); |
|
|
|
const tree = new PublicationTree(rootEvent, ndk); |
|
|
|
const tree = new PublicationTree(rootEvent, ndk); |
|
|
|
contentEvents.push(rootEvent); |
|
|
|
contentEvents.push(rootEvent); |
|
|
|
@ -530,16 +531,22 @@ function buildLevel2Structure( |
|
|
|
const level2Groups = groupSegmentsByLevel2(segments); |
|
|
|
const level2Groups = groupSegmentsByLevel2(segments); |
|
|
|
console.log(`[TreeProcessor] Level 2 groups:`, level2Groups.length, level2Groups.map(g => g.title)); |
|
|
|
console.log(`[TreeProcessor] Level 2 groups:`, level2Groups.length, level2Groups.map(g => g.title)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Generate publication abbreviation for namespacing
|
|
|
|
|
|
|
|
const pubAbbrev = generateTitleAbbreviation(title); |
|
|
|
|
|
|
|
|
|
|
|
for (const group of level2Groups) { |
|
|
|
for (const group of level2Groups) { |
|
|
|
const contentEvent = createContentEvent(group, ndk); |
|
|
|
const contentEvent = createContentEvent(group, ndk, title); |
|
|
|
contentEvents.push(contentEvent); |
|
|
|
contentEvents.push(contentEvent); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sectionDTag = generateDTag(group.title); |
|
|
|
|
|
|
|
const namespacedDTag = `${pubAbbrev}-${sectionDTag}`; |
|
|
|
|
|
|
|
|
|
|
|
const childNode = { |
|
|
|
const childNode = { |
|
|
|
title: group.title, |
|
|
|
title: group.title, |
|
|
|
level: group.level, |
|
|
|
level: group.level, |
|
|
|
eventType: "content" as const, |
|
|
|
eventType: "content" as const, |
|
|
|
eventKind: 30041 as const, |
|
|
|
eventKind: 30041 as const, |
|
|
|
dTag: generateDTag(group.title), |
|
|
|
dTag: namespacedDTag, |
|
|
|
children: [], |
|
|
|
children: [], |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
@ -590,7 +597,8 @@ function buildHierarchicalStructure( |
|
|
|
rootNode, |
|
|
|
rootNode, |
|
|
|
contentEvents, |
|
|
|
contentEvents, |
|
|
|
ndk, |
|
|
|
ndk, |
|
|
|
parseLevel |
|
|
|
parseLevel, |
|
|
|
|
|
|
|
title |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
return { tree, indexEvent, contentEvents, eventStructure }; |
|
|
|
return { tree, indexEvent, contentEvents, eventStructure }; |
|
|
|
@ -618,10 +626,15 @@ function createIndexEvent( |
|
|
|
// Add document attributes as tags
|
|
|
|
// Add document attributes as tags
|
|
|
|
addDocumentAttributesToTags(tags, attributes, event.pubkey); |
|
|
|
addDocumentAttributesToTags(tags, attributes, event.pubkey); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Generate publication abbreviation for namespacing sections
|
|
|
|
|
|
|
|
const pubAbbrev = generateTitleAbbreviation(title); |
|
|
|
|
|
|
|
|
|
|
|
// Add a-tags for each content section
|
|
|
|
// Add a-tags for each content section
|
|
|
|
|
|
|
|
// Using new format: kind:pubkey:{abbv}-{section-d-tag}
|
|
|
|
segments.forEach((segment) => { |
|
|
|
segments.forEach((segment) => { |
|
|
|
const sectionDTag = generateDTag(segment.title); |
|
|
|
const sectionDTag = generateDTag(segment.title); |
|
|
|
tags.push(["a", `30041:${event.pubkey}:${sectionDTag}`]); |
|
|
|
const namespacedDTag = `${pubAbbrev}-${sectionDTag}`; |
|
|
|
|
|
|
|
tags.push(["a", `30041:${event.pubkey}:${namespacedDTag}`]); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
event.tags = tags; |
|
|
|
event.tags = tags; |
|
|
|
@ -635,13 +648,25 @@ function createIndexEvent( |
|
|
|
/** |
|
|
|
/** |
|
|
|
* Create a 30041 content event from segment |
|
|
|
* Create a 30041 content event from segment |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
function createContentEvent(segment: ContentSegment, ndk: NDK): NDKEvent { |
|
|
|
function createContentEvent( |
|
|
|
|
|
|
|
segment: ContentSegment, |
|
|
|
|
|
|
|
ndk: NDK, |
|
|
|
|
|
|
|
publicationTitle?: string, |
|
|
|
|
|
|
|
): NDKEvent { |
|
|
|
const event = new NDKEvent(ndk); |
|
|
|
const event = new NDKEvent(ndk); |
|
|
|
event.kind = 30041; |
|
|
|
event.kind = 30041; |
|
|
|
event.created_at = Math.floor(Date.now() / 1000); |
|
|
|
event.created_at = Math.floor(Date.now() / 1000); |
|
|
|
event.pubkey = ndk.activeUser?.pubkey || "preview-placeholder-pubkey"; |
|
|
|
event.pubkey = ndk.activeUser?.pubkey || "preview-placeholder-pubkey"; |
|
|
|
|
|
|
|
|
|
|
|
const dTag = generateDTag(segment.title); |
|
|
|
// Generate namespaced d-tag if publication title is provided
|
|
|
|
|
|
|
|
const sectionDTag = generateDTag(segment.title); |
|
|
|
|
|
|
|
let dTag = sectionDTag; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (publicationTitle) { |
|
|
|
|
|
|
|
const pubAbbrev = generateTitleAbbreviation(publicationTitle); |
|
|
|
|
|
|
|
dTag = `${pubAbbrev}-${sectionDTag}`; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const [mTag, MTag] = getMimeTags(30041); |
|
|
|
const [mTag, MTag] = getMimeTags(30041); |
|
|
|
|
|
|
|
|
|
|
|
const tags: string[][] = [["d", dTag], mTag, MTag, ["title", segment.title]]; |
|
|
|
const tags: string[][] = [["d", dTag], mTag, MTag, ["title", segment.title]]; |
|
|
|
@ -652,7 +677,6 @@ function createContentEvent(segment: ContentSegment, ndk: NDK): NDKEvent { |
|
|
|
event.tags = tags; |
|
|
|
event.tags = tags; |
|
|
|
event.content = segment.content; |
|
|
|
event.content = segment.content; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return event; |
|
|
|
return event; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -690,6 +714,32 @@ function generateDTag(title: string): string { |
|
|
|
); |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Generate title abbreviation from first letters of each word |
|
|
|
|
|
|
|
* Used for namespacing section a-tags |
|
|
|
|
|
|
|
* @param title - The publication title |
|
|
|
|
|
|
|
* @returns Abbreviation string (e.g., "My Test Article" → "mta") |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
function generateTitleAbbreviation(title: string): string { |
|
|
|
|
|
|
|
if (!title || !title.trim()) { |
|
|
|
|
|
|
|
return "u"; // "untitled"
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Split on non-alphanumeric characters and filter out empty strings
|
|
|
|
|
|
|
|
const words = title |
|
|
|
|
|
|
|
.split(/[^\p{L}\p{N}]+/u) |
|
|
|
|
|
|
|
.filter((word) => word.length > 0); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (words.length === 0) { |
|
|
|
|
|
|
|
return "u"; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Take first letter of each word and join
|
|
|
|
|
|
|
|
return words |
|
|
|
|
|
|
|
.map((word) => word.charAt(0).toLowerCase()) |
|
|
|
|
|
|
|
.join(""); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Add document attributes as Nostr tags |
|
|
|
* Add document attributes as Nostr tags |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@ -925,21 +975,35 @@ function processHierarchicalGroup( |
|
|
|
parentStructureNode: EventStructureNode, |
|
|
|
parentStructureNode: EventStructureNode, |
|
|
|
contentEvents: NDKEvent[], |
|
|
|
contentEvents: NDKEvent[], |
|
|
|
ndk: NDK, |
|
|
|
ndk: NDK, |
|
|
|
parseLevel: number |
|
|
|
parseLevel: number, |
|
|
|
|
|
|
|
publicationTitle: string, |
|
|
|
): void { |
|
|
|
): void { |
|
|
|
|
|
|
|
const pubAbbrev = generateTitleAbbreviation(publicationTitle); |
|
|
|
|
|
|
|
|
|
|
|
for (const node of nodes) { |
|
|
|
for (const node of nodes) { |
|
|
|
if (node.hasChildren && node.segment.level < parseLevel) { |
|
|
|
if (node.hasChildren && node.segment.level < parseLevel) { |
|
|
|
// This section has children and is not at parse level
|
|
|
|
// This section has children and is not at parse level
|
|
|
|
// Create BOTH an index event AND a content event
|
|
|
|
// Create BOTH an index event AND a content event
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Create the index event (30040)
|
|
|
|
// 1. Create the index event (30040)
|
|
|
|
const indexEvent = createIndexEventForHierarchicalNode(node, ndk); |
|
|
|
const indexEvent = createIndexEventForHierarchicalNode( |
|
|
|
|
|
|
|
node, |
|
|
|
|
|
|
|
ndk, |
|
|
|
|
|
|
|
publicationTitle, |
|
|
|
|
|
|
|
); |
|
|
|
contentEvents.push(indexEvent); |
|
|
|
contentEvents.push(indexEvent); |
|
|
|
|
|
|
|
|
|
|
|
// 2. Create the content event (30041) for the section's own content
|
|
|
|
// 2. Create the content event (30041) for the section's own content
|
|
|
|
const contentEvent = createContentEvent(node.segment, ndk); |
|
|
|
const contentEvent = createContentEvent( |
|
|
|
|
|
|
|
node.segment, |
|
|
|
|
|
|
|
ndk, |
|
|
|
|
|
|
|
publicationTitle, |
|
|
|
|
|
|
|
); |
|
|
|
contentEvents.push(contentEvent); |
|
|
|
contentEvents.push(contentEvent); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sectionDTag = generateDTag(node.segment.title); |
|
|
|
|
|
|
|
const namespacedDTag = `${pubAbbrev}-${sectionDTag}`; |
|
|
|
|
|
|
|
|
|
|
|
// 3. Add index node to structure
|
|
|
|
// 3. Add index node to structure
|
|
|
|
const indexNode: EventStructureNode = { |
|
|
|
const indexNode: EventStructureNode = { |
|
|
|
title: node.segment.title, |
|
|
|
title: node.segment.title, |
|
|
|
@ -957,7 +1021,7 @@ function processHierarchicalGroup( |
|
|
|
level: node.segment.level, |
|
|
|
level: node.segment.level, |
|
|
|
eventType: "content", |
|
|
|
eventType: "content", |
|
|
|
eventKind: 30041, |
|
|
|
eventKind: 30041, |
|
|
|
dTag: generateDTag(node.segment.title), |
|
|
|
dTag: namespacedDTag, |
|
|
|
children: [], |
|
|
|
children: [], |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
@ -967,19 +1031,27 @@ function processHierarchicalGroup( |
|
|
|
indexNode, |
|
|
|
indexNode, |
|
|
|
contentEvents, |
|
|
|
contentEvents, |
|
|
|
ndk, |
|
|
|
ndk, |
|
|
|
parseLevel |
|
|
|
parseLevel, |
|
|
|
|
|
|
|
publicationTitle, |
|
|
|
); |
|
|
|
); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
// This is either a leaf node or at parse level - just create content event
|
|
|
|
// This is either a leaf node or at parse level - just create content event
|
|
|
|
const contentEvent = createContentEvent(node.segment, ndk); |
|
|
|
const contentEvent = createContentEvent( |
|
|
|
|
|
|
|
node.segment, |
|
|
|
|
|
|
|
ndk, |
|
|
|
|
|
|
|
publicationTitle, |
|
|
|
|
|
|
|
); |
|
|
|
contentEvents.push(contentEvent); |
|
|
|
contentEvents.push(contentEvent); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sectionDTag = generateDTag(node.segment.title); |
|
|
|
|
|
|
|
const namespacedDTag = `${pubAbbrev}-${sectionDTag}`; |
|
|
|
|
|
|
|
|
|
|
|
parentStructureNode.children.push({ |
|
|
|
parentStructureNode.children.push({ |
|
|
|
title: node.segment.title, |
|
|
|
title: node.segment.title, |
|
|
|
level: node.segment.level, |
|
|
|
level: node.segment.level, |
|
|
|
eventType: "content", |
|
|
|
eventType: "content", |
|
|
|
eventKind: 30041, |
|
|
|
eventKind: 30041, |
|
|
|
dTag: generateDTag(node.segment.title), |
|
|
|
dTag: namespacedDTag, |
|
|
|
children: [], |
|
|
|
children: [], |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
@ -991,7 +1063,8 @@ function processHierarchicalGroup( |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
function createIndexEventForHierarchicalNode( |
|
|
|
function createIndexEventForHierarchicalNode( |
|
|
|
node: HierarchicalNode, |
|
|
|
node: HierarchicalNode, |
|
|
|
ndk: NDK |
|
|
|
ndk: NDK, |
|
|
|
|
|
|
|
publicationTitle: string, |
|
|
|
): NDKEvent { |
|
|
|
): NDKEvent { |
|
|
|
const event = new NDKEvent(ndk); |
|
|
|
const event = new NDKEvent(ndk); |
|
|
|
event.kind = 30040; |
|
|
|
event.kind = 30040; |
|
|
|
@ -1001,28 +1074,38 @@ function createIndexEventForHierarchicalNode( |
|
|
|
const dTag = generateDTag(node.segment.title); |
|
|
|
const dTag = generateDTag(node.segment.title); |
|
|
|
const [mTag, MTag] = getMimeTags(30040); |
|
|
|
const [mTag, MTag] = getMimeTags(30040); |
|
|
|
|
|
|
|
|
|
|
|
const tags: string[][] = [["d", dTag], mTag, MTag, ["title", node.segment.title]]; |
|
|
|
const tags: string[][] = [ |
|
|
|
|
|
|
|
["d", dTag], |
|
|
|
|
|
|
|
mTag, |
|
|
|
|
|
|
|
MTag, |
|
|
|
|
|
|
|
["title", node.segment.title], |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
// Add section attributes as tags
|
|
|
|
// Add section attributes as tags
|
|
|
|
addSectionAttributesToTags(tags, node.segment.attributes); |
|
|
|
addSectionAttributesToTags(tags, node.segment.attributes); |
|
|
|
|
|
|
|
|
|
|
|
// Add a-tags for the section's own content event
|
|
|
|
const pubAbbrev = generateTitleAbbreviation(publicationTitle); |
|
|
|
tags.push(["a", `30041:${event.pubkey}:${dTag}`]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add a-tags for each child section
|
|
|
|
// Add a-tags for the section's own content event with namespace
|
|
|
|
|
|
|
|
const sectionDTag = generateDTag(node.segment.title); |
|
|
|
|
|
|
|
const namespacedDTag = `${pubAbbrev}-${sectionDTag}`; |
|
|
|
|
|
|
|
tags.push(["a", `30041:${event.pubkey}:${namespacedDTag}`]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add a-tags for each child section with namespace
|
|
|
|
for (const child of node.children) { |
|
|
|
for (const child of node.children) { |
|
|
|
const childDTag = generateDTag(child.segment.title); |
|
|
|
const childDTag = generateDTag(child.segment.title); |
|
|
|
|
|
|
|
const namespacedChildDTag = `${pubAbbrev}-${childDTag}`; |
|
|
|
if (child.hasChildren && child.segment.level < node.segment.level + 1) { |
|
|
|
if (child.hasChildren && child.segment.level < node.segment.level + 1) { |
|
|
|
// Child will be an index
|
|
|
|
// Child will be an index
|
|
|
|
tags.push(["a", `30040:${event.pubkey}:${childDTag}`]); |
|
|
|
tags.push(["a", `30040:${event.pubkey}:${childDTag}`]); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
// Child will be content
|
|
|
|
// Child will be content with namespace
|
|
|
|
tags.push(["a", `30041:${event.pubkey}:${childDTag}`]); |
|
|
|
tags.push(["a", `30041:${event.pubkey}:${namespacedChildDTag}`]); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
event.tags = tags; |
|
|
|
event.tags = tags; |
|
|
|
event.content = ""; // NKBIP-01: Index events must have empty content
|
|
|
|
event.content = ""; // NKBIP-01: Index events must have empty content
|
|
|
|
|
|
|
|
|
|
|
|
return event; |
|
|
|
return event; |
|
|
|
} |
|
|
|
} |
|
|
|
@ -1059,10 +1142,13 @@ function buildSegmentHierarchy( |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Create a 30040 index event for a section with children |
|
|
|
* Create a 30040 index event for a section with children |
|
|
|
|
|
|
|
* Note: This function appears to be unused in the current codebase |
|
|
|
|
|
|
|
* but is updated for consistency with the new namespacing scheme |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
function createIndexEventForSection( |
|
|
|
function createIndexEventForSection( |
|
|
|
section: HierarchicalSegment, |
|
|
|
section: HierarchicalSegment, |
|
|
|
ndk: NDK, |
|
|
|
ndk: NDK, |
|
|
|
|
|
|
|
publicationTitle: string, |
|
|
|
): NDKEvent { |
|
|
|
): NDKEvent { |
|
|
|
const event = new NDKEvent(ndk); |
|
|
|
const event = new NDKEvent(ndk); |
|
|
|
event.kind = 30040; |
|
|
|
event.kind = 30040; |
|
|
|
@ -1077,10 +1163,13 @@ function createIndexEventForSection( |
|
|
|
// Add section attributes as tags
|
|
|
|
// Add section attributes as tags
|
|
|
|
addSectionAttributesToTags(tags, section.attributes); |
|
|
|
addSectionAttributesToTags(tags, section.attributes); |
|
|
|
|
|
|
|
|
|
|
|
// Add a-tags for each child content section
|
|
|
|
const pubAbbrev = generateTitleAbbreviation(publicationTitle); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add a-tags for each child content section with namespace
|
|
|
|
section.children.forEach((child) => { |
|
|
|
section.children.forEach((child) => { |
|
|
|
const childDTag = generateDTag(child.title); |
|
|
|
const childDTag = generateDTag(child.title); |
|
|
|
tags.push(["a", `30041:${event.pubkey}:${childDTag}`]); |
|
|
|
const namespacedChildDTag = `${pubAbbrev}-${childDTag}`; |
|
|
|
|
|
|
|
tags.push(["a", `30041:${event.pubkey}:${namespacedChildDTag}`]); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
event.tags = tags; |
|
|
|
event.tags = tags; |
|
|
|
|