Browse Source

refactor: remove gradient, work on preview

master
limina1 7 months ago
parent
commit
0612f79a1e
  1. 180
      package-lock.json
  2. 308
      src/lib/components/ZettelEditor.svelte
  3. 139
      src/lib/utils/asciidoc_metadata.ts

180
package-lock.json generated

@ -2783,19 +2783,39 @@
} }
}, },
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"readdirp": "^4.0.1" "anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
}, },
"engines": { "engines": {
"node": ">= 14.16.0" "node": ">= 8.10.0"
}, },
"funding": { "funding": {
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
} }
}, },
"node_modules/cliui": { "node_modules/cliui": {
@ -3711,6 +3731,16 @@
} }
} }
}, },
"node_modules/eslint-plugin-svelte/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
"version": "8.4.0", "version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
@ -5953,17 +5983,27 @@
} }
}, },
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "4.1.2", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": { "engines": {
"node": ">= 14.18.0" "node": ">=8.10.0"
}
},
"node_modules/readdirp/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
}, },
"funding": { "funding": {
"type": "individual", "url": "https://github.com/sponsors/jonschlinkert"
"url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/require-directory": { "node_modules/require-directory": {
@ -6471,6 +6511,36 @@
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
} }
}, },
"node_modules/svelte-check/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/svelte-check/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/svelte-eslint-parser": { "node_modules/svelte-eslint-parser": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.3.1.tgz", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.3.1.tgz",
@ -6671,54 +6741,6 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/tailwindcss/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/tailwindcss/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tailwindcss/node_modules/postcss-load-config": { "node_modules/tailwindcss/node_modules/postcss-load-config": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
@ -6767,30 +6789,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/tailwindcss/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/tailwindcss/node_modules/yaml": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/thenify": { "node_modules/thenify": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -7376,13 +7374,15 @@
} }
}, },
"node_modules/yaml": { "node_modules/yaml": {
"version": "1.10.2", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": { "engines": {
"node": ">= 6" "node": ">= 14.6"
} }
}, },
"node_modules/yargs": { "node_modules/yargs": {

308
src/lib/components/ZettelEditor.svelte

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Textarea, Button } from "flowbite-svelte"; import { Textarea, Button } from "flowbite-svelte";
import { EyeOutline } from "flowbite-svelte-icons"; import { EyeOutline, QuestionCircleOutline } from "flowbite-svelte-icons";
import { import {
extractSmartMetadata, extractSmartMetadata,
parseAsciiDocWithMetadata, parseAsciiDocWithMetadata,
@ -11,42 +11,15 @@
metadataToTags, metadataToTags,
parseSimpleAttributes, parseSimpleAttributes,
} from "$lib/utils/asciidoc_metadata"; } from "$lib/utils/asciidoc_metadata";
import asciidoctor from "asciidoctor"; import Asciidoctor from "asciidoctor";
// Initialize Asciidoctor processor
const asciidoctor = Asciidoctor();
// Component props // Component props
let { let {
content = "", content = "",
placeholder = `// ITERATIVE PARSING - Choose your publishing level: placeholder = "Start writing your AsciiDoc content here...",
// 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, showPreview = false,
parseLevel = 2, parseLevel = 2,
onContentChange = (content: string) => {}, onContentChange = (content: string) => {},
@ -117,16 +90,20 @@ Understanding the nature of knowledge itself...
let parsedSections = $derived.by(() => { let parsedSections = $derived.by(() => {
if (!parsedContent) return []; if (!parsedContent) return [];
return parsedContent.sections.map((section: { metadata: AsciiDocMetadata; content: string; title: string }) => { return parsedContent.sections.map((section: { metadata: AsciiDocMetadata; content: string; title: string; level?: number }) => {
// Use simple parsing directly on section content for accurate tag extraction // Use simple parsing directly on section content for accurate tag extraction
const tags = parseSimpleAttributes(section.content); const tags = parseSimpleAttributes(section.content);
const level = getSectionLevel(section.content); const level = section.level || getSectionLevel(section.content);
// Determine if this is an index section (just title) or content section (full content)
const isIndex = parseLevel > 2 && level < parseLevel;
return { return {
title: section.title || "Untitled", title: section.title || "Untitled",
content: section.content.trim(), content: section.content.trim(),
tags, tags,
level, level,
isIndex,
}; };
}); });
}); });
@ -147,12 +124,20 @@ Understanding the nature of knowledge itself...
} }
} }
// Tutorial sidebar state
let showTutorial = $state(false);
// Toggle preview panel // Toggle preview panel
function togglePreview() { function togglePreview() {
const newShowPreview = !showPreview; const newShowPreview = !showPreview;
onPreviewToggle(newShowPreview); onPreviewToggle(newShowPreview);
} }
// Toggle tutorial sidebar
function toggleTutorial() {
showTutorial = !showTutorial;
}
// Handle content changes // Handle content changes
function handleContentChange(event: Event) { function handleContentChange(event: Event) {
const target = event.target as HTMLTextAreaElement; const target = event.target as HTMLTextAreaElement;
@ -162,7 +147,7 @@ Understanding the nature of knowledge itself...
<div class="flex flex-col space-y-4"> <div class="flex flex-col space-y-4">
<!-- Smart Publishing Interface --> <!-- 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="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 items-start justify-between">
<div class="flex-1"> <div class="flex-1">
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-200 mb-2"> <h3 class="text-sm font-medium text-gray-800 dark:text-gray-200 mb-2">
@ -206,6 +191,7 @@ Understanding the nature of knowledge itself...
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<Button <Button
color="light" color="light"
size="sm" size="sm"
@ -221,24 +207,28 @@ Understanding the nature of knowledge itself...
{/if} {/if}
</Button> </Button>
<!-- Smart Publishing Button --> <Button
color="light"
size="sm"
on:click={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'} {#if generatedEvents && contentType !== 'none'}
<Button <Button
color={contentType === 'article' ? 'blue' : 'green'} color="primary"
size="sm" size="sm"
on:click={handlePublish} on:click={handlePublish}
class="flex items-center space-x-1"
> >
{#if contentType === 'article'} Publish
<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> </Button>
{:else} {:else}
<div class="text-xs text-gray-500 dark:text-gray-400 italic"> <div class="text-xs text-gray-500 dark:text-gray-400">
Add content to enable publishing Add content to enable publishing
</div> </div>
{/if} {/if}
@ -246,7 +236,7 @@ Understanding the nature of knowledge itself...
<div class="flex space-x-6 h-96"> <div class="flex space-x-6 h-96">
<!-- Editor Panel --> <!-- Editor Panel -->
<div class="{showPreview ? 'w-1/2' : 'w-full'} flex flex-col"> <div class="{showPreview && showTutorial ? 'w-1/3' : showPreview || showTutorial ? 'w-1/2' : 'w-full'} flex flex-col">
<div class="flex-1 relative border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-900"> <div class="flex-1 relative border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-900">
<Textarea <Textarea
bind:value={content} bind:value={content}
@ -295,89 +285,203 @@ Understanding the nature of knowledge itself...
</div> </div>
{/if} {/if}
{#snippet previewContent()} <div class="prose prose-sm dark:prose-invert max-w-none">
{@const levelColors = { <!-- Render full document with title if it's an article -->
2: 'bg-red-400', {#if contentType === 'article' && parsedContent?.title}
3: 'bg-blue-400', {@const documentHeader = content.split(/\n==\s+/)[0]}
4: 'bg-green-400', <div class="mb-6 border-b border-gray-200 dark:border-gray-700 pb-4">
5: 'bg-yellow-400', <div class="asciidoc-content">
6: 'bg-purple-400' {@html asciidoctor.convert(documentHeader, {
} as Record<number, string>} standalone: false,
attributes: {
<!-- Calculate continuous indent guides that span multiple sections --> showtitle: true,
{@const maxLevel = Math.max(...parsedSections.map(s => s.level))} sectids: false,
{@const guideLevels = Array.from({length: maxLevel - 1}, (_, i) => i + 2)} }
})}
{@const minLevel = Math.min(...parsedSections.map(s => s.level))} </div>
{@const maxIndentLevel = Math.max(...parsedSections.map(s => Math.max(0, s.level - minLevel)))} <!-- Document-level tags -->
{@const containerPadding = 24} {#if parsedContent.content}
{@const documentTags = parseSimpleAttributes(parsedContent.content)}
<div class="prose prose-sm dark:prose-invert max-w-none relative" style="padding-left: {containerPadding}px;"> {#if documentTags.filter(tag => tag[0] === 't').length > 0}
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 mt-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} {#each parsedSections as section, index}
{#snippet sectionContent()}
{@const indentLevel = Math.max(0, section.level - 2)} {@const indentLevel = Math.max(0, section.level - 2)}
{@const currentColor = levelColors[section.level] || 'bg-gray-500'} {@const levelColors = {
2: 'bg-yellow-400',
3: 'bg-yellow-500',
4: 'bg-yellow-600',
5: 'bg-gray-400',
6: 'bg-gray-500'
} as Record<number, string>}
{@const currentColor = levelColors[section.level] || 'bg-gray-600'}
<div class="mb-12 relative" style="margin-left: {indentLevel * 24 - containerPadding}px; padding-left: 12px;"> <div class="mb-6 relative" style="margin-left: {indentLevel * 24}px; padding-left: 12px;">
<!-- Current level highlight guide --> <!-- Vertical indent guide -->
<div <div
class="absolute top-0 w-1.5 {currentColor} opacity-60" class="absolute top-0 w-1 {currentColor} opacity-60"
style="left: {-4}px; height: 100%;" style="left: 0; height: 100%;"
></div> ></div>
<!-- Section content --> <div
<div class="prose-content"> class="text-sm text-gray-800 dark:text-gray-200 asciidoc-content"
{@html asciidoctor().convert( >
`${'='.repeat(section.level)} ${section.title}\n\n${section.content}`, {#if section.isIndex}
{ <!-- Index section: just show the title as a header -->
<div class="font-semibold text-gray-900 dark:text-gray-100 py-2">
{section.title}
</div>
{:else}
<!-- Content section: render full content -->
<div class="prose prose-sm dark:prose-invert">
{@html asciidoctor.convert(section.content, {
standalone: false, standalone: false,
doctype: "article",
attributes: { attributes: {
showtitle: true, showtitle: true,
sectids: true, sectanchors: true,
}, sectids: true
}, }
)} })}
</div>
{/if}
</div> </div>
<!-- Tags --> <!-- Gray area with tag bubbles only for content sections -->
{#if !section.isIndex}
<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} {#if section.tags && section.tags.filter(tag => tag[0] === 't').length > 0}
<div class="mt-4 pt-3 border-t border-gray-100 dark:border-gray-800"> <!-- Show only hashtags (t-tags) -->
<div class="flex flex-wrap gap-2">
{#each section.tags.filter(tag => tag[0] === 't') as tag} {#each section.tags.filter(tag => tag[0] === 't') as tag}
<span class="bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 px-2 py-1 rounded text-xs"> <div
#{tag[1]} class="bg-blue-600 text-blue-100 px-2 py-1 rounded-full text-xs font-medium flex items-baseline"
</span> >
<span class="mr-1">#</span>
<span>{tag[1]}</span>
</div>
{/each} {/each}
{:else}
<span
class="text-gray-500 dark:text-gray-400 text-xs italic"
>No hashtags</span
>
{/if}
</div> </div>
</div> </div>
{/if}
<!-- Event boundary indicator --> {#if index < parsedSections.length - 1 && !parsedSections[index + 1].isIndex}
{#if index < parsedSections.length - 1} <!-- Event boundary line only between content sections -->
<div class="mt-8 pt-4 border-t border-dashed border-gray-300 dark:border-gray-600 relative"> <div
<div class="absolute -top-2.5 left-1/2 transform -translate-x-1/2 bg-white dark:bg-gray-900 px-2"> class="border-t-2 border-dashed border-blue-400 relative"
<span class="text-xs text-gray-500 dark:text-gray-400 font-medium">Event Boundary</span> >
<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>
</div> </div>
{/if} {/if}
</div> </div>
{/snippet} {/if}
{@render sectionContent()} </div>
{/each} {/each}
</div> </div>
<!-- Event count summary --> <div
<div class="mt-8 pt-4 border-t border-gray-200 dark:border-gray-700"> class="mt-4 text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-900 p-2 rounded border"
<div class="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 px-3 py-2 rounded"> >
<strong>Total Events:</strong> {parsedSections.length + (contentType === 'article' ? 1 : 0)} <strong>Event Count:</strong>
{parsedSections.length + (contentType === 'article' ? 1 : 0)} event{(parsedSections.length + (contentType === 'article' ? 1 : 0)) !== 1
? "s"
: ""}
({contentType === 'article' ? '1 index + ' : ''}{parsedSections.length} content) ({contentType === 'article' ? '1 index + ' : ''}{parsedSections.length} content)
</div> </div>
{/if}
</div>
</div>
</div> </div>
{/snippet}
{@render previewContent()}
{/if} {/if}
<!-- Tutorial Sidebar -->
{#if showTutorial}
<div class="{showPreview ? 'w-1/3' : 'w-1/2'} 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">
<div>
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">Publishing Levels</h4>
<ul class="space-y-1 text-xs">
<li><strong>Level 2:</strong> Only == sections become events (containing === and deeper)</li>
<li><strong>Level 3:</strong> == sections become indices, === sections become events</li>
<li><strong>Level 4:</strong> === sections become indices, ==== sections become events</li>
</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> </div>
</div> </div>

139
src/lib/utils/asciidoc_metadata.ts

@ -75,6 +75,30 @@ function createProcessor() {
return Processor(); return Processor();
} }
/**
* Decodes HTML entities in a string
*/
function decodeHtmlEntities(text: string): string {
const entities: Record<string, string> = {
'&#8217;': "'",
'&#8216;': "'",
'&#8220;': '"',
'&#8221;': '"',
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'",
'&apos;': "'",
};
let result = text;
for (const [entity, char] of Object.entries(entities)) {
result = result.replace(new RegExp(entity, 'g'), char);
}
return result;
}
/** /**
* Extracts tags from attributes, combining tags and keywords * Extracts tags from attributes, combining tags and keywords
*/ */
@ -305,7 +329,7 @@ export function extractDocumentMetadata(inputContent: string): {
// Extract basic metadata // Extract basic metadata
const title = document.getTitle(); const title = document.getTitle();
if (title) metadata.title = title; if (title) metadata.title = decodeHtmlEntities(title);
// Handle multiple authors - combine header line and attributes // Handle multiple authors - combine header line and attributes
const authors = extractAuthorsFromHeader(document.getSource()); const authors = extractAuthorsFromHeader(document.getSource());
@ -367,32 +391,24 @@ export function extractSectionMetadata(inputSectionContent: string): {
content: string; content: string;
title: string; title: string;
} { } {
const asciidoctor = createProcessor(); // Extract title directly from the content using regex for more control
const document = asciidoctor.load(`= Temp\n\n${inputSectionContent}`, { standalone: false }) as Document; const titleMatch = inputSectionContent.match(/^(=+)\s+(.+)$/m);
const sections = document.getSections(); let title = '';
if (titleMatch) {
if (sections.length === 0) { title = titleMatch[2].trim();
return { metadata: {}, content: inputSectionContent, title: '' };
} }
const section = sections[0];
const title = section.getTitle() || '';
const metadata: SectionMetadata = { title }; const metadata: SectionMetadata = { title };
// Parse attributes from the section content (no longer used - we use simple parsing in generateNostrEvents)
const attributes = {};
// Extract authors from section content // Extract authors from section content
const authors = extractAuthorsFromHeader(inputSectionContent, true); const authors = extractAuthorsFromHeader(inputSectionContent, true);
if (authors.length > 0) { if (authors.length > 0) {
metadata.authors = authors; metadata.authors = authors;
} }
// Map attributes to metadata (sections can have authors) // Extract tags using parseSimpleAttributes (which is what's used in generateNostrEvents)
mapAttributesToMetadata(attributes, metadata, false); const simpleAttrs = parseSimpleAttributes(inputSectionContent);
const tags = simpleAttrs.filter(attr => attr[0] === 't').map(attr => attr[1]);
// Handle tags and keywords
const tags = extractTagsFromAttributes(attributes);
if (tags.length > 0) { if (tags.length > 0) {
metadata.tags = tags; metadata.tags = tags;
} }
@ -590,7 +606,12 @@ export function parseAsciiDocIterative(content: string, parseLevel: number = 2):
// Save previous section if exists // Save previous section if exists
if (currentSection) { if (currentSection) {
const sectionContent = currentSectionContent.join('\n'); const sectionContent = currentSectionContent.join('\n');
sections.push(extractSectionMetadata(sectionContent)); const sectionMeta = extractSectionMetadata(sectionContent);
// For level 2, preserve the full content including the header
sections.push({
...sectionMeta,
content: sectionContent // Use full content, not stripped
});
} }
// Start new section // Start new section
@ -606,7 +627,12 @@ export function parseAsciiDocIterative(content: string, parseLevel: number = 2):
// Save the last section // Save the last section
if (currentSection) { if (currentSection) {
const sectionContent = currentSectionContent.join('\n'); const sectionContent = currentSectionContent.join('\n');
sections.push(extractSectionMetadata(sectionContent)); const sectionMeta = extractSectionMetadata(sectionContent);
// For level 2, preserve the full content including the header
sections.push({
...sectionMeta,
content: sectionContent // Use full content, not stripped
});
} }
const docContent = documentContent.join('\n'); const docContent = documentContent.join('\n');
@ -618,30 +644,36 @@ export function parseAsciiDocIterative(content: string, parseLevel: number = 2):
}; };
} }
// Level 3+: Parse both index level (parseLevel-1) and content level (parseLevel) // Level 3+: Parse hierarchically
const indexLevelPattern = new RegExp(`^${'='.repeat(parseLevel - 1)}\\s+`); // All levels from 2 to parseLevel-1 are indices (title only)
const contentLevelPattern = new RegExp(`^${'='.repeat(parseLevel)}\\s+`); // Level parseLevel are content sections (full content)
// First, collect all sections at the content level (parseLevel)
const contentLevelPattern = new RegExp(`^${'='.repeat(parseLevel)}\\s+`);
let currentSection: string | null = null; let currentSection: string | null = null;
let currentSectionContent: string[] = []; let currentSectionContent: string[] = [];
let documentContent: string[] = []; let documentContent: string[] = [];
let inDocumentHeader = true; let inDocumentHeader = true;
for (const line of lines) { for (const line of lines) {
// Check for both index level and content level headers if (line.match(contentLevelPattern)) {
if (line.match(indexLevelPattern) || line.match(contentLevelPattern)) {
inDocumentHeader = false; inDocumentHeader = false;
// Save previous section if exists // Save previous section if exists
if (currentSection) { if (currentSection) {
const sectionContent = currentSectionContent.join('\n'); const sectionContent = currentSectionContent.join('\n');
sections.push(extractSectionMetadata(sectionContent)); const sectionMeta = extractSectionMetadata(sectionContent);
sections.push({
...sectionMeta,
content: sectionContent // Full content including headers
});
} }
// Start new section // Start new content section
currentSection = line; currentSection = line;
currentSectionContent = [line]; currentSectionContent = [line];
} else if (currentSection) { } else if (currentSection) {
// Continue collecting content for current section
currentSectionContent.push(line); currentSectionContent.push(line);
} else if (inDocumentHeader) { } else if (inDocumentHeader) {
documentContent.push(line); documentContent.push(line);
@ -651,15 +683,60 @@ export function parseAsciiDocIterative(content: string, parseLevel: number = 2):
// Save the last section // Save the last section
if (currentSection) { if (currentSection) {
const sectionContent = currentSectionContent.join('\n'); const sectionContent = currentSectionContent.join('\n');
sections.push(extractSectionMetadata(sectionContent)); const sectionMeta = extractSectionMetadata(sectionContent);
sections.push({
...sectionMeta,
content: sectionContent // Full content including headers
});
} }
// Now collect index sections (all levels from 2 to parseLevel-1)
// These should be shown as navigation/structure but not full content
const indexSections: Array<{
metadata: SectionMetadata;
content: string;
title: string;
level: number;
}> = [];
for (let level = 2; level < parseLevel; level++) {
const levelPattern = new RegExp(`^${'='.repeat(level)}\\s+(.+)$`, 'gm');
const matches = content.matchAll(levelPattern);
for (const match of matches) {
const title = match[1].trim();
indexSections.push({
metadata: { title },
content: `${'='.repeat(level)} ${title}`, // Just the header line for index sections
title,
level
});
}
}
// Add actual level to content sections based on their content
const contentSectionsWithLevel = sections.map(s => ({
...s,
level: getSectionLevel(s.content)
}));
// Combine index sections and content sections
// Sort by position in original content to maintain order
const allSections = [...indexSections, ...contentSectionsWithLevel];
// Sort sections by their appearance in the original content
allSections.sort((a, b) => {
const posA = content.indexOf(a.content.split('\n')[0]);
const posB = content.indexOf(b.content.split('\n')[0]);
return posA - posB;
});
const docContent = documentContent.join('\n'); const docContent = documentContent.join('\n');
return { return {
metadata: docMetadata, metadata: docMetadata,
content: docContent, content: docContent,
title: docMetadata.title || '', title: docMetadata.title || '',
sections: sections sections: allSections
}; };
} }
@ -721,9 +798,9 @@ export function generateNostrEvents(parsed: ParsedAsciiDoc, parseLevel: number =
const generateSectionId = (title: string): string => { const generateSectionId = (title: string): string => {
return title return title
.toLowerCase() .toLowerCase()
.replace(/[^a-z0-9\s]/g, '') .replace(/[^\p{L}\p{N}]/gu, "-")
.replace(/\s+/g, '-') .replace(/-+/g, "-")
.trim(); .replace(/^-|-$/g, "");
}; };
// Build hierarchical tree structure // Build hierarchical tree structure

Loading…
Cancel
Save