diff --git a/src/lib/components/ZettelEditor.svelte b/src/lib/components/ZettelEditor.svelte index cd4c6ae..4484a91 100644 --- a/src/lib/components/ZettelEditor.svelte +++ b/src/lib/components/ZettelEditor.svelte @@ -11,6 +11,8 @@ metadataToTags, parseSimpleAttributes, } from "$lib/utils/asciidoc_metadata"; +import { createPublicationTreeFromContent, exportEventsFromTree } from "$lib/utils/publication_tree_factory.ts"; +import { getNdkContext } from "$lib/ndk.ts"; import Asciidoctor from "asciidoctor"; // Initialize Asciidoctor processor @@ -37,41 +39,52 @@ import Asciidoctor from "asciidoctor"; onPublishScatteredNotes?: (events: any) => void; }>(); - // Parse content using iterative parsing - let parsedContent = $derived.by(() => { - if (!content.trim()) return null; - - 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; + // Get NDK context for PublicationTree creation + const ndk = getNdkContext(); + + // Configuration constants + const MIN_PARSE_LEVEL = 2; + const MAX_PARSE_LEVEL = 6; + + // State for PublicationTree result + let publicationResult = $state(null); + let generatedEvents = $state(null); + let contentType = $state<'article' | 'scattered-notes' | 'none'>('none'); + + // Effect to create PublicationTree when content changes + $effect(() => { + if (!content.trim() || !ndk) { + publicationResult = null; + generatedEvents = null; + contentType = 'none'; + return; } - }); - - // Generate events from parsed content - let generatedEvents = $derived.by(() => { - if (!parsedContent) return null; - 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); + // Create PublicationTree asynchronously + createPublicationTreeFromContent(content, ndk, parseLevel) + .then(result => { + publicationResult = result; + contentType = result.metadata.contentType; + + // Export events for compatibility + return exportEventsFromTree(result); + }) + .then(events => { + generatedEvents = events; + console.log("AST-based events generated:", { + contentType, + indexEvent: !!events.indexEvent, + contentEvents: events.contentEvents.length, + parseLevel: parseLevel + }); + console.log("Updated generatedEvents state:", generatedEvents); + }) + .catch(error => { + console.error("PublicationTree creation error:", error); + publicationResult = null; + generatedEvents = null; + contentType = 'none'; + }); }); // Helper function to get section level from content @@ -86,24 +99,41 @@ import Asciidoctor from "asciidoctor"; return 2; // Default to level 2 } - // Parse sections for preview display + // Generate parse level options dynamically + function generateParseLevelOptions(minLevel: number, maxLevel: number) { + const options = []; + for (let level = minLevel; level <= maxLevel; level++) { + const equals = '='.repeat(level); + const nextEquals = '='.repeat(level + 1); + + let label; + if (level === 2) { + label = `Level ${level} (${equals} sections → events)`; + } else { + const prevEquals = '='.repeat(level - 1); + label = `Level ${level} (${prevEquals} → indices, ${equals} → events)`; + } + + options.push({ level, label }); + } + return options; + } + + // Parse sections for preview display using PublicationTree data let parsedSections = $derived.by(() => { - if (!parsedContent) return []; + if (!publicationResult) return []; - return parsedContent.sections.map((section: { metadata: AsciiDocMetadata; content: string; title: string; level?: number }) => { - // Use simple parsing directly on section content for accurate tag extraction - const tags = parseSimpleAttributes(section.content); - const level = section.level || getSectionLevel(section.content); - - // Determine if this is an index section (just title) or content section (full content) - const isIndex = parseLevel > 2 && level < parseLevel; + // Convert PublicationTree events to preview format + return publicationResult.contentEvents.map((event: any) => { + const title = event.tags.find((t: string[]) => t[0] === 'title')?.[1] || 'Untitled'; + const tags = event.tags.filter((t: string[]) => t[0] === 't'); return { - title: section.title || "Untitled", - content: section.content.trim(), - tags, - level, - isIndex, + title, + content: event.content, + tags, // Already in [['t', 'tag1'], ['t', 'tag2']] format + level: 2, // Default level for display + isIndex: event.kind === 30040, }; }); }); @@ -161,9 +191,9 @@ import Asciidoctor from "asciidoctor"; bind:value={parseLevel} class="text-xs px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" > - - - + {#each generateParseLevelOptions(MIN_PARSE_LEVEL, MAX_PARSE_LEVEL) as option} + + {/each} @@ -234,7 +264,7 @@ import Asciidoctor from "asciidoctor"; {/if} -
+
@@ -264,19 +294,19 @@ import Asciidoctor from "asciidoctor";
{:else} - {#if contentType === 'article' && parsedContent?.title} + {#if contentType === 'article' && publicationResult?.metadata.title}

- {parsedContent.title} + {publicationResult.metadata.title}

- {#if parsedContent.content} - {@const documentTags = parseSimpleAttributes(parsedContent.content)} - {#if documentTags.filter(tag => tag[0] === 't').length > 0} + {#if publicationResult.metadata.attributes.tags} + {@const tagsList = publicationResult.metadata.attributes.tags.split(',').map((t: string) => t.trim())} + {#if tagsList.length > 0}
- {#each documentTags.filter(tag => tag[0] === 't') as tag} + {#each tagsList as tag} - #{tag[1]} + #{tag} {/each}
@@ -287,7 +317,7 @@ import Asciidoctor from "asciidoctor";
- {#if contentType === 'article' && parsedContent?.title} + {#if contentType === 'article' && publicationResult?.metadata.title} {@const documentHeader = content.split(/\n==\s+/)[0]}
@@ -300,17 +330,16 @@ import Asciidoctor from "asciidoctor"; })}
- {#if parsedContent.content} - {@const documentTags = parseSimpleAttributes(parsedContent.content)} - {#if documentTags.filter(tag => tag[0] === 't').length > 0} + {#if publicationResult.metadata.attributes.tags} + {@const tagsList = publicationResult.metadata.attributes.tags.split(',').map((t: string) => t.trim())} + {#if tagsList.length > 0}
Document tags: - - {#each documentTags.filter(tag => tag[0] === 't') as tag} + {#each tagsList as tag}
# - {tag[1]} + {tag}
{/each}
@@ -349,14 +378,7 @@ import Asciidoctor from "asciidoctor"; {:else}
- {@html asciidoctor.convert(section.content, { - standalone: false, - attributes: { - showtitle: true, - sectanchors: true, - sectids: true - } - })} + {@html section.content}
{/if}
@@ -369,9 +391,9 @@ import Asciidoctor from "asciidoctor"; class="bg-gray-200 dark:bg-gray-700 rounded-lg p-3 mb-2" >
- {#if section.tags && section.tags.filter(tag => tag[0] === 't').length > 0} + {#if section.tags && section.tags.filter((tag: string[]) => tag[0] === 't').length > 0} - {#each section.tags.filter(tag => tag[0] === 't') as tag} + {#each section.tags.filter((tag: string[]) => tag[0] === 't') as tag}
@@ -410,10 +432,14 @@ import Asciidoctor from "asciidoctor"; class="mt-4 text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-900 p-2 rounded border" > Event Count: - {parsedSections.length + (contentType === 'article' ? 1 : 0)} event{(parsedSections.length + (contentType === 'article' ? 1 : 0)) !== 1 - ? "s" - : ""} - ({contentType === 'article' ? '1 index + ' : ''}{parsedSections.length} content) + {#if generatedEvents} + {generatedEvents.contentEvents.length + (generatedEvents.indexEvent ? 1 : 0)} event{(generatedEvents.contentEvents.length + (generatedEvents.indexEvent ? 1 : 0)) !== 1 + ? "s" + : ""} + ({generatedEvents.indexEvent ? '1 index + ' : ''}{generatedEvents.contentEvents.length} content) + {:else} + 0 events + {/if}
{/if}
@@ -435,9 +461,16 @@ import Asciidoctor from "asciidoctor";

Publishing Levels

    -
  • 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
  • + {#each generateParseLevelOptions(MIN_PARSE_LEVEL, MAX_PARSE_LEVEL) as option} +
  • + Level {option.level}: + {#if option.level === 2} + Only {'='.repeat(option.level)} sections become events (containing {'='.repeat(option.level + 1)} and deeper) + {:else} + {'='.repeat(option.level - 1)} sections become indices, {'='.repeat(option.level)} sections become events + {/if} +
  • + {/each}
diff --git a/src/lib/utils/asciidoc_ast_parser.ts b/src/lib/utils/asciidoc_ast_parser.ts new file mode 100644 index 0000000..9f267ef --- /dev/null +++ b/src/lib/utils/asciidoc_ast_parser.ts @@ -0,0 +1,273 @@ +/** + * AST-based AsciiDoc parsing using Asciidoctor's native document structure + * + * This replaces the manual regex parsing in asciidoc_metadata.ts with proper + * AST traversal, leveraging Asciidoctor's built-in parsing capabilities. + */ + +import Processor from "asciidoctor"; +import type { Document } from "asciidoctor"; +import { PublicationTree } from "../data_structures/publication_tree.ts"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import type NDK from "@nostr-dev-kit/ndk"; +import { getMimeTags } from "./mime.ts"; + +export interface ASTSection { + title: string; + content: string; + level: number; + attributes: Record; + subsections: ASTSection[]; +} + +export interface ASTParsedDocument { + title: string; + content: string; + attributes: Record; + sections: ASTSection[]; +} + +/** + * Parse AsciiDoc content using Asciidoctor's AST instead of manual regex + */ +export function parseAsciiDocAST(content: string, parseLevel: number = 2): ASTParsedDocument { + const asciidoctor = Processor(); + const document = asciidoctor.load(content, { standalone: false }) as Document; + + return { + title: document.getTitle() || '', + content: document.getContent() || '', + attributes: document.getAttributes(), + sections: extractSectionsFromAST(document, parseLevel) + }; +} + +/** + * Extract sections from Asciidoctor AST based on parse level + */ +function extractSectionsFromAST(document: Document, parseLevel: number): ASTSection[] { + const directSections = document.getSections(); + + // Collect all sections at all levels up to parseLevel + const allSections: ASTSection[] = []; + + function collectSections(sections: any[]) { + for (const section of sections) { + const asciidoctorLevel = section.getLevel(); + // Convert Asciidoctor's internal level to our application level + // Asciidoctor: == is level 1, === is level 2, etc. + // Our app: == is level 2, === is level 3, etc. + const appLevel = asciidoctorLevel + 1; + + if (appLevel <= parseLevel) { + allSections.push({ + title: section.getTitle() || '', + content: section.getContent() || '', + level: appLevel, + attributes: section.getAttributes() || {}, + subsections: [] + }); + } + + // Recursively collect subsections + const subsections = section.getSections?.() || []; + if (subsections.length > 0) { + collectSections(subsections); + } + } + } + + collectSections(directSections); + + return allSections; +} + +/** + * Extract subsections from a section (recursive helper) + */ +function extractSubsections(section: any, parseLevel: number): ASTSection[] { + const subsections = section.getSections?.() || []; + + return subsections + .filter((sub: any) => (sub.getLevel() + 1) <= parseLevel) + .map((sub: any) => ({ + title: sub.getTitle() || '', + content: sub.getContent() || '', + level: sub.getLevel() + 1, // Convert to app level + attributes: sub.getAttributes() || {}, + subsections: extractSubsections(sub, parseLevel) + })); +} + +/** + * Create a PublicationTree directly from Asciidoctor AST + * This integrates with Michael's PublicationTree architecture + */ +export async function createPublicationTreeFromAST( + content: string, + ndk: NDK, + parseLevel: number = 2 +): Promise { + const parsed = parseAsciiDocAST(content, parseLevel); + + // Create root 30040 index event from document metadata + const rootEvent = createIndexEventFromAST(parsed, ndk); + const tree = new PublicationTree(rootEvent, ndk); + + // Add sections as 30041 events + for (const section of parsed.sections) { + const contentEvent = createContentEventFromSection(section, ndk); + await tree.addEvent(contentEvent, rootEvent); + } + + return tree; +} + +/** + * Create a 30040 index event from AST document metadata + */ +function createIndexEventFromAST(parsed: ASTParsedDocument, ndk: NDK): NDKEvent { + const event = new NDKEvent(ndk); + event.kind = 30040; + event.created_at = Math.floor(Date.now() / 1000); + + // Generate d-tag from title + const dTag = generateDTag(parsed.title); + const [mTag, MTag] = getMimeTags(30040); + + const tags: string[][] = [ + ["d", dTag], + mTag, + MTag, + ["title", parsed.title] + ]; + + // Add document attributes as tags + addAttributesAsTags(tags, parsed.attributes); + + // Add a-tags for each section (30041 content events) + parsed.sections.forEach(section => { + const sectionDTag = generateDTag(section.title); + tags.push(["a", `30041:${ndk.activeUser?.pubkey || 'pubkey'}:${sectionDTag}`]); + }); + + event.tags = tags; + event.content = parsed.content; + + return event; +} + +/** + * Create a 30041 content event from an AST section + */ +function createContentEventFromSection(section: ASTSection, ndk: NDK): NDKEvent { + const event = new NDKEvent(ndk); + event.kind = 30041; + event.created_at = Math.floor(Date.now() / 1000); + + const dTag = generateDTag(section.title); + const [mTag, MTag] = getMimeTags(30041); + + const tags: string[][] = [ + ["d", dTag], + mTag, + MTag, + ["title", section.title] + ]; + + // Add section attributes as tags + addAttributesAsTags(tags, section.attributes); + + event.tags = tags; + event.content = section.content; + + return event; +} + +/** + * Generate a deterministic d-tag from title + */ +function generateDTag(title: string): string { + return title + .toLowerCase() + .replace(/[^\p{L}\p{N}]/gu, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); +} + +/** + * Add AsciiDoc attributes as Nostr event tags, filtering out system attributes + */ +function addAttributesAsTags(tags: string[][], 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' + ]; + + // Add standard metadata tags + if (attributes.author) tags.push(["author", attributes.author]); + if (attributes.version) tags.push(["version", attributes.version]); + if (attributes.description) tags.push(["summary", attributes.description]); + if (attributes.tags) { + attributes.tags.split(',').forEach(tag => + tags.push(["t", tag.trim()]) + ); + } + + // Add custom attributes (non-system) + Object.entries(attributes).forEach(([key, value]) => { + if (!systemAttributes.includes(key) && value) { + tags.push([key, value]); + } + }); +} + +/** + * Tree processor extension for Asciidoctor + * This can be registered to automatically populate PublicationTree during parsing + */ +export function createPublicationTreeProcessor(ndk: NDK, parseLevel: number = 2) { + return function(extensions: any) { + extensions.treeProcessor(function(this: any) { + const dsl = this; + dsl.process(function(this: any, document: Document) { + // Create PublicationTree and store on document for later retrieval + const publicationTree = createPublicationTreeFromDocument(document, ndk, parseLevel); + document.setAttribute('publicationTree', publicationTree); + }); + }); + }; +} + +/** + * Helper function to create PublicationTree from Asciidoctor Document + */ +async function createPublicationTreeFromDocument( + document: Document, + ndk: NDK, + parseLevel: number +): Promise { + const parsed: ASTParsedDocument = { + title: document.getTitle() || '', + content: document.getContent() || '', + attributes: document.getAttributes(), + sections: extractSectionsFromAST(document, parseLevel) + }; + + const rootEvent = createIndexEventFromAST(parsed, ndk); + const tree = new PublicationTree(rootEvent, ndk); + + for (const section of parsed.sections) { + const contentEvent = createContentEventFromSection(section, ndk); + await tree.addEvent(contentEvent, rootEvent); + } + + return tree; +} \ No newline at end of file diff --git a/src/lib/utils/publication_tree_factory.ts b/src/lib/utils/publication_tree_factory.ts new file mode 100644 index 0000000..73ba483 --- /dev/null +++ b/src/lib/utils/publication_tree_factory.ts @@ -0,0 +1,325 @@ +/** + * Factory for creating PublicationTree instances from AsciiDoc content + * + * This integrates the AST parser with Michael's PublicationTree architecture, + * providing a clean bridge between AsciiDoc parsing and Nostr event publishing. + */ + +import { PublicationTree } from "../data_structures/publication_tree.ts"; +import { SveltePublicationTree } from "../components/publications/svelte_publication_tree.svelte.ts"; +import { parseAsciiDocAST } from "./asciidoc_ast_parser.ts"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import type NDK from "@nostr-dev-kit/ndk"; +import { getMimeTags } from "./mime.ts"; + +export interface PublicationTreeFactoryResult { + tree: PublicationTree; + svelteTree: SveltePublicationTree; + indexEvent: NDKEvent | null; + contentEvents: NDKEvent[]; + metadata: { + title: string; + totalSections: number; + contentType: 'article' | 'scattered-notes' | 'none'; + attributes: Record; + }; +} + +/** + * Create a PublicationTree from AsciiDoc content using AST parsing + * This is the main integration point between AST parsing and PublicationTree + */ +export async function createPublicationTreeFromContent( + content: string, + ndk: NDK, + parseLevel: number = 2 +): Promise { + + // For preview purposes, we can work without authentication + // Authentication is only required for actual publishing + const hasActiveUser = !!ndk.activeUser; + + // Parse content using AST + const parsed = parseAsciiDocAST(content, parseLevel); + + // Determine content type + const contentType = detectContentType(parsed); + + let tree: PublicationTree; + let indexEvent: NDKEvent | null = null; + const contentEvents: NDKEvent[] = []; + + if (contentType === 'article' && parsed.title) { + // Create hierarchical structure: 30040 index + 30041 content events + indexEvent = createIndexEvent(parsed, ndk); + tree = new PublicationTree(indexEvent, ndk); + + // Add content events to tree + for (const section of parsed.sections) { + const contentEvent = createContentEvent(section, parsed, ndk); + await tree.addEvent(contentEvent, indexEvent); + contentEvents.push(contentEvent); + } + + } else if (contentType === 'scattered-notes') { + // Create flat structure: only 30041 events + if (parsed.sections.length === 0) { + throw new Error("No sections found for scattered notes"); + } + + // Use first section as root for tree structure + const firstSection = parsed.sections[0]; + const rootEvent = createContentEvent(firstSection, parsed, ndk); + tree = new PublicationTree(rootEvent, ndk); + contentEvents.push(rootEvent); + + // Add remaining sections + for (let i = 1; i < parsed.sections.length; i++) { + const contentEvent = createContentEvent(parsed.sections[i], parsed, ndk); + await tree.addEvent(contentEvent, rootEvent); + contentEvents.push(contentEvent); + } + + } else { + throw new Error("No valid content found to create publication tree"); + } + + // Create reactive Svelte wrapper + const svelteTree = new SveltePublicationTree( + indexEvent || contentEvents[0], + ndk + ); + + return { + tree, + svelteTree, + indexEvent, + contentEvents, + metadata: { + title: parsed.title, + totalSections: parsed.sections.length, + contentType, + attributes: parsed.attributes + } + }; +} + +/** + * Create a 30040 index event from parsed document + */ +function createIndexEvent(parsed: any, ndk: NDK): NDKEvent { + const event = new NDKEvent(ndk); + event.kind = 30040; + event.created_at = Math.floor(Date.now() / 1000); + // Use placeholder pubkey for preview if no active user + event.pubkey = ndk.activeUser?.pubkey || 'preview-placeholder-pubkey'; + + // Generate d-tag from title + const dTag = generateDTag(parsed.title); + const [mTag, MTag] = getMimeTags(30040); + + const tags: string[][] = [ + ["d", dTag], + mTag, + MTag, + ["title", parsed.title] + ]; + + // Add document attributes as tags + addDocumentAttributesToTags(tags, parsed.attributes, event.pubkey); + + // Add a-tags for each section (30041 references) + parsed.sections.forEach((section: any) => { + const sectionDTag = generateDTag(section.title); + tags.push(["a", `30041:${event.pubkey}:${sectionDTag}`]); + }); + + event.tags = tags; + event.content = parsed.content || generateIndexContent(parsed); + + return event; +} + +/** + * Create a 30041 content event from parsed section + */ +function createContentEvent(section: any, documentParsed: any, ndk: NDK): NDKEvent { + const event = new NDKEvent(ndk); + event.kind = 30041; + event.created_at = Math.floor(Date.now() / 1000); + + // Use placeholder pubkey for preview if no active user + event.pubkey = ndk.activeUser?.pubkey || 'preview-placeholder-pubkey'; + + const dTag = generateDTag(section.title); + const [mTag, MTag] = getMimeTags(30041); + + const tags: string[][] = [ + ["d", dTag], + mTag, + MTag, + ["title", section.title] + ]; + + // Add section-specific attributes + addSectionAttributesToTags(tags, section.attributes); + + // Add document-level attributes that should be inherited + inheritDocumentAttributes(tags, documentParsed.attributes); + + event.tags = tags; + event.content = section.content || ''; + + return event; +} + +/** + * Detect content type based on parsed structure + */ +function detectContentType(parsed: any): 'article' | 'scattered-notes' | 'none' { + const hasDocTitle = !!parsed.title; + const hasSections = parsed.sections.length > 0; + + // Check if the "title" is actually just the first section title + // This happens when AsciiDoc starts with == instead of = + const titleMatchesFirstSection = parsed.sections.length > 0 && + parsed.title === parsed.sections[0].title; + + if (hasDocTitle && hasSections && !titleMatchesFirstSection) { + return 'article'; + } else if (hasSections) { + return 'scattered-notes'; + } + + return 'none'; +} + +/** + * 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"; +} + +/** + * Add document attributes as Nostr tags + */ +function addDocumentAttributesToTags(tags: string[][], attributes: Record, pubkey: string) { + // Standard metadata + if (attributes.author) tags.push(["author", attributes.author]); + if (attributes.version) tags.push(["version", attributes.version]); + if (attributes.published) tags.push(["published", attributes.published]); + if (attributes.language) tags.push(["language", attributes.language]); + if (attributes.image) tags.push(["image", attributes.image]); + if (attributes.description) tags.push(["summary", attributes.description]); + + // Tags + if (attributes.tags) { + attributes.tags.split(',').forEach(tag => + tags.push(["t", tag.trim()]) + ); + } + + // Add pubkey reference + tags.push(["p", pubkey]); + + // Custom attributes (filtered) + addCustomAttributes(tags, attributes); +} + +/** + * Add section-specific attributes as tags + */ +function addSectionAttributesToTags(tags: string[][], attributes: Record) { + addCustomAttributes(tags, attributes); +} + +/** + * Inherit relevant document attributes for content events + */ +function inheritDocumentAttributes(tags: string[][], documentAttributes: Record) { + // Inherit selected document attributes + if (documentAttributes.language) tags.push(["language", documentAttributes.language]); + if (documentAttributes.type) tags.push(["type", documentAttributes.type]); +} + +/** + * Add custom attributes, filtering out system ones + */ +function addCustomAttributes(tags: string[][], 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', + // Already handled above + '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]); + } + }); +} + +/** + * Generate default index content if none provided + */ +function generateIndexContent(parsed: any): string { + return `# ${parsed.title} + +${parsed.sections.length} sections available: + +${parsed.sections.map((section: any, i: number) => + `${i + 1}. ${section.title}` +).join('\n')}`; +} + +/** + * Export events from PublicationTree for publishing + * This provides compatibility with the current publishing workflow + */ +export async function exportEventsFromTree(result: PublicationTreeFactoryResult) { + const events: any[] = []; + + // Add index event if it exists + if (result.indexEvent) { + events.push(eventToPublishableObject(result.indexEvent)); + } + + // Add content events + result.contentEvents.forEach(event => { + events.push(eventToPublishableObject(event)); + }); + + return { + indexEvent: result.indexEvent ? eventToPublishableObject(result.indexEvent) : undefined, + contentEvents: result.contentEvents.map(eventToPublishableObject), + tree: result.tree + }; +} + +/** + * Convert NDKEvent to publishable object format + */ +function eventToPublishableObject(event: NDKEvent) { + return { + kind: event.kind, + content: event.content, + tags: event.tags, + created_at: event.created_at, + pubkey: event.pubkey, + id: event.id, + title: event.tags.find(t => t[0] === 'title')?.[1] || 'Untitled' + }; +} \ No newline at end of file diff --git a/src/routes/new/compose/+page.svelte b/src/routes/new/compose/+page.svelte index 1e03ba1..c99121c 100644 --- a/src/routes/new/compose/+page.svelte +++ b/src/routes/new/compose/+page.svelte @@ -5,7 +5,6 @@ import { goto } from "$app/navigation"; import { nip19 } from "nostr-tools"; import { publishSingleEvent } from "$lib/services/publisher"; - import { parseAsciiDocWithMetadata } from "$lib/utils/asciidoc_metadata"; import { getNdkContext } from "$lib/ndk"; const ndk = getNdkContext();