diff --git a/src/lib/components/ZettelEditor.svelte b/src/lib/components/ZettelEditor.svelte index fe9b123..650b175 100644 --- a/src/lib/components/ZettelEditor.svelte +++ b/src/lib/components/ZettelEditor.svelte @@ -46,6 +46,18 @@ import Asciidoctor from "asciidoctor"; let publicationResult = $state(null); let generatedEvents = $state(null); let contentType = $state<'article' | 'scattered-notes' | 'none'>('none'); + + // Gutter visualization state for Phase 2 + let gutterIndicators = $state>([]); + let hoveredLineNumber = $state(null); + let textareaRef = $state(null); // Flowbite Textarea component ref + let gutterElement = $state(null); // Effect to create PublicationTree when content changes // Uses tree processor extension as Michael envisioned: @@ -57,6 +69,7 @@ import Asciidoctor from "asciidoctor"; publicationResult = null; generatedEvents = null; contentType = 'none'; + gutterIndicators = []; return; } @@ -79,12 +92,47 @@ import Asciidoctor from "asciidoctor"; contentEvents: events.contentEvents.length, parseLevel: parseLevel }); + + // Temporary: Create sample gutter indicators for testing + // This will be replaced with proper line detection in Checkpoint 2.2 + if (publicationResult?.metadata?.eventStructure) { + const tempIndicators: typeof gutterIndicators = []; + const lines = content.split('\n'); + + // Simple detection of section headers for testing + lines.forEach((line: string, index: number) => { + const match = line.match(/^(=+)\s+(.+)/); + if (match) { + const level = match[1].length; + const title = match[2].trim(); + + // Find matching event structure node + const node = publicationResult.metadata.eventStructure.find((n: any) => + n.title === title || n.title.includes(title) + ); + + if (node) { + tempIndicators.push({ + lineNumber: index + 1, + eventKind: node.eventKind as 30040 | 30041, + eventType: node.eventType as 'index' | 'content', + level: level, + title: title + }); + } + } + }); + + gutterIndicators = tempIndicators; + console.log("Gutter indicators:", gutterIndicators); + } }) .catch(error => { console.error("Tree factory error:", error); publicationResult = null; generatedEvents = null; contentType = 'none'; + gutterIndicators = []; }); }); @@ -140,14 +188,55 @@ import Asciidoctor from "asciidoctor"; }); }; - // Use eventStructure for accurate hierarchy display - return publicationResult.metadata.eventStructure.map((node: any) => { - const event = findEventByDTag(publicationResult.contentEvents, node.dTag); + // 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 (node.dTag === publicationResult.indexEvent?.tagValue('d')) { + event = publicationResult.indexEvent; + } else { + // contentEvents can contain both 30040 and 30041 events at parse level 3+ + event = findEventByDTag(publicationResult.contentEvents, node.dTag); + } + 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 first heading line (which should match the title) + const lines = processedContent.split('\n'); + const firstHeadingIndex = lines.findIndex((line: string) => line.match(/^=+\s+/)); + if (firstHeadingIndex !== -1) { + // Remove the heading line and join back + lines.splice(firstHeadingIndex, 1); + processedContent = lines.join('\n').trim(); + } + } + return { - title: node.title, - content: event?.content || '', + title: eventTitle, + content: processedContent, tags, // Already in [['t', 'tag1'], ['t', 'tag2']] format level: node.level, isIndex: node.eventKind === 30040, @@ -192,6 +281,21 @@ import Asciidoctor from "asciidoctor"; const target = event.target as HTMLTextAreaElement; onContentChange(target.value); } + + // Synchronize gutter scroll with textarea scroll + function handleTextareaScroll(event: Event) { + if (!gutterElement) return; + const target = event.target as HTMLTextAreaElement; + gutterElement.scrollTop = target.scrollTop; + } + + // Calculate top position for a line number in the gutter + function calculateLineTop(lineNumber: number): number { + // Approximate line height based on textarea's line-height + // We use 1.5rem (24px) as specified in the textarea class + const lineHeight = 24; // 1.5rem with text-sm + return (lineNumber - 1) * lineHeight; + }
@@ -287,11 +391,46 @@ import Asciidoctor from "asciidoctor";
+ +
+
+ +
+ + + {#each gutterIndicators as indicator} +
+ +
+
+ {/each} + + + {#if hoveredLineNumber} +
+ {/if} +
+
+ +