Browse Source

Fix TypeScript errors and improve ZettelEditor preview

- Fix {@const} placement errors by using Svelte 5 snippets
- Add proper TypeScript types to levelColors objects
- Rename and fix test file from .js to .ts with proper typing
- Remove indent guides from editor text area for cleaner writing
- Improve preview layout with proper indentation and spacing
- Add continuous vertical guides in preview that don't overlap text

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
master
limina1 7 months ago
parent
commit
f5685b2f3a
  1. 217
      src/lib/components/ZettelEditor.svelte
  2. 560
      tests/zettel-publisher-tdd.test.ts

217
src/lib/components/ZettelEditor.svelte

@ -101,6 +101,18 @@ Understanding the nature of knowledge itself... @@ -101,6 +101,18 @@ Understanding the nature of knowledge itself...
return detectContentType(content);
});
// 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
}
// Parse sections for preview display
let parsedSections = $derived.by(() => {
if (!parsedContent) return [];
@ -108,11 +120,13 @@ Understanding the nature of knowledge itself... @@ -108,11 +120,13 @@ Understanding the nature of knowledge itself...
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);
const level = getSectionLevel(section.content);
return {
title: section.title || "Untitled",
content: section.content.trim(),
tags,
level,
};
});
});
@ -230,136 +244,139 @@ Understanding the nature of knowledge itself... @@ -230,136 +244,139 @@ Understanding the nature of knowledge itself...
{/if}
</div>
<div class="flex space-x-4 {showPreview ? 'h-96' : ''}">
<div class="flex space-x-6 h-96">
<!-- Editor Panel -->
<div class="{showPreview ? 'w-1/2' : 'w-full'} flex flex-col space-y-4">
<div class="flex-1">
<div class="{showPreview ? '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">
<Textarea
bind:value={content}
on:input={handleContentChange}
{placeholder}
class="h-full min-h-64 resize-none"
rows={12}
class="w-full h-full resize-none font-mono text-sm leading-relaxed p-4 bg-white dark:bg-gray-900 border-none outline-none"
/>
</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"
>
<div class="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 Preview
</h3>
</div>
<div class="flex-1 overflow-y-auto p-6 bg-white dark:bg-gray-900">
{#if !content.trim()}
<div class="text-gray-500 dark:text-gray-400 text-sm">
<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">
<!-- 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}
<!-- Show document title and tags for articles -->
{#if contentType === 'article' && parsedContent?.title}
<div class="mb-8 pb-6 border-b border-gray-200 dark:border-gray-700">
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-4">
{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="flex flex-wrap gap-2">
{#each documentTags.filter(tag => tag[0] === 't') as tag}
<span class="bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 px-3 py-1 rounded-full text-sm font-medium">
#{tag[1]}
</span>
{/each}
</div>
{/if}
</div>
{/if}
{/if}
</div>
{/if}
{#snippet previewContent()}
{@const levelColors = {
2: 'bg-red-400',
3: 'bg-blue-400',
4: 'bg-green-400',
5: 'bg-yellow-400',
6: 'bg-purple-400'
} as Record<number, string>}
<!-- Calculate continuous indent guides that span multiple sections -->
{@const maxLevel = Math.max(...parsedSections.map(s => s.level))}
{@const guideLevels = Array.from({length: maxLevel - 1}, (_, i) => i + 2)}
{@const minLevel = Math.min(...parsedSections.map(s => s.level))}
{@const maxIndentLevel = Math.max(...parsedSections.map(s => Math.max(0, s.level - minLevel)))}
{@const containerPadding = 24}
<div class="prose prose-sm dark:prose-invert max-w-none relative" style="padding-left: {containerPadding}px;">
{#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>
{#snippet sectionContent()}
{@const indentLevel = Math.max(0, section.level - 2)}
{@const currentColor = levelColors[section.level] || 'bg-gray-500'}
<!-- Gray area with tag bubbles for all sections -->
<div class="my-4 relative">
<!-- Gray background area -->
<div class="mb-12 relative" style="margin-left: {indentLevel * 24 - containerPadding}px; padding-left: 12px;">
<!-- Current level highlight guide -->
<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) -->
class="absolute top-0 w-1.5 {currentColor} opacity-60"
style="left: {-4}px; height: 100%;"
></div>
<!-- Section content -->
<div class="prose-content">
{@html asciidoctor().convert(
`${'='.repeat(section.level)} ${section.title}\n\n${section.content}`,
{
standalone: false,
doctype: "article",
attributes: {
showtitle: true,
sectids: true,
},
},
)}
</div>
<!-- Tags -->
{#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">
<div class="flex flex-wrap gap-2">
{#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>
<span class="bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 px-2 py-1 rounded text-xs">
#{tag[1]}
</span>
{/each}
{:else}
<span
class="text-gray-500 dark:text-gray-400 text-xs italic"
>No hashtags</span
>
{/if}
</div>
</div>
</div>
{/if}
<!-- Event boundary indicator -->
{#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 class="mt-8 pt-4 border-t border-dashed border-gray-300 dark:border-gray-600 relative">
<div class="absolute -top-2.5 left-1/2 transform -translate-x-1/2 bg-white dark:bg-gray-900 px-2">
<span class="text-xs text-gray-500 dark:text-gray-400 font-medium">Event Boundary</span>
</div>
</div>
{/if}
</div>
{/snippet}
{@render sectionContent()}
{/each}
</div>
<!-- Event count summary -->
<div class="mt-8 pt-4 border-t border-gray-200 dark:border-gray-700">
<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)}
({contentType === 'article' ? '1 index + ' : ''}{parsedSections.length} content)
</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>
</div>
{/snippet}
{@render previewContent()}
{/if}
</div>
</div>

560
tests/zettel-publisher-tdd.test.ts

@ -0,0 +1,560 @@ @@ -0,0 +1,560 @@
#!/usr/bin/env node
/**
* Test-Driven Development for ZettelPublisher Enhancement
* Based on understanding_knowledge.adoc, desire.adoc, and docreference.md
*
* Key Requirements Discovered:
* 1. ITERATIVE parsing (not recursive): sections at target level become events
* 2. Level 2: == sections become 30041 events containing ALL subsections (===, ====, etc.)
* 3. Level 3: == sections become 30040 indices, === sections become 30041 events
* 4. 30040 metadata: from document level (= title with :attributes:)
* 5. 30041 metadata: from section level attributes
* 6. Smart publishing: articles (=) vs scattered notes (==)
* 7. Custom attributes: all :key: value pairs preserved as event tags
*/
import fs from 'fs';
import path from 'path';
// Test framework
interface TestCase {
name: string;
fn: () => void | Promise<void>;
}
class TestFramework {
private tests: TestCase[] = [];
private passed: number = 0;
private failed: number = 0;
test(name: string, fn: () => void | Promise<void>): void {
this.tests.push({ name, fn });
}
expect(actual: any) {
return {
toBe: (expected: any) => {
if (actual === expected) return true;
throw new Error(`Expected ${expected}, got ${actual}`);
},
toEqual: (expected: any) => {
if (JSON.stringify(actual) === JSON.stringify(expected)) return true;
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
},
toContain: (expected: any) => {
if (actual && actual.includes && actual.includes(expected)) return true;
throw new Error(`Expected "${actual}" to contain "${expected}"`);
},
not: {
toContain: (expected: any) => {
if (actual && actual.includes && !actual.includes(expected)) return true;
throw new Error(`Expected "${actual}" NOT to contain "${expected}"`);
}
},
toBeTruthy: () => {
if (actual) return true;
throw new Error(`Expected truthy value, got ${actual}`);
},
toHaveLength: (expected: number) => {
if (actual && actual.length === expected) return true;
throw new Error(`Expected length ${expected}, got ${actual ? actual.length : 'undefined'}`);
}
};
}
async run() {
console.log(`🧪 Running ${this.tests.length} tests...\n`);
for (const { name, fn } of this.tests) {
try {
await fn();
console.log(`${name}`);
this.passed++;
} catch (error: unknown) {
console.log(`${name}`);
const message = error instanceof Error ? error.message : String(error);
console.log(` ${message}\n`);
this.failed++;
}
}
console.log(`\n📊 Results: ${this.passed} passed, ${this.failed} failed`);
return this.failed === 0;
}
}
const test = new TestFramework();
// Load test data files
const testDataPath = path.join(process.cwd(), 'test_data', 'AsciidocFiles');
const understandingKnowledge = fs.readFileSync(path.join(testDataPath, 'understanding_knowledge.adoc'), 'utf-8');
const desire = fs.readFileSync(path.join(testDataPath, 'desire.adoc'), 'utf-8');
// =============================================================================
// PHASE 1: Core Data Structure Tests (Based on Real Test Data)
// =============================================================================
test.test('Understanding Knowledge: Document metadata should be extracted from = level', () => {
// Expected 30040 metadata from understanding_knowledge.adoc
const expectedDocMetadata = {
title: 'Understanding Knowledge',
image: 'https://i.nostr.build/IUs0xNyUEf5hXTFL.jpg',
published: '2025-04-21',
language: 'en, ISO-639-1',
tags: ['knowledge', 'philosophy', 'education'],
type: 'text'
};
// Test will pass when document parsing extracts these correctly
test.expect(expectedDocMetadata.title).toBe('Understanding Knowledge');
test.expect(expectedDocMetadata.tags).toHaveLength(3);
test.expect(expectedDocMetadata.type).toBe('text');
});
test.test('Desire: Document metadata should include all custom attributes', () => {
// Expected 30040 metadata from desire.adoc
const expectedDocMetadata = {
title: 'Desire Part 1: Mimesis',
image: 'https://i.nostr.build/hGzyi4c3YhTwoCCe.png',
published: '2025-07-02',
language: 'en, ISO-639-1',
tags: ['memetics', 'philosophy', 'desire'],
type: 'podcastArticle'
};
test.expect(expectedDocMetadata.type).toBe('podcastArticle');
test.expect(expectedDocMetadata.tags).toContain('memetics');
});
test.test('Iterative ParsedAsciiDoc interface should support level-based parsing', () => {
// Test the ITERATIVE interface structure (not recursive)
// Based on docreference.md - Level 2 parsing example
const mockLevel2Structure = {
metadata: { title: 'Programming Fundamentals Guide', tags: ['programming', 'fundamentals'] },
content: 'This is the main introduction to the programming guide.',
title: 'Programming Fundamentals Guide',
sections: [
{
metadata: { title: 'Data Structures', tags: ['arrays', 'lists', 'trees'], difficulty: 'intermediate' },
content: `Understanding fundamental data structures is crucial for effective programming.
=== Arrays and Lists
Arrays are contiguous memory blocks that store elements of the same type.
Lists provide dynamic sizing capabilities.
==== Dynamic Arrays
Dynamic arrays automatically resize when capacity is exceeded.
==== Linked Lists
Linked lists use pointers to connect elements.
=== Trees and Graphs
Tree and graph structures enable hierarchical and networked data representation.`,
title: 'Data Structures'
},
{
metadata: { title: 'Algorithms', tags: ['sorting', 'searching', 'optimization'], difficulty: 'advanced' },
content: `Algorithmic thinking forms the foundation of efficient problem-solving.
=== Sorting Algorithms
Different sorting algorithms offer various trade-offs between time and space complexity.
==== Bubble Sort
Bubble sort repeatedly steps through the list, compares adjacent elements.
==== Quick Sort
Quick sort uses divide-and-conquer approach with pivot selection.`,
title: 'Algorithms'
}
]
};
// Verify ITERATIVE structure: only level 2 sections, containing ALL subsections
test.expect(mockLevel2Structure.sections).toHaveLength(2);
test.expect(mockLevel2Structure.sections[0].title).toBe('Data Structures');
test.expect(mockLevel2Structure.sections[0].content).toContain('=== Arrays and Lists');
test.expect(mockLevel2Structure.sections[0].content).toContain('==== Dynamic Arrays');
test.expect(mockLevel2Structure.sections[1].content).toContain('==== Quick Sort');
});
// =============================================================================
// PHASE 2: Content Processing Tests (Header Separation)
// =============================================================================
test.test('Section content should NOT contain its own header', () => {
// From understanding_knowledge.adoc: "== Preface" section
const expectedPrefaceContent = `[NOTE]
This essay was written to outline and elaborate on the purpose of the Nostr client Alexandria. No formal academic citations are included as this serves primarily as a conceptual foundation, inviting readers to experience related ideas connecting and forming as more content becomes uploaded. Traces of AI edits and guidance are left, but the essay style is still my own. Over time this essay may change its wording, structure and content.
-- liminal`;
// Should NOT contain "== Preface"
test.expect(expectedPrefaceContent).not.toContain('== Preface');
test.expect(expectedPrefaceContent).toContain('[NOTE]');
});
test.test('Introduction section should separate from its subsections', () => {
// From understanding_knowledge.adoc
const expectedIntroContent = `image:https://i.nostr.build/IUs0xNyUEf5hXTFL.jpg[library]`;
// Should NOT contain subsection content or headers
test.expect(expectedIntroContent).not.toContain('=== Why Investigate');
test.expect(expectedIntroContent).not.toContain('Understanding the nature of knowledge');
test.expect(expectedIntroContent).toContain('image:https://i.nostr.build');
});
test.test('Subsection content should be cleanly separated', () => {
// "=== Why Investigate the Nature of Knowledge?" subsection
const expectedSubsectionContent = `Understanding the nature of knowledge itself is fundamental, distinct from simply studying how we learn or communicate. Knowledge exests first as representations within individuals, separate from how we interact with it...`;
// Should NOT contain its own header
test.expect(expectedSubsectionContent).not.toContain('=== Why Investigate');
test.expect(expectedSubsectionContent).toContain('Understanding the nature');
});
test.test('Deep headers (====) should have proper newlines', () => {
// From "=== The Four Perspectives" section with ==== subsections
const expectedFormatted = `
==== 1. The Building Blocks (Material Cause)
Just as living organisms are made up of cells, knowledge systems are built from fundamental units of understanding.
==== 2. The Pattern of Organization (Formal Cause)
If you've ever seen how mushrooms connect through underground networks...`;
test.expect(expectedFormatted).toContain('\n==== 1. The Building Blocks (Material Cause)\n');
test.expect(expectedFormatted).toContain('\n==== 2. The Pattern of Organization (Formal Cause)\n');
});
// =============================================================================
// PHASE 3: Publishing Logic Tests (30040/30041 Structure)
// =============================================================================
test.test('Understanding Knowledge should create proper 30040 index event', () => {
// Expected 30040 index event structure
const expectedIndexEvent = {
kind: 30040,
content: '', // Index events have empty content
tags: [
['d', 'understanding-knowledge'],
['title', 'Understanding Knowledge'],
['image', 'https://i.nostr.build/IUs0xNyUEf5hXTFL.jpg'],
['published', '2025-04-21'],
['language', 'en, ISO-639-1'],
['t', 'knowledge'],
['t', 'philosophy'],
['t', 'education'],
['type', 'text'],
// a-tags referencing sections
['a', '30041:pubkey:understanding-knowledge-preface'],
['a', '30041:pubkey:understanding-knowledge-introduction-knowledge-as-a-living-ecosystem'],
['a', '30041:pubkey:understanding-knowledge-i-material-cause-the-substance-of-knowledge'],
// ... more a-tags for each section
]
};
test.expect(expectedIndexEvent.kind).toBe(30040);
test.expect(expectedIndexEvent.content).toBe('');
test.expect(expectedIndexEvent.tags.filter(([k]) => k === 't')).toHaveLength(3);
test.expect(expectedIndexEvent.tags.find(([k, v]) => k === 'type' && v === 'text')).toBeTruthy();
});
test.test('Understanding Knowledge sections should create proper 30041 events', () => {
// Expected 30041 events for main sections
const expectedSectionEvents = [
{
kind: 30041,
content: `[NOTE]\nThis essay was written to outline and elaborate on the purpose of the Nostr client Alexandria...`,
tags: [
['d', 'understanding-knowledge-preface'],
['title', 'Preface']
]
},
{
kind: 30041,
content: `image:https://i.nostr.build/IUs0xNyUEf5hXTFL.jpg[library]`,
tags: [
['d', 'understanding-knowledge-introduction-knowledge-as-a-living-ecosystem'],
['title', 'Introduction: Knowledge as a Living Ecosystem']
]
}
];
expectedSectionEvents.forEach(event => {
test.expect(event.kind).toBe(30041);
test.expect(event.content).toBeTruthy();
test.expect(event.tags.find(([k]) => k === 'd')).toBeTruthy();
test.expect(event.tags.find(([k]) => k === 'title')).toBeTruthy();
});
});
test.test('Level-based parsing should create correct 30040/30041 structure', () => {
// Based on docreference.md examples
// Level 2 parsing: only == sections become events, containing all subsections
const expectedLevel2Events = {
mainIndex: {
kind: 30040,
content: '',
tags: [
['d', 'programming-fundamentals-guide'],
['title', 'Programming Fundamentals Guide'],
['a', '30041:author_pubkey:data-structures'],
['a', '30041:author_pubkey:algorithms']
]
},
dataStructuresSection: {
kind: 30041,
content: 'Understanding fundamental data structures...\n\n=== Arrays and Lists\n\n...==== Dynamic Arrays\n\n...==== Linked Lists\n\n...',
tags: [
['d', 'data-structures'],
['title', 'Data Structures'],
['difficulty', 'intermediate']
]
}
};
// Level 3 parsing: == sections become 30040 indices, === sections become 30041 events
const expectedLevel3Events = {
mainIndex: {
kind: 30040,
content: '',
tags: [
['d', 'programming-fundamentals-guide'],
['title', 'Programming Fundamentals Guide'],
['a', '30040:author_pubkey:data-structures'], // Now references sub-index
['a', '30040:author_pubkey:algorithms']
]
},
dataStructuresIndex: {
kind: 30040,
content: '',
tags: [
['d', 'data-structures'],
['title', 'Data Structures'],
['a', '30041:author_pubkey:data-structures-content'],
['a', '30041:author_pubkey:arrays-and-lists'],
['a', '30041:author_pubkey:trees-and-graphs']
]
},
arraysAndListsSection: {
kind: 30041,
content: 'Arrays are contiguous...\n\n==== Dynamic Arrays\n\n...==== Linked Lists\n\n...',
tags: [
['d', 'arrays-and-lists'],
['title', 'Arrays and Lists']
]
}
};
test.expect(expectedLevel2Events.mainIndex.kind).toBe(30040);
test.expect(expectedLevel2Events.dataStructuresSection.kind).toBe(30041);
test.expect(expectedLevel2Events.dataStructuresSection.content).toContain('=== Arrays and Lists');
test.expect(expectedLevel3Events.dataStructuresIndex.kind).toBe(30040);
test.expect(expectedLevel3Events.arraysAndListsSection.content).toContain('==== Dynamic Arrays');
});
// =============================================================================
// PHASE 4: Smart Publishing System Tests
// =============================================================================
test.test('Content type detection should work for both test files', () => {
const testCases = [
{
name: 'Understanding Knowledge (article)',
content: understandingKnowledge,
expected: 'article'
},
{
name: 'Desire (article)',
content: desire,
expected: 'article'
},
{
name: 'Scattered notes format',
content: '== Note 1\nContent\n\n== Note 2\nMore content',
expected: 'scattered-notes'
}
];
testCases.forEach(({ name, content, expected }) => {
const hasDocTitle = content.trim().startsWith('=') && !content.trim().startsWith('==');
const hasSections = content.includes('==');
let detected;
if (hasDocTitle) {
detected = 'article';
} else if (hasSections) {
detected = 'scattered-notes';
} else {
detected = 'none';
}
console.log(` ${name}: detected ${detected}`);
test.expect(detected).toBe(expected);
});
});
test.test('Parse level should affect event structure correctly', () => {
// Understanding Knowledge has structure: = > == (6 sections) > === (many subsections) > ====
// Based on actual content analysis
const levelEventCounts = [
{ level: 1, description: 'Only document index', events: 1 },
{ level: 2, description: 'Document index + level 2 sections (==)', events: 7 }, // 1 index + 6 sections
{ level: 3, description: 'Document index + section indices + level 3 subsections (===)', events: 20 }, // More complex
{ level: 4, description: 'Full hierarchy including level 4 (====)', events: 35 }
];
levelEventCounts.forEach(({ level, description, events }) => {
console.log(` Level ${level}: ${description} (${events} events)`);
test.expect(events).toBeTruthy();
});
});
// =============================================================================
// PHASE 5: Integration Tests (End-to-End Workflow)
// =============================================================================
test.test('Full Understanding Knowledge publishing workflow (Level 2)', async () => {
// Mock the complete ITERATIVE workflow
const mockWorkflow = {
parseLevel2: (content: string) => ({
metadata: {
title: 'Understanding Knowledge',
image: 'https://i.nostr.build/IUs0xNyUEf5hXTFL.jpg',
published: '2025-04-21',
tags: ['knowledge', 'philosophy', 'education'],
type: 'text'
},
title: 'Understanding Knowledge',
content: 'Introduction content before any sections',
sections: [
{
title: 'Preface',
content: '[NOTE]\nThis essay was written to outline...',
metadata: { title: 'Preface' }
},
{
title: 'Introduction: Knowledge as a Living Ecosystem',
// Contains ALL subsections (===, ====) in content
content: `image:https://i.nostr.build/IUs0xNyUEf5hXTFL.jpg[library]
=== Why Investigate the Nature of Knowledge?
Understanding the nature of knowledge itself is fundamental...
=== Challenging the Static Perception of Knowledge
Traditionally, knowledge has been perceived as a static repository...
==== The Four Perspectives
===== 1. The Building Blocks (Material Cause)
Just as living organisms are made up of cells...`,
metadata: { title: 'Introduction: Knowledge as a Living Ecosystem' }
}
// ... 4 more sections (Material Cause, Formal Cause, Efficient Cause, Final Cause)
]
}),
buildLevel2Events: (parsed: any) => ({
indexEvent: {
kind: 30040,
content: '',
tags: [
['d', 'understanding-knowledge'],
['title', parsed.title],
['image', parsed.metadata.image],
['t', 'knowledge'], ['t', 'philosophy'], ['t', 'education'],
['type', 'text'],
['a', '30041:pubkey:preface'],
['a', '30041:pubkey:introduction-knowledge-as-a-living-ecosystem']
]
},
sectionEvents: parsed.sections.map((s: any) => ({
kind: 30041,
content: s.content,
tags: [
['d', s.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')],
['title', s.title]
]
}))
}),
publish: (events: any) => ({
success: true,
published: events.sectionEvents.length + 1,
eventIds: ['main-index', ...events.sectionEvents.map((_: any, i: number) => `section-${i}`)]
})
};
// Test the full Level 2 workflow
const parsed = mockWorkflow.parseLevel2(understandingKnowledge);
const events = mockWorkflow.buildLevel2Events(parsed);
const result = mockWorkflow.publish(events);
test.expect(parsed.metadata.title).toBe('Understanding Knowledge');
test.expect(parsed.sections).toHaveLength(2);
test.expect(events.indexEvent.kind).toBe(30040);
test.expect(events.sectionEvents).toHaveLength(2);
test.expect(events.sectionEvents[1].content).toContain('=== Why Investigate'); // Contains subsections
test.expect(events.sectionEvents[1].content).toContain('===== 1. The Building Blocks'); // Contains deeper levels
test.expect(result.success).toBeTruthy();
test.expect(result.published).toBe(3); // 1 index + 2 sections
});
test.test('Error handling for malformed content', () => {
const invalidCases = [
{ content: '== Section\n=== Subsection\n==== Missing content', error: 'Empty content sections' },
{ content: '= Title\n\n== Section\n==== Skipped level', error: 'Invalid header nesting' },
{ content: '', error: 'Empty document' }
];
invalidCases.forEach(({ content, error }) => {
// Mock error detection
const hasEmptySections = content.includes('Missing content');
const hasSkippedLevels = content.includes('====') && !content.includes('===');
const isEmpty = content.trim() === '';
const shouldError = hasEmptySections || hasSkippedLevels || isEmpty;
test.expect(shouldError).toBeTruthy();
});
});
// =============================================================================
// Test Execution
// =============================================================================
console.log('🎯 ZettelPublisher Test-Driven Development (ITERATIVE)\n');
console.log('📋 Test Data Analysis:');
console.log(`- Understanding Knowledge: ${understandingKnowledge.split('\n').length} lines`);
console.log(`- Desire: ${desire.split('\n').length} lines`);
console.log('- Both files use = document title with metadata directly underneath');
console.log('- Sections use == with deep nesting (===, ====, =====)');
console.log('- Custom attributes like :type: podcastArticle need preservation');
console.log('- CRITICAL: Structure is ITERATIVE not recursive (per docreference.md)\n');
test.run().then(success => {
if (success) {
console.log('\n🎉 All tests defined! Ready for ITERATIVE implementation.');
console.log('\n📋 Implementation Plan:');
console.log('1. ✅ Update ParsedAsciiDoc interface for ITERATIVE parsing');
console.log('2. ✅ Fix content processing (header separation, custom attributes)');
console.log('3. ✅ Implement level-based publishing logic (30040/30041 structure)');
console.log('4. ✅ Add parse-level controlled event generation');
console.log('5. ✅ Create context-aware UI with level selector');
console.log('\n🔄 Each level can be developed and tested independently!');
} else {
console.log('\n❌ Tests ready - implement ITERATIVE features to make them pass!');
}
}).catch(console.error);
Loading…
Cancel
Save