- {#await Promise.all( [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches], )}
-
- {:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches]}
- {#each divergingBranches as [branch, depth]}
- {@render sectionHeading(
- getMatchingTags(branch, "title")[0]?.[1] ?? "",
- depth,
- )}
- {/each}
- {#if leafTitle}
- {@const leafDepth = leafHierarchy.length - 1}
- {@render sectionHeading(leafTitle, leafDepth)}
+
+
+
+
+
+ {#await leafEvent then event}
+ {#if event}
+
+
+
+
{/if}
- {@render contentParagraph(
- leafContent.toString(),
- publicationType ?? "article",
- false,
- )}
{/await}
-
+
+
+
+
+
+
+
+
diff --git a/src/lib/components/util/CardActions.svelte b/src/lib/components/util/CardActions.svelte
index 3a05a8e..236ff24 100644
--- a/src/lib/components/util/CardActions.svelte
+++ b/src/lib/components/util/CardActions.svelte
@@ -1,27 +1,37 @@
+ {#if sectionAddress}
+ -
+
+
+ {/if}
-
+ {#if canDelete}
+ -
+
+
+ {/if}
@@ -265,4 +557,90 @@
+
+
+ {#if sectionAddress}
+
+
+ {#if user.profile}
+
+ {#if user.profile.picture}
+

+ {/if}
+
+ {user.profile.displayName || user.profile.name || "Anonymous"}
+
+
+ {/if}
+
+
+
+ {#if commentError}
+
{commentError}
+ {/if}
+
+ {#if commentSuccess}
+
Comment posted successfully!
+ {/if}
+
+
+ {#if showJsonPreview && previewJson}
+
+
Event JSON Preview:
+
{JSON.stringify(previewJson, null, 2)}
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+ {/if}
diff --git a/src/lib/components/util/Details.svelte b/src/lib/components/util/Details.svelte
index 1088565..56c1a85 100644
--- a/src/lib/components/util/Details.svelte
+++ b/src/lib/components/util/Details.svelte
@@ -14,7 +14,11 @@
// isModal
// - don't show interactions in modal view
// - don't show all the details when _not_ in modal view
- let { event, isModal = false } = $props();
+ let { event, isModal = false, onDelete } = $props<{
+ event: any;
+ isModal?: boolean;
+ onDelete?: () => void;
+ }>();
let title: string = $derived(getMatchingTags(event, "title")[0]?.[1]);
let author: string = $derived(
@@ -43,6 +47,7 @@
);
let rootId: string = $derived(getMatchingTags(event, "d")[0]?.[1] ?? null);
let kind = $derived(event.kind);
+ let address: string = $derived(`${kind}:${event.pubkey}:${rootId}`);
let authorTag: string = $derived(
getMatchingTags(event, "author")[0]?.[1] ?? "",
@@ -67,7 +72,9 @@
{@render userBadge(event.pubkey, undefined, ndk)}
-
+
+
+
{/if}
{
const parsed = parseAsciiDocAST(content, parseLevel);
-
+
// Create root 30040 index event from document metadata
const rootEvent = createIndexEventFromAST(parsed, ndk);
const tree = new PublicationTree(rootEvent, ndk);
-
- // Add sections as 30041 events
+
+ // Add sections as 30041 events with proper namespacing
for (const section of parsed.sections) {
- const contentEvent = createContentEventFromSection(section, ndk);
+ const contentEvent = createContentEventFromSection(
+ section,
+ ndk,
+ parsed.title,
+ );
await tree.addEvent(contentEvent, rootEvent);
}
-
+
return tree;
}
@@ -130,57 +134,80 @@ function createIndexEventFromAST(parsed: ASTParsedDocument, ndk: NDK): NDKEvent
const event = new NDKEvent(ndk);
event.kind = 30040;
event.created_at = Math.floor(Date.now() / 1000);
-
+
// Generate d-tag from title
const dTag = generateDTag(parsed.title);
const [mTag, MTag] = getMimeTags(30040);
-
+
const tags: string[][] = [
["d", dTag],
mTag,
MTag,
- ["title", parsed.title]
+ ["title", parsed.title],
];
-
+
// Add document attributes as tags
addAttributesAsTags(tags, parsed.attributes);
-
+
+ // Generate publication abbreviation for namespacing sections
+ const pubAbbrev = generateTitleAbbreviation(parsed.title);
+
// Add a-tags for each section (30041 content events)
- parsed.sections.forEach(section => {
+ // Using new format: kind:pubkey:{abbv}-{section-d-tag}
+ parsed.sections.forEach((section) => {
const sectionDTag = generateDTag(section.title);
- tags.push(["a", `30041:${ndk.activeUser?.pubkey || 'pubkey'}:${sectionDTag}`]);
+ const namespacedDTag = `${pubAbbrev}-${sectionDTag}`;
+ tags.push([
+ "a",
+ `30041:${ndk.activeUser?.pubkey || "pubkey"}:${namespacedDTag}`,
+ ]);
});
-
+
event.tags = tags;
event.content = parsed.content;
-
+
return event;
}
/**
* Create a 30041 content event from an AST section
+ * Note: This function needs the publication title for proper namespacing
+ * but the current implementation doesn't have access to it.
+ * Consider using createPublicationTreeFromAST instead which handles this correctly.
*/
-function createContentEventFromSection(section: ASTSection, ndk: NDK): NDKEvent {
+function createContentEventFromSection(
+ section: ASTSection,
+ ndk: NDK,
+ publicationTitle?: string,
+): NDKEvent {
const event = new NDKEvent(ndk);
event.kind = 30041;
event.created_at = Math.floor(Date.now() / 1000);
-
- const dTag = generateDTag(section.title);
+
+ // Generate namespaced d-tag if publication title is provided
+ const sectionDTag = generateDTag(section.title);
+ let dTag = sectionDTag;
+
+ if (publicationTitle) {
+ const pubAbbrev = generateTitleAbbreviation(publicationTitle);
+ dTag = `${pubAbbrev}-${sectionDTag}`;
+ }
+
const [mTag, MTag] = getMimeTags(30041);
-
+
const tags: string[][] = [
["d", dTag],
mTag,
MTag,
- ["title", section.title]
+ ["title", section.title],
];
-
+
// Add section attributes as tags
addAttributesAsTags(tags, section.attributes);
-
+
event.tags = tags;
event.content = section.content;
-
+
return event;
}
@@ -195,6 +222,32 @@ function generateDTag(title: string): string {
.replace(/^-|-$/g, "");
}
+/**
+ * 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 AsciiDoc attributes as Nostr event tags, filtering out system attributes
*/
@@ -250,24 +303,28 @@ export function createPublicationTreeProcessor(ndk: NDK, parseLevel: number = 2)
* Helper function to create PublicationTree from Asciidoctor Document
*/
async function createPublicationTreeFromDocument(
- document: Document,
- ndk: NDK,
- parseLevel: number
+ document: Document,
+ ndk: NDK,
+ parseLevel: number,
): Promise
{
const parsed: ASTParsedDocument = {
- title: document.getTitle() || '',
- content: document.getContent() || '',
+ title: document.getTitle() || "",
+ content: document.getContent() || "",
attributes: document.getAttributes(),
- sections: extractSectionsFromAST(document, parseLevel)
+ sections: extractSectionsFromAST(document, parseLevel),
};
-
+
const rootEvent = createIndexEventFromAST(parsed, ndk);
const tree = new PublicationTree(rootEvent, ndk);
-
+
for (const section of parsed.sections) {
- const contentEvent = createContentEventFromSection(section, ndk);
+ const contentEvent = createContentEventFromSection(
+ section,
+ ndk,
+ parsed.title,
+ );
await tree.addEvent(contentEvent, rootEvent);
}
-
+
return tree;
}
\ No newline at end of file
diff --git a/src/lib/utils/publication_tree_factory.ts b/src/lib/utils/publication_tree_factory.ts
index 2e0e0e8..33cf31f 100644
--- a/src/lib/utils/publication_tree_factory.ts
+++ b/src/lib/utils/publication_tree_factory.ts
@@ -120,10 +120,15 @@ function createIndexEvent(parsed: any, ndk: NDK): NDKEvent {
// Add document attributes as tags
addDocumentAttributesToTags(tags, parsed.attributes, event.pubkey);
+ // Generate publication abbreviation for namespacing sections
+ const pubAbbrev = generateTitleAbbreviation(parsed.title);
+
// Add a-tags for each section (30041 references)
+ // Using new format: kind:pubkey:{abbv}-{section-d-tag}
parsed.sections.forEach((section: any) => {
const sectionDTag = generateDTag(section.title);
- tags.push(["a", `30041:${event.pubkey}:${sectionDTag}`]);
+ const namespacedDTag = `${pubAbbrev}-${sectionDTag}`;
+ tags.push(["a", `30041:${event.pubkey}:${namespacedDTag}`]);
});
event.tags = tags;
@@ -147,10 +152,19 @@ function createContentEvent(
// Use placeholder pubkey for preview if no active user
event.pubkey = ndk.activeUser?.pubkey || "preview-placeholder-pubkey";
- const dTag = generateDTag(section.title);
+ // Generate namespaced d-tag using publication abbreviation
+ const sectionDTag = generateDTag(section.title);
+ const pubAbbrev = generateTitleAbbreviation(documentParsed.title);
+ const namespacedDTag = `${pubAbbrev}-${sectionDTag}`;
+
const [mTag, MTag] = getMimeTags(30041);
- const tags: string[][] = [["d", dTag], mTag, MTag, ["title", section.title]];
+ const tags: string[][] = [
+ ["d", namespacedDTag],
+ mTag,
+ MTag,
+ ["title", section.title],
+ ];
// Add section-specific attributes
addSectionAttributesToTags(tags, section.attributes);
@@ -200,6 +214,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
*/
diff --git a/src/lib/utils/publication_tree_processor.ts b/src/lib/utils/publication_tree_processor.ts
index c714d52..0cf36a8 100644
--- a/src/lib/utils/publication_tree_processor.ts
+++ b/src/lib/utils/publication_tree_processor.ts
@@ -13,7 +13,7 @@ import type NDK from "@nostr-dev-kit/ndk";
import { getMimeTags } from "$lib/utils/mime";
// For debugging tree structure
-const DEBUG = process.env.DEBUG_TREE_PROCESSOR === false;
+const DEBUG = process.env.DEBUG_TREE_PROCESSOR === "true";
export interface ProcessorResult {
tree: PublicationTree;
indexEvent: NDKEvent | null;
@@ -435,6 +435,7 @@ function buildScatteredNotesStructure(
const eventStructure: EventStructureNode[] = [];
const firstSegment = segments[0];
+ // No publication title for scattered notes
const rootEvent = createContentEvent(firstSegment, ndk);
const tree = new PublicationTree(rootEvent, ndk);
contentEvents.push(rootEvent);
@@ -530,16 +531,22 @@ function buildLevel2Structure(
const level2Groups = groupSegmentsByLevel2(segments);
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) {
- const contentEvent = createContentEvent(group, ndk);
+ const contentEvent = createContentEvent(group, ndk, title);
contentEvents.push(contentEvent);
+ const sectionDTag = generateDTag(group.title);
+ const namespacedDTag = `${pubAbbrev}-${sectionDTag}`;
+
const childNode = {
title: group.title,
level: group.level,
eventType: "content" as const,
eventKind: 30041 as const,
- dTag: generateDTag(group.title),
+ dTag: namespacedDTag,
children: [],
};
@@ -590,7 +597,8 @@ function buildHierarchicalStructure(
rootNode,
contentEvents,
ndk,
- parseLevel
+ parseLevel,
+ title
);
return { tree, indexEvent, contentEvents, eventStructure };
@@ -618,10 +626,15 @@ function createIndexEvent(
// Add document attributes as tags
addDocumentAttributesToTags(tags, attributes, event.pubkey);
+ // Generate publication abbreviation for namespacing sections
+ const pubAbbrev = generateTitleAbbreviation(title);
+
// Add a-tags for each content section
+ // Using new format: kind:pubkey:{abbv}-{section-d-tag}
segments.forEach((segment) => {
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;
@@ -635,13 +648,25 @@ function createIndexEvent(
/**
* 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);
event.kind = 30041;
event.created_at = Math.floor(Date.now() / 1000);
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 tags: string[][] = [["d", dTag], mTag, MTag, ["title", segment.title]];
@@ -652,7 +677,6 @@ function createContentEvent(segment: ContentSegment, ndk: NDK): NDKEvent {
event.tags = tags;
event.content = segment.content;
-
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
*/
@@ -925,21 +975,35 @@ function processHierarchicalGroup(
parentStructureNode: EventStructureNode,
contentEvents: NDKEvent[],
ndk: NDK,
- parseLevel: number
+ parseLevel: number,
+ publicationTitle: string,
): void {
+ const pubAbbrev = generateTitleAbbreviation(publicationTitle);
+
for (const node of nodes) {
if (node.hasChildren && node.segment.level < parseLevel) {
// This section has children and is not at parse level
// Create BOTH an index event AND a content event
-
+
// 1. Create the index event (30040)
- const indexEvent = createIndexEventForHierarchicalNode(node, ndk);
+ const indexEvent = createIndexEventForHierarchicalNode(
+ node,
+ ndk,
+ publicationTitle,
+ );
contentEvents.push(indexEvent);
-
+
// 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);
-
+
+ const sectionDTag = generateDTag(node.segment.title);
+ const namespacedDTag = `${pubAbbrev}-${sectionDTag}`;
+
// 3. Add index node to structure
const indexNode: EventStructureNode = {
title: node.segment.title,
@@ -950,36 +1014,44 @@ function processHierarchicalGroup(
children: [],
};
parentStructureNode.children.push(indexNode);
-
+
// 4. Add content node as first child of index
indexNode.children.push({
title: node.segment.title,
level: node.segment.level,
eventType: "content",
eventKind: 30041,
- dTag: generateDTag(node.segment.title),
+ dTag: namespacedDTag,
children: [],
});
-
+
// 5. Process children recursively
processHierarchicalGroup(
node.children,
indexNode,
contentEvents,
ndk,
- parseLevel
+ parseLevel,
+ publicationTitle,
);
} else {
// 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);
-
+
+ const sectionDTag = generateDTag(node.segment.title);
+ const namespacedDTag = `${pubAbbrev}-${sectionDTag}`;
+
parentStructureNode.children.push({
title: node.segment.title,
level: node.segment.level,
eventType: "content",
eventKind: 30041,
- dTag: generateDTag(node.segment.title),
+ dTag: namespacedDTag,
children: [],
});
}
@@ -991,7 +1063,8 @@ function processHierarchicalGroup(
*/
function createIndexEventForHierarchicalNode(
node: HierarchicalNode,
- ndk: NDK
+ ndk: NDK,
+ publicationTitle: string,
): NDKEvent {
const event = new NDKEvent(ndk);
event.kind = 30040;
@@ -1001,28 +1074,38 @@ function createIndexEventForHierarchicalNode(
const dTag = generateDTag(node.segment.title);
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
addSectionAttributesToTags(tags, node.segment.attributes);
- // Add a-tags for the section's own content event
- tags.push(["a", `30041:${event.pubkey}:${dTag}`]);
-
- // Add a-tags for each child section
+ const pubAbbrev = generateTitleAbbreviation(publicationTitle);
+
+ // 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) {
const childDTag = generateDTag(child.segment.title);
+ const namespacedChildDTag = `${pubAbbrev}-${childDTag}`;
if (child.hasChildren && child.segment.level < node.segment.level + 1) {
// Child will be an index
tags.push(["a", `30040:${event.pubkey}:${childDTag}`]);
} else {
- // Child will be content
- tags.push(["a", `30041:${event.pubkey}:${childDTag}`]);
+ // Child will be content with namespace
+ tags.push(["a", `30041:${event.pubkey}:${namespacedChildDTag}`]);
}
}
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;
}
@@ -1059,10 +1142,13 @@ function buildSegmentHierarchy(
/**
* 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(
section: HierarchicalSegment,
ndk: NDK,
+ publicationTitle: string,
): NDKEvent {
const event = new NDKEvent(ndk);
event.kind = 30040;
@@ -1077,10 +1163,13 @@ function createIndexEventForSection(
// Add section attributes as tags
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) => {
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;