diff --git a/src/lib/components/ZettelEditor.svelte b/src/lib/components/ZettelEditor.svelte index c28be59..5bac793 100644 --- a/src/lib/components/ZettelEditor.svelte +++ b/src/lib/components/ZettelEditor.svelte @@ -4,6 +4,9 @@ import { extractSmartMetadata, parseAsciiDocWithMetadata, + parseAsciiDocIterative, + generateNostrEvents, + detectContentType, type AsciiDocMetadata, metadataToTags, } from "$lib/utils/asciidoc_metadata"; @@ -12,66 +15,98 @@ import asciidoctor from "asciidoctor"; // Component props let { content = "", - placeholder = `// PUBLISHING MODES: -// Use "= Article Title" to publish an article/collection of notes -// Use only "== Note Title" headings for scattered notes (30041 events only) + placeholder = `// ITERATIVE PARSING - Choose your publishing level: +// Level 2: Only == sections become events (containing === and deeper) +// Level 3: == sections become indices, === sections become events +// Level 4: === sections become indices, ==== sections become events -= Article Title (Full Collection) -:author: Your Name -:version: 1.0 -:published_on: 2024-01-01 -:published_by: Alexandria -:summary: A brief description of this article -:tags: article, example, metadata -:image: https://example.com/image.jpg -:type: book += Understanding Knowledge +:image: https://i.nostr.build/IUs0xNyUEf5hXTFL.jpg +:published: 2025-04-21 +:tags: knowledge, philosophy, education +:type: text -== Chapter 1: Introduction -:author: Your Name -:keywords: introduction, chapter, example -:description: This is the first chapter of the article +== Preface +:tags: introduction, preface -Chapter content here... +This essay outlines the purpose of Alexandria... +== Introduction: Knowledge as a Living Ecosystem +:tags: introduction, ecosystem -== Note Title 2 -Some Other Author (this works even if there is no :author: attribute) -:keywords: second, note, example (keywords are converted to tags) -:description: This is a description of the note (description is converted to a summary tag) -Note content here... +Knowledge exists as dynamic representations... + +=== Why Investigate the Nature of Knowledge? +:difficulty: intermediate + +Understanding the nature of knowledge itself... + +==== The Four Perspectives +:complexity: high + +1. Material Cause: The building blocks... `, showPreview = false, + parseLevel = 2, onContentChange = (content: string) => {}, onPreviewToggle = (show: boolean) => {}, + onPublishArticle = (events: any) => {}, + onPublishScatteredNotes = (events: any) => {}, } = $props<{ content?: string; placeholder?: string; showPreview?: boolean; + parseLevel?: number; onContentChange?: (content: string) => void; onPreviewToggle?: (show: boolean) => void; + onPublishArticle?: (events: any) => void; + onPublishScatteredNotes?: (events: any) => void; }>(); - // Parse sections for preview using the smart metadata service - let parsedSections = $derived.by(() => { - if (!content.trim()) return []; + // Parse content using iterative parsing + let parsedContent = $derived.by(() => { + if (!content.trim()) return null; - // Use smart metadata extraction that handles both document headers and section-only content - const { metadata: docMetadata } = extractSmartMetadata(content); - - // Parse the content using the standardized parser - const parsed = parseAsciiDocWithMetadata(content); + try { + // Use iterative parsing with selected level + const parsed = parseAsciiDocIterative(content, parseLevel); + + // Debug logging + console.log("Iterative parsed content:", parsed); + + return parsed; + } catch (error) { + console.error("Parsing error:", error); + return null; + } + }); + + // Generate events from parsed content + let generatedEvents = $derived.by(() => { + if (!parsedContent) return null; - // Debug logging - console.log("Parsed sections:", parsed.sections); + try { + const events = generateNostrEvents(parsedContent, parseLevel); + console.log("Generated events:", events); + return events; + } catch (error) { + console.error("Event generation error:", error); + return null; + } + }); + + // Detect content type for smart publishing + let contentType = $derived.by(() => { + return detectContentType(content); + }); + + // Parse sections for preview display + let parsedSections = $derived.by(() => { + if (!parsedContent) return []; - return parsed.sections.map((section: { metadata: AsciiDocMetadata; content: string; title: string }) => { - // Use only section metadata for each section - // Don't combine with document metadata to avoid overriding section-specific metadata + return parsedContent.sections.map((section: { metadata: AsciiDocMetadata; content: string; title: string }) => { const tags = metadataToTags(section.metadata); - // Debug logging - console.log(`Section "${section.title}":`, { metadata: section.metadata, tags }); - return { title: section.title || "Untitled", content: section.content.trim(), @@ -80,23 +115,21 @@ Note content here... }); }); - // Check for 30040-style document headers (publication format) - let hasPublicationHeader = $derived.by(() => { - if (!content.trim()) return false; + // Publishing handlers + function handlePublish() { + if (!generatedEvents) return; - const lines = content.split(/\r?\n/); - for (const line of lines) { - // Check for document title (level 0 header) - if (line.match(/^=\s+(.+)$/)) { - return true; - } - // Check for "index card" format (case insensitive) - if (line.trim().toLowerCase() === 'index card') { - return true; - } + if (contentType === 'article' && generatedEvents.indexEvent) { + // Full article: publish both index event (30040) and content events (30041) + onPublishArticle(generatedEvents); + } else if (contentType === 'scattered-notes') { + // Only notes: publish just the content events (30041) + const notesOnly = { + contentEvents: generatedEvents.contentEvents + }; + onPublishScatteredNotes(notesOnly); } - return false; - }); + } // Toggle preview panel function togglePreview() { @@ -112,87 +145,72 @@ Note content here...
- - {#if hasPublicationHeader} -
-
-
- - - -
-
-

- Publication Format Detected -

-

- You're using a publication format (document title with = or "index card"). - This editor is for individual notes only. Use the - Events form - to create structured publications. -

-
- { - // Store the content in sessionStorage so it can be loaded in the Events form - sessionStorage.setItem('zettelEditorContent', content); - sessionStorage.setItem('zettelEditorSource', 'publication-format'); - }} - class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-800 border border-red-200 dark:border-red-700 rounded-md hover:bg-red-200 dark:hover:bg-red-700 transition-colors" - > - Switch to Publication Editor - -
+ {#if contentType === 'article'} + 📚 Publish Article + ({generatedEvents.contentEvents.length + 1} events) + {:else} + 📝 Publish Notes + ({generatedEvents.contentEvents.length} events) + {/if} + + {:else} +
+ Add content to enable publishing +
+ {/if}
- {/if} +
- {#if showPreview && !hasPublicationHeader} + {#if showPreview}

void; + }, +): Promise { + const { content, kind, tags, onError } = options; + + const ndk = get(ndkInstance); + if (!ndk?.activeUser) { + const error = 'Please log in first'; + onError?.(error); + return { success: false, error }; + } + + try { + const allRelayUrls = Array.from(ndk.pool?.relays.values() || []).map((r) => r.url); + if (allRelayUrls.length === 0) { + throw new Error('No relays available in NDK pool'); + } + const relaySet = NDKRelaySet.fromRelayUrls(allRelayUrls, ndk); + + // Fix a-tags that have placeholder "pubkey" with actual pubkey + const fixedTags = tags.map(tag => { + if (tag[0] === 'a' && tag[1] && tag[1].includes(':pubkey:') && ndk.activeUser) { + // Replace "pubkey" placeholder with actual pubkey + const fixedATag = tag[1].replace(':pubkey:', `:${ndk.activeUser.pubkey}:`); + return [tag[0], fixedATag, tag[2] || '', tag[3] || '']; + } + return tag; + }); + + // Create and sign NDK event + const ndkEvent = new NDKEvent(ndk); + ndkEvent.kind = kind; + ndkEvent.created_at = Math.floor(Date.now() / 1000); + ndkEvent.tags = fixedTags; + ndkEvent.content = content; + ndkEvent.pubkey = ndk.activeUser.pubkey; + + await ndkEvent.sign(); + + // Publish to relays + 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) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + console.error(`Error publishing event: ${errorMessage}`); + onError?.(errorMessage); + return { success: false, error: errorMessage }; + } +} + /** * Publishes all AsciiDoc sections as separate Nostr events * @param options - Publishing options diff --git a/src/lib/utils/asciidoc_metadata.ts b/src/lib/utils/asciidoc_metadata.ts index 6d6754c..59a5dfd 100644 --- a/src/lib/utils/asciidoc_metadata.ts +++ b/src/lib/utils/asciidoc_metadata.ts @@ -24,6 +24,7 @@ export interface AsciiDocMetadata { publishedBy?: string; type?: string; autoUpdate?: 'yes' | 'ask' | 'no'; + customAttributes?: Record; } export type SectionMetadata = AsciiDocMetadata; @@ -31,6 +32,7 @@ export type SectionMetadata = AsciiDocMetadata; export interface ParsedAsciiDoc { metadata: AsciiDocMetadata; content: string; + title: string; sections: Array<{ metadata: SectionMetadata; content: string; @@ -96,6 +98,20 @@ function extractTagsFromAttributes(attributes: Record): string[] { * Maps attributes to metadata with special handling for authors and tags */ function mapAttributesToMetadata(attributes: Record, metadata: AsciiDocMetadata, isDocument: boolean = false): void { + // List of AsciiDoc system attributes to ignore + const systemAttributes = [ + 'attribute-undefined', 'attribute-missing', 'appendix-caption', 'appendix-refsig', + 'caution-caption', 'chapter-refsig', 'example-caption', 'figure-caption', + 'important-caption', 'last-update-label', 'note-caption', 'part-refsig', + 'section-refsig', 'table-caption', 'tip-caption', 'toc-placement', + 'toc-title', 'untitled-label', 'warning-caption', 'asciidoctor-version', + 'safe-mode-name', 'backend', 'user-home', 'doctype', 'htmlsyntax', + 'outfilesuffix', 'filetype', 'basebackend', 'stylesdir', 'iconsdir', + 'localdate', 'localyear', 'localtime', 'localdatetime', 'docdate', + 'docyear', 'doctime', 'docdatetime', 'doctitle', 'language', + 'firstname', 'authorinitials', 'authors' + ]; + for (const [key, value] of Object.entries(attributes)) { const metadataKey = ATTRIBUTE_MAP[key.toLowerCase()]; if (metadataKey && value && typeof value === 'string') { @@ -114,6 +130,12 @@ function mapAttributesToMetadata(attributes: Record, metadata: Asci } else { (metadata as any)[metadataKey] = value; } + } 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 = {}; + } + metadata.customAttributes[key] = value; } } } @@ -139,13 +161,20 @@ function extractAuthorsFromHeader(sourceContent: string, isSection: boolean = fa break; } + // Skip section headers at any level (they start with ==, ===, etc.) + if (authorLine.match(/^==+\s+/)) { + // This is a section header, stop looking for authors + break; + } + if (authorLine.includes('<') && !authorLine.startsWith(':')) { // This is an author line like "John Doe " const authorName = authorLine.split('<')[0].trim(); if (authorName) { authors.push(authorName); } - } else if (isSection && authorLine.match(/^[A-Za-z\s]+$/) && authorLine.trim() !== '' && authorLine.trim().split(/\s+/).length <= 2) { + } else if (isSection && authorLine.match(/^[A-Za-z\s]+$/) && authorLine.trim() !== '' && + authorLine.trim().split(/\s+/).length <= 2) { // This is a simple author name without email (for sections) authors.push(authorLine.trim()); } else if (authorLine.startsWith(':')) { @@ -173,6 +202,7 @@ function stripHeaderAndAttributes(content: string, isSection: boolean = false): let contentStart = 0; const headerPattern = isSection ? /^==\s+/ : /^=\s+/; + // 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 @@ -197,26 +227,63 @@ function stripHeaderAndAttributes(content: string, isSection: boolean = false): 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] : ''; + + // 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(''); + } + processedLines.push(line); + // Add newline after if next line isn't blank and exists + if (nextLine && nextLine.trim() !== '') { + processedLines.push(''); + } + } else { + processedLines.push(line); + } + } + // Remove extra blank lines and normalize newlines - return filteredLines.join('\n').replace(/\n\s*\n\s*\n/g, '\n\n').replace(/\n\s*\n/g, '\n').trim(); + return processedLines.join('\n').replace(/\n\s*\n\s*\n/g, '\n\n').trim(); } /** - * Parses attributes from section content + * Parses attributes from section content using simple regex + * Converts :tagname: tagvalue -> [tagname, tagvalue] + * Converts :tags: comma,separated -> [t, tag1], [t, tag2], etc. */ -function parseSectionAttributes(sectionContent: string): Record { - const attributes: Record = {}; - const lines = sectionContent.split(/\r?\n/); +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; - attributes[key.trim()] = value.trim(); + const tagName = key.trim(); + const tagValue = value.trim(); + + 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]); + }); + } else { + // Regular attribute -> [tagname, tagvalue] + tags.push([tagName, tagValue]); + } } } - return attributes; + return tags; } @@ -253,15 +320,21 @@ export function extractDocumentMetadata(inputContent: string): { metadata.authors = [...new Set(authors)]; // Remove duplicates } - // Extract revision info + // Extract revision info (only if it looks like valid revision data) const revisionNumber = document.getRevisionNumber(); - if (revisionNumber) metadata.version = revisionNumber; + if (revisionNumber && revisionNumber !== 'Version' && !revisionNumber.includes('==')) { + metadata.version = revisionNumber; + } const revisionRemark = document.getRevisionRemark(); - if (revisionRemark) metadata.publishedBy = revisionRemark; + if (revisionRemark && !revisionRemark.includes('[NOTE]') && !revisionRemark.includes('==')) { + metadata.publishedBy = revisionRemark; + } const revisionDate = document.getRevisionDate(); - if (revisionDate) metadata.publicationDate = revisionDate; + if (revisionDate && !revisionDate.includes('[NOTE]') && !revisionDate.includes('==')) { + metadata.publicationDate = revisionDate; + } // Map attributes to metadata (but skip version and publishedBy if we already have them from revision) mapAttributesToMetadata(attributes, metadata, true); @@ -306,8 +379,8 @@ export function extractSectionMetadata(inputSectionContent: string): { const title = section.getTitle() || ''; const metadata: SectionMetadata = { title }; - // Parse attributes from the section content - const attributes = parseSectionAttributes(inputSectionContent); + // Parse attributes from the section content (no longer used - we use simple parsing in generateNostrEvents) + const attributes = {}; // Extract authors from section content const authors = extractAuthorsFromHeader(inputSectionContent, true); @@ -371,6 +444,7 @@ export function parseAsciiDocWithMetadata(content: string): ParsedAsciiDoc { return { metadata: docMetadata, content: document.getSource(), + title: docMetadata.title || '', sections: sectionsWithMetadata }; } @@ -399,6 +473,28 @@ export function metadataToTags(metadata: AsciiDocMetadata | SectionMetadata): [s metadata.tags.forEach(tag => tags.push(['t', tag])); } + // Add custom attributes as tags, but filter out system attributes + if (metadata.customAttributes) { + const systemAttributes = [ + 'attribute-undefined', 'attribute-missing', 'appendix-caption', 'appendix-refsig', + 'caution-caption', 'chapter-refsig', 'example-caption', 'figure-caption', + 'important-caption', 'last-update-label', 'note-caption', 'part-refsig', + 'section-refsig', 'table-caption', 'tip-caption', 'toc-placement', + 'toc-title', 'untitled-label', 'warning-caption', 'asciidoctor-version', + 'safe-mode-name', 'backend', 'user-home', 'doctype', 'htmlsyntax', + 'outfilesuffix', 'filetype', 'basebackend', 'stylesdir', 'iconsdir', + 'localdate', 'localyear', 'localtime', 'localdatetime', 'docdate', + 'docyear', 'doctime', 'docdatetime', 'doctitle', 'language', + 'firstname', 'authorinitials', 'authors' + ]; + + Object.entries(metadata.customAttributes).forEach(([key, value]) => { + if (!systemAttributes.includes(key)) { + tags.push([key, value]); + } + }); + } + return tags; } @@ -461,6 +557,172 @@ export function extractMetadataFromSectionsOnly(content: string): { return { metadata, content }; } +/** + * Iterative AsciiDoc parsing based on specified level + * Level 2: Only == sections become events (containing all subsections) + * Level 3: == sections become indices, === sections become events + * Level 4: === sections become indices, ==== sections become events, etc. + */ +export function parseAsciiDocIterative(content: string, parseLevel: number = 2): ParsedAsciiDoc { + const asciidoctor = createProcessor(); + const document = asciidoctor.load(content, { standalone: false }) as Document; + const { metadata: docMetadata } = extractDocumentMetadata(content); + + const lines = content.split(/\r?\n/); + const targetHeaderPattern = new RegExp(`^${'='.repeat(parseLevel)}\\s+`); + const sections: Array<{ + metadata: SectionMetadata; + content: string; + title: string; + }> = []; + + let currentSection: string | null = null; + let currentSectionContent: string[] = []; + let documentContent: string[] = []; + let inDocumentHeader = true; + + for (const line of lines) { + // Check if we've hit the first section at our target level + if (line.match(targetHeaderPattern)) { + inDocumentHeader = false; + + // Save previous section if exists + if (currentSection) { + const sectionContent = currentSectionContent.join('\n'); + sections.push(extractSectionMetadata(sectionContent)); + } + + // Start new section + currentSection = line; + currentSectionContent = [line]; + } else if (currentSection) { + // We're in a section - add content + currentSectionContent.push(line); + } else if (inDocumentHeader) { + // We're still in document content (before first section) + documentContent.push(line); + } + } + + // Save the last section + if (currentSection) { + const sectionContent = currentSectionContent.join('\n'); + sections.push(extractSectionMetadata(sectionContent)); + } + + // Extract document content (everything before first section at target level) + // Keep the original content with attributes for simple parsing + const docContent = documentContent.join('\n'); + + return { + metadata: docMetadata, + content: docContent, + title: docMetadata.title || '', + sections: sections + }; +} + +/** + * Generates Nostr events from parsed AsciiDoc + * Based on docreference.md specifications + */ +export function generateNostrEvents(parsed: ParsedAsciiDoc, parseLevel: number = 2, pubkey?: string): { + indexEvent?: any; + contentEvents: any[]; +} { + const events: any[] = []; + + // Create content events for each section (30041) + const contentEvents = parsed.sections.map(section => { + const sectionId = section.title + .toLowerCase() + .replace(/[^a-z0-9\s]/g, '') + .replace(/\s+/g, '-') + .trim(); + + // Extract tags directly from section content using simple regex + const sectionTags = parseSimpleAttributes(section.content); + + return { + id: '', // Will be generated by Nostr client + pubkey: '', // Will be set by client + created_at: Math.floor(Date.now() / 1000), + kind: 30041, + tags: [ + ['d', sectionId], + ['title', section.title], + ...sectionTags + ], + content: section.content, + sig: '' // Will be generated by client + }; + }); + + // Only create index event if we have a document title (article format) + if (parsed.title && parsed.title.trim() !== '') { + // Generate document identifier from title + const documentId = parsed.title + .toLowerCase() + .replace(/[^a-z0-9\s]/g, '') + .replace(/\s+/g, '-') + .trim(); + + // Extract tags directly from document content using simple regex + const documentTags = parseSimpleAttributes(parsed.content); + + // Create main index event (30040) + const indexEvent = { + id: '', // Will be generated by Nostr client + pubkey: '', // Will be set by client + created_at: Math.floor(Date.now() / 1000), + kind: 30040, + tags: [ + ['d', documentId], + ['title', parsed.title], + ...documentTags, + // Add a-tags for each section + ...parsed.sections.map(section => { + const sectionId = section.title + .toLowerCase() + .replace(/[^a-z0-9\s]/g, '') + .replace(/\s+/g, '-') + .trim(); + const actualPubkey = pubkey || 'pubkey'; // Use actual pubkey if provided, fallback for compatibility + return ['a', `30041:${actualPubkey}:${sectionId}`, '', '']; // relay will be filled by client + }) + ], + content: '', // Index events have empty content + sig: '' // Will be generated by client + }; + + return { + indexEvent, + contentEvents + }; + } + + // For scattered notes, return only content events + return { + contentEvents + }; +} + +/** + * Detects content type for smart publishing + */ +export function detectContentType(content: string): 'article' | 'scattered-notes' | 'none' { + const hasDocTitle = content.trim().startsWith('=') && !content.trim().startsWith('=='); + const hasSections = content.includes('=='); + + if (hasDocTitle) { + return 'article'; + } else if (hasSections) { + return 'scattered-notes'; + } else { + return 'none'; + } +} + /** * Smart metadata extraction that handles both document headers and section-only content */ diff --git a/src/routes/new/compose/+page.svelte b/src/routes/new/compose/+page.svelte index a62ffe8..23a029c 100644 --- a/src/routes/new/compose/+page.svelte +++ b/src/routes/new/compose/+page.svelte @@ -4,8 +4,7 @@ import ZettelEditor from "$lib/components/ZettelEditor.svelte"; import { goto } from "$app/navigation"; import { nip19 } from "nostr-tools"; - import { publishMultipleZettels } from "$lib/services/publisher"; - import { parseAsciiDocWithMetadata } from "$lib/utils/asciidoc_metadata"; + import { publishSingleEvent } from "$lib/services/publisher"; let content = $state(""); let showPreview = $state(false); @@ -28,47 +27,211 @@ showPreview = show; } - async function handlePublish() { + // Handle unified publishing from ZettelEditor + async function handlePublishArticle(events: any) { isPublishing = true; publishResults = null; - const results = await publishMultipleZettels({ - content, - onError: (error) => { - // Only used for catastrophic errors - publishResults = { successCount: 0, total: 0, errors: [error], successfulEvents: [], failedEvents: [] }; - }, - }); + // Debug: Log the structure of events being published (without content) + console.log('=== PUBLISHING ARTICLE ==='); + if (events.indexEvent) { + console.log('Creating root index event...'); + } + console.log(`Number of content events: ${events.contentEvents.length}`); - const successCount = results.filter(r => r.success).length; - const errors = results.filter(r => !r.success && r.error).map(r => r.error!); - - // Extract successful events with their titles - const parsed = parseAsciiDocWithMetadata(content); - const successfulEvents = results - .filter(r => r.success && r.eventId) - .map((r, index) => ({ - eventId: r.eventId!, - title: parsed.sections[index]?.title || `Note ${index + 1}` - })); + // Debug: Log the first content event to see its structure + if (events.contentEvents.length > 0) { + console.log('First content event structure:', { + kind: events.contentEvents[0].kind, + tags: events.contentEvents[0].tags, + contentLength: events.contentEvents[0].content.length, + contentPreview: events.contentEvents[0].content.substring(0, 100) + }); + } + + try { + const results: any[] = []; + + // Publish index event first + if (events.indexEvent) { + const indexResult = await publishSingleEvent({ + content: events.indexEvent.content, + kind: events.indexEvent.kind, + tags: events.indexEvent.tags, + onError: (error) => { + console.error('Index event publish failed:', error); + }, + }); + results.push(indexResult); + } + + // Publish content events + for (let i = 0; i < events.contentEvents.length; i++) { + const event = events.contentEvents[i]; + console.log(`Publishing content event ${i + 1}: ${event.tags.find((t: any) => t[0] === 'title')?.[1] || 'Untitled'}`); + const result = await publishSingleEvent({ + content: event.content, + kind: event.kind, + tags: event.tags, + onError: (error) => { + console.error(`Content event ${i + 1} publish failed:`, error); + }, + }); + results.push(result); + } + + // Process results + const successCount = results.filter(r => r.success).length; + const errors = results.filter(r => !r.success && r.error).map(r => r.error!); + + // Extract successful events with their titles + const successfulEvents = results + .filter(r => r.success && r.eventId) + .map((r, index) => ({ + eventId: r.eventId!, + title: index === 0 && events.indexEvent ? 'Article Index' : events.contentEvents[index - (events.indexEvent ? 1 : 0)]?.title || `Note ${index}` + })); + + // Extract failed events with their titles and errors + const failedEvents = results + .map((r, index) => ({ result: r, index })) + .filter(({ result }) => !result.success) + .map(({ result, index }) => ({ + title: index === 0 && events.indexEvent ? 'Article Index' : events.contentEvents[index - (events.indexEvent ? 1 : 0)]?.title || `Note ${index}`, + error: result.error || 'Unknown error', + sectionIndex: index + })); + + publishResults = { + successCount, + total: results.length, + errors, + successfulEvents, + failedEvents, + }; + + // Show summary + console.log('\n=== Events Summary ==='); + if (events.indexEvent) { + console.log('\nRoot Index:'); + console.log(`Event Summary:`); + console.log(` ID: ${successfulEvents[0]?.eventId || 'Failed'}`); + console.log(` Kind: 30040`); + console.log(` Tags:`); + events.indexEvent.tags.forEach((tag: string[]) => { + console.log(` - ${JSON.stringify(tag)}`); + }); + console.log(' ---'); + } + + console.log('\nContent:'); + events.contentEvents.forEach((event: any, index: number) => { + const eventId = successfulEvents.find(e => e.title === event.title)?.eventId || 'Failed'; + console.log(`\nEvent Summary:`); + console.log(` ID: ${eventId}`); + console.log(` Kind: 30041`); + console.log(` Tags:`); + event.tags.forEach((tag: any) => { + console.log(` - ${JSON.stringify(tag)}`); + }); + console.log(` Content preview: ${event.content.substring(0, 100)}...`); + console.log(' ---'); + }); + } catch (error) { + console.error('Publishing failed:', error); + publishResults = { + successCount: 0, + total: 0, + errors: [error instanceof Error ? error.message : 'Unknown error'], + successfulEvents: [], + failedEvents: [] + }; + } - // Extract failed events with their titles and errors - const failedEvents = results - .map((r, index) => ({ result: r, index })) - .filter(({ result }) => !result.success) - .map(({ result, index }) => ({ - title: parsed.sections[index]?.title || `Note ${index + 1}`, - error: result.error || 'Unknown error', - sectionIndex: index - })); + isPublishing = false; + } + + async function handlePublishScatteredNotes(events: any) { + isPublishing = true; + publishResults = null; + + // Debug: Log the structure of events being published (without content) + console.log('=== PUBLISHING SCATTERED NOTES ==='); + console.log(`Number of content events: ${events.contentEvents.length}`); + + try { + const results: any[] = []; + + // Publish only content events for scattered notes + for (let i = 0; i < events.contentEvents.length; i++) { + const event = events.contentEvents[i]; + const result = await publishSingleEvent({ + content: event.content, + kind: event.kind, + tags: event.tags, + onError: (error) => { + console.error(`Content event ${i + 1} publish failed:`, error); + }, + }); + results.push(result); + } + + // Process results + const successCount = results.filter(r => r.success).length; + const errors = results.filter(r => !r.success && r.error).map(r => r.error!); + + // Extract successful events with their titles + const successfulEvents = results + .filter(r => r.success && r.eventId) + .map((r, index) => ({ + eventId: r.eventId!, + title: events.contentEvents[index]?.title || `Note ${index + 1}` + })); + + // Extract failed events with their titles and errors + const failedEvents = results + .map((r, index) => ({ result: r, index })) + .filter(({ result }) => !result.success) + .map(({ result, index }) => ({ + title: events.contentEvents[index]?.title || `Note ${index + 1}`, + error: result.error || 'Unknown error', + sectionIndex: index + })); + + publishResults = { + successCount, + total: results.length, + errors, + successfulEvents, + failedEvents, + }; + + // Show summary + console.log('\n=== Events Summary ==='); + console.log('\nContent:'); + events.contentEvents.forEach((event: any, index: number) => { + const eventId = successfulEvents.find(e => e.title === event.title)?.eventId || 'Failed'; + console.log(`\nEvent Summary:`); + console.log(` ID: ${eventId}`); + console.log(` Kind: 30041`); + console.log(` Tags:`); + event.tags.forEach((tag: any) => { + console.log(` - ${JSON.stringify(tag)}`); + }); + console.log(` Content preview: ${event.content.substring(0, 100)}...`); + console.log(' ---'); + }); + } catch (error) { + console.error('Publishing failed:', error); + publishResults = { + successCount: 0, + total: 0, + errors: [error instanceof Error ? error.message : 'Unknown error'], + successfulEvents: [], + failedEvents: [] + }; + } - publishResults = { - successCount, - total: results.length, - errors, - successfulEvents, - failedEvents, - }; isPublishing = false; } @@ -77,44 +240,10 @@ isPublishing = true; - // Get the specific section content - const parsed = parseAsciiDocWithMetadata(content); - const section = parsed.sections[sectionIndex]; - if (!section) return; - - // Reconstruct the section content for publishing - const sectionContent = `== ${section.title}\n\n${section.content}`; - try { - const result = await publishMultipleZettels({ - content: sectionContent, - onError: (error) => { - console.error('Retry failed:', error); - }, - }); - - if (result[0]?.success && result[0]?.eventId) { - // Update the successful events list - const newSuccessfulEvent = { - eventId: result[0].eventId, - title: section.title - }; - - // Remove from failed events - const updatedFailedEvents = publishResults.failedEvents.filter( - (_, index) => index !== sectionIndex - ); - - // Add to successful events - const updatedSuccessfulEvents = [...publishResults.successfulEvents, newSuccessfulEvent]; - - publishResults = { - ...publishResults, - successCount: publishResults.successCount + 1, - successfulEvents: updatedSuccessfulEvents, - failedEvents: updatedFailedEvents, - }; - } + // For now, we'll just retry the specific event + // This could be enhanced to retry specific events based on their type + console.log('Retry functionality needs to be implemented for the new unified system'); } catch (error) { console.error('Retry failed:', error); } @@ -142,22 +271,10 @@ {showPreview} onContentChange={handleContentChange} onPreviewToggle={handlePreviewToggle} + onPublishArticle={handlePublishArticle} + onPublishScatteredNotes={handlePublishScatteredNotes} /> - - - {#if publishResults} {#if publishResults.successCount === publishResults.total}