From c369a4620ccce87865ffd4d1c9d7f1d4420d3baa Mon Sep 17 00:00:00 2001 From: limina1 Date: Wed, 27 Aug 2025 12:01:23 -0400 Subject: [PATCH] Add syntax highlighting for event parsing --- src/lib/components/ZettelEditor.svelte | 176 +++++++++++++++++++++++-- 1 file changed, 167 insertions(+), 9 deletions(-) diff --git a/src/lib/components/ZettelEditor.svelte b/src/lib/components/ZettelEditor.svelte index c25beac..f992373 100644 --- a/src/lib/components/ZettelEditor.svelte +++ b/src/lib/components/ZettelEditor.svelte @@ -2,9 +2,11 @@ import { Button } from "flowbite-svelte"; import { EyeOutline, QuestionCircleOutline } from "flowbite-svelte-icons"; import { EditorView, basicSetup } from "codemirror"; - import { EditorState } from "@codemirror/state"; + import { EditorState, StateField, StateEffect } from "@codemirror/state"; import { markdown } from "@codemirror/lang-markdown"; import { oneDark } from "@codemirror/theme-one-dark"; + import { ViewPlugin, Decoration, type DecorationSet } from "@codemirror/view"; + import { RangeSet } from "@codemirror/state"; import { onMount } from "svelte"; import { extractSmartMetadata, @@ -58,6 +60,15 @@ import Asciidoctor from "asciidoctor"; updateEditorContent(); }); + // Effect to update syntax highlighting when parsedSections change + $effect(() => { + if (editorView && parsedSections) { + editorView.dispatch({ + effects: updateHighlighting.of(parsedSections) + }); + } + }); + // Effect to create PublicationTree when content changes // Uses tree processor extension as Michael envisioned: // "register a tree processor extension in our Asciidoctor instance" @@ -244,19 +255,142 @@ import Asciidoctor from "asciidoctor"; let editorContainer = $state(null); let editorView = $state(null); + // Create update effect for highlighting + const updateHighlighting = StateEffect.define(); + + // State field to track header decorations + const headerDecorations = StateField.define({ + 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'); + + // Create a map of header text to section info for fast lookup + const sectionMap = new Map(); + if (sections) { + sections.forEach(section => { + if (section.title) { + sectionMap.set(section.title.toLowerCase().trim(), { + level: section.level, + isEventTitle: true, + eventType: section.eventType, + eventKind: section.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; + + if (sectionInfo && sectionInfo.isEventTitle) { + // Event title - different colors based on event type + if (sectionInfo.eventKind === 30040) { + className = 'cm-header-index-event'; + } else if (sectionInfo.eventKind === 30041) { + className = 'cm-header-content-event'; + } else { + className = 'cm-header-event-title'; + } + } else { + // Regular subheader within content + if (level <= parseLevel) { + className = 'cm-header-potential-event'; + } else { + className = 'cm-header-subcontent'; + } + } + + 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))); + } + // Initialize CodeMirror editor function createEditor() { if (!editorContainer) return; - // Create custom theme with header highlighting based on parse level + // Create custom theme with header highlighting classes const headerHighlighting = EditorView.theme({ - '.cm-asciidoc-header-1': { color: '#6B7280' }, // gray-500 - '.cm-asciidoc-header-2-index': { color: '#3B82F6', fontWeight: '600' }, // blue-500 for index - '.cm-asciidoc-header-2-content': { color: '#22C55E', fontWeight: '600' }, // green-500 for content - '.cm-asciidoc-header-3-index': { color: '#3B82F6', fontWeight: '600' }, - '.cm-asciidoc-header-3-content': { color: '#22C55E', fontWeight: '600' }, - '.cm-asciidoc-header-4': { color: '#6B7280', fontWeight: '600' }, - '.cm-asciidoc-header-5': { color: '#6B7280', fontWeight: '600' }, + // 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({ @@ -264,6 +398,7 @@ import Asciidoctor from "asciidoctor"; extensions: [ basicSetup, markdown(), // AsciiDoc is similar to markdown syntax + headerDecorations, headerHighlighting, EditorView.updateListener.of((update) => { if (update.docChanged) { @@ -606,6 +741,29 @@ import Asciidoctor from "asciidoctor";
+ +
+

Header Highlighting

+
+
+
+ Blue: Index Events (30040) +
+
+
+ Green: Content Events (30041) +
+
+
+ Amber: Potential Events (at parse level) +
+
+
+ Gray: Subheaders (within content) +
+
+
+

Publishing Levels