From 5843aaa5f7f1a790ba9655ab99572c9b71c27668 Mon Sep 17 00:00:00 2001 From: limina1 Date: Mon, 8 Sep 2025 15:15:45 -0400 Subject: [PATCH] Remove debug logging - @src/lib/components/ZettelEditor - @src/lib/services/publisher - @src/lib/utils/asciidoc_metadata == - @src/lib/routes/my-notes/+page.svelte, will move to event structure preview --- src/lib/components/ZettelEditor.svelte | 24 +- src/lib/services/publisher.ts | 32 -- src/lib/utils/asciidoc_metadata.ts | 250 +++++----- src/lib/utils/publication_tree_processor.ts | 479 +++++++++++--------- src/routes/my-notes/+page.svelte | 27 +- 5 files changed, 436 insertions(+), 376 deletions(-) diff --git a/src/lib/components/ZettelEditor.svelte b/src/lib/components/ZettelEditor.svelte index dbbec9f..f42bdb2 100644 --- a/src/lib/components/ZettelEditor.svelte +++ b/src/lib/components/ZettelEditor.svelte @@ -4,8 +4,7 @@ import { EditorView, basicSetup } from "codemirror"; import { EditorState, StateField, StateEffect } from "@codemirror/state"; import { markdown } from "@codemirror/lang-markdown"; - import { oneDark } from "@codemirror/theme-one-dark"; - import { ViewPlugin, Decoration, type DecorationSet } from "@codemirror/view"; + import { Decoration, type DecorationSet } from "@codemirror/view"; import { RangeSet } from "@codemirror/state"; import { onMount } from "svelte"; import { @@ -92,27 +91,6 @@ import Asciidoctor from "asciidoctor"; // Export events for publishing workflow return exportEventsFromTree(result); }) - .then(events => { - // Debug: Check what we're getting from exportEventsFromTree - console.log("Events from exportEventsFromTree:", events); - console.log("Event keys:", Object.keys(events)); - if (events.indexEvent) { - console.log("Index event keys:", Object.keys(events.indexEvent)); - } - if (events.contentEvents?.[0]) { - console.log("First content event keys:", Object.keys(events.contentEvents[0])); - } - - generatedEvents = events; - - console.log("Tree factory result:", { - contentType, - indexEvent: !!events.indexEvent, - contentEvents: events.contentEvents.length, - parseLevel: parseLevel - }); - - }) .catch(error => { console.error("Tree factory error:", error); publicationResult = null; diff --git a/src/lib/services/publisher.ts b/src/lib/services/publisher.ts index 806a544..f735446 100644 --- a/src/lib/services/publisher.ts +++ b/src/lib/services/publisher.ts @@ -191,21 +191,8 @@ export async function publishSingleEvent( const publishedToRelays = await ndkEvent.publish(relaySet); if (publishedToRelays.size > 0) { - // Debug: Log the event structure in a clean, concise format - const dTagEntry = tags.find((t) => t[0] === "d"); - const dTag = dTagEntry ? dTagEntry[1] : ""; - const titleTag = tags.find((t) => t[0] === "title"); - const title = titleTag ? titleTag[1] : "Untitled"; - - console.log(`Event verified: ${ndkEvent.id}`); - return { success: true, eventId: ndkEvent.id }; } else { - const titleTag = tags.find((t) => t[0] === "title"); - const title = titleTag ? titleTag[1] : "Untitled"; - console.error( - `Failed to publish event: ${title} (${kind}) - no relays responded`, - ); throw new Error("Failed to publish to any relays"); } } catch (error) { @@ -290,25 +277,6 @@ export async function publishMultipleZettels( results.push({ success: false, error: errorMessage }); } } - // Debug: extract and log 'e' and 'a' tags from all published events - publishedEvents.forEach((ev) => { - // Extract d-tag from tags - const dTagEntry = ev.tags.find((t) => t[0] === "d"); - const dTag = dTagEntry ? dTagEntry[1] : ""; - const aTag = `${ev.kind}:${ev.pubkey}:${dTag}`; - console.log(`Event ${ev.id} tags:`); - console.log(" e:", ev.id); - console.log(" a:", aTag); - // Print nevent and naddr using nip19 - const nevent = nip19.neventEncode({ id: ev.id }); - const naddr = nip19.naddrEncode({ - kind: ev.kind, - pubkey: ev.pubkey, - identifier: dTag, - }); - console.log(" nevent:", nevent); - console.log(" naddr:", naddr); - }); return results; } catch (error) { const errorMessage = diff --git a/src/lib/utils/asciidoc_metadata.ts b/src/lib/utils/asciidoc_metadata.ts index b6eedd7..6dcd854 100644 --- a/src/lib/utils/asciidoc_metadata.ts +++ b/src/lib/utils/asciidoc_metadata.ts @@ -2,7 +2,6 @@ * AsciiDoc Metadata Extraction Service using Asciidoctor * * Thin wrapper around Asciidoctor's built-in metadata extraction capabilities. - * Leverages the existing Pharos parser to avoid duplication. */ // @ts-ignore @@ -23,38 +22,37 @@ export interface AsciiDocMetadata { source?: string; publishedBy?: string; type?: string; - autoUpdate?: 'yes' | 'ask' | 'no'; + autoUpdate?: "yes" | "ask" | "no"; customAttributes?: Record; } export type SectionMetadata = AsciiDocMetadata; - // Shared attribute mapping based on Asciidoctor standard attributes const ATTRIBUTE_MAP: Record = { // Standard Asciidoctor attributes - "author": "authors", - "description": "summary", - "keywords": "tags", - "revnumber": "version", - "revdate": "publicationDate", - "revremark": "edition", - "title": "title", + author: "authors", + description: "summary", + keywords: "tags", + revnumber: "version", + revdate: "publicationDate", + revremark: "edition", + title: "title", // Custom attributes for Alexandria - "published_by": "publishedBy", - "publisher": "publisher", - "summary": "summary", - "image": "coverImage", - "cover": "coverImage", - "isbn": "isbn", - "source": "source", - "type": "type", + published_by: "publishedBy", + publisher: "publisher", + summary: "summary", + image: "coverImage", + cover: "coverImage", + isbn: "isbn", + source: "source", + type: "type", "auto-update": "autoUpdate", - "version": "version", - "edition": "edition", - "published_on": "publicationDate", - "date": "publicationDate", + version: "version", + edition: "edition", + published_on: "publicationDate", + date: "publicationDate", "version-label": "version", }; @@ -70,21 +68,21 @@ function createProcessor() { */ function decodeHtmlEntities(text: string): string { const entities: Record = { - '’': "'", - '‘': "'", - '“': '"', - '”': '"', - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': "'", - ''': "'", + "’": "'", + "‘": "'", + "“": '"', + "”": '"', + "&": "&", + "<": "<", + ">": ">", + """: '"', + "'": "'", + "'": "'", }; - + let result = text; for (const [entity, char] of Object.entries(entities)) { - result = result.replace(new RegExp(entity, 'g'), char); + result = result.replace(new RegExp(entity, "g"), char); } return result; } @@ -141,7 +139,11 @@ function mapAttributesToMetadata( } else { (metadata as any)[metadataKey] = value; } - } else if (value && typeof value === 'string' && !systemAttributes.includes(key)) { + } else if ( + value && + typeof value === "string" && + !systemAttributes.includes(key) + ) { // Handle unknown/custom attributes - but only if they're not system attributes if (!metadata.customAttributes) { metadata.customAttributes = {}; @@ -157,7 +159,7 @@ function mapAttributesToMetadata( function extractDocumentAuthors(sourceContent: string): string[] { const authors: string[] = []; const lines = sourceContent.split(/\r?\n/); - + // Find the document title line let titleLineIndex = -1; for (let i = 0; i < lines.length; i++) { @@ -166,21 +168,21 @@ function extractDocumentAuthors(sourceContent: string): string[] { break; } } - + if (titleLineIndex === -1) { return authors; } - + // Look for authors in the lines immediately following the title let i = titleLineIndex + 1; while (i < lines.length) { const line = lines[i]; - + // Stop if we hit a blank line, section header, or content that's not an author if (line.trim() === "" || line.match(/^==\s+/)) { break; } - + if (line.includes("<") && !line.startsWith(":")) { // This is an author line like "John Doe " const authorName = line.split("<")[0].trim(); @@ -194,10 +196,10 @@ function extractDocumentAuthors(sourceContent: string): string[] { // Not an author line, stop looking break; } - + i++; } - + return authors; } @@ -207,7 +209,7 @@ function extractDocumentAuthors(sourceContent: string): string[] { function extractSectionAuthors(sectionContent: string): string[] { const authors: string[] = []; const lines = sectionContent.split(/\r?\n/); - + // Find the section title line let titleLineIndex = -1; for (let i = 0; i < lines.length; i++) { @@ -216,21 +218,21 @@ function extractSectionAuthors(sectionContent: string): string[] { break; } } - + if (titleLineIndex === -1) { return authors; } - + // Look for authors in the lines immediately following the section title let i = titleLineIndex + 1; while (i < lines.length) { const line = lines[i]; - + // Stop if we hit a blank line, another section header, or content that's not an author if (line.trim() === "" || line.match(/^==\s+/)) { break; } - + if (line.includes("<") && !line.startsWith(":")) { // This is an author line like "John Doe " const authorName = line.split("<")[0].trim(); @@ -239,7 +241,7 @@ function extractSectionAuthors(sectionContent: string): string[] { } } else if ( line.match(/^[A-Za-z\s]+$/) && - line.trim() !== "" && + line.trim() !== "" && line.trim().split(/\s+/).length <= 2 && !line.startsWith(":") ) { @@ -252,20 +254,36 @@ function extractSectionAuthors(sectionContent: string): string[] { // Not an author line, stop looking break; } - + i++; } - + return authors; } // System attributes to filter out when adding custom attributes as tags 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' + "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", ]; /** @@ -274,38 +292,38 @@ const systemAttributes = [ function stripSectionHeader(sectionContent: string): string { const lines = sectionContent.split(/\r?\n/); let contentStart = 0; - + // Find where the section header ends for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Skip section title line and attribute lines if ( - !line.match(/^=+\s+/) && + !line.match(/^=+\s+/) && !line.includes("<") && !line.match(/^.+,\s*.+:\s*.+$/) && - !line.match(/^:[^:]+:\s*.+$/) && + !line.match(/^:[^:]+:\s*.+$/) && line.trim() !== "" ) { contentStart = i; break; } } - + const processedLines: string[] = []; let lastWasEmpty = false; - + for (let i = contentStart; i < lines.length; i++) { const line = lines[i]; - + // Skip attribute lines within content if (line.match(/^:[^:]+:\s*.+$/)) { continue; } - + // Handle empty lines - don't add more than one consecutive empty line - if (line.trim() === '') { + if (line.trim() === "") { if (!lastWasEmpty) { - processedLines.push(''); + processedLines.push(""); } lastWasEmpty = true; } else { @@ -313,9 +331,12 @@ function stripSectionHeader(sectionContent: string): string { lastWasEmpty = false; } } - + // Remove extra blank lines and normalize newlines - return processedLines.join('\n').replace(/\n\s*\n\s*\n/g, '\n\n').trim(); + return processedLines + .join("\n") + .replace(/\n\s*\n\s*\n/g, "\n\n") + .trim(); } /** @@ -324,23 +345,23 @@ function stripSectionHeader(sectionContent: string): string { function stripDocumentHeader(content: string): string { const lines = content.split(/\r?\n/); let contentStart = 0; - + // Find the first line that is actual content (not header, author, or attribute) for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Skip title line, author line, revision line, and attribute lines if ( - !line.match(/^=\s+/) && + !line.match(/^=\s+/) && !line.includes("<") && !line.match(/^.+,\s*.+:\s*.+$/) && - !line.match(/^:[^:]+:\s*.+$/) && + !line.match(/^:[^:]+:\s*.+$/) && line.trim() !== "" ) { contentStart = i; break; } } - + // Filter out all attribute lines and author lines from the content const contentLines = lines.slice(contentStart); const filteredLines = contentLines.filter((line) => { @@ -350,55 +371,61 @@ function stripDocumentHeader(content: string): string { } return true; }); - + // Ensure deeper headers (====) have proper newlines around them const processedLines = []; for (let i = 0; i < filteredLines.length; i++) { const line = filteredLines[i]; - const prevLine = i > 0 ? filteredLines[i - 1] : ''; - const nextLine = i < filteredLines.length - 1 ? filteredLines[i + 1] : ''; - + const prevLine = i > 0 ? filteredLines[i - 1] : ""; + const nextLine = i < filteredLines.length - 1 ? filteredLines[i + 1] : ""; + // If this is a deeper header (====+), ensure it has newlines around it if (line.match(/^====+\s+/)) { // Add newline before if previous line isn't blank - if (prevLine && prevLine.trim() !== '') { - processedLines.push(''); + if (prevLine && prevLine.trim() !== "") { + processedLines.push(""); } processedLines.push(line); // Add newline after if next line isn't blank and exists - if (nextLine && nextLine.trim() !== '') { - processedLines.push(''); + if (nextLine && nextLine.trim() !== "") { + processedLines.push(""); } } else { processedLines.push(line); } } - + // Remove extra blank lines and normalize newlines - return processedLines.join('\n').replace(/\n\s*\n\s*\n/g, '\n\n').trim(); + return processedLines + .join("\n") + .replace(/\n\s*\n\s*\n/g, "\n\n") + .trim(); } /** * Parses attributes from section content using simple regex - * Converts :tagname: tagvalue -> [tagname, tagvalue] + * Converts :tagname: tagvalue -> [tagname, tagvalue] * Converts :tags: comma,separated -> [t, tag1], [t, tag2], etc. */ export function parseSimpleAttributes(content: string): [string, string][] { const tags: [string, string][] = []; const lines = content.split(/\r?\n/); - + for (const line of lines) { const match = line.match(/^:([^:]+):\s*(.+)$/); if (match) { const [, key, value] = match; const tagName = key.trim(); const tagValue = value.trim(); - - if (tagName === 'tags') { + + if (tagName === "tags") { // Special handling for :tags: - split into individual t-tags - const tags_list = tagValue.split(',').map(t => t.trim()).filter(t => t.length > 0); - tags_list.forEach(tag => { - tags.push(['t', tag]); + const tags_list = tagValue + .split(",") + .map((t) => t.trim()) + .filter((t) => t.length > 0); + tags_list.forEach((tag) => { + tags.push(["t", tag]); }); } else { // Regular attribute -> [tagname, tagvalue] @@ -406,7 +433,7 @@ export function parseSimpleAttributes(content: string): [string, string][] { } } } - + return tags; } @@ -441,7 +468,7 @@ export function extractDocumentMetadata(inputContent: string): { inDocumentHeader = false; break; } - + // Process :author: attributes regardless of other content if (inDocumentHeader) { const match = line.match(/^:author:\s*(.+)$/); @@ -460,17 +487,29 @@ export function extractDocumentMetadata(inputContent: string): { // Extract revision info (only if it looks like valid revision data) const revisionNumber = document.getRevisionNumber(); - if (revisionNumber && revisionNumber !== 'Version' && !revisionNumber.includes('==')) { + if ( + revisionNumber && + revisionNumber !== "Version" && + !revisionNumber.includes("==") + ) { metadata.version = revisionNumber; } const revisionRemark = document.getRevisionRemark(); - if (revisionRemark && !revisionRemark.includes('[NOTE]') && !revisionRemark.includes('==')) { + if ( + revisionRemark && + !revisionRemark.includes("[NOTE]") && + !revisionRemark.includes("==") + ) { metadata.publishedBy = revisionRemark; } const revisionDate = document.getRevisionDate(); - if (revisionDate && !revisionDate.includes('[NOTE]') && !revisionDate.includes('==')) { + if ( + revisionDate && + !revisionDate.includes("[NOTE]") && + !revisionDate.includes("==") + ) { metadata.publicationDate = revisionDate; } @@ -507,16 +546,16 @@ export function extractSectionMetadata(inputSectionContent: string): { } { // Extract title directly from the content using regex for more control const titleMatch = inputSectionContent.match(/^(=+)\s+(.+)$/m); - let title = ''; + let title = ""; if (titleMatch) { title = titleMatch[2].trim(); } - + const metadata: SectionMetadata = { title }; - + // Extract authors from section content const authors = extractSectionAuthors(inputSectionContent); - + // Get authors from attributes (including multiple :author: lines) const lines = inputSectionContent.split(/\r?\n/); for (const line of lines) { @@ -528,14 +567,16 @@ export function extractSectionMetadata(inputSectionContent: string): { } } } - + if (authors.length > 0) { metadata.authors = authors; } // Extract tags using parseSimpleAttributes (which is what's used in generateNostrEvents) const simpleAttrs = parseSimpleAttributes(inputSectionContent); - const tags = simpleAttrs.filter(attr => attr[0] === 't').map(attr => attr[1]); + const tags = simpleAttrs + .filter((attr) => attr[0] === "t") + .map((attr) => attr[1]); if (tags.length > 0) { metadata.tags = tags; } @@ -544,7 +585,6 @@ export function extractSectionMetadata(inputSectionContent: string): { return { metadata, content, title }; } - /** * Converts metadata to Nostr event tags */ @@ -644,11 +684,6 @@ export function extractMetadataFromSectionsOnly(content: string): { return { metadata, content }; } - - - - - /** * Smart metadata extraction that handles both document headers and section-only content */ @@ -663,9 +698,10 @@ export function extractSmartMetadata(content: string): { // Check if it's a minimal document header (just title, no other metadata) const lines = content.split(/\r?\n/); const titleLine = lines.find((line) => line.match(/^=\s+/)); - const hasOtherMetadata = lines.some((line) => - line.includes("<") || // author line - line.match(/^.+,\s*.+:\s*.+$/) // revision line + const hasOtherMetadata = lines.some( + (line) => + line.includes("<") || // author line + line.match(/^.+,\s*.+:\s*.+$/), // revision line ); if (hasOtherMetadata) { diff --git a/src/lib/utils/publication_tree_processor.ts b/src/lib/utils/publication_tree_processor.ts index c0248e0..c6e4444 100644 --- a/src/lib/utils/publication_tree_processor.ts +++ b/src/lib/utils/publication_tree_processor.ts @@ -1,6 +1,6 @@ /** * NKBIP-01 Compliant Publication Tree Processor - * + * * Implements proper Asciidoctor tree processor extension pattern for building * PublicationTree structures during document parsing. Supports iterative parsing * at different hierarchy levels (2-7) as defined in NKBIP-01 specification. @@ -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,47 +59,58 @@ 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 + author: attributes.author, + 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, indexEvent, @@ -108,23 +121,24 @@ 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; } - + return doc; }); }); - + return { - getResult: () => processorResult + getResult: () => processorResult, }; } @@ -135,25 +149,26 @@ 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,10 +182,10 @@ 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), }; } - + return sections.map(buildNode); } @@ -188,20 +203,20 @@ interface SectionNode { function extractSegmentsAtLevel( lines: string[], hierarchy: SectionNode[], - parseLevel: number + parseLevel: number, ): ContentSegment[] { const segments: ContentSegment[] = []; - + // Collect all sections at the target parse level const targetSections = collectSectionsAtLevel(hierarchy, parseLevel); - + for (const section of targetSections) { const segment = extractSegmentContent(lines, section, parseLevel); if (segment) { segments.push(segment); } } - + return segments; } @@ -209,23 +224,26 @@ 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[]) { for (const node of nodes) { // Include sections from level 2 up to target level if (node.level >= 2 && node.level <= targetLevel) { collected.push(node); } - + // Continue traversing children to find more sections if (node.children.length > 0) { traverse(node.children); } } } - + traverse(hierarchy); return collected; } @@ -236,24 +254,28 @@ 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++) { if (sectionPattern.test(lines[i])) { startIdx = i; break; } } - + 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; } - + // Find the end of this section let endIdx = lines.length; for (let i = startIdx + 1; i < lines.length; i++) { @@ -263,51 +285,54 @@ function extractSegmentContent( break; } } - + // Extract section content const sectionLines = lines.slice(startIdx, endIdx); - + // Parse attributes and content const { attributes, content } = parseSegmentContent(sectionLines, parseLevel); - + return { title: section.title, content, 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; content: string; } { const attributes: Record = {}; let contentStartIdx = 1; // Skip the title line - + // 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,20 +341,21 @@ 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"; } else if (hasSections) { return "scattered-notes"; } - + return "none"; } @@ -345,7 +371,7 @@ function buildEventsFromSegments( attributes: Record, contentType: "article" | "scattered-notes" | "none", parseLevel: number, - ndk: NDK + ndk: NDK, ): { tree: PublicationTree; indexEvent: NDKEvent | null; @@ -355,11 +381,11 @@ function buildEventsFromSegments( if (contentType === "scattered-notes" && segments.length > 0) { return buildScatteredNotesStructure(segments, ndk); } - + if (contentType === "article" && title) { return buildArticleStructure(segments, title, attributes, parseLevel, ndk); } - + throw new Error("No valid content found to create publication tree"); } @@ -368,7 +394,7 @@ function buildEventsFromSegments( */ function buildScatteredNotesStructure( segments: ContentSegment[], - ndk: NDK + ndk: NDK, ): { tree: PublicationTree; indexEvent: NDKEvent | null; @@ -377,36 +403,36 @@ function buildScatteredNotesStructure( } { const contentEvents: NDKEvent[] = []; const eventStructure: EventStructureNode[] = []; - + const firstSegment = segments[0]; const rootEvent = createContentEvent(firstSegment, ndk); const tree = new PublicationTree(rootEvent, ndk); contentEvents.push(rootEvent); - + eventStructure.push({ title: firstSegment.title, level: firstSegment.level, eventType: "content", eventKind: 30041, dTag: generateDTag(firstSegment.title), - children: [] + children: [], }); - + // Add remaining segments for (let i = 1; i < segments.length; i++) { const contentEvent = createContentEvent(segments[i], ndk); contentEvents.push(contentEvent); - + eventStructure.push({ title: segments[i].title, level: segments[i].level, eventType: "content", eventKind: 30041, dTag: generateDTag(segments[i].title), - children: [] + children: [], }); } - + return { tree, indexEvent: null, contentEvents, eventStructure }; } @@ -418,7 +444,7 @@ function buildArticleStructure( title: string, attributes: Record, parseLevel: number, - ndk: NDK + ndk: NDK, ): { tree: PublicationTree; indexEvent: NDKEvent | null; @@ -427,11 +453,18 @@ function buildArticleStructure( } { const indexEvent = createIndexEvent(title, attributes, segments, ndk); const tree = new PublicationTree(indexEvent, ndk); - + 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( title: string, indexEvent: NDKEvent, tree: PublicationTree, - ndk: NDK + ndk: NDK, ): { tree: PublicationTree; indexEvent: NDKEvent | null; @@ -452,7 +485,7 @@ function buildLevel2Structure( } { const contentEvents: NDKEvent[] = []; const eventStructure: EventStructureNode[] = []; - + // Add index to structure eventStructure.push({ title, @@ -460,26 +493,26 @@ function buildLevel2Structure( eventType: "index", eventKind: 30040, dTag: generateDTag(title), - children: [] + children: [], }); - + // Group segments by level 2 sections const level2Groups = groupSegmentsByLevel2(segments); - + for (const group of level2Groups) { const contentEvent = createContentEvent(group, ndk); contentEvents.push(contentEvent); - + eventStructure[0].children.push({ title: group.title, level: group.level, - eventType: "content", + eventType: "content", eventKind: 30041, dTag: generateDTag(group.title), - children: [] + children: [], }); } - + return { tree, indexEvent, contentEvents, eventStructure }; } @@ -492,7 +525,7 @@ function buildHierarchicalStructure( indexEvent: NDKEvent, tree: PublicationTree, parseLevel: number, - ndk: NDK + ndk: NDK, ): { tree: PublicationTree; indexEvent: NDKEvent | null; @@ -501,7 +534,7 @@ function buildHierarchicalStructure( } { const contentEvents: NDKEvent[] = []; const eventStructure: EventStructureNode[] = []; - + // Add root index to structure eventStructure.push({ title, @@ -509,59 +542,59 @@ function buildHierarchicalStructure( eventType: "index", eventKind: 30040, dTag: generateDTag(title), - children: [] + children: [], }); - + // Build hierarchical structure const hierarchy = buildSegmentHierarchy(segments); - + for (const level2Section of hierarchy) { if (level2Section.hasChildren) { // Create 30040 for level 2 section with children const level2Index = createIndexEventForSection(level2Section, ndk); contentEvents.push(level2Index); - + const level2Node: EventStructureNode = { title: level2Section.title, level: level2Section.level, eventType: "index", eventKind: 30040, dTag: generateDTag(level2Section.title), - children: [] + children: [], }; - + // Add children as 30041 content events for (const child of level2Section.children) { const childEvent = createContentEvent(child, ndk); contentEvents.push(childEvent); - + level2Node.children.push({ title: child.title, level: child.level, eventType: "content", eventKind: 30041, dTag: generateDTag(child.title), - children: [] + children: [], }); } - + eventStructure[0].children.push(level2Node); } else { // Create 30041 for level 2 section without children const contentEvent = createContentEvent(level2Section, ndk); contentEvents.push(contentEvent); - + eventStructure[0].children.push({ title: level2Section.title, level: level2Section.level, eventType: "content", eventKind: 30041, dTag: generateDTag(level2Section.title), - children: [] + children: [], }); } } - + return { tree, indexEvent, contentEvents, eventStructure }; } @@ -572,37 +605,32 @@ function createIndexEvent( title: string, attributes: Record, segments: ContentSegment[], - ndk: NDK + ndk: NDK, ): NDKEvent { const event = new NDKEvent(ndk); event.kind = 30040; event.created_at = Math.floor(Date.now() / 1000); event.pubkey = ndk.activeUser?.pubkey || "preview-placeholder-pubkey"; - + 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}`]); }); - + event.tags = tags; console.log(`[TreeProcessor] Index event tags:`, tags.slice(0, 10)); // NKBIP-01: Index events must have empty content event.content = ""; - + return event; } @@ -614,53 +642,53 @@ function createContentEvent(segment: ContentSegment, ndk: NDK): NDKEvent { event.kind = 30041; event.created_at = Math.floor(Date.now() / 1000); event.pubkey = ndk.activeUser?.pubkey || "preview-placeholder-pubkey"; - + 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); - + event.tags = tags; event.content = segment.content; - + return event; } /** * 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 { function addDocumentAttributesToTags( tags: string[][], attributes: Record, - pubkey: string + pubkey: string, ) { // Standard metadata if (attributes.author) tags.push(["author", attributes.author]); @@ -679,15 +707,15 @@ function addDocumentAttributesToTags( if (attributes.image) tags.push(["image", attributes.image]); if (attributes.description) tags.push(["summary", attributes.description]); if (attributes.type) tags.push(["type", attributes.type]); - + // 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 tags.push(["p", pubkey]); - + // Custom attributes addCustomAttributes(tags, attributes); } @@ -697,13 +725,13 @@ function addDocumentAttributesToTags( */ function addSectionAttributesToTags( tags: string[][], - attributes: Record + attributes: Record, ) { // 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 addCustomAttributes(tags, attributes); } @@ -713,24 +741,63 @@ function addSectionAttributesToTags( */ function addCustomAttributes( tags: string[][], - attributes: Record + attributes: Record, ) { 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]) => { if (!systemAttributes.includes(key) && value && typeof value === "string") { tags.push([key, value]); @@ -744,88 +811,94 @@ function addCustomAttributes( */ function groupSegmentsByLevel2(segments: ContentSegment[]): ContentSegment[] { const level2Groups: ContentSegment[] = []; - + // Find all level 2 segments and include their nested content 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, }); } } - + return level2Groups; } /** * 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, }); } - + return hierarchy; } /** * 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); event.pubkey = ndk.activeUser?.pubkey || "preview-placeholder-pubkey"; - + 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}`]); }); - + event.tags = tags; // NKBIP-01: Index events must have empty content event.content = ""; - + return event; -} \ No newline at end of file +} diff --git a/src/routes/my-notes/+page.svelte b/src/routes/my-notes/+page.svelte index c8d645e..8314d79 100644 --- a/src/routes/my-notes/+page.svelte +++ b/src/routes/my-notes/+page.svelte @@ -8,7 +8,7 @@ import asciidoctor from "asciidoctor"; import { postProcessAsciidoctorHtml } from "$lib/utils/markup/asciidoctorPostProcessor"; import { getNdkContext } from "$lib/ndk"; - + const ndk = getNdkContext(); let events: NDKEvent[] = $state([]); @@ -172,34 +172,33 @@ // AI-NOTE: Check authentication status and redirect if not logged in // Wait for authentication state to be properly initialized before checking let authCheckTimeout: ReturnType | null = null; - + $effect(() => { const user = $userStore; - + // Clear any existing timeout if (authCheckTimeout) { clearTimeout(authCheckTimeout); authCheckTimeout = null; } - + // If user is signed in, we're good if (user.signedIn) { checkingAuth = false; return; } - + // If user is not signed in, wait a bit for auth restoration to complete // This handles the case where the page loads before auth restoration finishes authCheckTimeout = setTimeout(() => { const currentUser = get(userStore); if (!currentUser.signedIn) { - console.debug('[MyNotes] User not signed in after auth restoration, redirecting to home page'); - goto('/'); + goto("/"); } else { checkingAuth = false; } }, 1500); // 1.5 second delay to allow auth restoration to complete - + // Cleanup function return () => { if (authCheckTimeout) { @@ -272,7 +271,9 @@ -
+

My Notes

{#if checkingAuth}
Checking authentication...
@@ -285,9 +286,13 @@ {:else}
    {#each filteredEvents as event} -
  • +
  • -
    {getTitle(event)}
    +
    + {getTitle(event)} +