|
|
|
|
@ -12,6 +12,8 @@ import { NDKEvent } from "@nostr-dev-kit/ndk";
@@ -12,6 +12,8 @@ import { NDKEvent } from "@nostr-dev-kit/ndk";
|
|
|
|
|
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; |
|
|
|
|
export interface ProcessorResult { |
|
|
|
|
tree: PublicationTree; |
|
|
|
|
indexEvent: NDKEvent | null; |
|
|
|
|
@ -57,46 +59,57 @@ export function registerPublicationTreeProcessor(
@@ -57,46 +59,57 @@ export function registerPublicationTreeProcessor(
|
|
|
|
|
registry: Registry, |
|
|
|
|
ndk: NDK, |
|
|
|
|
parseLevel: number = 2, |
|
|
|
|
originalContent: string |
|
|
|
|
originalContent: string, |
|
|
|
|
): { getResult: () => ProcessorResult | null } { |
|
|
|
|
let processorResult: ProcessorResult | null = null; |
|
|
|
|
|
|
|
|
|
registry.treeProcessor(function() { |
|
|
|
|
registry.treeProcessor(function () { |
|
|
|
|
const self = this; |
|
|
|
|
|
|
|
|
|
self.process(function(doc: Document) { |
|
|
|
|
self.process(function (doc: Document) { |
|
|
|
|
try { |
|
|
|
|
// Extract document metadata from AST
|
|
|
|
|
const title = doc.getTitle() || ''; |
|
|
|
|
const title = doc.getTitle() || ""; |
|
|
|
|
const attributes = doc.getAttributes(); |
|
|
|
|
const sections = doc.getSections(); |
|
|
|
|
|
|
|
|
|
console.log(`[TreeProcessor] Document attributes:`, { |
|
|
|
|
tags: attributes.tags, |
|
|
|
|
author: attributes.author, |
|
|
|
|
type: attributes.type |
|
|
|
|
type: attributes.type, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
console.log(`[TreeProcessor] Processing document: "${title}" at parse level ${parseLevel}`); |
|
|
|
|
console.log(`[TreeProcessor] Found ${sections.length} top-level sections`); |
|
|
|
|
console.log( |
|
|
|
|
`[TreeProcessor] Processing document: "${title}" at parse level ${parseLevel}`, |
|
|
|
|
); |
|
|
|
|
console.log( |
|
|
|
|
`[TreeProcessor] Found ${sections.length} top-level sections`, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Extract content segments from original text based on parse level
|
|
|
|
|
const contentSegments = extractContentSegments(originalContent, sections, parseLevel); |
|
|
|
|
console.log(`[TreeProcessor] Extracted ${contentSegments.length} content segments for level ${parseLevel}`); |
|
|
|
|
const contentSegments = extractContentSegments( |
|
|
|
|
originalContent, |
|
|
|
|
sections, |
|
|
|
|
parseLevel, |
|
|
|
|
); |
|
|
|
|
console.log( |
|
|
|
|
`[TreeProcessor] Extracted ${contentSegments.length} content segments for level ${parseLevel}`, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Determine content type based on structure
|
|
|
|
|
const contentType = detectContentType(title, contentSegments); |
|
|
|
|
console.log(`[TreeProcessor] Detected content type: ${contentType}`); |
|
|
|
|
|
|
|
|
|
// Build events and tree structure
|
|
|
|
|
const { tree, indexEvent, contentEvents, eventStructure } = buildEventsFromSegments( |
|
|
|
|
contentSegments,
|
|
|
|
|
title,
|
|
|
|
|
attributes,
|
|
|
|
|
contentType,
|
|
|
|
|
parseLevel,
|
|
|
|
|
ndk |
|
|
|
|
); |
|
|
|
|
const { tree, indexEvent, contentEvents, eventStructure } = |
|
|
|
|
buildEventsFromSegments( |
|
|
|
|
contentSegments, |
|
|
|
|
title, |
|
|
|
|
attributes, |
|
|
|
|
contentType, |
|
|
|
|
parseLevel, |
|
|
|
|
ndk, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
processorResult = { |
|
|
|
|
tree, |
|
|
|
|
@ -108,14 +121,15 @@ export function registerPublicationTreeProcessor(
@@ -108,14 +121,15 @@ export function registerPublicationTreeProcessor(
|
|
|
|
|
contentType, |
|
|
|
|
attributes, |
|
|
|
|
parseLevel, |
|
|
|
|
eventStructure |
|
|
|
|
} |
|
|
|
|
eventStructure, |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
console.log(`[TreeProcessor] Built tree with ${contentEvents.length} content events and ${indexEvent ? '1' : '0'} index events`); |
|
|
|
|
|
|
|
|
|
console.log( |
|
|
|
|
`[TreeProcessor] Built tree with ${contentEvents.length} content events and ${indexEvent ? "1" : "0"} index events`, |
|
|
|
|
); |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('[TreeProcessor] Error processing document:', error); |
|
|
|
|
console.error("[TreeProcessor] Error processing document:", error); |
|
|
|
|
processorResult = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -124,7 +138,7 @@ export function registerPublicationTreeProcessor(
@@ -124,7 +138,7 @@ export function registerPublicationTreeProcessor(
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
getResult: () => processorResult |
|
|
|
|
getResult: () => processorResult, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -135,24 +149,25 @@ export function registerPublicationTreeProcessor(
@@ -135,24 +149,25 @@ export function registerPublicationTreeProcessor(
|
|
|
|
|
function extractContentSegments( |
|
|
|
|
originalContent: string, |
|
|
|
|
sections: any[], |
|
|
|
|
parseLevel: number |
|
|
|
|
parseLevel: number, |
|
|
|
|
): ContentSegment[] { |
|
|
|
|
const lines = originalContent.split('\n'); |
|
|
|
|
const lines = originalContent.split("\n"); |
|
|
|
|
|
|
|
|
|
// Build hierarchy map from AST
|
|
|
|
|
const sectionHierarchy = buildSectionHierarchy(sections); |
|
|
|
|
|
|
|
|
|
// Debug: Show hierarchy depths
|
|
|
|
|
console.log(`[TreeProcessor] Section hierarchy depth analysis:`); |
|
|
|
|
function showDepth(nodes: SectionNode[], depth = 0) { |
|
|
|
|
for (const node of nodes) { |
|
|
|
|
console.log(`${' '.repeat(depth)}Level ${node.level}: ${node.title}`); |
|
|
|
|
console.log(`${" ".repeat(depth)}Level ${node.level}: ${node.title}`); |
|
|
|
|
if (node.children.length > 0) { |
|
|
|
|
showDepth(node.children, depth + 1); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
showDepth(sectionHierarchy); |
|
|
|
|
if (DEBUG) { |
|
|
|
|
showDepth(sectionHierarchy); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Extract segments at the target parse level
|
|
|
|
|
return extractSegmentsAtLevel(lines, sectionHierarchy, parseLevel); |
|
|
|
|
@ -167,7 +182,7 @@ function buildSectionHierarchy(sections: any[]): SectionNode[] {
@@ -167,7 +182,7 @@ function buildSectionHierarchy(sections: any[]): SectionNode[] {
|
|
|
|
|
title: section.getTitle(), |
|
|
|
|
level: section.getLevel() + 1, // Convert to app level (Asciidoctor uses 0-based)
|
|
|
|
|
attributes: section.getAttributes() || {}, |
|
|
|
|
children: (section.getSections() || []).map(buildNode) |
|
|
|
|
children: (section.getSections() || []).map(buildNode), |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -188,7 +203,7 @@ interface SectionNode {
@@ -188,7 +203,7 @@ interface SectionNode {
|
|
|
|
|
function extractSegmentsAtLevel( |
|
|
|
|
lines: string[], |
|
|
|
|
hierarchy: SectionNode[], |
|
|
|
|
parseLevel: number |
|
|
|
|
parseLevel: number, |
|
|
|
|
): ContentSegment[] { |
|
|
|
|
const segments: ContentSegment[] = []; |
|
|
|
|
|
|
|
|
|
@ -209,7 +224,10 @@ function extractSegmentsAtLevel(
@@ -209,7 +224,10 @@ function extractSegmentsAtLevel(
|
|
|
|
|
* Recursively collect sections at or above the specified level |
|
|
|
|
* NKBIP-01: Level N parsing includes sections from level 2 through level N |
|
|
|
|
*/ |
|
|
|
|
function collectSectionsAtLevel(hierarchy: SectionNode[], targetLevel: number): SectionNode[] { |
|
|
|
|
function collectSectionsAtLevel( |
|
|
|
|
hierarchy: SectionNode[], |
|
|
|
|
targetLevel: number, |
|
|
|
|
): SectionNode[] { |
|
|
|
|
const collected: SectionNode[] = []; |
|
|
|
|
|
|
|
|
|
function traverse(nodes: SectionNode[]) { |
|
|
|
|
@ -236,10 +254,12 @@ function collectSectionsAtLevel(hierarchy: SectionNode[], targetLevel: number):
@@ -236,10 +254,12 @@ function collectSectionsAtLevel(hierarchy: SectionNode[], targetLevel: number):
|
|
|
|
|
function extractSegmentContent( |
|
|
|
|
lines: string[], |
|
|
|
|
section: SectionNode, |
|
|
|
|
parseLevel: number |
|
|
|
|
parseLevel: number, |
|
|
|
|
): ContentSegment | null { |
|
|
|
|
// Find the section header in the original content
|
|
|
|
|
const sectionPattern = new RegExp(`^${'='.repeat(section.level)}\\s+${escapeRegex(section.title)}`); |
|
|
|
|
const sectionPattern = new RegExp( |
|
|
|
|
`^${"=".repeat(section.level)}\\s+${escapeRegex(section.title)}`, |
|
|
|
|
); |
|
|
|
|
let startIdx = -1; |
|
|
|
|
|
|
|
|
|
for (let i = 0; i < lines.length; i++) { |
|
|
|
|
@ -250,7 +270,9 @@ function extractSegmentContent(
@@ -250,7 +270,9 @@ function extractSegmentContent(
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (startIdx === -1) { |
|
|
|
|
console.warn(`[TreeProcessor] Could not find section "${section.title}" at level ${section.level}`); |
|
|
|
|
console.warn( |
|
|
|
|
`[TreeProcessor] Could not find section "${section.title}" at level ${section.level}`, |
|
|
|
|
); |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -276,14 +298,17 @@ function extractSegmentContent(
@@ -276,14 +298,17 @@ function extractSegmentContent(
|
|
|
|
|
level: section.level, |
|
|
|
|
attributes, |
|
|
|
|
startLine: startIdx, |
|
|
|
|
endLine: endIdx |
|
|
|
|
endLine: endIdx, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Parse attributes and content from section lines |
|
|
|
|
*/ |
|
|
|
|
function parseSegmentContent(sectionLines: string[], parseLevel: number): { |
|
|
|
|
function parseSegmentContent( |
|
|
|
|
sectionLines: string[], |
|
|
|
|
parseLevel: number, |
|
|
|
|
): { |
|
|
|
|
attributes: Record<string, string>; |
|
|
|
|
content: string; |
|
|
|
|
} { |
|
|
|
|
@ -293,20 +318,20 @@ function parseSegmentContent(sectionLines: string[], parseLevel: number): {
@@ -293,20 +318,20 @@ function parseSegmentContent(sectionLines: string[], parseLevel: number): {
|
|
|
|
|
// Look for attribute lines after the title
|
|
|
|
|
for (let i = 1; i < sectionLines.length; i++) { |
|
|
|
|
const line = sectionLines[i].trim(); |
|
|
|
|
if (line.startsWith(':') && line.includes(':')) { |
|
|
|
|
if (line.startsWith(":") && line.includes(":")) { |
|
|
|
|
const match = line.match(/^:([^:]+):\\s*(.*)$/); |
|
|
|
|
if (match) { |
|
|
|
|
attributes[match[1]] = match[2]; |
|
|
|
|
contentStartIdx = i + 1; |
|
|
|
|
} |
|
|
|
|
} else if (line !== '') { |
|
|
|
|
} else if (line !== "") { |
|
|
|
|
// Non-empty, non-attribute line - content starts here
|
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Extract content (everything after attributes)
|
|
|
|
|
const content = sectionLines.slice(contentStartIdx).join('\n').trim(); |
|
|
|
|
const content = sectionLines.slice(contentStartIdx).join("\n").trim(); |
|
|
|
|
|
|
|
|
|
return { attributes, content }; |
|
|
|
|
} |
|
|
|
|
@ -316,13 +341,14 @@ function parseSegmentContent(sectionLines: string[], parseLevel: number): {
@@ -316,13 +341,14 @@ function parseSegmentContent(sectionLines: string[], parseLevel: number): {
|
|
|
|
|
*/ |
|
|
|
|
function detectContentType( |
|
|
|
|
title: string, |
|
|
|
|
segments: ContentSegment[] |
|
|
|
|
segments: ContentSegment[], |
|
|
|
|
): "article" | "scattered-notes" | "none" { |
|
|
|
|
const hasDocTitle = !!title; |
|
|
|
|
const hasSections = segments.length > 0; |
|
|
|
|
|
|
|
|
|
// Check if the title matches the first section title
|
|
|
|
|
const titleMatchesFirstSection = segments.length > 0 && title === segments[0].title; |
|
|
|
|
const titleMatchesFirstSection = |
|
|
|
|
segments.length > 0 && title === segments[0].title; |
|
|
|
|
|
|
|
|
|
if (hasDocTitle && hasSections && !titleMatchesFirstSection) { |
|
|
|
|
return "article"; |
|
|
|
|
@ -345,7 +371,7 @@ function buildEventsFromSegments(
@@ -345,7 +371,7 @@ function buildEventsFromSegments(
|
|
|
|
|
attributes: Record<string, string>, |
|
|
|
|
contentType: "article" | "scattered-notes" | "none", |
|
|
|
|
parseLevel: number, |
|
|
|
|
ndk: NDK |
|
|
|
|
ndk: NDK, |
|
|
|
|
): { |
|
|
|
|
tree: PublicationTree; |
|
|
|
|
indexEvent: NDKEvent | null; |
|
|
|
|
@ -368,7 +394,7 @@ function buildEventsFromSegments(
@@ -368,7 +394,7 @@ function buildEventsFromSegments(
|
|
|
|
|
*/ |
|
|
|
|
function buildScatteredNotesStructure( |
|
|
|
|
segments: ContentSegment[], |
|
|
|
|
ndk: NDK |
|
|
|
|
ndk: NDK, |
|
|
|
|
): { |
|
|
|
|
tree: PublicationTree; |
|
|
|
|
indexEvent: NDKEvent | null; |
|
|
|
|
@ -389,7 +415,7 @@ function buildScatteredNotesStructure(
@@ -389,7 +415,7 @@ function buildScatteredNotesStructure(
|
|
|
|
|
eventType: "content", |
|
|
|
|
eventKind: 30041, |
|
|
|
|
dTag: generateDTag(firstSegment.title), |
|
|
|
|
children: [] |
|
|
|
|
children: [], |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Add remaining segments
|
|
|
|
|
@ -403,7 +429,7 @@ function buildScatteredNotesStructure(
@@ -403,7 +429,7 @@ function buildScatteredNotesStructure(
|
|
|
|
|
eventType: "content", |
|
|
|
|
eventKind: 30041, |
|
|
|
|
dTag: generateDTag(segments[i].title), |
|
|
|
|
children: [] |
|
|
|
|
children: [], |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -418,7 +444,7 @@ function buildArticleStructure(
@@ -418,7 +444,7 @@ function buildArticleStructure(
|
|
|
|
|
title: string, |
|
|
|
|
attributes: Record<string, string>, |
|
|
|
|
parseLevel: number, |
|
|
|
|
ndk: NDK |
|
|
|
|
ndk: NDK, |
|
|
|
|
): { |
|
|
|
|
tree: PublicationTree; |
|
|
|
|
indexEvent: NDKEvent | null; |
|
|
|
|
@ -431,7 +457,14 @@ function buildArticleStructure(
@@ -431,7 +457,14 @@ function buildArticleStructure(
|
|
|
|
|
if (parseLevel === 2) { |
|
|
|
|
return buildLevel2Structure(segments, title, indexEvent, tree, ndk); |
|
|
|
|
} else { |
|
|
|
|
return buildHierarchicalStructure(segments, title, indexEvent, tree, parseLevel, ndk); |
|
|
|
|
return buildHierarchicalStructure( |
|
|
|
|
segments, |
|
|
|
|
title, |
|
|
|
|
indexEvent, |
|
|
|
|
tree, |
|
|
|
|
parseLevel, |
|
|
|
|
ndk, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -443,7 +476,7 @@ function buildLevel2Structure(
@@ -443,7 +476,7 @@ function buildLevel2Structure(
|
|
|
|
|
title: string, |
|
|
|
|
indexEvent: NDKEvent, |
|
|
|
|
tree: PublicationTree, |
|
|
|
|
ndk: NDK |
|
|
|
|
ndk: NDK, |
|
|
|
|
): { |
|
|
|
|
tree: PublicationTree; |
|
|
|
|
indexEvent: NDKEvent | null; |
|
|
|
|
@ -460,7 +493,7 @@ function buildLevel2Structure(
@@ -460,7 +493,7 @@ function buildLevel2Structure(
|
|
|
|
|
eventType: "index", |
|
|
|
|
eventKind: 30040, |
|
|
|
|
dTag: generateDTag(title), |
|
|
|
|
children: [] |
|
|
|
|
children: [], |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Group segments by level 2 sections
|
|
|
|
|
@ -476,7 +509,7 @@ function buildLevel2Structure(
@@ -476,7 +509,7 @@ function buildLevel2Structure(
|
|
|
|
|
eventType: "content", |
|
|
|
|
eventKind: 30041, |
|
|
|
|
dTag: generateDTag(group.title), |
|
|
|
|
children: [] |
|
|
|
|
children: [], |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -492,7 +525,7 @@ function buildHierarchicalStructure(
@@ -492,7 +525,7 @@ function buildHierarchicalStructure(
|
|
|
|
|
indexEvent: NDKEvent, |
|
|
|
|
tree: PublicationTree, |
|
|
|
|
parseLevel: number, |
|
|
|
|
ndk: NDK |
|
|
|
|
ndk: NDK, |
|
|
|
|
): { |
|
|
|
|
tree: PublicationTree; |
|
|
|
|
indexEvent: NDKEvent | null; |
|
|
|
|
@ -509,7 +542,7 @@ function buildHierarchicalStructure(
@@ -509,7 +542,7 @@ function buildHierarchicalStructure(
|
|
|
|
|
eventType: "index", |
|
|
|
|
eventKind: 30040, |
|
|
|
|
dTag: generateDTag(title), |
|
|
|
|
children: [] |
|
|
|
|
children: [], |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Build hierarchical structure
|
|
|
|
|
@ -527,7 +560,7 @@ function buildHierarchicalStructure(
@@ -527,7 +560,7 @@ function buildHierarchicalStructure(
|
|
|
|
|
eventType: "index", |
|
|
|
|
eventKind: 30040, |
|
|
|
|
dTag: generateDTag(level2Section.title), |
|
|
|
|
children: [] |
|
|
|
|
children: [], |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// Add children as 30041 content events
|
|
|
|
|
@ -541,7 +574,7 @@ function buildHierarchicalStructure(
@@ -541,7 +574,7 @@ function buildHierarchicalStructure(
|
|
|
|
|
eventType: "content", |
|
|
|
|
eventKind: 30041, |
|
|
|
|
dTag: generateDTag(child.title), |
|
|
|
|
children: [] |
|
|
|
|
children: [], |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -557,7 +590,7 @@ function buildHierarchicalStructure(
@@ -557,7 +590,7 @@ function buildHierarchicalStructure(
|
|
|
|
|
eventType: "content", |
|
|
|
|
eventKind: 30041, |
|
|
|
|
dTag: generateDTag(level2Section.title), |
|
|
|
|
children: [] |
|
|
|
|
children: [], |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -572,7 +605,7 @@ function createIndexEvent(
@@ -572,7 +605,7 @@ function createIndexEvent(
|
|
|
|
|
title: string, |
|
|
|
|
attributes: Record<string, string>, |
|
|
|
|
segments: ContentSegment[], |
|
|
|
|
ndk: NDK |
|
|
|
|
ndk: NDK, |
|
|
|
|
): NDKEvent { |
|
|
|
|
const event = new NDKEvent(ndk); |
|
|
|
|
event.kind = 30040; |
|
|
|
|
@ -582,18 +615,13 @@ function createIndexEvent(
@@ -582,18 +615,13 @@ function createIndexEvent(
|
|
|
|
|
const dTag = generateDTag(title); |
|
|
|
|
const [mTag, MTag] = getMimeTags(30040); |
|
|
|
|
|
|
|
|
|
const tags: string[][] = [ |
|
|
|
|
["d", dTag], |
|
|
|
|
mTag, |
|
|
|
|
MTag, |
|
|
|
|
["title", title] |
|
|
|
|
]; |
|
|
|
|
const tags: string[][] = [["d", dTag], mTag, MTag, ["title", title]]; |
|
|
|
|
|
|
|
|
|
// Add document attributes as tags
|
|
|
|
|
addDocumentAttributesToTags(tags, attributes, event.pubkey); |
|
|
|
|
|
|
|
|
|
// Add a-tags for each content section
|
|
|
|
|
segments.forEach(segment => { |
|
|
|
|
segments.forEach((segment) => { |
|
|
|
|
const sectionDTag = generateDTag(segment.title); |
|
|
|
|
tags.push(["a", `30041:${event.pubkey}:${sectionDTag}`]); |
|
|
|
|
}); |
|
|
|
|
@ -618,12 +646,7 @@ function createContentEvent(segment: ContentSegment, ndk: NDK): NDKEvent {
@@ -618,12 +646,7 @@ function createContentEvent(segment: ContentSegment, ndk: NDK): NDKEvent {
|
|
|
|
|
const dTag = generateDTag(segment.title); |
|
|
|
|
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]]; |
|
|
|
|
|
|
|
|
|
// Add segment attributes as tags
|
|
|
|
|
addSectionAttributesToTags(tags, segment.attributes); |
|
|
|
|
@ -637,30 +660,35 @@ function createContentEvent(segment: ContentSegment, ndk: NDK): NDKEvent {
@@ -637,30 +660,35 @@ function createContentEvent(segment: ContentSegment, ndk: NDK): NDKEvent {
|
|
|
|
|
/** |
|
|
|
|
* Generate default index content |
|
|
|
|
*/ |
|
|
|
|
function generateIndexContent(title: string, segments: ContentSegment[]): string { |
|
|
|
|
function generateIndexContent( |
|
|
|
|
title: string, |
|
|
|
|
segments: ContentSegment[], |
|
|
|
|
): string { |
|
|
|
|
return `# ${title} |
|
|
|
|
|
|
|
|
|
${segments.length} sections available: |
|
|
|
|
|
|
|
|
|
${segments.map((segment, i) => `${i + 1}. ${segment.title}`).join('\n')}`;
|
|
|
|
|
${segments.map((segment, i) => `${i + 1}. ${segment.title}`).join("\n")}`;
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Escape regex special characters |
|
|
|
|
*/ |
|
|
|
|
function escapeRegex(str: string): string { |
|
|
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
|
|
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Generate deterministic d-tag from title |
|
|
|
|
*/ |
|
|
|
|
function generateDTag(title: string): string { |
|
|
|
|
return title |
|
|
|
|
.toLowerCase() |
|
|
|
|
.replace(/[^\p{L}\p{N}]/gu, "-") |
|
|
|
|
.replace(/-+/g, "-") |
|
|
|
|
.replace(/^-|-$/g, "") || "untitled"; |
|
|
|
|
return ( |
|
|
|
|
title |
|
|
|
|
.toLowerCase() |
|
|
|
|
.replace(/[^\p{L}\p{N}]/gu, "-") |
|
|
|
|
.replace(/-+/g, "-") |
|
|
|
|
.replace(/^-|-$/g, "") || "untitled" |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
@ -669,7 +697,7 @@ function generateDTag(title: string): string {
@@ -669,7 +697,7 @@ function generateDTag(title: string): string {
|
|
|
|
|
function addDocumentAttributesToTags( |
|
|
|
|
tags: string[][], |
|
|
|
|
attributes: Record<string, string>, |
|
|
|
|
pubkey: string |
|
|
|
|
pubkey: string, |
|
|
|
|
) { |
|
|
|
|
// Standard metadata
|
|
|
|
|
if (attributes.author) tags.push(["author", attributes.author]); |
|
|
|
|
@ -682,7 +710,7 @@ function addDocumentAttributesToTags(
@@ -682,7 +710,7 @@ function addDocumentAttributesToTags(
|
|
|
|
|
|
|
|
|
|
// Tags
|
|
|
|
|
if (attributes.tags) { |
|
|
|
|
attributes.tags.split(",").forEach(tag => tags.push(["t", tag.trim()])); |
|
|
|
|
attributes.tags.split(",").forEach((tag) => tags.push(["t", tag.trim()])); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Add pubkey reference
|
|
|
|
|
@ -697,11 +725,11 @@ function addDocumentAttributesToTags(
@@ -697,11 +725,11 @@ function addDocumentAttributesToTags(
|
|
|
|
|
*/ |
|
|
|
|
function addSectionAttributesToTags( |
|
|
|
|
tags: string[][], |
|
|
|
|
attributes: Record<string, string> |
|
|
|
|
attributes: Record<string, string>, |
|
|
|
|
) { |
|
|
|
|
// Section tags
|
|
|
|
|
if (attributes.tags) { |
|
|
|
|
attributes.tags.split(",").forEach(tag => tags.push(["t", tag.trim()])); |
|
|
|
|
attributes.tags.split(",").forEach((tag) => tags.push(["t", tag.trim()])); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Custom attributes
|
|
|
|
|
@ -713,22 +741,61 @@ function addSectionAttributesToTags(
@@ -713,22 +741,61 @@ function addSectionAttributesToTags(
|
|
|
|
|
*/ |
|
|
|
|
function addCustomAttributes( |
|
|
|
|
tags: string[][], |
|
|
|
|
attributes: Record<string, string> |
|
|
|
|
attributes: Record<string, string>, |
|
|
|
|
) { |
|
|
|
|
const systemAttributes = [ |
|
|
|
|
"attribute-undefined", "attribute-missing", "appendix-caption", |
|
|
|
|
"appendix-refsig", "caution-caption", "chapter-refsig", "example-caption", |
|
|
|
|
"figure-caption", "important-caption", "last-update-label", "manname-title", |
|
|
|
|
"note-caption", "part-refsig", "preface-title", "section-refsig", |
|
|
|
|
"table-caption", "tip-caption", "toc-title", "untitled-label", |
|
|
|
|
"version-label", "warning-caption", "asciidoctor", "asciidoctor-version", |
|
|
|
|
"safe-mode-name", "backend", "doctype", "basebackend", "filetype", |
|
|
|
|
"outfilesuffix", "stylesdir", "iconsdir", "localdate", "localyear", |
|
|
|
|
"localtime", "localdatetime", "docdate", "docyear", "doctime", |
|
|
|
|
"docdatetime", "doctitle", "embedded", "notitle", |
|
|
|
|
"attribute-undefined", |
|
|
|
|
"attribute-missing", |
|
|
|
|
"appendix-caption", |
|
|
|
|
"appendix-refsig", |
|
|
|
|
"caution-caption", |
|
|
|
|
"chapter-refsig", |
|
|
|
|
"example-caption", |
|
|
|
|
"figure-caption", |
|
|
|
|
"important-caption", |
|
|
|
|
"last-update-label", |
|
|
|
|
"manname-title", |
|
|
|
|
"note-caption", |
|
|
|
|
"part-refsig", |
|
|
|
|
"preface-title", |
|
|
|
|
"section-refsig", |
|
|
|
|
"table-caption", |
|
|
|
|
"tip-caption", |
|
|
|
|
"toc-title", |
|
|
|
|
"untitled-label", |
|
|
|
|
"version-label", |
|
|
|
|
"warning-caption", |
|
|
|
|
"asciidoctor", |
|
|
|
|
"asciidoctor-version", |
|
|
|
|
"safe-mode-name", |
|
|
|
|
"backend", |
|
|
|
|
"doctype", |
|
|
|
|
"basebackend", |
|
|
|
|
"filetype", |
|
|
|
|
"outfilesuffix", |
|
|
|
|
"stylesdir", |
|
|
|
|
"iconsdir", |
|
|
|
|
"localdate", |
|
|
|
|
"localyear", |
|
|
|
|
"localtime", |
|
|
|
|
"localdatetime", |
|
|
|
|
"docdate", |
|
|
|
|
"docyear", |
|
|
|
|
"doctime", |
|
|
|
|
"docdatetime", |
|
|
|
|
"doctitle", |
|
|
|
|
"embedded", |
|
|
|
|
"notitle", |
|
|
|
|
// Already handled above
|
|
|
|
|
"author", "version", "published", "language", "image", "description", |
|
|
|
|
"tags", "title", "type" |
|
|
|
|
"author", |
|
|
|
|
"version", |
|
|
|
|
"published", |
|
|
|
|
"language", |
|
|
|
|
"image", |
|
|
|
|
"description", |
|
|
|
|
"tags", |
|
|
|
|
"title", |
|
|
|
|
"type", |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
Object.entries(attributes).forEach(([key, value]) => { |
|
|
|
|
@ -749,21 +816,24 @@ function groupSegmentsByLevel2(segments: ContentSegment[]): ContentSegment[] {
@@ -749,21 +816,24 @@ function groupSegmentsByLevel2(segments: ContentSegment[]): ContentSegment[] {
|
|
|
|
|
for (const segment of segments) { |
|
|
|
|
if (segment.level === 2) { |
|
|
|
|
// Find all content that belongs to this level 2 section
|
|
|
|
|
const nestedSegments = segments.filter(s =>
|
|
|
|
|
s.level > 2 &&
|
|
|
|
|
s.startLine > segment.startLine &&
|
|
|
|
|
(segments.find(next => next.level <= 2 && next.startLine > segment.startLine)?.startLine || Infinity) > s.startLine |
|
|
|
|
const nestedSegments = segments.filter( |
|
|
|
|
(s) => |
|
|
|
|
s.level > 2 && |
|
|
|
|
s.startLine > segment.startLine && |
|
|
|
|
(segments.find( |
|
|
|
|
(next) => next.level <= 2 && next.startLine > segment.startLine, |
|
|
|
|
)?.startLine || Infinity) > s.startLine, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Combine the level 2 content with all nested content
|
|
|
|
|
let combinedContent = segment.content; |
|
|
|
|
for (const nested of nestedSegments) { |
|
|
|
|
combinedContent += `\n\n${'='.repeat(nested.level)} ${nested.title}\n${nested.content}`; |
|
|
|
|
combinedContent += `\n\n${"=".repeat(nested.level)} ${nested.title}\n${nested.content}`; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
level2Groups.push({ |
|
|
|
|
...segment, |
|
|
|
|
content: combinedContent |
|
|
|
|
content: combinedContent, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -774,21 +844,26 @@ function groupSegmentsByLevel2(segments: ContentSegment[]): ContentSegment[] {
@@ -774,21 +844,26 @@ function groupSegmentsByLevel2(segments: ContentSegment[]): ContentSegment[] {
|
|
|
|
|
/** |
|
|
|
|
* Build hierarchical segment structure for Level 3+ parsing |
|
|
|
|
*/ |
|
|
|
|
function buildSegmentHierarchy(segments: ContentSegment[]): HierarchicalSegment[] { |
|
|
|
|
function buildSegmentHierarchy( |
|
|
|
|
segments: ContentSegment[], |
|
|
|
|
): HierarchicalSegment[] { |
|
|
|
|
const hierarchy: HierarchicalSegment[] = []; |
|
|
|
|
|
|
|
|
|
// Process level 2 sections
|
|
|
|
|
for (const level2Segment of segments.filter(s => s.level === 2)) { |
|
|
|
|
const children = segments.filter(s =>
|
|
|
|
|
s.level > 2 &&
|
|
|
|
|
s.startLine > level2Segment.startLine &&
|
|
|
|
|
(segments.find(next => next.level <= 2 && next.startLine > level2Segment.startLine)?.startLine || Infinity) > s.startLine |
|
|
|
|
for (const level2Segment of segments.filter((s) => s.level === 2)) { |
|
|
|
|
const children = segments.filter( |
|
|
|
|
(s) => |
|
|
|
|
s.level > 2 && |
|
|
|
|
s.startLine > level2Segment.startLine && |
|
|
|
|
(segments.find( |
|
|
|
|
(next) => next.level <= 2 && next.startLine > level2Segment.startLine, |
|
|
|
|
)?.startLine || Infinity) > s.startLine, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
hierarchy.push({ |
|
|
|
|
...level2Segment, |
|
|
|
|
hasChildren: children.length > 0, |
|
|
|
|
children |
|
|
|
|
children, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -798,7 +873,10 @@ function buildSegmentHierarchy(segments: ContentSegment[]): HierarchicalSegment[
@@ -798,7 +873,10 @@ function buildSegmentHierarchy(segments: ContentSegment[]): HierarchicalSegment[
|
|
|
|
|
/** |
|
|
|
|
* Create a 30040 index event for a section with children |
|
|
|
|
*/ |
|
|
|
|
function createIndexEventForSection(section: HierarchicalSegment, ndk: NDK): NDKEvent { |
|
|
|
|
function createIndexEventForSection( |
|
|
|
|
section: HierarchicalSegment, |
|
|
|
|
ndk: NDK, |
|
|
|
|
): NDKEvent { |
|
|
|
|
const event = new NDKEvent(ndk); |
|
|
|
|
event.kind = 30040; |
|
|
|
|
event.created_at = Math.floor(Date.now() / 1000); |
|
|
|
|
@ -807,18 +885,13 @@ function createIndexEventForSection(section: HierarchicalSegment, ndk: NDK): NDK
@@ -807,18 +885,13 @@ function createIndexEventForSection(section: HierarchicalSegment, ndk: NDK): NDK
|
|
|
|
|
const dTag = generateDTag(section.title); |
|
|
|
|
const [mTag, MTag] = getMimeTags(30040); |
|
|
|
|
|
|
|
|
|
const tags: string[][] = [ |
|
|
|
|
["d", dTag], |
|
|
|
|
mTag, |
|
|
|
|
MTag, |
|
|
|
|
["title", section.title] |
|
|
|
|
]; |
|
|
|
|
const tags: string[][] = [["d", dTag], mTag, MTag, ["title", section.title]]; |
|
|
|
|
|
|
|
|
|
// Add section attributes as tags
|
|
|
|
|
addSectionAttributesToTags(tags, section.attributes); |
|
|
|
|
|
|
|
|
|
// Add a-tags for each child content section
|
|
|
|
|
section.children.forEach(child => { |
|
|
|
|
section.children.forEach((child) => { |
|
|
|
|
const childDTag = generateDTag(child.title); |
|
|
|
|
tags.push(["a", `30041:${event.pubkey}:${childDTag}`]); |
|
|
|
|
}); |
|
|
|
|
|