clone of repo on github
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.
 
 
 
 

369 lines
13 KiB

<script lang="ts">
import { Textarea, Button } from "flowbite-svelte";
import { EyeOutline } from "flowbite-svelte-icons";
import {
extractSmartMetadata,
parseAsciiDocWithMetadata,
parseAsciiDocIterative,
generateNostrEvents,
detectContentType,
type AsciiDocMetadata,
metadataToTags,
parseSimpleAttributes,
} from "$lib/utils/asciidoc_metadata";
import asciidoctor from "asciidoctor";
// Component props
let {
content = "",
placeholder = `// ITERATIVE PARSING - Choose your publishing level:
// 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
= Understanding Knowledge
:image: https://i.nostr.build/IUs0xNyUEf5hXTFL.jpg
:published: 2025-04-21
:tags: knowledge, philosophy, education
:type: text
== Preface
:tags: introduction, preface
This essay outlines the purpose of Alexandria...
== Introduction: Knowledge as a Living Ecosystem
:tags: introduction, ecosystem
Knowledge exists as dynamic representations...
=== Why Investigate the Nature of Knowledge?
:difficulty: intermediate
Understanding the nature of knowledge itself...
==== The Four Perspectives
:complexity: high
1. Material Cause: The building blocks...
`,
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;
}>();
// 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;
}
});
// 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);
});
// Parse sections for preview display
let parsedSections = $derived.by(() => {
if (!parsedContent) return [];
return parsedContent.sections.map((section: { metadata: AsciiDocMetadata; content: string; title: string }) => {
// Use simple parsing directly on section content for accurate tag extraction
const tags = parseSimpleAttributes(section.content);
return {
title: section.title || "Untitled",
content: section.content.trim(),
tags,
};
});
});
// Publishing handlers
function handlePublish() {
if (!generatedEvents) return;
if (contentType === 'article' && generatedEvents.indexEvent) {
// Full article: publish both index event (30040) and content events (30041)
onPublishArticle(generatedEvents);
} else if (contentType === 'scattered-notes') {
// Only notes: publish just the content events (30041)
const notesOnly = {
contentEvents: generatedEvents.contentEvents
};
onPublishScatteredNotes(notesOnly);
}
}
// Toggle preview panel
function togglePreview() {
const newShowPreview = !showPreview;
onPreviewToggle(newShowPreview);
}
// Handle content changes
function handleContentChange(event: Event) {
const target = event.target as HTMLTextAreaElement;
onContentChange(target.value);
}
</script>
<div class="flex flex-col space-y-4">
<!-- Smart Publishing Interface -->
<div class="bg-gradient-to-r from-blue-50 to-green-50 dark:from-blue-900/20 dark:to-green-900/20 border border-blue-200 dark:border-blue-800 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 items-center space-x-4 mb-3">
<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"
>
<option value={2}>Level 2 (== sections events)</option>
<option value={3}>Level 3 (== indices, === events)</option>
<option value={4}>Level 4 (=== indices, ==== events)</option>
</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>
</div>
</div>
<div class="flex items-center justify-between">
<Button
color="light"
size="sm"
on:click={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>
<!-- Smart Publishing Button -->
{#if generatedEvents && contentType !== 'none'}
<Button
color={contentType === 'article' ? 'blue' : 'green'}
size="sm"
on:click={handlePublish}
class="flex items-center space-x-1"
>
{#if contentType === 'article'}
<span>📚 Publish Article</span>
<span class="text-xs opacity-75">({generatedEvents.contentEvents.length + 1} events)</span>
{:else}
<span>📝 Publish Notes</span>
<span class="text-xs opacity-75">({generatedEvents.contentEvents.length} events)</span>
{/if}
</Button>
{:else}
<div class="text-xs text-gray-500 dark:text-gray-400 italic">
Add content to enable publishing
</div>
{/if}
</div>
<div class="flex space-x-4 {showPreview ? 'h-96' : ''}">
<!-- Editor Panel -->
<div class="{showPreview ? 'w-1/2' : 'w-full'} flex flex-col space-y-4">
<div class="flex-1">
<Textarea
bind:value={content}
on:input={handleContentChange}
{placeholder}
class="h-full min-h-64 resize-none"
rows={12}
/>
</div>
</div>
<!-- Preview Panel -->
{#if showPreview}
<div class="w-1/2 border-l border-gray-200 dark:border-gray-700 pl-4">
<div class="sticky top-4">
<h3
class="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100"
>
AsciiDoc Preview
</h3>
<div
class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 max-h-80 overflow-y-auto"
>
{#if !content.trim()}
<div class="text-gray-500 dark:text-gray-400 text-sm">
Start typing to see the preview...
</div>
{:else}
<div class="prose prose-sm dark:prose-invert max-w-none">
<!-- Show document title and tags for articles -->
{#if contentType === 'article' && parsedContent?.title}
<div class="mb-6 border-b border-gray-200 dark:border-gray-700 pb-4">
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100 mb-3">
{parsedContent.title}
</h1>
<!-- Document-level tags -->
{#if parsedContent.content}
{@const documentTags = parseSimpleAttributes(parsedContent.content)}
{#if documentTags.filter(tag => tag[0] === 't').length > 0}
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3">
<div class="flex flex-wrap gap-2 items-center">
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Document tags:</span>
<!-- Show only hashtags (t-tags) -->
{#each documentTags.filter(tag => tag[0] === 't') as tag}
<div class="bg-blue-600 text-blue-100 px-2 py-1 rounded-full text-xs font-medium flex items-baseline">
<span class="mr-1">#</span>
<span>{tag[1]}</span>
</div>
{/each}
</div>
</div>
{/if}
{/if}
</div>
{/if}
{#each parsedSections as section, index}
<div class="mb-6">
<div
class="text-sm text-gray-800 dark:text-gray-200 asciidoc-content"
>
{@html asciidoctor().convert(
`== ${section.title}\n\n${section.content}`,
{
standalone: false,
doctype: "article",
attributes: {
showtitle: true,
sectids: true,
},
},
)}
</div>
<!-- Gray area with tag bubbles for all sections -->
<div class="my-4 relative">
<!-- Gray background area -->
<div
class="bg-gray-200 dark:bg-gray-700 rounded-lg p-3 mb-2"
>
<div class="flex flex-wrap gap-2 items-center">
{#if section.tags && section.tags.filter(tag => tag[0] === 't').length > 0}
<!-- Show only hashtags (t-tags) -->
{#each section.tags.filter(tag => tag[0] === 't') as tag}
<div
class="bg-blue-600 text-blue-100 px-2 py-1 rounded-full text-xs font-medium flex items-baseline"
>
<span class="mr-1">#</span>
<span>{tag[1]}</span>
</div>
{/each}
{:else}
<span
class="text-gray-500 dark:text-gray-400 text-xs italic"
>No hashtags</span
>
{/if}
</div>
</div>
{#if index < parsedSections.length - 1}
<!-- Event boundary line only between sections -->
<div
class="border-t-2 border-dashed border-blue-400 relative"
>
<div
class="absolute -top-2 left-1/2 transform -translate-x-1/2 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded text-xs font-medium"
>
Event Boundary
</div>
</div>
{/if}
</div>
</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>
{parsedSections.length} event{parsedSections.length !== 1
? "s"
: ""}
<br />
</div>
{/if}
</div>
</div>
</div>
{/if}
</div>
</div>