From c12e3a50ebcd2e349ebeed6420724ef44030cb25 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 27 Jan 2026 22:12:32 +0100 Subject: [PATCH] handle creating and publishing events remove published_at time stamp from non-replaceable events --- src/asciidocParser.ts | 39 +++++------ src/asciidocValidator.ts | 3 +- src/commands/commandHandlers.ts | 81 +++++++++++++++++++++-- src/eventManager.ts | 57 +++++++++++----- src/nostr/eventBuilder.ts | 113 +++++++++++++++++++++----------- src/nostr/nkbip08Tags.ts | 12 ++-- src/ui/metadataReminderModal.ts | 9 ++- src/ui/structurePreviewModal.ts | 51 ++++++++++++-- src/utils/eventKind.ts | 5 ++ 9 files changed, 273 insertions(+), 97 deletions(-) diff --git a/src/asciidocParser.ts b/src/asciidocParser.ts index ab08719..9961b0d 100644 --- a/src/asciidocParser.ts +++ b/src/asciidocParser.ts @@ -82,12 +82,11 @@ export function parseAsciiDocStructure( const headerInfo = parseHeaderLine(line); if (headerInfo) { - // Save content to current node if any + // Save content to current node if any (save to all nodes, we'll determine kind later) if (currentContent.length > 0 && stack.length > 0) { const currentNode = stack[stack.length - 1]; - if (currentNode.kind === 30041) { - currentNode.content = currentContent.join("\n").trim(); - } + // Save content to the node - it will be used if it becomes a 30041 + currentNode.content = currentContent.join("\n").trim(); currentContent = []; } @@ -124,12 +123,10 @@ export function parseAsciiDocStructure( } } - // Save remaining content to the last node + // Save remaining content to the last node (save to all nodes, we'll determine kind later) if (currentContent.length > 0 && stack.length > 0) { const currentNode = stack[stack.length - 1]; - if (currentNode.kind === 30041) { - currentNode.content = currentContent.join("\n").trim(); - } + currentNode.content = currentContent.join("\n").trim(); } // Post-process: mark lowest level nodes as 30041 @@ -140,25 +137,23 @@ export function parseAsciiDocStructure( /** * Recursively mark the lowest level nodes in each branch as 30041 + * Leaf nodes (nodes with no children) should always be 30041 + * Nodes with children should be 30040 (index events) */ function markLowestLevelAs30041(node: StructureNode): void { - if (node.children.length === 0) { - // Leaf node - should be 30041 if it has content - if (node.content && node.content.trim().length > 0) { - node.kind = 30041; - } - return; - } - - // Process children first + // Process children first (depth-first) node.children.forEach((child) => markLowestLevelAs30041(child)); - // Check if all children are 30041 - if so, this node should be 30040 - // Otherwise, find the deepest 30040 node - const has30040Children = node.children.some((child) => child.kind === 30040); - if (!has30040Children) { - // All children are 30041, so this is an index (30040) + // After processing children, determine this node's kind + if (node.children.length === 0) { + // Leaf node - always 30041 (content event) + node.kind = 30041; + // Ensure content is preserved (it was collected during parsing) + } else { + // Node with children - always 30040 (index event) node.kind = 30040; + // Clear content for index nodes (they don't have content, only references) + node.content = ""; } } diff --git a/src/asciidocValidator.ts b/src/asciidocValidator.ts index c48bfb5..dcded74 100644 --- a/src/asciidocValidator.ts +++ b/src/asciidocValidator.ts @@ -144,7 +144,8 @@ function validateHeaderText(headerLines: HeaderInfo[], errors: string[]): void { * Validate header hierarchy - no skipped levels */ function validateHeaderHierarchy(headerLines: HeaderInfo[], errors: string[]): void { - let previousLevel = 0; + // Start with level 1 (document header level) since section headers start at level 2 + let previousLevel = 1; for (const header of headerLines) { // Only check for skipped levels when going deeper (not when going back up) diff --git a/src/commands/commandHandlers.ts b/src/commands/commandHandlers.ts index 6b8542e..9249d41 100644 --- a/src/commands/commandHandlers.ts +++ b/src/commands/commandHandlers.ts @@ -2,7 +2,7 @@ import { TFile, TFolder, App, Notice } from "obsidian"; import { EventKind, EventMetadata, ScriptoriumSettings } from "../types"; import { readMetadata, writeMetadata, createDefaultMetadata, validateMetadata, mergeWithHeaderTitle } from "../metadataManager"; import { buildEvents } from "../eventManager"; -import { saveEvents, loadEvents, eventsFileExists } from "../eventStorage"; +import { saveEvents, loadEvents, eventsFileExists, getEventsFilePath } from "../eventStorage"; import { publishEventsWithRetry } from "../nostr/relayClient"; import { getWriteRelays } from "../relayManager"; import { parseAsciiDocStructure, isAsciiDocDocument } from "../asciidocParser"; @@ -98,8 +98,10 @@ export async function handleCreateEvents( // Show reminder modal before proceeding new MetadataReminderModal(app, eventKind, async () => { - // Re-read metadata after user confirms (they may have updated it) - const updatedContent = await app.vault.read(file); + try { + log("Metadata reminder modal confirmed, starting event creation"); + // Re-read metadata after user confirms (they may have updated it) + const updatedContent = await app.vault.read(file); let updatedMetadata: EventMetadata = await readMetadata(file, app) || metadata || createDefaultMetadata(eventKind); // Ensure we have valid metadata @@ -142,10 +144,21 @@ export async function handleCreateEvents( new Notice("Please set your private key in settings"); return; } + + log(`Building events for file: ${file.path}, kind: ${eventKind}`); const result = await buildEvents(file, updatedContent, updatedMetadata, settings.privateKey, app); + log(`buildEvents returned: ${result.events.length} events, ${result.errors.length} errors`); if (result.errors.length > 0) { new Notice(`Errors: ${result.errors.join(", ")}`); + logError("buildEvents returned errors", result.errors); + return; + } + + // Check if any events were created + if (result.events.length === 0) { + new Notice("No events were created. Check metadata and content."); + logError("buildEvents returned 0 events", { file: file.path, metadata: updatedMetadata }); return; } @@ -161,12 +174,66 @@ export async function handleCreateEvents( // Show preview for structured documents if (result.structure.length > 0) { new StructurePreviewModal(app, result.structure, async () => { - await saveEvents(file, result.events, app); - new Notice(`Created ${result.events.length} event(s) and saved to ${file.basename}_events.jsonl`); + try { + const eventsPath = getEventsFilePath(file); + await saveEvents(file, result.events, app); + + // Try to open the events file in Obsidian + const eventsFile = app.vault.getAbstractFileByPath(eventsPath); + if (eventsFile && eventsFile instanceof TFile) { + try { + const leaf = app.workspace.getMostRecentLeaf(); + if (leaf && leaf.view) { + await leaf.openFile(eventsFile, { active: true }); + } else { + const newLeaf = app.workspace.getLeaf("tab"); + await newLeaf.openFile(eventsFile, { active: true }); + } + } catch (openError: any) { + // If opening fails, just show the notice - file was saved successfully + logError("Error opening events file", openError); + } + } + + new Notice(`Created ${result.events.length} event(s) and saved to ${eventsPath}`); + log(`Events saved to: ${eventsPath}`); + } catch (error: any) { + showErrorNotice("Error saving events", error); + logError("Error saving events", error); + } }).open(); } else { - await saveEvents(file, result.events, app); - new Notice(`Created ${result.events.length} event(s) and saved to ${file.basename}_events.jsonl`); + try { + const eventsPath = getEventsFilePath(file); + await saveEvents(file, result.events, app); + + // Try to open the events file in Obsidian + const eventsFile = app.vault.getAbstractFileByPath(eventsPath); + if (eventsFile && eventsFile instanceof TFile) { + try { + const leaf = app.workspace.getMostRecentLeaf(); + if (leaf && leaf.view) { + await leaf.openFile(eventsFile, { active: true }); + } else { + const newLeaf = app.workspace.getLeaf("tab"); + await newLeaf.openFile(eventsFile, { active: true }); + } + } catch (openError: any) { + // If opening fails, just show the notice - file was saved successfully + logError("Error opening events file", openError); + } + } + + new Notice(`Created ${result.events.length} event(s) and saved to ${eventsPath}`); + log(`Events saved to: ${eventsPath}`); + } catch (error: any) { + showErrorNotice("Error saving events", error); + logError("Error saving events", error); + } + } + } catch (error: any) { + showErrorNotice("Error creating events", error); + logError("Error in event creation callback", error); } }).open(); } catch (error: any) { diff --git a/src/eventManager.ts b/src/eventManager.ts index 2f4afe3..f6e512b 100644 --- a/src/eventManager.ts +++ b/src/eventManager.ts @@ -12,6 +12,7 @@ import { createSignedEvent, buildTagsFromMetadata, getPubkeyFromPrivkey, + normalizeDTag, } from "./nostr/eventBuilder"; import { parseAsciiDocStructure, isAsciiDocDocument } from "./asciidocParser"; import { readMetadata, mergeWithHeaderTitle, stripMetadataFromContent } from "./metadataManager"; @@ -98,9 +99,14 @@ export async function buildAsciiDocEvents( } // Build base 30041 metadata + // Ensure title is always a string (required for 30041) + if (!node.title || typeof node.title !== "string") { + errors.push(`30041 event missing required title at level ${node.level}`); + return; + } const baseMetadata: Kind30041Metadata = { kind: 30041, - title: node.title, + title: String(node.title), }; // Determine if this 30041 is directly under root (making it a chapter) or under a chapter (making it a section) @@ -129,25 +135,35 @@ export async function buildAsciiDocEvents( const childEvents: Array<{ kind: number; dTag: string; eventId?: string }> = []; // Merge parent metadata with node metadata for nested 30040 events - const baseMetadata = node.metadata as Kind30040Metadata; + // If node.metadata is undefined, create a minimal metadata object + // Ensure title is always a string (required for 30040) + if (!node.title || typeof node.title !== "string") { + errors.push(`30040 event missing required title at level ${node.level}`); + return; + } + const baseMetadata = (node.metadata as Kind30040Metadata | undefined) || { + kind: 30040, + title: String(node.title), + } as Kind30040Metadata; // Merge NKBIP-08 tags (inherits collection_id from root, version_tag from parent if present, otherwise uses own) const mergedNKBIP08Tags = mergeNKBIP08TagsFor30040(parentMetadata, baseMetadata, currentRootMetadata); // Build merged metadata with inherited NKBIP-08 tags + // Ensure all string properties are properly normalized const mergedMetadata: Kind30040Metadata = { ...baseMetadata, kind: 30040, - title: node.title, - // Inherit other 30040 tags from parent - author: parentMetadata?.author || baseMetadata.author, - type: parentMetadata?.type || baseMetadata.type, - version: parentMetadata?.version || baseMetadata.version, - published_on: parentMetadata?.published_on || baseMetadata.published_on, - published_by: parentMetadata?.published_by || baseMetadata.published_by, - summary: parentMetadata?.summary || baseMetadata.summary, - source: parentMetadata?.source || baseMetadata.source, - image: parentMetadata?.image || baseMetadata.image, + title: String(node.title), // Ensure title is always a string + // Inherit other 30040 tags from parent, ensuring strings + author: parentMetadata?.author ? String(parentMetadata.author) : baseMetadata.author ? String(baseMetadata.author) : undefined, + type: parentMetadata?.type ? String(parentMetadata.type) : baseMetadata.type ? String(baseMetadata.type) : undefined, + version: parentMetadata?.version ? String(parentMetadata.version) : baseMetadata.version ? String(baseMetadata.version) : undefined, + published_on: parentMetadata?.published_on ? String(parentMetadata.published_on) : baseMetadata.published_on ? String(baseMetadata.published_on) : undefined, + published_by: parentMetadata?.published_by ? String(parentMetadata.published_by) : baseMetadata.published_by ? String(baseMetadata.published_by) : undefined, + summary: parentMetadata?.summary ? String(parentMetadata.summary) : baseMetadata.summary ? String(baseMetadata.summary) : undefined, + source: parentMetadata?.source ? String(parentMetadata.source) : baseMetadata.source ? String(baseMetadata.source) : undefined, + image: parentMetadata?.image ? String(parentMetadata.image) : baseMetadata.image ? String(baseMetadata.image) : undefined, auto_update: parentMetadata?.auto_update || baseMetadata.auto_update, }; @@ -204,8 +220,17 @@ export async function buildAsciiDocEvents( // Build events starting from root (no parent, book title is root title, isParentRoot=false for root itself) await buildEventsFromNode(rootNode, metadata as Kind30040Metadata, rootBookTitle, false, metadata as Kind30040Metadata); - // Sort events: indexes first, then content (for proper dependency order) + // Sort events: root book first, then chapter indexes, then content (for proper dependency order) + // Root book is identified by having the root d-tag + const rootDTag = normalizeDTag(metadata.title); events.sort((a, b) => { + // Root book (30040 with root d-tag) comes first + const aIsRoot = a.kind === 30040 && a.tags.find(t => t[0] === "d")?.[1] === rootDTag; + const bIsRoot = b.kind === 30040 && b.tags.find(t => t[0] === "d")?.[1] === rootDTag; + if (aIsRoot && !bIsRoot) return -1; + if (!aIsRoot && bIsRoot) return 1; + + // Then other 30040 indexes, then 30041 content if (a.kind === 30040 && b.kind === 30041) return -1; if (a.kind === 30041 && b.kind === 30040) return 1; return 0; @@ -227,13 +252,15 @@ export async function buildEvents( // Check if this is an AsciiDoc document with structure const hasStructure = isAsciiDocFile(file) && isAsciiDocDocument(content); - if (hasStructure && (metadata.kind === 30040 || metadata.kind === 30041)) { + // Only use buildAsciiDocEvents for 30040 (publication index) with structure + // Standalone 30041 events should use buildSimpleEvent, even if they have a document header + if (hasStructure && metadata.kind === 30040) { // Parse header title and merge with metadata const headerTitle = content.split("\n")[0]?.replace(/^=+\s*/, "").trim() || ""; const mergedMetadata = mergeWithHeaderTitle(metadata, headerTitle); return buildAsciiDocEvents(file, content, mergedMetadata, privkey, app); } else { - // Simple event + // Simple event (including standalone 30041, even if it has a document header) const events = await buildSimpleEvent(file, content, metadata, privkey, app); return { events, structure: [], errors: [] }; } diff --git a/src/nostr/eventBuilder.ts b/src/nostr/eventBuilder.ts index 1f1d6c5..3adf88e 100644 --- a/src/nostr/eventBuilder.ts +++ b/src/nostr/eventBuilder.ts @@ -76,10 +76,27 @@ export function buildTagsFromMetadata( ): string[][] { const tags: string[][] = []; - // All event kinds in this plugin are replaceable (0-9999 range) - // Add published_at tag automatically with current UNIX timestamp - const publishedAt = Math.floor(Date.now() / 1000).toString(); - tags.push(["published_at", publishedAt]); + // Helper function to normalize topics (can be string or array) + const normalizeTopics = (topics: string | string[] | undefined): string[] => { + if (!topics) return []; + if (Array.isArray(topics)) return topics; + if (typeof topics === "string") { + // Split by comma and trim each topic + return topics.split(",").map(t => t.trim()).filter(t => t.length > 0); + } + return []; + }; + + // Helper function to add published_at tag for replaceable event kinds + const addPublishedAtIfReplaceable = () => { + // Replaceable event kinds: 30023, 30040, 30041, 30817, 30818 (0-9999 range) + // Non-replaceable: 1, 11 + const replaceableKinds = [30023, 30040, 30041, 30817, 30818]; + if (replaceableKinds.includes(metadata.kind)) { + const publishedAt = Math.floor(Date.now() / 1000).toString(); + tags.push(["published_at", publishedAt]); + } + }; switch (metadata.kind) { case 1: @@ -88,9 +105,7 @@ export function buildTagsFromMetadata( tags.push(["title", metadata.title]); } // Topics available for all events - if (metadata.topics) { - metadata.topics.forEach((topic) => tags.push(["t", topic])); - } + normalizeTopics(metadata.topics).forEach((topic) => tags.push(["t", topic])); break; case 11: @@ -100,13 +115,12 @@ export function buildTagsFromMetadata( } if (metadata.title) tags.push(["title", metadata.title]); // Topics available for all events - if (metadata.topics) { - metadata.topics.forEach((topic) => tags.push(["t", topic])); - } + normalizeTopics(metadata.topics).forEach((topic) => tags.push(["t", topic])); break; case 30023: - // Long-form article + // Long-form article (replaceable) + addPublishedAtIfReplaceable(); if (!metadata.title) { throw new Error("Title is mandatory for kind 30023"); } @@ -114,13 +128,12 @@ export function buildTagsFromMetadata( if (metadata.title) tags.push(["title", metadata.title]); if (metadata.image) tags.push(["image", metadata.image]); if (metadata.summary) tags.push(["summary", metadata.summary]); - if (metadata.topics) { - metadata.topics.forEach((topic) => tags.push(["t", topic])); - } + normalizeTopics(metadata.topics).forEach((topic) => tags.push(["t", topic])); break; case 30040: - // Publication index + // Publication index (replaceable) + addPublishedAtIfReplaceable(); if (!metadata.title) { throw new Error("Title is mandatory for kind 30040"); } @@ -147,30 +160,39 @@ export function buildTagsFromMetadata( tags.push(eTag); } // Topics available for all events - if (metadata.topics) { - metadata.topics.forEach((topic) => tags.push(["t", topic])); - } + normalizeTopics(metadata.topics).forEach((topic) => tags.push(["t", topic])); // NKBIP-08 tags // Note: For structured documents, NKBIP-08 tags are added in eventManager.ts // with proper book/chapter identification. For simple 30040 events, treat as book. const meta30040 = metadata as Kind30040Metadata; addNKBIP08TagsTo30040(tags, meta30040, true, false, undefined, meta30040); // Simple 30040 is a book, use itself as root - // Additional tags + // Additional tags - ensure all values are strings if (metadata.additional_tags) { - metadata.additional_tags.forEach((tag) => tags.push(tag)); + metadata.additional_tags.forEach((tag) => { + if (Array.isArray(tag) && tag.length > 0) { + const normalizedTag = tag.map(val => String(val ?? "")); + tags.push(normalizedTag); + } + }); } // a tags for child events + // Format: ["a", "kind:pubkey:d-tag", "relay-url", "event-id"] + // If no relay URL, use empty string as placeholder before event ID if (childEvents) { childEvents.forEach((child) => { - const aTag = ["a", `${child.kind}:${pubkey}:${child.dTag}`]; - if (child.eventId) aTag.push("", child.eventId); + const aTag: string[] = ["a", `${child.kind}:${pubkey}:${child.dTag}`]; + if (child.eventId) { + // Add empty relay URL placeholder, then event ID + aTag.push("", String(child.eventId)); + } tags.push(aTag); }); } break; case 30041: - // Publication content + // Publication content (replaceable) + addPublishedAtIfReplaceable(); if (!metadata.title) { throw new Error("Title is mandatory for kind 30041"); } @@ -181,16 +203,15 @@ export function buildTagsFromMetadata( // Stand-alone 30041 can have same tags as 30023 if (meta30041.image) tags.push(["image", meta30041.image]); if (meta30041.summary) tags.push(["summary", meta30041.summary]); - if (meta30041.topics) { - meta30041.topics.forEach((topic) => tags.push(["t", topic])); - } + normalizeTopics(meta30041.topics).forEach((topic) => tags.push(["t", topic])); // NKBIP-08 tags (only for nested 30041 under 30040) addNKBIP08TagsTo30041(tags, meta30041); break; case 30817: - // Wiki page (Markdown) + // Wiki page (Markdown) (replaceable) + addPublishedAtIfReplaceable(); if (!metadata.title) { throw new Error("Title is mandatory for kind 30817"); } @@ -199,13 +220,12 @@ export function buildTagsFromMetadata( if (metadata.summary) tags.push(["summary", metadata.summary]); const meta30817 = metadata as any; if (meta30817.image) tags.push(["image", meta30817.image]); - if (metadata.topics) { - metadata.topics.forEach((topic) => tags.push(["t", topic])); - } + normalizeTopics(metadata.topics).forEach((topic) => tags.push(["t", topic])); break; case 30818: - // Wiki page (AsciiDoc) + // Wiki page (AsciiDoc) (replaceable) + addPublishedAtIfReplaceable(); if (!metadata.title) { throw new Error("Title is mandatory for kind 30818"); } @@ -214,9 +234,7 @@ export function buildTagsFromMetadata( if (metadata.summary) tags.push(["summary", metadata.summary]); const meta30818 = metadata as any; if (meta30818.image) tags.push(["image", meta30818.image]); - if (metadata.topics) { - metadata.topics.forEach((topic) => tags.push(["t", topic])); - } + normalizeTopics(metadata.topics).forEach((topic) => tags.push(["t", topic])); break; } @@ -261,17 +279,32 @@ export function createSignedEvent( const pubkey = getPublicKey(normalizedKey); const created_at = createdAt || Math.floor(Date.now() / 1000); + // Ensure tags is always an array (never undefined or null) + const safeTags = Array.isArray(tags) ? tags : []; + // Ensure all tag values are strings (required by Nostr spec) + const normalizedTags = safeTags.map(tag => + Array.isArray(tag) ? tag.map(val => String(val ?? "")) : tag + ); + // Ensure content is always a string (never undefined or null) + const safeContent = typeof content === "string" ? content : ""; + const eventTemplate = { - kind, - created_at, - tags, - content, + kind: Number(kind), + created_at: Number(created_at), + tags: normalizedTags, + content: safeContent, }; const signedEvent = finalizeEvent(eventTemplate, normalizedKey); + // Ensure all required properties are present return { - ...signedEvent, - kind: kind as EventKind, + id: signedEvent.id, + pubkey: signedEvent.pubkey, + created_at: signedEvent.created_at, + kind: signedEvent.kind as EventKind, + tags: signedEvent.tags, + content: signedEvent.content, + sig: signedEvent.sig, }; } diff --git a/src/nostr/nkbip08Tags.ts b/src/nostr/nkbip08Tags.ts index 42fee68..17b0c24 100644 --- a/src/nostr/nkbip08Tags.ts +++ b/src/nostr/nkbip08Tags.ts @@ -23,8 +23,8 @@ export const NKBIP08_TAGS = { * IMPORTANT: This handles hierarchical paths with colons (e.g., "part-1:question-2:article-3") * by converting colons to hyphens, resulting in "part-1-question-2-article-3" as per NKBIP-08 spec. */ -export function normalizeNKBIP08TagValue(text: string): string { - if (!text) { +export function normalizeNKBIP08TagValue(text: string | undefined | null): string { + if (!text || typeof text !== "string") { return ""; } @@ -110,7 +110,7 @@ export function extractNKBIP08TagsFrom30040( */ export function buildNKBIP08TagsFor30041( parentMetadata: Kind30040Metadata, - rootMetadata: Kind30040Metadata, + rootMetadata: Kind30040Metadata | undefined, bookTitle: string, chapterTitle: string, sectionTitle: string, @@ -118,7 +118,7 @@ export function buildNKBIP08TagsFor30041( ): NKBIP08_30041Tags { return { // C tag: Inherited from root 30040 (optional - for compendiums, digests, libraries) - collection_id: rootMetadata.collection_id ? normalizeNKBIP08TagValue(rootMetadata.collection_id) : undefined, + collection_id: rootMetadata?.collection_id ? normalizeNKBIP08TagValue(rootMetadata.collection_id) : undefined, // Inherit from parent 30040 version_tag: parentMetadata.version_tag ? normalizeNKBIP08TagValue(parentMetadata.version_tag) : undefined, // T tag: Normalized book title (from root 30040) @@ -145,8 +145,8 @@ export function mergeNKBIP08TagsFor30040( rootMetadata?: Kind30040Metadata ): NKBIP08_30040Tags { // Collection ID is inherited from root (if present), not from parent - const collectionId = rootMetadata?.collection_id || childMetadata.collection_id; - const versionTag = parentMetadata?.version_tag || childMetadata.version_tag; + const collectionId = rootMetadata?.collection_id || childMetadata?.collection_id; + const versionTag = parentMetadata?.version_tag || childMetadata?.version_tag; return { collection_id: collectionId ? normalizeNKBIP08TagValue(collectionId) : undefined, diff --git a/src/ui/metadataReminderModal.ts b/src/ui/metadataReminderModal.ts index 467f23b..7f90197 100644 --- a/src/ui/metadataReminderModal.ts +++ b/src/ui/metadataReminderModal.ts @@ -61,9 +61,14 @@ export class MetadataReminderModal extends Modal { text: "OK, I've Updated the Metadata", cls: "mod-cta", }); - okButton.addEventListener("click", () => { - this.onConfirm(); + okButton.addEventListener("click", async () => { this.close(); + try { + await this.onConfirm(); + } catch (error: any) { + // Error handling is done in the callback, but ensure we catch any unhandled errors + console.error("Error in metadata reminder modal callback:", error); + } }); const cancelButton = buttonContainer.createEl("button", { text: "Cancel" }); diff --git a/src/ui/structurePreviewModal.ts b/src/ui/structurePreviewModal.ts index 6b58c33..7fe3bd8 100644 --- a/src/ui/structurePreviewModal.ts +++ b/src/ui/structurePreviewModal.ts @@ -15,18 +15,37 @@ export class StructurePreviewModal extends Modal { } onOpen() { - const { contentEl } = this; + const { contentEl, modalEl } = this; contentEl.empty(); - contentEl.createEl("h2", { text: "Document Structure Preview" }); + // Set max height on the modal content + modalEl.style.maxHeight = "1000px"; + modalEl.style.display = "flex"; + modalEl.style.flexDirection = "column"; + contentEl.style.display = "flex"; + contentEl.style.flexDirection = "column"; + contentEl.style.maxHeight = "1000px"; + contentEl.style.overflow = "hidden"; + + const title = contentEl.createEl("h2", { text: "Document Structure Preview" }); + title.style.marginBottom = "1.5em"; + title.style.flexShrink = "0"; const structureContainer = contentEl.createDiv({ cls: "scriptorium-structure-preview" }); + structureContainer.style.marginBottom = "2em"; + structureContainer.style.overflowY = "auto"; + structureContainer.style.flex = "1"; + structureContainer.style.minHeight = "0"; this.structure.forEach((node) => { this.renderNode(structureContainer, node, 0); }); const buttonContainer = contentEl.createDiv({ cls: "scriptorium-modal-buttons" }); + buttonContainer.style.marginTop = "1.5em"; + buttonContainer.style.paddingTop = "1em"; + buttonContainer.style.borderTop = "1px solid var(--background-modifier-border)"; + buttonContainer.style.flexShrink = "0"; const confirmButton = buttonContainer.createEl("button", { text: "Create Events", cls: "mod-cta", @@ -44,22 +63,44 @@ export class StructurePreviewModal extends Modal { private renderNode(container: HTMLElement, node: StructureNode, indent: number) { const nodeDiv = container.createDiv({ cls: "scriptorium-structure-node" }); - nodeDiv.style.paddingLeft = `${indent * 20}px`; + nodeDiv.style.marginBottom = "1.2em"; + nodeDiv.style.paddingTop = "0.8em"; + nodeDiv.style.paddingBottom = "0.8em"; + + // For nested nodes, indent the entire div and add border at the indented position + if (indent > 0) { + const indentPx = indent * 24; + // Position the border at the indentation level (not at the left edge) + nodeDiv.style.marginLeft = `${indentPx}px`; + nodeDiv.style.borderLeft = "2px solid var(--background-modifier-border)"; + nodeDiv.style.paddingLeft = "8px"; + } else { + nodeDiv.style.paddingLeft = "0px"; + } const kindBadge = nodeDiv.createSpan({ cls: `scriptorium-kind-badge kind-${node.kind}`, text: `Kind ${node.kind}`, }); + kindBadge.style.marginBottom = "0.5em"; + kindBadge.style.display = "block"; const titleEl = nodeDiv.createEl("div", { cls: "scriptorium-node-title" }); + titleEl.style.marginBottom = "0.4em"; titleEl.createEl("strong", { text: node.title }); const dTagEl = nodeDiv.createEl("div", { cls: "scriptorium-node-dtag" }); + dTagEl.style.marginTop = "0.3em"; + dTagEl.style.fontSize = "0.9em"; + dTagEl.style.color = "var(--text-muted)"; dTagEl.createEl("span", { text: `d-tag: `, cls: "scriptorium-label" }); dTagEl.createEl("code", { text: node.dTag }); if (node.kind === 30041 && node.content) { const contentPreview = nodeDiv.createDiv({ cls: "scriptorium-content-preview" }); + contentPreview.style.marginTop = "0.5em"; + contentPreview.style.fontSize = "0.85em"; + contentPreview.style.color = "var(--text-muted)"; const previewText = node.content.substring(0, 100); contentPreview.createEl("em", { text: previewText + (node.content.length > 100 ? "..." : ""), @@ -67,8 +108,10 @@ export class StructurePreviewModal extends Modal { } if (node.children.length > 0) { + const childrenContainer = container.createDiv(); + childrenContainer.style.marginTop = "0.5em"; node.children.forEach((child) => { - this.renderNode(container, child, indent + 1); + this.renderNode(childrenContainer, child, indent + 1); }); } } diff --git a/src/utils/eventKind.ts b/src/utils/eventKind.ts index 0672c2b..b9f750a 100644 --- a/src/utils/eventKind.ts +++ b/src/utils/eventKind.ts @@ -13,6 +13,11 @@ export function determineEventKind( metadataKind?: EventKind ): EventKind { if (isAsciiDocFile(file)) { + // If metadata specifies a kind, use it (allows standalone 30041 or 30818) + if (metadataKind) { + return metadataKind; + } + // Otherwise, determine from content structure if (isAsciiDocDocument(content)) { return 30040; }