You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1433 lines
51 KiB
1433 lines
51 KiB
<script lang="ts"> |
|
import { Button } from "flowbite-svelte"; |
|
import { |
|
EyeOutline, |
|
QuestionCircleOutline, |
|
ChartPieOutline, |
|
} from "flowbite-svelte-icons"; |
|
import { EditorView, basicSetup } from "codemirror"; |
|
import { EditorState, StateField, StateEffect } from "@codemirror/state"; |
|
import { markdown } from "@codemirror/lang-markdown"; |
|
import { Decoration, type DecorationSet } from "@codemirror/view"; |
|
import { RangeSet } from "@codemirror/state"; |
|
import { onMount } from "svelte"; |
|
import { |
|
extractSmartMetadata, |
|
type AsciiDocMetadata, |
|
metadataToTags, |
|
parseSimpleAttributes, |
|
} from "$lib/utils/asciidoc_metadata"; |
|
import { |
|
parseAsciiDocWithTree, |
|
exportEventsFromTree, |
|
} from "$lib/utils/asciidoc_publication_parser"; |
|
import { getNdkContext } from "$lib/ndk"; |
|
import Asciidoctor from "asciidoctor"; |
|
|
|
// Initialize Asciidoctor processor |
|
const asciidoctor = Asciidoctor(); |
|
|
|
// Component props |
|
let { |
|
content = "", |
|
placeholder = "Start writing your AsciiDoc content here...", |
|
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; |
|
}>(); |
|
|
|
// Get NDK context for PublicationTree creation |
|
const ndk = getNdkContext(); |
|
|
|
// Configuration constants |
|
const MIN_PARSE_LEVEL = 2; |
|
const MAX_PARSE_LEVEL = 5; |
|
|
|
// State for PublicationTree result |
|
let publicationResult = $state<any>(null); |
|
let generatedEvents = $state<any>(null); |
|
let contentType = $state<"article" | "scattered-notes" | "none">("none"); |
|
|
|
// Note: updateEditorContent() is only called manually when needed |
|
// The automatic effect was causing feedback loops with user typing |
|
|
|
// Effect to update syntax highlighting when parsing results change |
|
$effect(() => { |
|
if ( |
|
editorView && |
|
(parsedSections || publicationResult?.metadata?.eventStructure) |
|
) { |
|
editorView.dispatch({ |
|
effects: updateHighlighting.of(parsedSections || []), |
|
}); |
|
} |
|
}); |
|
|
|
// Effect to create PublicationTree when content changes |
|
// Uses tree processor: |
|
// "register a tree processor extension in our Asciidoctor instance" |
|
// "use the AST that Asciidoctor generates during parsing" |
|
// "publication tree side-loads into memory as AsciiDoc is parsed" |
|
$effect(() => { |
|
if (!content.trim() || !ndk) { |
|
publicationResult = null; |
|
generatedEvents = null; |
|
contentType = "none"; |
|
return; |
|
} |
|
|
|
// Use new hierarchical tree processor for NKBIP-01 compliance |
|
parseAsciiDocWithTree(content, ndk, parseLevel) |
|
.then((result) => { |
|
console.log("Tree factory result:", result); |
|
publicationResult = result; |
|
contentType = result.metadata.contentType; |
|
|
|
// Export events for publishing workflow |
|
const events = exportEventsFromTree(result); |
|
generatedEvents = events; |
|
console.log("Generated events:", events); |
|
console.log("Event structure:", result.metadata.eventStructure); |
|
return events; |
|
}) |
|
.catch((error) => { |
|
console.error("Tree factory error:", error); |
|
publicationResult = null; |
|
generatedEvents = null; |
|
contentType = "none"; |
|
}); |
|
}); |
|
|
|
// Helper function to get section level from content |
|
function getSectionLevel(sectionContent: string): number { |
|
const lines = sectionContent.split(/\r?\n/); |
|
for (const line of lines) { |
|
const match = line.match(/^(=+)\s+/); |
|
if (match) { |
|
return match[1].length; |
|
} |
|
} |
|
return 2; // Default to level 2 |
|
} |
|
|
|
// 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} → content events with nested AsciiDoc)`; |
|
} else { |
|
const prevEquals = "=".repeat(level - 1); |
|
label = `Level ${level} (${prevEquals} → index events, ${equals} → content events)`; |
|
} |
|
|
|
options.push({ level, label }); |
|
} |
|
return options; |
|
} |
|
|
|
// Parse sections for preview display using hierarchical eventStructure |
|
let parsedSections = $derived.by(() => { |
|
if (!publicationResult || !publicationResult.metadata?.eventStructure) |
|
return []; |
|
|
|
console.log("Preview: publicationResult structure:", { |
|
hasContentEvents: !!publicationResult.contentEvents, |
|
contentEventsLength: publicationResult.contentEvents?.length, |
|
hasEventStructure: !!publicationResult.metadata.eventStructure, |
|
eventStructureLength: publicationResult.metadata.eventStructure?.length, |
|
keys: Object.keys(publicationResult), |
|
}); |
|
|
|
console.log("Event structure details:", JSON.stringify(publicationResult.metadata.eventStructure, null, 2)); |
|
console.log("Content events details:", publicationResult.contentEvents?.map(e => ({ |
|
dTag: e.tags?.find(t => t[0] === 'd')?.[1], |
|
title: e.tags?.find(t => t[0] === 'title')?.[1], |
|
content: e.content?.substring(0, 100) + '...' |
|
}))); |
|
|
|
// Helper to get d-tag from event (works with both NDK events and serialized events) |
|
const getEventDTag = (event: any) => { |
|
if (event?.tagValue) { |
|
// NDK event |
|
return event.tagValue("d"); |
|
} else if (event?.tags) { |
|
// Serialized event |
|
return event.tags.find((t: string[]) => t[0] === "d")?.[1]; |
|
} |
|
return null; |
|
}; |
|
|
|
// Helper to find event by dTag and kind |
|
const findEventByDTag = (events: any[], dTag: string, eventKind?: number) => { |
|
return events.find((event) => { |
|
const matchesDTag = getEventDTag(event) === dTag; |
|
if (eventKind !== undefined) { |
|
const eventKindValue = event?.kind || (event?.tagValue ? event.tagValue("k") : null); |
|
return matchesDTag && eventKindValue === eventKind; |
|
} |
|
return matchesDTag; |
|
}); |
|
}; |
|
|
|
// Flatten eventStructure recursively to show all nodes |
|
function flattenNodes(nodes: any[], result: any[] = []): any[] { |
|
for (const node of nodes) { |
|
result.push(node); |
|
if (node.children && node.children.length > 0) { |
|
flattenNodes(node.children, result); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
let flatNodes: any[] = []; |
|
if (publicationResult.metadata.eventStructure.length > 0) { |
|
flatNodes = flattenNodes(publicationResult.metadata.eventStructure); |
|
} |
|
|
|
// Map nodes to display sections |
|
return flatNodes.map((node: any) => { |
|
// For the root index, use indexEvent. For others, find in contentEvents |
|
let event; |
|
if ( |
|
publicationResult.indexEvent && |
|
node.dTag === getEventDTag(publicationResult.indexEvent) |
|
) { |
|
event = publicationResult.indexEvent; |
|
} else { |
|
// contentEvents can contain both 30040 and 30041 events at parse level 3+ |
|
// Use eventKind to find the correct event type |
|
event = findEventByDTag(publicationResult.contentEvents, node.dTag, node.eventKind); |
|
} |
|
|
|
const tags = event?.tags.filter((t: string[]) => t[0] === "t") || []; |
|
|
|
// Extract the title from the title tag |
|
const titleTag = event?.tags.find((t: string[]) => t[0] === "title"); |
|
const eventTitle = titleTag ? titleTag[1] : node.title; |
|
|
|
|
|
// For content events, remove the first heading from content since we'll use the title tag |
|
let processedContent = event?.content || ""; |
|
if (event && node.eventType === "content") { |
|
// Remove the heading line that matches this section's title and level (if present) |
|
// This is important because content events should not include their own title heading |
|
// since the title is displayed separately from the "title" tag |
|
const lines = processedContent.split("\n"); |
|
const expectedHeading = `${"=".repeat(node.level)} ${node.title}`; |
|
const titleHeadingIndex = lines.findIndex((line: string) => |
|
line.trim() === expectedHeading.trim(), |
|
); |
|
if (titleHeadingIndex !== -1) { |
|
// Remove only the specific title heading line |
|
lines.splice(titleHeadingIndex, 1); |
|
processedContent = lines.join("\n").trim(); |
|
} |
|
} |
|
|
|
|
|
return { |
|
title: eventTitle, |
|
content: processedContent, |
|
tags, // Already in [['t', 'tag1'], ['t', 'tag2']] format |
|
level: node.level, |
|
isIndex: node.eventKind === 30040, |
|
eventKind: node.eventKind, |
|
eventType: node.eventType, |
|
}; |
|
}); |
|
}); |
|
|
|
// Publishing handlers |
|
function handlePublish() { |
|
if (!generatedEvents) return; |
|
|
|
try { |
|
// Deep clone the events to ensure they're fully serializable |
|
// This prevents postMessage cloning errors |
|
const serializableEvents = JSON.parse(JSON.stringify(generatedEvents)); |
|
|
|
if (contentType === "article" && serializableEvents.indexEvent) { |
|
// Full article: publish both index event (30040) and content events (30041) |
|
onPublishArticle(serializableEvents); |
|
} else if (contentType === "scattered-notes") { |
|
// Only notes: publish just the content events (30041) |
|
const notesOnly = { |
|
contentEvents: serializableEvents.contentEvents, |
|
}; |
|
onPublishScatteredNotes(notesOnly); |
|
} |
|
} catch (error) { |
|
console.error("Failed to serialize events:", error); |
|
console.error("generatedEvents structure:", generatedEvents); |
|
// Try to identify the non-serializable part |
|
if (generatedEvents) { |
|
console.error("Keys in generatedEvents:", Object.keys(generatedEvents)); |
|
if (generatedEvents.indexEvent) { |
|
console.error( |
|
"indexEvent type:", |
|
typeof generatedEvents.indexEvent, |
|
generatedEvents.indexEvent?.constructor?.name, |
|
); |
|
} |
|
if (generatedEvents.contentEvents?.[0]) { |
|
console.error( |
|
"First contentEvent type:", |
|
typeof generatedEvents.contentEvents[0], |
|
generatedEvents.contentEvents[0]?.constructor?.name, |
|
); |
|
} |
|
} |
|
alert( |
|
"Error: Events contain non-serializable data. Check console for details.", |
|
); |
|
} |
|
} |
|
|
|
// Tutorial sidebar state |
|
let showTutorial = $state(false); |
|
|
|
// Structure preview sidebar state |
|
let showStructurePreview = $state(false); |
|
|
|
// Toggle preview panel |
|
function togglePreview() { |
|
const newShowPreview = !showPreview; |
|
onPreviewToggle(newShowPreview); |
|
} |
|
|
|
// Toggle tutorial sidebar |
|
function toggleTutorial() { |
|
showTutorial = !showTutorial; |
|
} |
|
|
|
// Toggle structure preview sidebar |
|
function toggleStructurePreview() { |
|
showStructurePreview = !showStructurePreview; |
|
} |
|
|
|
// CodeMirror editor state |
|
let editorContainer = $state<HTMLDivElement | null>(null); |
|
let editorView = $state<EditorView | null>(null); |
|
|
|
// Create update effect for highlighting |
|
const updateHighlighting = StateEffect.define<any>(); |
|
|
|
// State field to track header decorations |
|
const headerDecorations = StateField.define<DecorationSet>({ |
|
create() { |
|
return Decoration.none; |
|
}, |
|
update(decorations, tr) { |
|
// Update decorations when content changes or highlighting is updated |
|
decorations = decorations.map(tr.changes); |
|
|
|
for (let effect of tr.effects) { |
|
if (effect.is(updateHighlighting)) { |
|
decorations = createHeaderDecorations(tr.state, effect.value); |
|
} |
|
} |
|
|
|
return decorations; |
|
}, |
|
provide: (f) => EditorView.decorations.from(f), |
|
}); |
|
|
|
// Function to create header decorations based on parsed sections |
|
function createHeaderDecorations( |
|
state: EditorState, |
|
sections: any[], |
|
): DecorationSet { |
|
const ranges: Array<{ from: number; to: number; decoration: any }> = []; |
|
const doc = state.doc; |
|
const content = doc.toString(); |
|
const lines = content.split("\n"); |
|
|
|
// Analyze document structure for ambiguity detection |
|
const documentStructure = analyzeDocumentStructure(lines); |
|
|
|
// Create a map of header text to section info for fast lookup from actual event structure |
|
const sectionMap = new Map(); |
|
if (publicationResult?.metadata?.eventStructure) { |
|
// Flatten the event structure to get all nodes with their actual event types |
|
const flattenEventStructure = ( |
|
nodes: any[], |
|
result: any[] = [], |
|
): any[] => { |
|
for (const node of nodes) { |
|
result.push(node); |
|
if (node.children && node.children.length > 0) { |
|
flattenEventStructure(node.children, result); |
|
} |
|
} |
|
return result; |
|
}; |
|
|
|
const allEventNodes = flattenEventStructure( |
|
publicationResult.metadata.eventStructure, |
|
); |
|
|
|
// Debug: log the event structure |
|
console.log( |
|
"Event structure nodes for highlighting:", |
|
allEventNodes.map((n) => ({ |
|
title: n.title, |
|
level: n.level, |
|
eventType: n.eventType, |
|
eventKind: n.eventKind, |
|
})), |
|
); |
|
|
|
allEventNodes.forEach((node) => { |
|
if (node.title) { |
|
sectionMap.set(node.title.toLowerCase().trim(), { |
|
level: node.level, |
|
isEventTitle: true, |
|
eventType: node.eventType, |
|
eventKind: node.eventKind, |
|
}); |
|
} |
|
}); |
|
} |
|
|
|
let pos = 0; |
|
for (let i = 0; i < lines.length; i++) { |
|
const line = lines[i]; |
|
const headerMatch = line.match(/^(=+)\s+(.+)$/); |
|
|
|
if (headerMatch) { |
|
const level = headerMatch[1].length; |
|
const headerText = headerMatch[2].trim().toLowerCase(); |
|
const lineStart = pos; |
|
const lineEnd = pos + line.length; |
|
|
|
// Check if this header is an event title |
|
const sectionInfo = sectionMap.get(headerText); |
|
let className: string; |
|
|
|
// Check for ambiguous syntax first (highest priority) |
|
if ( |
|
isAmbiguousHeader( |
|
level, |
|
headerText, |
|
documentStructure, |
|
parseLevel, |
|
lines, |
|
i, |
|
publicationResult, |
|
) |
|
) { |
|
className = "cm-header-potential-event"; // Amber for ambiguous |
|
} |
|
// Determine highlighting based on structural analysis |
|
else if (level === 1) { |
|
// Document title is always an index event (blue) |
|
className = "cm-header-index-event"; |
|
} else if (level === parseLevel) { |
|
// Headers at parse level are content events (green) |
|
className = "cm-header-content-event"; |
|
} else if (level < parseLevel) { |
|
// Headers above parse level that could have children are index events (blue) |
|
// Check if this header has children by looking ahead in the document |
|
const hasChildren = headerHasChildren(lines, i, level); |
|
if (hasChildren) { |
|
className = "cm-header-index-event"; // Blue for sections with children |
|
} else { |
|
className = "cm-header-content-event"; // Green for sections without children |
|
} |
|
} else if (level > parseLevel) { |
|
className = "cm-header-subcontent"; // Gray for subheaders below parse level |
|
} else { |
|
className = "cm-header-potential-event"; // Amber for unclear cases |
|
} |
|
|
|
ranges.push({ |
|
from: lineStart, |
|
to: lineEnd, |
|
decoration: Decoration.mark({ class: className }), |
|
}); |
|
} |
|
|
|
pos += line.length + 1; // +1 for newline |
|
} |
|
|
|
console.log(`Created ${ranges.length} header decorations`); |
|
return RangeSet.of(ranges.map((r) => r.decoration.range(r.from, r.to))); |
|
} |
|
|
|
// Analyze document structure to detect ambiguous patterns |
|
function analyzeDocumentStructure(lines: string[]) { |
|
let hasDocumentTitle = false; |
|
let level1Headers = 0; |
|
let level2Headers = 0; |
|
let level3PlusHeaders = 0; |
|
|
|
for (const line of lines) { |
|
const headerMatch = line.match(/^(=+)\s+(.+)$/); |
|
if (headerMatch) { |
|
const level = headerMatch[1].length; |
|
if (level === 1) { |
|
hasDocumentTitle = true; |
|
level1Headers++; |
|
} else if (level === 2) { |
|
level2Headers++; |
|
} else if (level >= 3) { |
|
level3PlusHeaders++; |
|
} |
|
} |
|
} |
|
|
|
return { |
|
hasDocumentTitle, |
|
level1Headers, |
|
level2Headers, |
|
level3PlusHeaders, |
|
contentType: |
|
hasDocumentTitle && level2Headers > 0 |
|
? "article" |
|
: level2Headers > 0 |
|
? "scattered-notes" |
|
: "none", |
|
}; |
|
} |
|
|
|
// Check if a header has children by looking ahead in the document |
|
function headerHasChildren( |
|
lines: string[], |
|
headerIndex: number, |
|
headerLevel: number, |
|
): boolean { |
|
// Look ahead to see if there are any headers at a deeper level |
|
for (let i = headerIndex + 1; i < lines.length; i++) { |
|
const line = lines[i]; |
|
const headerMatch = line.match(/^(=+)\s+(.+)$/); |
|
|
|
if (headerMatch) { |
|
const nextLevel = headerMatch[1].length; |
|
|
|
if (nextLevel > headerLevel) { |
|
// Found a deeper header - this header has children |
|
return true; |
|
} else if (nextLevel <= headerLevel) { |
|
// Found a header at the same or higher level - no children |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
// Reached end of document with no headers found - no children |
|
return false; |
|
} |
|
|
|
// Check if a header represents ambiguous syntax |
|
function isAmbiguousHeader( |
|
level: number, |
|
headerText: string, |
|
structure: any, |
|
parseLevel: number, |
|
lines: string[] = [], |
|
headerIndex: number = -1, |
|
pubResult: any = null, |
|
): boolean { |
|
// Case 1: Header immediately follows another header or attributes without blank line separation |
|
// This is invalid AsciiDoc syntax and won't parse correctly |
|
if (headerIndex > 0 && lines.length > headerIndex) { |
|
const prevLine = lines[headerIndex - 1]; |
|
|
|
// Check if previous line is also a header |
|
if (prevLine.match(/^=+\s+/)) { |
|
// No blank line between headers - this is invalid AsciiDoc |
|
return true; |
|
} |
|
|
|
// Check if previous line is an attribute (like :tags: test, notes) |
|
if (prevLine.match(/^:[^:]+:/)) { |
|
// Header immediately follows attribute line - missing required blank line |
|
return true; |
|
} |
|
|
|
// Check if this header should be parsed but isn't due to improper separation |
|
// This specifically catches cases where AsciiDoc parser failed to separate sections |
|
if (level === parseLevel && pubResult) { |
|
// Check if this header exists in the parsed structure - if not, it might be improperly separated |
|
const headerTitle = headerText.toLowerCase().trim(); |
|
|
|
// Look through the event structure to see if this header was parsed as a separate event |
|
const sectionMap = new Map(); |
|
if (publicationResult.metadata?.eventStructure) { |
|
const flattenEventStructure = ( |
|
nodes: any[], |
|
result: any[] = [], |
|
): any[] => { |
|
for (const node of nodes) { |
|
result.push(node); |
|
if (node.children && node.children.length > 0) { |
|
flattenEventStructure(node.children, result); |
|
} |
|
} |
|
return result; |
|
}; |
|
|
|
const allEventNodes = flattenEventStructure( |
|
publicationResult.metadata.eventStructure, |
|
); |
|
allEventNodes.forEach((node) => { |
|
if (node.title) { |
|
sectionMap.set(node.title.toLowerCase().trim(), true); |
|
} |
|
}); |
|
} |
|
|
|
// If this header isn't in the parsed structure, it might be improperly separated |
|
if (!sectionMap.has(headerTitle)) { |
|
return true; // Header exists in source but not in parsed structure |
|
} |
|
} |
|
} |
|
|
|
// Case 2: Document has title (=) but user might expect level 2 headers to be top-level notes |
|
if ( |
|
level === 2 && |
|
structure.hasDocumentTitle && |
|
structure.level2Headers > 0 |
|
) { |
|
// This is actually correct for articles, not ambiguous |
|
return false; |
|
} |
|
|
|
// Case 3: Multiple level 1 headers (ambiguous document structure) |
|
if (level === 1 && structure.level1Headers > 1) { |
|
return true; // Multiple document titles are ambiguous |
|
} |
|
|
|
// Case 4: Headers at parse level that aren't being extracted due to structural issues |
|
if (level === parseLevel) { |
|
// If this header level should be extracted but isn't in the event structure, |
|
// it might be ambiguous (this requires checking against publicationResult) |
|
if (structure.contentType === "none" && level === 2) { |
|
return true; // Level 2 headers with no clear structure |
|
} |
|
} |
|
|
|
// Case 5: Orphaned high-level headers (e.g., === without ==) |
|
if ( |
|
level === 3 && |
|
structure.level2Headers === 0 && |
|
structure.hasDocumentTitle |
|
) { |
|
return true; // Level 3 without level 2 parent in article structure |
|
} |
|
|
|
return false; |
|
} |
|
|
|
// Initialize CodeMirror editor |
|
function createEditor() { |
|
if (!editorContainer) return; |
|
|
|
// Create custom theme with header highlighting classes |
|
const headerHighlighting = EditorView.theme({ |
|
// Event titles (extracted as separate events) |
|
".cm-header-index-event": { |
|
color: "#3B82F6", // blue-500 for index events (30040) |
|
fontWeight: "700", |
|
fontSize: "1.1em", |
|
backgroundColor: "rgba(59, 130, 246, 0.1)", |
|
borderLeft: "3px solid #3B82F6", |
|
paddingLeft: "8px", |
|
}, |
|
".cm-header-content-event": { |
|
color: "#10B981", // emerald-500 for content events (30041) |
|
fontWeight: "700", |
|
fontSize: "1.1em", |
|
backgroundColor: "rgba(16, 185, 129, 0.1)", |
|
borderLeft: "3px solid #10B981", |
|
paddingLeft: "8px", |
|
}, |
|
".cm-header-event-title": { |
|
color: "#8B5CF6", // violet-500 for other event types |
|
fontWeight: "700", |
|
fontSize: "1.1em", |
|
backgroundColor: "rgba(139, 92, 246, 0.1)", |
|
borderLeft: "3px solid #8B5CF6", |
|
paddingLeft: "8px", |
|
}, |
|
// Potential events (at parse level but not extracted yet) |
|
".cm-header-potential-event": { |
|
color: "#F59E0B", // amber-500 for headers at parse level |
|
fontWeight: "600", |
|
textDecoration: "underline", |
|
textDecorationStyle: "dotted", |
|
}, |
|
// Subcontent headers (below parse level, part of content) |
|
".cm-header-subcontent": { |
|
color: "#6B7280", // gray-500 for regular subheaders |
|
fontWeight: "500", |
|
fontStyle: "italic", |
|
}, |
|
}); |
|
|
|
const state = EditorState.create({ |
|
doc: content, |
|
extensions: [ |
|
basicSetup, |
|
markdown(), // AsciiDoc is similar to markdown syntax |
|
headerDecorations, |
|
headerHighlighting, |
|
EditorView.updateListener.of((update) => { |
|
if (update.docChanged) { |
|
onContentChange(update.state.doc.toString()); |
|
} |
|
}), |
|
EditorView.theme({ |
|
"&": { |
|
fontSize: "14px", |
|
fontFamily: |
|
'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace', |
|
height: "100%", |
|
}, |
|
".cm-content": { |
|
padding: "16px", |
|
minHeight: "100%", |
|
maxWidth: "900px", |
|
margin: "0 auto", |
|
}, |
|
".cm-editor": { |
|
borderRadius: "0.5rem", |
|
height: "100%", |
|
}, |
|
".cm-scroller": { |
|
overflow: "auto", |
|
height: "100%", |
|
fontFamily: "inherit", |
|
}, |
|
".cm-focused": { |
|
outline: "none", |
|
}, |
|
}), |
|
], |
|
}); |
|
|
|
editorView = new EditorView({ |
|
state, |
|
parent: editorContainer, |
|
}); |
|
} |
|
|
|
// Update editor content when content prop changes |
|
function updateEditorContent() { |
|
if (!editorView) return; |
|
|
|
const currentContent = editorView.state.doc.toString(); |
|
if (currentContent !== content) { |
|
editorView.dispatch({ |
|
changes: { |
|
from: 0, |
|
to: currentContent.length, |
|
insert: content, |
|
}, |
|
}); |
|
} |
|
} |
|
|
|
// Mount CodeMirror when component mounts |
|
onMount(() => { |
|
createEditor(); |
|
|
|
return () => { |
|
if (editorView) { |
|
editorView.destroy(); |
|
} |
|
}; |
|
}); |
|
</script> |
|
|
|
<div class="flex flex-col space-y-4"> |
|
<!-- Smart Publishing Interface --> |
|
<div |
|
class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-4" |
|
> |
|
<div class="flex items-start justify-between"> |
|
<div class="flex-1"> |
|
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-200 mb-2"> |
|
Unified AsciiDoc Publisher |
|
</h3> |
|
<div |
|
class="flex flex-col lg:flex-row lg:items-center lg:space-x-4 mb-3 space-y-2 lg:space-y-0" |
|
> |
|
<div class="flex items-center space-x-2"> |
|
<label |
|
for="parse-level" |
|
class="text-xs text-gray-600 dark:text-gray-400 font-medium" |
|
>Parse Level:</label |
|
> |
|
<select |
|
id="parse-level" |
|
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} |
|
<option value={option.level}>{option.label}</option> |
|
{/each} |
|
</select> |
|
</div> |
|
|
|
<div class="text-xs text-gray-600 dark:text-gray-400"> |
|
<span class="font-medium">Content Type:</span> |
|
<span |
|
class="ml-1 px-2 py-0.5 rounded-full text-xs font-medium {contentType === |
|
'article' |
|
? 'bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-200' |
|
: contentType === 'scattered-notes' |
|
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-200' |
|
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'}" |
|
> |
|
{contentType === "article" |
|
? "Article" |
|
: contentType === "scattered-notes" |
|
? "Notes" |
|
: "None"} |
|
</span> |
|
</div> |
|
|
|
{#if generatedEvents} |
|
<div class="text-xs text-gray-600 dark:text-gray-400"> |
|
<span class="font-medium">Events:</span> |
|
<span class="ml-1" |
|
>{generatedEvents.contentEvents.length + |
|
(generatedEvents.indexEvent ? 1 : 0)}</span |
|
> |
|
</div> |
|
{/if} |
|
</div> |
|
</div> |
|
|
|
<!-- Button on the right side of publisher --> |
|
{#if publicationResult?.metadata?.eventStructure && generatedEvents} |
|
<Button |
|
color="light" |
|
size="xs" |
|
onclick={toggleStructurePreview} |
|
class="flex items-center space-x-1 ml-4" |
|
> |
|
<ChartPieOutline class="w-3 h-3" /> |
|
<span class="text-xs">Structure</span> |
|
</Button> |
|
{/if} |
|
</div> |
|
</div> |
|
|
|
<div |
|
class="flex flex-col lg:flex-row items-center justify-between space-y-2 lg:space-y-0" |
|
> |
|
<div class="flex items-center space-x-2"> |
|
<Button |
|
color="light" |
|
size="sm" |
|
onclick={togglePreview} |
|
class="flex items-center space-x-1" |
|
> |
|
{#if showPreview} |
|
<EyeOutline class="w-4 h-4" /> |
|
<span>Hide Preview</span> |
|
{:else} |
|
<EyeOutline class="w-4 h-4" /> |
|
<span>Show Preview</span> |
|
{/if} |
|
</Button> |
|
|
|
<Button |
|
color="light" |
|
size="sm" |
|
onclick={toggleTutorial} |
|
class="flex items-center space-x-1" |
|
> |
|
<QuestionCircleOutline class="w-4 h-4" /> |
|
<span>{showTutorial ? "Hide" : "Show"} Help</span> |
|
</Button> |
|
</div> |
|
|
|
<!-- Publishing Button --> |
|
{#if generatedEvents && contentType !== "none"} |
|
<Button color="primary" size="sm" onclick={handlePublish}>Publish</Button> |
|
{:else} |
|
<div class="text-xs text-gray-500 dark:text-gray-400"> |
|
Add content to enable publishing |
|
</div> |
|
{/if} |
|
</div> |
|
|
|
<div |
|
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-6 h-[60vh] min-h-[400px] max-h-[800px]" |
|
> |
|
<!-- Editor Panel --> |
|
<div |
|
class="{(showPreview && (showTutorial || showStructurePreview)) || |
|
(showTutorial && showStructurePreview) |
|
? 'lg:w-1/3' |
|
: showPreview || showTutorial || showStructurePreview |
|
? 'lg:w-3/5' |
|
: 'w-full'} flex flex-col" |
|
> |
|
<div |
|
class="flex-1 relative border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900" |
|
style="overflow: hidden;" |
|
> |
|
<!-- CodeMirror Editor Container --> |
|
<div bind:this={editorContainer} class="w-full h-full"></div> |
|
</div> |
|
</div> |
|
|
|
<!-- Preview Panel --> |
|
{#if showPreview} |
|
<div |
|
class="{showTutorial || showStructurePreview |
|
? 'lg:w-1/3' |
|
: 'lg:w-2/5'} flex flex-col" |
|
> |
|
<div |
|
class="border border-gray-200 dark:border-gray-700 rounded-lg h-full flex flex-col overflow-hidden" |
|
> |
|
<div |
|
class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800" |
|
> |
|
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100"> |
|
AsciiDoc Preview |
|
</h3> |
|
</div> |
|
|
|
<div class="flex-1 overflow-y-auto p-6 bg-white dark:bg-gray-900"> |
|
<div class="max-w-4xl mx-auto"> |
|
{#if !content.trim()} |
|
<div |
|
class="text-gray-500 dark:text-gray-400 text-sm text-center py-8" |
|
> |
|
Start typing to see the preview... |
|
</div> |
|
{:else} |
|
<div class="prose prose-sm dark:prose-invert max-w-none"> |
|
<!-- Render full document with title if it's an article --> |
|
{#if contentType === "article" && publicationResult?.metadata.title} |
|
{@const documentHeader = content.split(/\n==\s+/)[0]} |
|
<div |
|
class="mb-6 border-b border-gray-200 dark:border-gray-700 pb-4" |
|
> |
|
<div class="asciidoc-content"> |
|
{@html asciidoctor.convert(documentHeader, { |
|
standalone: false, |
|
attributes: { |
|
showtitle: true, |
|
sectids: false, |
|
}, |
|
})} |
|
</div> |
|
</div> |
|
{/if} |
|
|
|
{#each parsedSections as section, index} |
|
<div |
|
class="mb-6 pb-6 border-b border-gray-200 dark:border-gray-700 last:border-0" |
|
> |
|
{#if section.isIndex} |
|
<!-- Index event: show title and tags --> |
|
<div class="space-y-3"> |
|
<!-- Event type indicator --> |
|
<div |
|
class="text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider" |
|
> |
|
Index Event (30040) |
|
</div> |
|
|
|
<!-- Title --> |
|
<h2 |
|
class="text-lg font-bold text-gray-900 dark:text-gray-100" |
|
> |
|
{section.title} |
|
</h2> |
|
|
|
<!-- Tags (blue for index events) --> |
|
{#if section.tags && section.tags.length > 0} |
|
<div class="flex flex-wrap gap-2"> |
|
{#each section.tags as tag} |
|
<span |
|
class="bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 px-2 py-1 rounded-full text-xs font-medium" |
|
> |
|
#{tag[1]} |
|
</span> |
|
{/each} |
|
</div> |
|
{/if} |
|
</div> |
|
{:else} |
|
<!-- Content event: show title, tags, then content --> |
|
<div class="space-y-3"> |
|
<!-- Event type indicator --> |
|
<div |
|
class="text-xs font-semibold text-green-600 dark:text-green-400 uppercase tracking-wider" |
|
> |
|
Content Event (30041) |
|
</div> |
|
|
|
<!-- Title at correct heading level --> |
|
<div |
|
class="prose prose-sm dark:prose-invert max-w-none" |
|
> |
|
{@html asciidoctor.convert( |
|
`${"=".repeat(section.level)} ${section.title}`, |
|
{ |
|
standalone: false, |
|
attributes: { |
|
showtitle: false, |
|
sectids: false, |
|
}, |
|
}, |
|
)} |
|
</div> |
|
|
|
<!-- Tags (green for content events) --> |
|
{#if section.tags && section.tags.length > 0} |
|
<div class="flex flex-wrap gap-2"> |
|
{#each section.tags as tag} |
|
<span |
|
class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-2 py-1 rounded-full text-xs font-medium" |
|
> |
|
#{tag[1]} |
|
</span> |
|
{/each} |
|
</div> |
|
{/if} |
|
|
|
<!-- Content rendered as AsciiDoc --> |
|
{#if section.content} |
|
<div |
|
class="prose prose-sm dark:prose-invert max-w-none mt-4" |
|
> |
|
{@html (() => { |
|
// Check if content contains nested headers |
|
const hasNestedHeaders = section.content.includes('\n===') || section.content.includes('\n===='); |
|
|
|
if (hasNestedHeaders) { |
|
// For proper nested header parsing, we need full document context |
|
// Create a complete AsciiDoc document structure |
|
// Important: Ensure proper level sequence for nested headers |
|
const fullDoc = `= Temporary Document\n\n${"=".repeat(section.level)} ${section.title}\n\n${section.content}`; |
|
|
|
|
|
const rendered = asciidoctor.convert(fullDoc, { |
|
standalone: false, |
|
attributes: { |
|
showtitle: false, |
|
sectids: false, |
|
}, |
|
}); |
|
|
|
|
|
// Extract just the content we want (remove the temporary structure) |
|
// Find the section we care about |
|
const sectionStart = rendered.indexOf(`<h${section.level}`); |
|
if (sectionStart !== -1) { |
|
const nextSectionStart = rendered.indexOf(`</h${section.level}>`, sectionStart); |
|
if (nextSectionStart !== -1) { |
|
// Get everything after our section header |
|
const afterHeader = rendered.substring(nextSectionStart + `</h${section.level}>`.length); |
|
// Find where the section ends (at the closing div) |
|
const sectionEnd = afterHeader.lastIndexOf('</div>'); |
|
if (sectionEnd !== -1) { |
|
const extracted = afterHeader.substring(0, sectionEnd); |
|
return extracted; |
|
} |
|
} |
|
} |
|
return rendered; |
|
} else { |
|
// Simple content without nested headers |
|
return asciidoctor.convert(section.content, { |
|
standalone: false, |
|
attributes: { |
|
showtitle: false, |
|
sectids: false, |
|
}, |
|
}); |
|
} |
|
})()} |
|
</div> |
|
{/if} |
|
</div> |
|
{/if} |
|
|
|
<!-- Event boundary indicator --> |
|
{#if index < parsedSections.length - 1} |
|
<div class="mt-6 relative"> |
|
<div class="absolute inset-0 flex items-center"> |
|
<div |
|
class="w-full border-t-2 border-dashed border-gray-300 dark:border-gray-600" |
|
></div> |
|
</div> |
|
<div class="relative flex justify-center"> |
|
<span |
|
class="bg-white dark:bg-gray-900 px-3 text-xs text-gray-500 dark:text-gray-400" |
|
> |
|
Event Boundary |
|
</span> |
|
</div> |
|
</div> |
|
{/if} |
|
</div> |
|
{/each} |
|
</div> |
|
|
|
<div |
|
class="mt-4 text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-900 p-2 rounded border" |
|
> |
|
<strong>Event Count:</strong> |
|
{#if generatedEvents} |
|
{@const indexEvents = generatedEvents.contentEvents.filter( |
|
(e: any) => e.kind === 30040, |
|
)} |
|
{@const contentOnlyEvents = |
|
generatedEvents.contentEvents.filter( |
|
(e: any) => e.kind === 30041, |
|
)} |
|
{@const totalIndexEvents = |
|
indexEvents.length + (generatedEvents.indexEvent ? 1 : 0)} |
|
{@const totalEvents = |
|
totalIndexEvents + contentOnlyEvents.length} |
|
{totalEvents} event{totalEvents !== 1 ? "s" : ""} |
|
({totalIndexEvents} index{totalIndexEvents !== 1 |
|
? " events" |
|
: ""} + {contentOnlyEvents.length} content{contentOnlyEvents.length !== |
|
1 |
|
? " events" |
|
: ""}) |
|
{:else} |
|
0 events |
|
{/if} |
|
</div> |
|
{/if} |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
{/if} |
|
|
|
<!-- Tutorial Sidebar --> |
|
{#if showTutorial} |
|
<div |
|
class="{showPreview || showStructurePreview |
|
? 'lg:w-1/3' |
|
: 'lg:w-2/5'} flex flex-col" |
|
> |
|
<div |
|
class="border border-gray-200 dark:border-gray-700 rounded-lg h-full flex flex-col overflow-hidden" |
|
> |
|
<div |
|
class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800" |
|
> |
|
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100"> |
|
AsciiDoc Guide |
|
</h3> |
|
</div> |
|
|
|
<div |
|
class="flex-1 overflow-y-auto p-4 text-sm text-gray-700 dark:text-gray-300 space-y-4" |
|
> |
|
<!-- Syntax Highlighting Legend --> |
|
<div> |
|
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2"> |
|
Header Highlighting |
|
</h4> |
|
<div class="space-y-2 text-xs"> |
|
<div class="flex items-center space-x-2"> |
|
<div |
|
class="w-4 h-4 rounded" |
|
style="background: linear-gradient(to right, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.1)); border-left: 2px solid #3B82F6;" |
|
></div> |
|
<span |
|
><strong class="text-blue-600">Blue:</strong> Index Events (30040)</span |
|
> |
|
</div> |
|
<div class="flex items-center space-x-2"> |
|
<div |
|
class="w-4 h-4 rounded" |
|
style="background: linear-gradient(to right, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1)); border-left: 2px solid #10B981;" |
|
></div> |
|
<span |
|
><strong class="text-green-600">Green:</strong> Content Events |
|
(30041)</span |
|
> |
|
</div> |
|
<div class="flex items-center space-x-2"> |
|
<div |
|
class="w-4 h-4 rounded bg-amber-200 dark:bg-amber-800" |
|
style="text-decoration: underline;" |
|
></div> |
|
<span |
|
><strong class="text-amber-600">Amber:</strong> Potential Events |
|
(at parse level)</span |
|
> |
|
</div> |
|
<div class="flex items-center space-x-2"> |
|
<div |
|
class="w-4 h-4 rounded bg-gray-200 dark:bg-gray-600" |
|
style="font-style: italic;" |
|
></div> |
|
<span |
|
><strong class="text-gray-600">Gray:</strong> Subheaders (within |
|
content)</span |
|
> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2"> |
|
Publishing Levels |
|
</h4> |
|
<ul class="space-y-1 text-xs"> |
|
{#each generateParseLevelOptions(MIN_PARSE_LEVEL, MAX_PARSE_LEVEL) as option} |
|
<li> |
|
<strong>Level {option.level}:</strong> |
|
{#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} |
|
</li> |
|
{/each} |
|
</ul> |
|
</div> |
|
|
|
<div> |
|
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2"> |
|
Example Structure |
|
</h4> |
|
<pre |
|
class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-xs font-mono overflow-x-auto">{`= Understanding Knowledge |
|
:image: https://i.nostr.build/example.jpg |
|
:published: 2025-04-21 |
|
:tags: knowledge, philosophy, education |
|
:type: text |
|
|
|
== Preface |
|
:tags: introduction, preface |
|
|
|
This essay outlines the purpose... |
|
|
|
== Introduction: Knowledge Ecosystem |
|
:tags: introduction, ecosystem |
|
|
|
Knowledge exists as dynamic representations... |
|
|
|
=== Why Investigate Knowledge? |
|
:difficulty: intermediate |
|
|
|
Understanding the nature of knowledge... |
|
|
|
==== The Four Perspectives |
|
:complexity: high |
|
|
|
1. Material Cause: The building blocks...`}</pre> |
|
</div> |
|
|
|
<div> |
|
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2"> |
|
Attributes |
|
</h4> |
|
<p class="text-xs"> |
|
Use <code>:key: value</code> format to add metadata that becomes |
|
event tags. |
|
</p> |
|
</div> |
|
|
|
<div> |
|
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2"> |
|
Content Types |
|
</h4> |
|
<ul class="space-y-1 text-xs"> |
|
<li> |
|
<strong>Article:</strong> Starts with = title, creates index + |
|
content events |
|
</li> |
|
<li> |
|
<strong>Notes:</strong> Just == sections, creates individual content |
|
events |
|
</li> |
|
</ul> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
{/if} |
|
|
|
<!-- Structure Preview Sidebar --> |
|
{#if showStructurePreview} |
|
<div |
|
class="{showPreview || showTutorial |
|
? 'lg:w-1/3' |
|
: 'lg:w-2/5'} flex flex-col" |
|
> |
|
<div |
|
class="border border-gray-200 dark:border-gray-700 rounded-lg h-full flex flex-col overflow-hidden" |
|
> |
|
<div |
|
class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800" |
|
> |
|
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100"> |
|
Event Structure |
|
</h3> |
|
</div> |
|
|
|
<div |
|
class="flex-1 overflow-y-auto p-4 text-sm text-gray-700 dark:text-gray-300" |
|
> |
|
{#if publicationResult?.metadata?.eventStructure && publicationResult.metadata.eventStructure.length > 0} |
|
<!-- Event counts summary --> |
|
<div class="mb-4 grid grid-cols-2 gap-2"> |
|
<div class="bg-blue-50 dark:bg-blue-900/20 p-2 rounded"> |
|
<div class="flex items-center gap-2"> |
|
<span class="text-lg">📁</span> |
|
<div> |
|
<div |
|
class="text-xs font-medium text-blue-800 dark:text-blue-200" |
|
> |
|
Index Events |
|
</div> |
|
<div class="text-xs text-blue-600 dark:text-blue-400"> |
|
{publicationResult.metadata.eventStructure.filter( |
|
(n: any) => n.eventKind === 30040, |
|
).length + |
|
publicationResult.metadata.eventStructure.reduce( |
|
(acc: number, n: any) => |
|
acc + |
|
(n.children?.filter?.( |
|
(c: any) => c.eventKind === 30040, |
|
)?.length || 0), |
|
0, |
|
)} × 30040 |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="bg-green-50 dark:bg-green-900/20 p-2 rounded"> |
|
<div class="flex items-center gap-2"> |
|
<span class="text-lg">📄</span> |
|
<div> |
|
<div |
|
class="text-xs font-medium text-green-800 dark:text-green-200" |
|
> |
|
Content Events |
|
</div> |
|
<div class="text-xs text-green-600 dark:text-green-400"> |
|
{generatedEvents.contentEvents.length} × 30041 |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- Hierarchical structure --> |
|
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3"> |
|
<div class="font-mono text-xs space-y-1"> |
|
{#snippet renderEventNode(node, depth = 0)} |
|
<div class="py-0.5" style="margin-left: {depth * 1}rem;"> |
|
{node.eventKind === 30040 ? "📁" : "📄"} |
|
[{node.eventKind}] {node.title || "Untitled"} |
|
</div> |
|
{#if node.children && node.children.length > 0} |
|
{#each node.children as child} |
|
{@render renderEventNode(child, depth + 1)} |
|
{/each} |
|
{/if} |
|
{/snippet} |
|
|
|
{#each publicationResult.metadata.eventStructure as node} |
|
{@render renderEventNode(node, 0)} |
|
{/each} |
|
</div> |
|
</div> |
|
|
|
<!-- Parse level info --> |
|
<div class="mt-4 p-3 bg-gray-50 dark:bg-gray-900 rounded text-xs"> |
|
<div class="font-medium text-gray-700 dark:text-gray-300 mb-1"> |
|
Parse Level {parseLevel} |
|
</div> |
|
<div class="text-gray-600 dark:text-gray-400"> |
|
{#if parseLevel === 2} |
|
Each == section becomes a 30041 event with all nested |
|
content. |
|
{:else if parseLevel === 3} |
|
Level 2 sections with children → 30040 indices<br /> |
|
Level 3 sections → 30041 content events |
|
{:else} |
|
Sections with children → 30040 indices<br /> |
|
Level {parseLevel} sections → 30041 content events |
|
{/if} |
|
</div> |
|
</div> |
|
|
|
<!-- Legend --> |
|
<div |
|
class="mt-4 text-xs text-gray-500 dark:text-gray-400 border-t pt-3" |
|
> |
|
<div class="space-y-1"> |
|
<div class="flex items-center gap-2"> |
|
<span>📁</span> |
|
<span>Index - references other events</span> |
|
</div> |
|
<div class="flex items-center gap-2"> |
|
<span>📄</span> |
|
<span>Content - contains article text</span> |
|
</div> |
|
</div> |
|
</div> |
|
{:else} |
|
<div class="text-center text-gray-500 dark:text-gray-400 py-8"> |
|
<div>Add content to see event structure</div> |
|
<!-- Debug info --> |
|
<div class="text-xs mt-2"> |
|
Debug: {JSON.stringify( |
|
{ |
|
hasResult: !!publicationResult, |
|
hasMetadata: !!publicationResult?.metadata, |
|
hasStructure: |
|
!!publicationResult?.metadata?.eventStructure, |
|
structureLength: |
|
publicationResult?.metadata?.eventStructure?.length || |
|
0, |
|
hasEvents: !!generatedEvents, |
|
contentLength: |
|
generatedEvents?.contentEvents?.length || 0, |
|
}, |
|
null, |
|
2, |
|
)} |
|
</div> |
|
</div> |
|
{/if} |
|
</div> |
|
</div> |
|
</div> |
|
{/if} |
|
</div> |
|
</div>
|
|
|